diff --git a/sample-apps/react/react-dogfood/components/EndCallSummary/EndCallSummaryView.tsx b/sample-apps/react/react-dogfood/components/EndCallSummary/EndCallSummaryView.tsx index 1d8361e3e6..36fd2298c8 100644 --- a/sample-apps/react/react-dogfood/components/EndCallSummary/EndCallSummaryView.tsx +++ b/sample-apps/react/react-dogfood/components/EndCallSummary/EndCallSummaryView.tsx @@ -1,25 +1,28 @@ import { Icon } from '@stream-io/video-react-sdk'; -import { useState, useCallback, useEffect } from 'react'; -import { Feedback } from '../Feedback/Feedback'; +import { useState, useCallback, useEffect, useMemo } from 'react'; +import { Feedback } from './Feedback'; import clsx from 'clsx'; +import Link from 'next/link'; import { Card } from './Card'; import { Badge, LinkBadge, Status } from './Badge'; import { NetworkStatus } from './NetworkStatus'; import { Rating } from './Rating'; import { Header } from './Header'; +import { Recordings } from './Recordings'; import { useStreamVideoClient, - useCall, useCallStateHooks, EdgeResponse, + useCall, } from '@stream-io/video-react-sdk'; + import { Notification } from './Notification'; -import { CallRecordings } from '../CallRecordings'; export interface EndCallSummaryViewProps { rejoin: () => void; startNewCall: () => void; + joinTime?: Date; } const toStatus = (config: { @@ -39,23 +42,63 @@ const toCountryName = (isoCountryCode: string) => { return regionNames.of(isoCountryCode); }; +const toEdgeStatus = (edge: EdgeResponse) => { + if (edge.green === 1) return 'green'; + if (edge.yellow === 1) return 'yellow'; + + return 'red'; +}; + +const toNetworkStatus = (quality: number) => { + if (quality >= 80) return 'green'; + if (quality >= 40) return 'yellow'; + + return 'red'; +}; + +const toNetworkNotification = ( + quality: number, +): { + message: string; + type: 'success' | 'info' | 'error' | 'caution'; +} => { + if (quality >= 80) + return { + message: 'Your Network is Stable.', + type: 'success', + }; + if (quality >= 40) + return { + message: 'Your Network is Average.', + type: 'caution', + }; + + return { + message: 'Your Network is Poor.', + type: 'error', + }; +}; + export function EndCallSummaryView({ rejoin, startNewCall, + joinTime, }: EndCallSummaryViewProps) { const [rating, setRating] = useState<{ current: number; maxAmount: number; success: boolean; }>({ current: 0, maxAmount: 5, success: false }); + const [callStats, setCallStats] = useState(undefined); const [showRecordings, setShowRecordings] = useState(false); const [edges, setEdges] = useState(undefined); - const client = useStreamVideoClient(); - const call = useCall(); - const { useCallStatsReport } = useCallStateHooks(); + const { useCallStatsReport, useCallStartedAt } = useCallStateHooks(); const callStatsReport = useCallStatsReport(); + const startedAt = useCallStartedAt(); + const call = useCall(); + const client = useStreamVideoClient(); const { publisherStats } = callStatsReport || {}; const handleSetRating = useCallback( @@ -75,15 +118,40 @@ export function EndCallSummaryView({ }); }, [client]); - const latencyComparison = { - lowBound: 75, - highBound: 400, - }; + useEffect(() => { + if (!client || !call) return; + + async function fetchCallStats() { + const res = await client?.queryCallStats({ + filter_conditions: { + call_cid: call?.cid, + }, + limit: 1, + }); + return res; + } + + const response = fetchCallStats(); + response.then((res) => { + setCallStats(res?.reports[0]); + }); + }, [client, call]); + + const timeToConnect = useMemo(() => { + if (!joinTime || !startedAt) return null; + const timeDifference = startedAt.getTime() - joinTime.getTime(); + const differenceDate = new Date(timeDifference); + return differenceDate.getMilliseconds(); + }, [joinTime, startedAt]); const handleSubmitSuccess = useCallback(() => { setRating({ current: 0, maxAmount: 5, success: true }); }, []); + const networkNotification = toNetworkNotification( + callStats?.quality_score || 0, + ); + return (
- {publisherStats?.averageJitterInMs || 0}ms + {callStats?.quality_score || 0}% - 0.4s + {timeToConnect}ms - {publisherStats?.codec || 'Unknown'} + + {publisherStats?.codec?.replace('video/', '') || 'Unknown'} +
- - Network - Device + + + Network + + Device - +
@@ -223,18 +311,18 @@ export function EndCallSummaryView({
- + - + Amsterdam - + Boston @@ -242,22 +330,29 @@ export function EndCallSummaryView({
{edges?.map((edge) => ( - + - {toCountryName(edge.country_iso_code)} + + {toCountryName(edge.country_iso_code)} + ))}
- +
- +

@@ -322,7 +417,9 @@ export function EndCallSummaryView({

- + + Contact Us +
@@ -336,7 +433,12 @@ export function EndCallSummaryView({ /> + setRating({ current: 0, maxAmount: 5, success: false }) + } />
)} @@ -347,7 +449,7 @@ export function EndCallSummaryView({ onClick={() => setShowRecordings(false)} />
- + setShowRecordings(false)} />
)} diff --git a/sample-apps/react/react-dogfood/components/EndCallSummary/Feedback.tsx b/sample-apps/react/react-dogfood/components/EndCallSummary/Feedback.tsx new file mode 100644 index 0000000000..a79b5161f4 --- /dev/null +++ b/sample-apps/react/react-dogfood/components/EndCallSummary/Feedback.tsx @@ -0,0 +1,213 @@ +import { HTMLInputTypeAttribute, useMemo, useState } from 'react'; +import clsx from 'clsx'; +import { useField, useForm } from 'react-form'; +import { useCall, Icon } from '@stream-io/video-react-sdk'; +import { getCookie } from '../../helpers/getCookie'; +import { FeedbackType } from './FeedbackType'; + +export type Props = { + className?: string; + callId?: string; + rating?: number; + submitSuccess: () => void; + callData?: any; + onClose: () => void; +}; + +function required(value: string | number, name: string) { + if (!value) { + return `Please enter a ${name}`; + } + return false; +} + +const Input = (props: { + className?: string; + type: HTMLInputTypeAttribute; + placeholder: string; + name: string; + required?: boolean; +}) => { + const { name, className, ...rest } = props; + const { + meta: { error, isTouched }, + getInputProps, + } = useField(name, { + validate: props.required ? (value) => required(value, name) : undefined, + }); + + return ( + + ); +}; + +const TextArea = (props: { + className?: string; + placeholder: string; + name: string; + required?: boolean; +}) => { + const { name, className, ...rest } = props; + const { + meta: { error, isTouched }, + getInputProps, + } = useField(name, { + validate: props.required ? (value) => required(value, name) : undefined, + }); + + return ( +