From 648c4ff41380f0cb43c0c8d176f626f0c9a5a4a4 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 8 Dec 2023 17:30:46 +0530 Subject: [PATCH] Production release PR (#2237) * Hotfix backmerge to main * fix: active speaker sorting * feat: leaderboard for quizzes * feat: handle polls via timed metadata * fix: low volume on mweb * fix: count for total quiz questions * fix: cta text for quiz, title for interaction * fix: peers with write permissions should be able to see leaderboard (#2243) * fix: quiz response ui * feat: added caption for hls stream * fix: poll notifs in stream (#2244) * fix: update env * fix: live streaming black screen for hls viewer (#2246) * fix: live streaming black screen for hls viewer * fix: update react icons size validation * fix: response ui, participation summary (#2247) * fix: response ui, part. summary * fix: clean up * fix: new msg pill padding * fix: refactor getpeerresponse * fix: closed captions design parity (#2248) * fix: closed captions design parity * fix: captions opacity * fix: hide shadow * fix: alignment (#2249) * fix: index for weight (#2250) * build: update versions for release Co-authored-by: eswarclynn --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Ravi theja Co-authored-by: Kaustubh Kumar Co-authored-by: Amar Bathwal <110378139+amar-1995@users.noreply.github.com> Co-authored-by: Saikat Mitra Co-authored-by: eswarclynn --- .github/workflows/deploy-prod.yml | 1 + .github/workflows/deploy-qa.yml | 1 + apps/100ms-custom-app/package.json | 4 +- apps/100ms-custom-app/src/App.jsx | 6 +- apps/100ms-web/package.json | 10 +- .../src/components/Polls/Voting/Voting.jsx | 2 +- package.json | 2 +- packages/hls-player/package.json | 4 +- .../src/controllers/HMSHLSPlayer.ts | 14 +++ packages/hls-player/src/interfaces/events.ts | 1 + packages/hls-player/src/utilies/constants.ts | 1 + packages/hls-stats/package.json | 2 +- packages/hms-noise-suppression/package.json | 4 +- packages/hms-video-store/package.json | 4 +- .../hmsSDKStore/HMSInteractivityCenter.ts | 5 + .../src/core/hmsSDKStore/common/mapping.ts | 1 + .../src/core/hmsSDKStore/sdkTypes.ts | 2 + packages/hms-video-store/src/core/index.ts | 1 + .../src/core/schema/interactivity-center.ts | 3 + .../src/core/schema/notification.ts | 3 + packages/hms-video-web/package.json | 2 +- .../audio-sink-manager/AudioSinkManager.ts | 15 +++ .../src/interfaces/session-store/polls.ts | 25 +++- .../src/interfaces/update-listener.ts | 1 + .../HMSInteractivityCenter.ts | 28 +++++ packages/hms-video-web/src/signal/ISignal.ts | 4 + .../src/signal/interfaces/polls.ts | 42 ++++++- .../hms-video-web/src/signal/jsonrpc/index.ts | 20 ++- .../src/signal/jsonrpc/models.ts | 1 + packages/hms-video-web/src/transport/index.ts | 6 + packages/hms-virtual-background/package.json | 6 +- .../react-icons/assets/ClosedCaptionIcon.svg | 8 +- .../react-icons/assets/OpenCaptionIcon.svg | 5 + .../react-icons/assets/QuizActiveIcon.svg | 5 + .../react-icons/assets/TrophyFilledIcon.svg | 9 ++ packages/react-icons/package.json | 2 +- .../react-icons/src/ClosedCaptionIcon.tsx | 8 +- packages/react-icons/src/OpenCaptionIcon.tsx | 13 ++ packages/react-icons/src/QuizActiveIcon.tsx | 21 ++++ packages/react-icons/src/TrophyFilledIcon.tsx | 27 ++++ packages/react-icons/src/index.ts | 3 + packages/react-sdk/package.json | 4 +- packages/roomkit-react/package.json | 10 +- .../src/Prebuilt/common/PeersSorter.ts | 15 ++- .../src/Prebuilt/common/constants.ts | 1 + .../src/Prebuilt/common/utils.js | 34 ++++++ .../src/Prebuilt/components/Chat/Chat.jsx | 7 +- .../HMSVideo/HLSCaptionSelector.tsx | 13 ++ .../Prebuilt/components/HMSVideo/HMSVideo.jsx | 36 +++++- .../Notifications/Notifications.tsx | 35 +++++- .../Polls/CreatePollQuiz/PollsQuizMenu.jsx | 12 +- .../Polls/CreateQuestions/CreateQuestions.jsx | 22 +++- .../Polls/CreateQuestions/QuestionForm.jsx | 41 +++++-- .../Polls/CreateQuestions/SavedQuestion.jsx | 4 +- .../src/Prebuilt/components/Polls/Polls.tsx | 3 + .../components/Polls/Voting/Leaderboard.tsx | 115 ++++++++++++++++++ .../Polls/Voting/LeaderboardEntry.tsx | 63 ++++++++++ .../Polls/Voting/PeerParticipationSummary.tsx | 38 ++++++ .../components/Polls/Voting/QuestionCard.jsx | 39 ++++-- .../Polls/Voting/StandardVoting.jsx | 8 +- .../components/Polls/Voting/Voting.jsx | 44 +++++-- .../Polls/common/MultipleChoiceOptions.jsx | 54 ++++---- .../Polls/common/SingleChoiceOptions.jsx | 82 +++++++------ .../Polls/common/StatusIndicator.jsx | 24 +--- .../components/Polls/common/VoteCount.jsx | 16 +-- .../VideoLayouts/EqualProminence.tsx | 11 +- .../components/VideoLayouts/GridLayout.tsx | 31 ++++- .../VideoLayouts/ScreenshareLayout.tsx | 1 - .../src/Prebuilt/layouts/HLSView.jsx | 54 +++++++- packages/roomkit-web/package.json | 4 +- 70 files changed, 921 insertions(+), 222 deletions(-) create mode 100644 packages/react-icons/assets/OpenCaptionIcon.svg create mode 100644 packages/react-icons/assets/QuizActiveIcon.svg create mode 100644 packages/react-icons/assets/TrophyFilledIcon.svg create mode 100644 packages/react-icons/src/OpenCaptionIcon.tsx create mode 100644 packages/react-icons/src/QuizActiveIcon.tsx create mode 100644 packages/react-icons/src/TrophyFilledIcon.tsx create mode 100644 packages/roomkit-react/src/Prebuilt/components/HMSVideo/HLSCaptionSelector.tsx create mode 100644 packages/roomkit-react/src/Prebuilt/components/Polls/Voting/Leaderboard.tsx create mode 100644 packages/roomkit-react/src/Prebuilt/components/Polls/Voting/LeaderboardEntry.tsx create mode 100644 packages/roomkit-react/src/Prebuilt/components/Polls/Voting/PeerParticipationSummary.tsx diff --git a/.github/workflows/deploy-prod.yml b/.github/workflows/deploy-prod.yml index 60f4d8761a..1215a0ef38 100644 --- a/.github/workflows/deploy-prod.yml +++ b/.github/workflows/deploy-prod.yml @@ -32,6 +32,7 @@ jobs: REACT_APP_ZIPY_KEY: ${{ secrets.PROD_ZIPY_KEY }} REACT_APP_ENV: 'prod' REACT_APP_ENABLE_BEAM_SPEAKERS_LOGGING: 'true' + REACT_APP_DASHBOARD_LINK: https://dashboard.100ms.live/ steps: - name: log inputs diff --git a/.github/workflows/deploy-qa.yml b/.github/workflows/deploy-qa.yml index 9e28c406f7..9fe13f7c53 100644 --- a/.github/workflows/deploy-qa.yml +++ b/.github/workflows/deploy-qa.yml @@ -46,6 +46,7 @@ jobs: REACT_APP_ROOM_LAYOUT_ENDPOINT: https://api-nonprod.100ms.live/v2/layouts/ui REACT_APP_TOKEN_BY_ROOM_CODE_ENDPOINT: https://auth-nonprod.100ms.live/v2/token REACT_APP_DASHBOARD_BASE_ENDPOINT: https://qa-in2-ipv6.100ms.live/hmsapi/ + REACT_APP_DASHBOARD_LINK: https://app-qa.100ms.live/ steps: - name: log inputs diff --git a/apps/100ms-custom-app/package.json b/apps/100ms-custom-app/package.json index 8adce0b174..c68608888f 100644 --- a/apps/100ms-custom-app/package.json +++ b/apps/100ms-custom-app/package.json @@ -3,8 +3,8 @@ "version": "0.1.0", "private": true, "dependencies": { - "@100mslive/react-icons": "0.8.23", - "@100mslive/roomkit-react": "0.1.14", + "@100mslive/react-icons": "0.8.24", + "@100mslive/roomkit-react": "0.1.15", "axios": "^0.21.1", "js-cookies": "^1.0.4", "lodash.merge": "^4.6.2", diff --git a/apps/100ms-custom-app/src/App.jsx b/apps/100ms-custom-app/src/App.jsx index fceebbbe1e..e63fdd3d0f 100644 --- a/apps/100ms-custom-app/src/App.jsx +++ b/apps/100ms-custom-app/src/App.jsx @@ -36,7 +36,11 @@ const App = () => { if ((authToken || roomCode) && hmsPrebuiltRef.current && isHeadless) { const { hmsActions } = hmsPrebuiltRef.current; hmsActions?.enableBeamSpeakerLabelsLogging?.(); - hmsActions?.ignoreMessageTypes?.(['chat', 'EMOJI_REACTION']); + hmsActions?.ignoreMessageTypes?.([ + 'chat', + 'EMOJI_REACTION', + 'POLL_STARTED', + ]); hmsActions?.setAppData?.('disableNotifications', true); } }, [authToken, roomCode, isHeadless]); diff --git a/apps/100ms-web/package.json b/apps/100ms-web/package.json index 97022cd055..b61a810eb2 100644 --- a/apps/100ms-web/package.json +++ b/apps/100ms-web/package.json @@ -9,11 +9,11 @@ "src" ], "dependencies": { - "@100mslive/hls-player": "0.1.23", - "@100mslive/hms-virtual-background": "1.11.23", - "@100mslive/react-icons": "0.8.23", - "@100mslive/react-sdk": "0.8.23", - "@100mslive/roomkit-react": "0.1.14", + "@100mslive/hls-player": "0.1.24", + "@100mslive/hms-virtual-background": "1.11.24", + "@100mslive/react-icons": "0.8.24", + "@100mslive/react-sdk": "0.8.24", + "@100mslive/roomkit-react": "0.1.15", "@emoji-mart/data": "^1.0.6", "@emoji-mart/react": "^1.0.1", "@tldraw/tldraw": "^1.18.4", diff --git a/apps/100ms-web/src/components/Polls/Voting/Voting.jsx b/apps/100ms-web/src/components/Polls/Voting/Voting.jsx index d2220d6a07..b16548f644 100644 --- a/apps/100ms-web/src/components/Polls/Voting/Voting.jsx +++ b/apps/100ms-web/src/components/Polls/Voting/Voting.jsx @@ -42,7 +42,7 @@ export const Voting = ({ id, toggleVoting }) => { borderBottom: "1px solid $border_default", }} > - {poll?.type?.toUpperCase()} + {poll.title} { + return this._hls.subtitleTracks.length > 0; + }; + + toggleCaption = () => { + // no subtitles, do nothing + if (!this.hasCaptions()) { + return; + } + this._hls.subtitleDisplay = !this._hls.subtitleDisplay; + this.emitEvent(HMSHLSPlayerEvents.CAPTION_ENABLED, this._hls.subtitleDisplay); + }; + private playVideo = async () => { try { if (this._videoEl.paused) { diff --git a/packages/hls-player/src/interfaces/events.ts b/packages/hls-player/src/interfaces/events.ts index 959780cf3c..18aabf0f33 100644 --- a/packages/hls-player/src/interfaces/events.ts +++ b/packages/hls-player/src/interfaces/events.ts @@ -9,6 +9,7 @@ type HMSHLSListenerDataMapping = { [HMSHLSPlayerEvents.TIMED_METADATA_LOADED]: HMSHLSCue; [HMSHLSPlayerEvents.STATS]: HlsPlayerStats; [HMSHLSPlayerEvents.PLAYBACK_STATE]: HMSHLSPlaybackState; + [HMSHLSPlayerEvents.CAPTION_ENABLED]: boolean; [HMSHLSPlayerEvents.ERROR]: HMSHLSException; [HMSHLSPlayerEvents.CURRENT_TIME]: number; diff --git a/packages/hls-player/src/utilies/constants.ts b/packages/hls-player/src/utilies/constants.ts index 7f701b91de..191621bf1e 100644 --- a/packages/hls-player/src/utilies/constants.ts +++ b/packages/hls-player/src/utilies/constants.ts @@ -9,6 +9,7 @@ export enum HMSHLSPlayerEvents { MANIFEST_LOADED = 'manifest-loaded', LAYER_UPDATED = 'layer-updated', + CAPTION_ENABLED = 'caption-enabled', ERROR = 'error', PLAYBACK_STATE = 'playback-state', diff --git a/packages/hls-stats/package.json b/packages/hls-stats/package.json index b7b731afed..1bdf1038de 100644 --- a/packages/hls-stats/package.json +++ b/packages/hls-stats/package.json @@ -1,6 +1,6 @@ { "name": "@100mslive/hls-stats", - "version": "0.2.23", + "version": "0.2.24", "description": "A simple library that provides stats for your hls stream", "main": "dist/index.cjs.js", "module": "dist/index.js", diff --git a/packages/hms-noise-suppression/package.json b/packages/hms-noise-suppression/package.json index 7c2002b128..d5a74cd366 100644 --- a/packages/hms-noise-suppression/package.json +++ b/packages/hms-noise-suppression/package.json @@ -1,5 +1,5 @@ { - "version": "0.9.23", + "version": "0.9.24", "license": "MIT", "main": "dist/index.cjs.js", "typings": "dist/index.d.ts", @@ -37,6 +37,6 @@ "author": "vishaldhull09", "module": "dist/index.js", "devDependencies": { - "@100mslive/hms-video": "0.9.23" + "@100mslive/hms-video": "0.9.24" } } diff --git a/packages/hms-video-store/package.json b/packages/hms-video-store/package.json index 2c046dd376..cf56b27674 100644 --- a/packages/hms-video-store/package.json +++ b/packages/hms-video-store/package.json @@ -1,5 +1,5 @@ { - "version": "0.10.23", + "version": "0.10.24", "license": "MIT", "main": "dist/index.cjs.js", "module": "dist/index.js", @@ -41,7 +41,7 @@ "author": "100ms", "sideEffects": false, "dependencies": { - "@100mslive/hms-video": "0.9.23", + "@100mslive/hms-video": "0.9.24", "eventemitter2": "^6.4.7", "immer": "^9.0.6", "reselect": "4.0.0", diff --git a/packages/hms-video-store/src/core/hmsSDKStore/HMSInteractivityCenter.ts b/packages/hms-video-store/src/core/hmsSDKStore/HMSInteractivityCenter.ts index beae52be3b..fd17913d5b 100644 --- a/packages/hms-video-store/src/core/hmsSDKStore/HMSInteractivityCenter.ts +++ b/packages/hms-video-store/src/core/hmsSDKStore/HMSInteractivityCenter.ts @@ -1,4 +1,5 @@ import { + HMSPoll, HMSPollCreateParams, HMSPollQuestionCreateParams, HMSPollQuestionResponseCreateParams, @@ -32,4 +33,8 @@ export class HMSInteractivityCenter implements IHMSInteractivityCenter { addResponsesToPoll(pollID: string, responses: HMSPollQuestionResponseCreateParams[]) { return this.sdkInteractivityCenter.addResponsesToPoll(pollID, responses); } + + fetchLeaderboard(poll: HMSPoll, offset: number, count: number) { + return this.sdkInteractivityCenter.fetchLeaderboard(poll, offset, count); + } } diff --git a/packages/hms-video-store/src/core/hmsSDKStore/common/mapping.ts b/packages/hms-video-store/src/core/hmsSDKStore/common/mapping.ts index 7603019729..0a0d076d9a 100644 --- a/packages/hms-video-store/src/core/hmsSDKStore/common/mapping.ts +++ b/packages/hms-video-store/src/core/hmsSDKStore/common/mapping.ts @@ -30,4 +30,5 @@ export const POLL_NOTIFICATION_TYPES: PollNotificationMap = { [sdkTypes.HMSPollsUpdate.POLL_STARTED]: HMSNotificationTypes.POLL_STARTED, [sdkTypes.HMSPollsUpdate.POLL_STOPPED]: HMSNotificationTypes.POLL_STOPPED, [sdkTypes.HMSPollsUpdate.POLL_STATS_UPDATED]: HMSNotificationTypes.POLL_VOTES_UPDATED, + // [sdkTypes.HMSPollsUpdate.POLL_LEADERBOARD_SHARED]: HMSNotificationTypes.POLL_LEADERBOARD_SHARED, }; diff --git a/packages/hms-video-store/src/core/hmsSDKStore/sdkTypes.ts b/packages/hms-video-store/src/core/hmsSDKStore/sdkTypes.ts index 9d04b801d7..48b51419a2 100644 --- a/packages/hms-video-store/src/core/hmsSDKStore/sdkTypes.ts +++ b/packages/hms-video-store/src/core/hmsSDKStore/sdkTypes.ts @@ -28,6 +28,7 @@ import { HMSPlaylistSettings, HMSPoll, HMSPollCreateParams, + HMSPollLeaderboardResponse, HMSPollQuestionAnswer, HMSPollQuestionCreateParams, HMSPollQuestionType, @@ -124,6 +125,7 @@ export type { TokenRequest, TokenRequestOptions, HMSPoll, + HMSPollLeaderboardResponse, HMSPollCreateParams, HMSPollQuestionAnswer, HMSPollQuestionCreateParams, diff --git a/packages/hms-video-store/src/core/index.ts b/packages/hms-video-store/src/core/index.ts index a72e3f7f38..c07db9e96a 100644 --- a/packages/hms-video-store/src/core/index.ts +++ b/packages/hms-video-store/src/core/index.ts @@ -43,4 +43,5 @@ export type { HMSPollCreateParams, HMSPollQuestionCreateParams, HMSPollQuestionAnswer, + HMSPollLeaderboardResponse, } from './hmsSDKStore/sdkTypes'; diff --git a/packages/hms-video-store/src/core/schema/interactivity-center.ts b/packages/hms-video-store/src/core/schema/interactivity-center.ts index 8c0c1ed1e6..0829d8b84c 100644 --- a/packages/hms-video-store/src/core/schema/interactivity-center.ts +++ b/packages/hms-video-store/src/core/schema/interactivity-center.ts @@ -1,5 +1,7 @@ import { + HMSPoll, HMSPollCreateParams, + HMSPollLeaderboardResponse, HMSPollQuestionCreateParams, HMSPollQuestionResponseCreateParams, } from '@100mslive/hms-video'; @@ -10,4 +12,5 @@ export interface IHMSInteractivityCenter { stopPoll(poll: string): Promise; addQuestionsToPoll(pollID: string, questions: HMSPollQuestionCreateParams[]): Promise; addResponsesToPoll(pollID: string, responses: HMSPollQuestionResponseCreateParams[]): Promise; + fetchLeaderboard(poll: HMSPoll, offset: number, count: number): Promise; } diff --git a/packages/hms-video-store/src/core/schema/notification.ts b/packages/hms-video-store/src/core/schema/notification.ts index 716bc648db..fda6c4654e 100644 --- a/packages/hms-video-store/src/core/schema/notification.ts +++ b/packages/hms-video-store/src/core/schema/notification.ts @@ -75,6 +75,7 @@ export interface HMSReconnectionNotification extends BaseNotification { export interface HMSPollNotification extends BaseNotification { type: HMSNotificationTypes.POLL_STARTED | HMSNotificationTypes.POLL_STOPPED | HMSNotificationTypes.POLL_VOTES_UPDATED; + // | HMSNotificationTypes.POLL_LEADERBOARD_SHARED; data: HMSPoll; } @@ -124,6 +125,7 @@ export enum HMSNotificationTypes { POLL_STARTED = 'POLL_STARTED', POLL_STOPPED = 'POLL_STOPPED', POLL_VOTES_UPDATED = 'POLL_VOTES_UPDATED', + // POLL_LEADERBOARD_SHARED = 'POLL_LEADERBOARD_SHARED', HAND_RAISE_CHANGED = 'HAND_RAISE_CHANGED', } @@ -155,6 +157,7 @@ export type HMSNotificationMapping = { [HMSNotificationTypes.POLL_STARTED]: HMSPollNotification; [HMSNotificationTypes.POLL_STOPPED]: HMSPollNotification; [HMSNotificationTypes.POLL_VOTES_UPDATED]: HMSPollNotification; + // [HMSNotificationTypes.POLL_LEADERBOARD_SHARED]: HMSPollNotification; [HMSNotificationTypes.POLL_CREATED]: HMSPollNotification; [HMSNotificationTypes.HAND_RAISE_CHANGED]: HMSPeerNotification; }[T]; diff --git a/packages/hms-video-web/package.json b/packages/hms-video-web/package.json index 99dfe3b720..07719a65d5 100644 --- a/packages/hms-video-web/package.json +++ b/packages/hms-video-web/package.json @@ -1,6 +1,6 @@ { "name": "@100mslive/hms-video", - "version": "0.9.23", + "version": "0.9.24", "license": "MIT", "main": "dist/index.cjs.js", "typings": "dist/index.d.ts", diff --git a/packages/hms-video-web/src/audio-sink-manager/AudioSinkManager.ts b/packages/hms-video-web/src/audio-sink-manager/AudioSinkManager.ts index 36547d9ff4..907c84ed90 100644 --- a/packages/hms-video-web/src/audio-sink-manager/AudioSinkManager.ts +++ b/packages/hms-video-web/src/audio-sink-manager/AudioSinkManager.ts @@ -157,6 +157,7 @@ export class AudioSinkManager { track.setVolume(this.volume); HMSLogger.d(this.TAG, 'Audio track added', `${track}`); this.init(); // call to create sink element if not already created + await this.autoSelectAudioOutput(); this.audioSink?.append(audioEl); this.outputDevice && (await track.setOutputDevice(this.outputDevice)); audioEl.srcObject = new MediaStream([track.nativeTrack]); @@ -259,4 +260,18 @@ export class AudioSinkManager { track.setAudioElement(null); } }; + + /** + * Mweb is not able to play via call channel by default, this is to switch from media channel to call channel + */ + private autoSelectAudioOutput = async () => { + if (this.audioSink?.children.length === 0) { + const device = this.deviceManager.audioInput?.find(device => device.label.includes('earpiece')); + const localAudioTrack = this.store.getLocalPeer()?.audioTrack; + if (localAudioTrack && device) { + await localAudioTrack.setSettings({ deviceId: device?.deviceId }); + await localAudioTrack.setSettings({ deviceId: 'default' }); + } + } + }; } diff --git a/packages/hms-video-web/src/interfaces/session-store/polls.ts b/packages/hms-video-web/src/interfaces/session-store/polls.ts index 338020de9a..276cbc19a7 100644 --- a/packages/hms-video-web/src/interfaces/session-store/polls.ts +++ b/packages/hms-video-web/src/interfaces/session-store/polls.ts @@ -63,6 +63,7 @@ export interface HMSPollQuestion { export interface HMSPollQuestionCreateParams extends Pick { index?: number; options?: HMSPollQuestionOptionCreateParams[]; + weight?: number; } export interface HMSPollQuestionAnswer { @@ -81,6 +82,12 @@ export enum HMSPollQuestionType { LONG_ANSWER = 'long-answer', } +export enum HMSPollStates { + CREATED = 'created', + STARTED = 'started', + STOPPED = 'stopped', +} + export interface HMSPollQuestionOption { index: number; text: string; @@ -101,9 +108,9 @@ export interface HMSPollQuestionResponse { option?: number; options?: number[]; text?: string; - update?: boolean; // SDK Needs to track wether we previously answered and set accordingly + update?: boolean; // SDK Needs to track whether we previously answered and set accordingly duration?: number; // Time it took to answer the question for leaderboard - responseFinal?: boolean; // Indicates wether this is last update when fetching responses + responseFinal?: boolean; // Indicates whether this is last update when fetching responses } export type HMSPollQuestionResponseCreateParams = Omit< @@ -135,3 +142,17 @@ export interface HMSPollQuestionResult { skippedCount?: number; totalResponses?: number; } + +export interface HMSPollLeaderboardEntry { + position: number; + score: number; + totalResponses: number; + correctResponses: number; + duration: number; + peer: HMSPollResponsePeerInfo; +} + +export interface HMSPollLeaderboardResponse { + entries: HMSPollLeaderboardEntry[]; + hasNext: boolean; +} diff --git a/packages/hms-video-web/src/interfaces/update-listener.ts b/packages/hms-video-web/src/interfaces/update-listener.ts index 7d0e45836e..43f87a6b6c 100644 --- a/packages/hms-video-web/src/interfaces/update-listener.ts +++ b/packages/hms-video-web/src/interfaces/update-listener.ts @@ -53,6 +53,7 @@ export enum HMSPollsUpdate { POLL_STARTED, POLL_STOPPED, POLL_STATS_UPDATED, + // POLL_LEADERBOARD_SHARED, } export interface HMSAudioListener { diff --git a/packages/hms-video-web/src/session-store/interactivity-center/HMSInteractivityCenter.ts b/packages/hms-video-web/src/session-store/interactivity-center/HMSInteractivityCenter.ts index f5647e3b21..aae1e0a1fd 100644 --- a/packages/hms-video-web/src/session-store/interactivity-center/HMSInteractivityCenter.ts +++ b/packages/hms-video-web/src/session-store/interactivity-center/HMSInteractivityCenter.ts @@ -3,11 +3,13 @@ import { HMSInteractivityCenter } from '../../interfaces/session-store/interacti import { HMSPoll, HMSPollCreateParams, + HMSPollLeaderboardResponse, HMSPollQuestionAnswer, HMSPollQuestionOption, HMSPollQuestionResponse, HMSPollQuestionResponseCreateParams, HMSPollQuestionType, + HMSPollStates, HMSPollUserTrackingMode, } from '../../interfaces/session-store/polls'; import { IStore } from '../../sdk/store'; @@ -170,6 +172,32 @@ export class InteractivityCenter implements HMSInteractivityCenter { return question; } + + async fetchLeaderboard(poll: HMSPoll, offset: number, count: number): Promise { + const canReadPolls = this.store.getLocalPeer()?.role?.permissions.pollRead || false; + + if (poll.anonymous || poll.state !== HMSPollStates.STOPPED || !canReadPolls) { + return { entries: [], hasNext: false }; + } + const pollLeaderboard = await this.transport.fetchLeaderboard({ + poll_id: poll.id, + count, + offset, + }); + + const leaderboardEntries = pollLeaderboard.questions.map(question => { + return { + position: question.position, + totalResponses: question.total_responses, + correctResponses: question.correct_responses, + duration: question.duration, + peer: question.peer, + score: question.score, + }; + }); + + return { entries: leaderboardEntries, hasNext: !pollLeaderboard.last }; + } } export const createHMSPollFromPollParams = (pollParams: PollInfoParams): HMSPoll => { diff --git a/packages/hms-video-web/src/signal/ISignal.ts b/packages/hms-video-web/src/signal/ISignal.ts index 4c0833abea..646fff04f1 100644 --- a/packages/hms-video-web/src/signal/ISignal.ts +++ b/packages/hms-video-web/src/signal/ISignal.ts @@ -13,6 +13,8 @@ import { PollInfoGetResponse, PollInfoSetParams, PollInfoSetResponse, + PollLeaderboardGetParams, + PollLeaderboardGetResponse, PollListParams, PollListResponse, PollQuestionsGetParams, @@ -128,6 +130,8 @@ export interface ISignal extends IAnalyticsTransportProvider { getPollResult(params: PollResultParams): Promise; + fetchPollLeaderboard(params: PollLeaderboardGetParams): Promise; + joinGroup(name: string): Promise; leaveGroup(name: string): Promise; diff --git a/packages/hms-video-web/src/signal/interfaces/polls.ts b/packages/hms-video-web/src/signal/interfaces/polls.ts index d242f70bc2..5a8b0b8bde 100644 --- a/packages/hms-video-web/src/signal/interfaces/polls.ts +++ b/packages/hms-video-web/src/signal/interfaces/polls.ts @@ -57,6 +57,7 @@ export interface PollQuestionParams { question: PollQuestionInfoParams; options?: HMSPollQuestionOption[]; answer?: HMSPollQuestionAnswer; + weight?: number; } export interface PollQuestionsSetParams { @@ -125,18 +126,20 @@ interface PollResponse response_id: string; } +export interface PollResponsePeerInfo { + hash: string; + peerid: string; + userid: string; + username: string; +} + export interface PollResponsesGetResponse { poll_id: string; last?: boolean; responses?: { final?: boolean; response: PollResponse; - peer: { - hash: string; - peerid: string; - userid: string; - username: string; - }; + peer: PollResponsePeerInfo; }[]; } @@ -157,3 +160,30 @@ export interface PollResult { export type PollResultParams = PollID; export type PollResultResponse = PollResult & PollID; + +export interface PollLeaderboardGetParams { + poll_id: string; + question?: number; // Question index + count?: number; // Number of peers to be included, sorted by duration in ascending order. Default: 10 + offset: number; // Position to start (response is paginated) +} + +export interface PollLeaderboardEntry { + position: number; // leaderboard position + score: number; // sum of weights of correct answers + total_responses: number; + correct_responses: number; + duration: number; // sum of ms to answer correct questions + peer: PollResponsePeerInfo; +} + +export interface PollLeaderboardGetResponse { + poll_id: string; + total_users: number; + voted_users: number; + correct_users: number; + avg_time: number; + avg_score: number; + questions: PollLeaderboardEntry[]; + last: boolean; +} diff --git a/packages/hms-video-web/src/signal/jsonrpc/index.ts b/packages/hms-video-web/src/signal/jsonrpc/index.ts index 58b28a46c1..a23f84b16c 100644 --- a/packages/hms-video-web/src/signal/jsonrpc/index.ts +++ b/packages/hms-video-web/src/signal/jsonrpc/index.ts @@ -32,6 +32,8 @@ import { PollInfoGetResponse, PollInfoSetParams, PollInfoSetResponse, + PollLeaderboardGetParams, + PollLeaderboardGetResponse, PollListParams, PollListResponse, PollQuestionsGetParams, @@ -426,56 +428,50 @@ export default class JsonRpcSignal implements ISignal { } setPollInfo(params: PollInfoSetParams) { - this.valiateConnection(); return this.call(HMSSignalMethod.POLL_INFO_SET, { ...params }); } getPollInfo(params: PollInfoGetParams) { - this.valiateConnection(); return this.call(HMSSignalMethod.POLL_INFO_GET, { ...params }); } setPollQuestions(params: PollQuestionsSetParams) { - this.valiateConnection(); return this.call(HMSSignalMethod.POLL_QUESTIONS_SET, { ...params }); } startPoll(params: PollStartParams) { - this.valiateConnection(); return this.call(HMSSignalMethod.POLL_START, { ...params }); } stopPoll(params: PollStopParams) { - this.valiateConnection(); return this.call(HMSSignalMethod.POLL_STOP, { ...params }); } getPollQuestions(params: PollQuestionsGetParams): Promise { - this.valiateConnection(); return this.call(HMSSignalMethod.POLL_QUESTIONS_GET, { ...params }); } setPollResponses(params: PollResponseSetParams): Promise { - this.valiateConnection(); return this.call(HMSSignalMethod.POLL_RESPONSE_SET, { ...params }); } getPollResponses(params: PollResponsesGetParams): Promise { - this.valiateConnection(); return this.call(HMSSignalMethod.POLL_RESPONSES, { ...params }); } getPollsList(params: PollListParams): Promise { - this.valiateConnection(); return this.call(HMSSignalMethod.POLL_LIST, { ...params }); } getPollResult(params: PollResultParams): Promise { - this.valiateConnection(); return this.call(HMSSignalMethod.POLL_RESULT, { ...params }); } - private valiateConnection() { + fetchPollLeaderboard(params: PollLeaderboardGetParams): Promise { + return this.call(HMSSignalMethod.POLL_LEADERBOARD, { ...params }); + } + + private validateConnection() { if (!this.isConnected) { throw ErrorFactory.WebSocketConnectionErrors.WebSocketConnectionLost( HMSAction.RECONNECT_SIGNAL, @@ -586,7 +582,7 @@ export default class JsonRpcSignal implements ISignal { private async call(method: HMSSignalMethod, params: Record): Promise { const MAX_RETRIES = 3; let error: HMSException = ErrorFactory.WebsocketMethodErrors.ServerErrors(500, method, `Default ${method} error`); - + this.validateConnection(); let retry; for (retry = 1; retry <= MAX_RETRIES; retry++) { try { diff --git a/packages/hms-video-web/src/signal/jsonrpc/models.ts b/packages/hms-video-web/src/signal/jsonrpc/models.ts index a065be28eb..d731f7ac03 100644 --- a/packages/hms-video-web/src/signal/jsonrpc/models.ts +++ b/packages/hms-video-web/src/signal/jsonrpc/models.ts @@ -53,6 +53,7 @@ export enum HMSSignalMethod { POLL_LIST = 'poll-list', POLL_RESPONSES = 'poll-responses', POLL_RESULT = 'poll-result', + POLL_LEADERBOARD = 'poll-leaderboard', GET_PEER = 'get-peer', FIND_PEER = 'find-peer', PEER_ITER_NEXT = 'peer-iter-next', diff --git a/packages/hms-video-web/src/transport/index.ts b/packages/hms-video-web/src/transport/index.ts index 275a30a300..28fd5ae59a 100644 --- a/packages/hms-video-web/src/transport/index.ts +++ b/packages/hms-video-web/src/transport/index.ts @@ -46,6 +46,8 @@ import { PollInfoGetResponse, PollInfoSetParams, PollInfoSetResponse, + PollLeaderboardGetParams, + PollLeaderboardGetResponse, PollListParams, PollListResponse, PollQuestionsGetParams, @@ -704,6 +706,10 @@ export default class HMSTransport implements ITransport { return this.signal.setPollInfo(params); } + async fetchLeaderboard(params: PollLeaderboardGetParams): Promise { + return this.signal.fetchPollLeaderboard(params); + } + getPollInfo(params: PollInfoGetParams): Promise { return this.signal.getPollInfo(params); } diff --git a/packages/hms-virtual-background/package.json b/packages/hms-virtual-background/package.json index 597585fd00..4a3cb60e86 100755 --- a/packages/hms-virtual-background/package.json +++ b/packages/hms-virtual-background/package.json @@ -1,5 +1,5 @@ { - "version": "1.11.23", + "version": "1.11.24", "license": "MIT", "main": "dist/index.cjs.js", "typings": "dist/index.d.ts", @@ -24,13 +24,13 @@ "format": "prettier --write src/**/*.ts" }, "peerDependencies": { - "@100mslive/hms-video": "0.9.23" + "@100mslive/hms-video": "0.9.24" }, "name": "@100mslive/hms-virtual-background", "author": "ashish17", "module": "dist/index.js", "devDependencies": { - "@100mslive/hms-video": "0.9.23" + "@100mslive/hms-video": "0.9.24" }, "dependencies": { "@mediapipe/selfie_segmentation": "^0.1.1632777926", diff --git a/packages/react-icons/assets/ClosedCaptionIcon.svg b/packages/react-icons/assets/ClosedCaptionIcon.svg index e114166763..a3cd03c537 100644 --- a/packages/react-icons/assets/ClosedCaptionIcon.svg +++ b/packages/react-icons/assets/ClosedCaptionIcon.svg @@ -1,5 +1,5 @@ - - - - + + + + diff --git a/packages/react-icons/assets/OpenCaptionIcon.svg b/packages/react-icons/assets/OpenCaptionIcon.svg new file mode 100644 index 0000000000..12d4fbc585 --- /dev/null +++ b/packages/react-icons/assets/OpenCaptionIcon.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/packages/react-icons/assets/QuizActiveIcon.svg b/packages/react-icons/assets/QuizActiveIcon.svg new file mode 100644 index 0000000000..d0c70d82e6 --- /dev/null +++ b/packages/react-icons/assets/QuizActiveIcon.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/packages/react-icons/assets/TrophyFilledIcon.svg b/packages/react-icons/assets/TrophyFilledIcon.svg new file mode 100644 index 0000000000..8393c8c3ca --- /dev/null +++ b/packages/react-icons/assets/TrophyFilledIcon.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/packages/react-icons/package.json b/packages/react-icons/package.json index 5cc089bdd9..c9bbee59fe 100644 --- a/packages/react-icons/package.json +++ b/packages/react-icons/package.json @@ -4,7 +4,7 @@ "main": "dist/index.cjs.js", "module": "dist/index.js", "typings": "dist/index.d.ts", - "version": "0.8.23", + "version": "0.8.24", "author": "100ms", "license": "MIT", "files": [ diff --git a/packages/react-icons/src/ClosedCaptionIcon.tsx b/packages/react-icons/src/ClosedCaptionIcon.tsx index 9f31ef7a16..c99ea444ab 100644 --- a/packages/react-icons/src/ClosedCaptionIcon.tsx +++ b/packages/react-icons/src/ClosedCaptionIcon.tsx @@ -1,11 +1,11 @@ import * as React from 'react'; import { SVGProps } from 'react'; const SvgClosedCaptionIcon = (props: SVGProps) => ( - - + ); diff --git a/packages/react-icons/src/OpenCaptionIcon.tsx b/packages/react-icons/src/OpenCaptionIcon.tsx new file mode 100644 index 0000000000..62fef59eb8 --- /dev/null +++ b/packages/react-icons/src/OpenCaptionIcon.tsx @@ -0,0 +1,13 @@ +import * as React from 'react'; +import { SVGProps } from 'react'; +const SvgOpenCaptionIcon = (props: SVGProps) => ( + + + + +); +export default SvgOpenCaptionIcon; diff --git a/packages/react-icons/src/QuizActiveIcon.tsx b/packages/react-icons/src/QuizActiveIcon.tsx new file mode 100644 index 0000000000..ab5504161f --- /dev/null +++ b/packages/react-icons/src/QuizActiveIcon.tsx @@ -0,0 +1,21 @@ +import * as React from 'react'; +import { SVGProps } from 'react'; +const SvgQuizActiveIcon = (props: SVGProps) => ( + + + + + +); +export default SvgQuizActiveIcon; diff --git a/packages/react-icons/src/TrophyFilledIcon.tsx b/packages/react-icons/src/TrophyFilledIcon.tsx new file mode 100644 index 0000000000..5eb84acaa3 --- /dev/null +++ b/packages/react-icons/src/TrophyFilledIcon.tsx @@ -0,0 +1,27 @@ +import * as React from 'react'; +import { SVGProps } from 'react'; +const SvgTrophyFilledIcon = (props: SVGProps) => ( + + + + + + + + + +); +export default SvgTrophyFilledIcon; diff --git a/packages/react-icons/src/index.ts b/packages/react-icons/src/index.ts index 6ef30f6ddf..0b9ae80e5e 100644 --- a/packages/react-icons/src/index.ts +++ b/packages/react-icons/src/index.ts @@ -167,6 +167,7 @@ export { default as NoEntryIcon } from './NoEntryIcon'; export { default as NotificationsIcon } from './NotificationsIcon'; export { default as OfferIcon } from './OfferIcon'; export { default as OpenBookIcon } from './OpenBookIcon'; +export { default as OpenCaptionIcon } from './OpenCaptionIcon'; export { default as PipIcon } from './PipIcon'; export { default as PadLockOnIcon } from './PadLockOnIcon'; export { default as PaletteIcon } from './PaletteIcon'; @@ -195,6 +196,7 @@ export { default as PrevIcon } from './PrevIcon'; export { default as QrCodeIcon } from './QrCodeIcon'; export { default as QuestionIcon } from './QuestionIcon'; export { default as QuestionMarkIcon } from './QuestionMarkIcon'; +export { default as QuizActiveIcon } from './QuizActiveIcon'; export { default as QuizIcon } from './QuizIcon'; export { default as RadioIcon } from './RadioIcon'; export { default as ReactIcon } from './ReactIcon'; @@ -241,6 +243,7 @@ export { default as ThumbsDownIcon } from './ThumbsDownIcon'; export { default as ThumbsUpIcon } from './ThumbsUpIcon'; export { default as TranscriptIcon } from './TranscriptIcon'; export { default as TrashIcon } from './TrashIcon'; +export { default as TrophyFilledIcon } from './TrophyFilledIcon'; export { default as TwitterIcon } from './TwitterIcon'; export { default as UnpinIcon } from './UnpinIcon'; export { default as UploadIcon } from './UploadIcon'; diff --git a/packages/react-sdk/package.json b/packages/react-sdk/package.json index 4d820d1e85..a3e9487fd3 100644 --- a/packages/react-sdk/package.json +++ b/packages/react-sdk/package.json @@ -4,7 +4,7 @@ "main": "dist/index.cjs.js", "module": "dist/index.js", "typings": "dist/index.d.ts", - "version": "0.8.23", + "version": "0.8.24", "author": "100ms", "license": "MIT", "files": [ @@ -43,7 +43,7 @@ "react": ">=16.8 <19.0.0" }, "dependencies": { - "@100mslive/hms-video-store": "0.10.23", + "@100mslive/hms-video-store": "0.10.24", "react-resize-detector": "^7.0.0", "zustand": "^3.6.2" } diff --git a/packages/roomkit-react/package.json b/packages/roomkit-react/package.json index 7e559f4969..45a62011a2 100644 --- a/packages/roomkit-react/package.json +++ b/packages/roomkit-react/package.json @@ -10,7 +10,7 @@ "prebuilt", "roomkit" ], - "version": "0.1.14", + "version": "0.1.15", "author": "100ms", "license": "MIT", "files": [ @@ -76,10 +76,10 @@ "react": ">=17.0.2 <19.0.0" }, "dependencies": { - "@100mslive/hls-player": "0.1.23", - "@100mslive/hms-virtual-background": "1.11.23", - "@100mslive/react-icons": "0.8.23", - "@100mslive/react-sdk": "0.8.23", + "@100mslive/hls-player": "0.1.24", + "@100mslive/hms-virtual-background": "1.11.24", + "@100mslive/react-icons": "0.8.24", + "@100mslive/react-sdk": "0.8.24", "@100mslive/types-prebuilt": "0.12.4", "@emoji-mart/data": "^1.0.6", "@emoji-mart/react": "^1.0.1", diff --git a/packages/roomkit-react/src/Prebuilt/common/PeersSorter.ts b/packages/roomkit-react/src/Prebuilt/common/PeersSorter.ts index 00c24de82b..6b6b34ebc7 100644 --- a/packages/roomkit-react/src/Prebuilt/common/PeersSorter.ts +++ b/packages/roomkit-react/src/Prebuilt/common/PeersSorter.ts @@ -17,6 +17,7 @@ class PeersSorter { } setPeersAndTilesPerPage = ({ peers, tilesPerPage }: { peers: HMSPeer[]; tilesPerPage: number }) => { + this.speaker = undefined; this.tilesPerPage = tilesPerPage; const peerIds = new Set(peers.map(peer => peer.id)); // remove existing peers which are no longer provided @@ -46,6 +47,8 @@ class PeersSorter { this.updateListeners(); this.listeners.clear(); this.storeUnsubscribe?.(); + this.storeUnsubscribe = undefined; + this.speaker = undefined; }; moveSpeakerToFront = (speaker?: HMSPeer) => { @@ -68,10 +71,16 @@ class PeersSorter { }; onDominantSpeakerChange = (speaker: HMSPeer | null) => { - if (speaker && speaker.id !== this?.speaker?.id) { - this.speaker = speaker; - this.moveSpeakerToFront(speaker); + // no speaker or is current speaker do nothing + if (!speaker || speaker.id === this.speaker?.id) { + return; + } + // if the active speaker is not from the peers passed ignore + if (!this.peers.has(speaker.id)) { + return; } + this.speaker = speaker; + this.moveSpeakerToFront(speaker); }; updateListeners = () => { diff --git a/packages/roomkit-react/src/Prebuilt/common/constants.ts b/packages/roomkit-react/src/Prebuilt/common/constants.ts index f6f67b60c6..5f1bbf3484 100644 --- a/packages/roomkit-react/src/Prebuilt/common/constants.ts +++ b/packages/roomkit-react/src/Prebuilt/common/constants.ts @@ -111,6 +111,7 @@ export enum SESSION_STORE_KEY { CHAT_PEER_BLACKLIST = 'chatPeerBlacklist', CHAT_MESSAGE_BLACKLIST = 'chatMessageBlacklist', CHAT_STATE = 'chatState', + SHARED_LEADERBOARDS = 'sharedLeaderboards', } export enum INTERACTION_TYPE { diff --git a/packages/roomkit-react/src/Prebuilt/common/utils.js b/packages/roomkit-react/src/Prebuilt/common/utils.js index 78219d318d..5300c15dd9 100644 --- a/packages/roomkit-react/src/Prebuilt/common/utils.js +++ b/packages/roomkit-react/src/Prebuilt/common/utils.js @@ -1,3 +1,4 @@ +import { isEqual } from 'lodash'; import { QUESTION_TYPE } from './constants'; // eslint-disable-next-line complexity @@ -137,3 +138,36 @@ export const calculateAvatarAndAttribBoxSize = (calculatedWidth, calculatedHeigh }; export const isMobileUserAgent = /Mobi|Android|iPhone/i.test(navigator.userAgent); + +export const getPeerResponses = (questions, peerid, userid) => { + return questions.map(question => + question.responses?.filter( + response => + ((response && response.peer?.peerid === peerid) || response.peer?.userid === userid) && !response.skipped, + ), + ); +}; + +export const getPeerParticipationSummary = (poll, localPeerID, localCustomerUserID) => { + let correctResponses = 0; + let score = 0; + const questions = poll.questions || []; + const peerResponses = getPeerResponses(questions, localPeerID, localCustomerUserID); + let totalResponses = peerResponses.length || 0; + + peerResponses.forEach(peerResponse => { + if (!peerResponse?.[0]) { + return; + } + const submission = [peerResponse[0].option] || peerResponse[0].options; + const answer = + [questions[peerResponse[0].questionIndex - 1].answer?.option] || + questions[peerResponse[0].questionIndex - 1].answer?.options; + const isCorrect = isEqual(submission, answer); + if (isCorrect) { + score += questions[peerResponse[0].questionIndex - 1]?.weight || 0; + correctResponses++; + } + }); + return { totalResponses, correctResponses, score }; +}; diff --git a/packages/roomkit-react/src/Prebuilt/components/Chat/Chat.jsx b/packages/roomkit-react/src/Prebuilt/components/Chat/Chat.jsx index 48b7b7bea5..48c9f43391 100644 --- a/packages/roomkit-react/src/Prebuilt/components/Chat/Chat.jsx +++ b/packages/roomkit-react/src/Prebuilt/components/Chat/Chat.jsx @@ -128,9 +128,8 @@ const NewMessageIndicator = ({ role, peerId, scrollToBottom }) => { }} icon css={{ - p: '$4', - pl: '$8', - pr: '$6', + p: '$3 $4', + pl: '$6', '& > svg': { ml: '$4' }, borderRadius: '$round', position: 'relative', @@ -141,7 +140,7 @@ const NewMessageIndicator = ({ role, peerId, scrollToBottom }) => { }} > New {unreadCount === 1 ? 'message' : 'messages'} - + ); diff --git a/packages/roomkit-react/src/Prebuilt/components/HMSVideo/HLSCaptionSelector.tsx b/packages/roomkit-react/src/Prebuilt/components/HMSVideo/HLSCaptionSelector.tsx new file mode 100644 index 0000000000..5c39026333 --- /dev/null +++ b/packages/roomkit-react/src/Prebuilt/components/HMSVideo/HLSCaptionSelector.tsx @@ -0,0 +1,13 @@ +import React from 'react'; +import { ClosedCaptionIcon, OpenCaptionIcon } from '@100mslive/react-icons'; +import { IconButton, Tooltip } from '../../../'; + +export function HLSCaptionSelector({ isEnabled, onClick }: { isEnabled: boolean; onClick: () => void }) { + return ( + + onClick()}> + {isEnabled ? : } + + + ); +} diff --git a/packages/roomkit-react/src/Prebuilt/components/HMSVideo/HMSVideo.jsx b/packages/roomkit-react/src/Prebuilt/components/HMSVideo/HMSVideo.jsx index f79bdb3aa4..fcd68de388 100644 --- a/packages/roomkit-react/src/Prebuilt/components/HMSVideo/HMSVideo.jsx +++ b/packages/roomkit-react/src/Prebuilt/components/HMSVideo/HMSVideo.jsx @@ -3,8 +3,40 @@ import { Flex } from '../../../'; export const HMSVideo = forwardRef(({ children, ...props }, videoRef) => { return ( - - + + {children} ); diff --git a/packages/roomkit-react/src/Prebuilt/components/Notifications/Notifications.tsx b/packages/roomkit-react/src/Prebuilt/components/Notifications/Notifications.tsx index 94c932ed33..0ef095d6fd 100644 --- a/packages/roomkit-react/src/Prebuilt/components/Notifications/Notifications.tsx +++ b/packages/roomkit-react/src/Prebuilt/components/Notifications/Notifications.tsx @@ -28,6 +28,7 @@ import { ReconnectNotifications } from './ReconnectNotifications'; import { TrackBulkUnmuteModal } from './TrackBulkUnmuteModal'; import { TrackNotifications } from './TrackNotifications'; import { TrackUnmuteModal } from './TrackUnmuteModal'; +import { useRoomLayoutConferencingScreen } from '../../provider/roomLayoutProvider/hooks/useRoomLayoutScreen'; // @ts-ignore: No implicit Any import { usePollViewToggle } from '../AppData/useSidepane'; // @ts-ignore: No implicit Any @@ -43,6 +44,7 @@ export function Notifications() { const roomState = useHMSStore(selectRoomState); const updateRoomLayoutForRole = useUpdateRoomLayout(); const isNotificationDisabled = useIsNotificationDisabled(); + const screenProps = useRoomLayoutConferencingScreen(); const vanillaStore = useHMSVanillaStore(); const togglePollView = usePollViewToggle(); @@ -53,7 +55,36 @@ export function Notifications() { }); }, []); + /* + const leaderboardResultsShared = useCallback( + (stringifiedPollDetails: string) => { + const pollDetails = JSON.parse(stringifiedPollDetails); + if (pollDetails.startedBy !== localPeerID) { + const pollStartedBy = pollDetails.initiatorName; + ToastManager.addToast({ + title: `${pollStartedBy} shared leaderboard for the quiz`, + action: ( + + ), + }); + } + }, + [localPeerID, togglePollView], + ); */ + useCustomEvent({ type: ROLE_CHANGE_DECLINED, onEvent: handleRoleChangeDenied }); + // useCustomEvent({ type: 'POLL_LEADERBOARD_SHARED', onEvent: leaderboardResultsShared }); useEffect(() => { if (!notification || isNotificationDisabled) { @@ -146,7 +177,7 @@ export function Notifications() { break; case HMSNotificationTypes.POLL_STARTED: - if (notification.data.startedBy !== localPeerID) { + if (notification.data.startedBy !== localPeerID && screenProps.screenType !== 'hls_live_streaming') { const pollStartedBy = vanillaStore.getState(selectPeerNameByID(notification.data.startedBy)) || 'Participant'; ToastManager.addToast({ title: `${pollStartedBy} started a ${notification.data.type}: ${notification.data.title}`, @@ -161,7 +192,7 @@ export function Notifications() { p: '$xs $md', }} > - Vote + {notification.data.type === 'quiz' ? 'Answer' : 'Vote'} ), }); diff --git a/packages/roomkit-react/src/Prebuilt/components/Polls/CreatePollQuiz/PollsQuizMenu.jsx b/packages/roomkit-react/src/Prebuilt/components/Polls/CreatePollQuiz/PollsQuizMenu.jsx index af27264a39..aa4995f33c 100644 --- a/packages/roomkit-react/src/Prebuilt/components/Polls/CreatePollQuiz/PollsQuizMenu.jsx +++ b/packages/roomkit-react/src/Prebuilt/components/Polls/CreatePollQuiz/PollsQuizMenu.jsx @@ -191,13 +191,7 @@ const PrevMenu = () => { {polls.map(poll => ( - 0} - /> + ))} @@ -205,7 +199,7 @@ const PrevMenu = () => { ) : null; }; -const InteractionCard = ({ id, title, isLive, isTimed }) => { +const InteractionCard = ({ id, title, isLive }) => { const { setPollState } = usePollViewState(); const goToVote = id => { @@ -221,7 +215,7 @@ const InteractionCard = ({ id, title, isLive, isTimed }) => { {title} - + )} {isQuiz ? ( - - setSkippable(checked)} /> - - Not required to answer - - + <> + + + Point Weightage + + + + + Allow to skip + + setSkippable(checked)} /> + + ) : null} ) : null} @@ -222,6 +243,7 @@ export const QuestionForm = ({ question, index, length, onSave, removeQuestion, options, skippable, draftID: question.draftID, + weight, }); }} > @@ -235,7 +257,7 @@ export const QuestionForm = ({ question, index, length, onSave, removeQuestion, ); }; -export const isValidQuestion = ({ text, type, options, isQuiz = false }) => { +export const isValidQuestion = ({ text, type, options, weight, isQuiz = false }) => { if (!isValidTextInput(text) || !type) { return false; } @@ -251,5 +273,10 @@ export const isValidQuestion = ({ text, type, options, isQuiz = false }) => { return everyOptionHasText; } + // The minimum acceptable value of weight is 1 + if (isQuiz && weight < 1) { + return false; + } + return everyOptionHasText && hasCorrectAnswer; }; diff --git a/packages/roomkit-react/src/Prebuilt/components/Polls/CreateQuestions/SavedQuestion.jsx b/packages/roomkit-react/src/Prebuilt/components/Polls/CreateQuestions/SavedQuestion.jsx index 9a53c6bb3b..dc4f097892 100644 --- a/packages/roomkit-react/src/Prebuilt/components/Polls/CreateQuestions/SavedQuestion.jsx +++ b/packages/roomkit-react/src/Prebuilt/components/Polls/CreateQuestions/SavedQuestion.jsx @@ -15,8 +15,8 @@ export const SavedQuestion = ({ question, index, length, convertToDraft, removeQ {question.text} - {question.options.map(option => ( - + {question.options.map((option, index) => ( + {option.text} diff --git a/packages/roomkit-react/src/Prebuilt/components/Polls/Polls.tsx b/packages/roomkit-react/src/Prebuilt/components/Polls/Polls.tsx index ca80fbe21f..c0c72c7de3 100644 --- a/packages/roomkit-react/src/Prebuilt/components/Polls/Polls.tsx +++ b/packages/roomkit-react/src/Prebuilt/components/Polls/Polls.tsx @@ -3,6 +3,7 @@ import React from 'react'; import { PollsQuizMenu } from './CreatePollQuiz/PollsQuizMenu'; // @ts-ignore: No implicit Any import { CreateQuestions } from './CreateQuestions/CreateQuestions'; +import { Leaderboard } from './Voting/Leaderboard'; // @ts-ignore: No implicit Any import { Voting } from './Voting/Voting'; // @ts-ignore: No implicit Any @@ -22,6 +23,8 @@ export const Polls = () => { return ; } else if (view === POLL_VIEWS.VOTE) { return ; + } else if (view === POLL_VIEWS.RESULTS) { + return ; } else { return null; } diff --git a/packages/roomkit-react/src/Prebuilt/components/Polls/Voting/Leaderboard.tsx b/packages/roomkit-react/src/Prebuilt/components/Polls/Voting/Leaderboard.tsx new file mode 100644 index 0000000000..2c07b84cbc --- /dev/null +++ b/packages/roomkit-react/src/Prebuilt/components/Polls/Voting/Leaderboard.tsx @@ -0,0 +1,115 @@ +import React, { useEffect, useState } from 'react'; +import { HMSPollLeaderboardResponse, selectPollByID, useHMSActions, useHMSStore } from '@100mslive/react-sdk'; +import { ChevronLeftIcon, CrossIcon } from '@100mslive/react-icons'; +import { Box, Flex } from '../../../../Layout'; +import { Loading } from '../../../../Loading'; +import { Text } from '../../../../Text'; +import { LeaderboardEntry } from './LeaderboardEntry'; +// @ts-ignore +import { useSidepaneToggle } from '../../AppData/useSidepane'; +// @ts-ignore +import { usePollViewState } from '../../AppData/useUISettings'; +// @ts-ignore +import { StatusIndicator } from '../common/StatusIndicator'; +import { POLL_VIEWS } from '../../../common/constants'; + +export const Leaderboard = ({ pollID }: { pollID: string }) => { + const hmsActions = useHMSActions(); + const poll = useHMSStore(selectPollByID(pollID)); + const [pollLeaderboard, setPollLeaderboard] = useState(); + const { setPollView } = usePollViewState(); + const toggleSidepane = useSidepaneToggle(); + + /* + const sharedLeaderboardRef = useRef(false); + const sharedLeaderboards = useHMSStore(selectSessionStore(SESSION_STORE_KEY.SHARED_LEADERBOARDS)); + const { sendEvent } = useCustomEvent({ + type: HMSNotificationTypes.POLL_LEADERBOARD_SHARED, + onEvent: () => { + return; + }, + }); + */ + + useEffect(() => { + const fetchLeaderboardData = async () => { + if (poll) { + const leaderboardData = await hmsActions.interactivityCenter.fetchLeaderboard(poll, 0, 50); + setPollLeaderboard(leaderboardData); + } + }; + fetchLeaderboardData(); + }, [poll, hmsActions.interactivityCenter]); + + if (!poll || !pollLeaderboard) + return ( + + + + ); + const maxPossibleScore = poll.questions?.reduce((total, question) => (total += question.weight || 0), 0) || 0; + const questionCount = poll.questions?.length || 0; + + return ( + + + + setPollView(POLL_VIEWS.VOTE)} + > + + + + {poll.title} + + + + + + + + + Leaderboard + + + Based on score and time taken to cast the correct answer + + + {pollLeaderboard?.entries && + pollLeaderboard.entries.map(question => ( + + ))} + + + {/* {!sharedLeaderboardRef.current ? ( + + ) : null} */} + + ); +}; diff --git a/packages/roomkit-react/src/Prebuilt/components/Polls/Voting/LeaderboardEntry.tsx b/packages/roomkit-react/src/Prebuilt/components/Polls/Voting/LeaderboardEntry.tsx new file mode 100644 index 0000000000..0dc8f26729 --- /dev/null +++ b/packages/roomkit-react/src/Prebuilt/components/Polls/Voting/LeaderboardEntry.tsx @@ -0,0 +1,63 @@ +import React from 'react'; +import { CheckCircleIcon, TrophyFilledIcon } from '@100mslive/react-icons'; +import { Box, Flex } from '../../../../Layout'; +import { Text } from '../../../../Text'; + +const positionColorMap: Record = { 1: '#D69516', 2: '#3E3E3E', 3: '#583B0F' }; + +export const LeaderboardEntry = ({ + position, + score, + questionCount, + correctResponses, + userName, + maxPossibleScore, +}: { + position: number; + score: number; + questionCount: number; + correctResponses: number; + userName: string; + maxPossibleScore: number; +}) => { + return ( + + + 3 ? '$on_surface_low' : '#FFF', + fontSize: '$xs', + fontWeight: '$semiBold', + }} + > + {position} + + + + + {userName} + + + + {score}/{maxPossibleScore} points + + + + + {position === 1 ? : null} + + {questionCount ? ( + + {correctResponses}/{questionCount} + + ) : null} + + + ); +}; diff --git a/packages/roomkit-react/src/Prebuilt/components/Polls/Voting/PeerParticipationSummary.tsx b/packages/roomkit-react/src/Prebuilt/components/Polls/Voting/PeerParticipationSummary.tsx new file mode 100644 index 0000000000..f59ff10d61 --- /dev/null +++ b/packages/roomkit-react/src/Prebuilt/components/Polls/Voting/PeerParticipationSummary.tsx @@ -0,0 +1,38 @@ +import React from 'react'; +import { HMSPoll, selectLocalPeer, useHMSStore } from '@100mslive/react-sdk'; +import { Box } from '../../../../Layout'; +import { Text } from '../../../../Text'; +// @ts-ignore +import { getPeerParticipationSummary } from '../../../common/utils'; + +export const PeerParticipationSummary = ({ poll }: { poll: HMSPoll }) => { + const localPeer = useHMSStore(selectLocalPeer); + const { totalResponses, correctResponses, score } = getPeerParticipationSummary( + poll, + localPeer?.id, + localPeer?.customerUserId, + ); + + const boxes = [ + { title: 'Points', value: score }, + { title: 'Correct Answers', value: `${correctResponses}/${totalResponses}` }, + ]; + return ( + + Participation Summary + + {boxes.map(box => ( + + + {box.title} + + {box.value} + + ))} + + + ); +}; diff --git a/packages/roomkit-react/src/Prebuilt/components/Polls/Voting/QuestionCard.jsx b/packages/roomkit-react/src/Prebuilt/components/Polls/Voting/QuestionCard.jsx index 95ef65c68c..da8d80fac2 100644 --- a/packages/roomkit-react/src/Prebuilt/components/Polls/Voting/QuestionCard.jsx +++ b/packages/roomkit-react/src/Prebuilt/components/Polls/Voting/QuestionCard.jsx @@ -1,7 +1,7 @@ // @ts-check import React, { useCallback, useMemo, useState } from 'react'; import { selectLocalPeer, selectLocalPeerRoleName, useHMSActions, useHMSStore } from '@100mslive/react-sdk'; -import { ChevronLeftIcon, ChevronRightIcon } from '@100mslive/react-icons'; +import { CheckCircleIcon, ChevronLeftIcon, ChevronRightIcon, CrossCircleIcon } from '@100mslive/react-icons'; import { Box, Button, Flex, IconButton, Input, styled, Text } from '../../../../'; import { checkCorrectAnswer } from '../../../common/utils'; import { MultipleChoiceOptions } from '../common/MultipleChoiceOptions'; @@ -48,7 +48,8 @@ export const QuestionCard = ({ !rolesThatCanViewResponses || rolesThatCanViewResponses.length === 0 || rolesThatCanViewResponses.includes(localPeerRoleName || ''); - const showVoteCount = roleCanViewResponse && (localPeerResponse || (isLocalPeerCreator && pollState === 'stopped')); + const showVoteCount = + roleCanViewResponse && (localPeerResponse || (isLocalPeerCreator && pollState === 'stopped')) && !isQuiz; const isLive = pollState === 'started'; const canRespond = isLive && !localPeerResponse; @@ -72,6 +73,8 @@ export const QuestionCard = ({ const stringAnswerExpected = [QUESTION_TYPE.LONG_ANSWER, QUESTION_TYPE.SHORT_ANSWER].includes(type); + const respondedToQuiz = isQuiz && localPeerResponse && !localPeerResponse.skipped; + const isValidVote = useMemo(() => { if (stringAnswerExpected) { return textAnswer.length > 0; @@ -113,14 +116,22 @@ export const QuestionCard = ({ borderRadius: '$1', p: '$md', mt: '$md', - border: - isQuiz && localPeerResponse && !localPeerResponse.skipped - ? `1px solid ${isCorrectAnswer ? '$alert_success' : '$alert_error_default'}` - : 'none', + border: respondedToQuiz ? `1px solid ${isCorrectAnswer ? '$alert_success' : '$alert_error_default'}` : 'none', }} > - + + {respondedToQuiz && isCorrectAnswer ? : null} + {respondedToQuiz && !isCorrectAnswer ? : null} QUESTION {index} OF {totalQuestions}: {type.toUpperCase()} @@ -196,6 +207,8 @@ export const QuestionCard = ({ setAnswer={setSingleOptionAnswer} totalResponses={result?.totalResponses} showVoteCount={showVoteCount} + localPeerResponse={localPeerResponse} + isStopped={pollState === 'stopped'} /> ) : null} @@ -211,6 +224,8 @@ export const QuestionCard = ({ setSelectedOptions={setMultipleOptionAnswer} totalResponses={result?.totalResponses} showVoteCount={showVoteCount} + localPeerResponse={localPeerResponse} + isStopped={pollState === 'stopped'} /> ) : null} @@ -221,14 +236,14 @@ export const QuestionCard = ({ onSkip={handleSkip} onVote={handleVote} response={localPeerResponse} - stringAnswerExpected={stringAnswerExpected} + isQuiz={isQuiz} /> )} ); }; -const QuestionActions = ({ isValidVote, skippable, response, stringAnswerExpected, onVote, onSkip }) => { +const QuestionActions = ({ isValidVote, skippable, response, isQuiz, onVote, onSkip }) => { return ( {skippable && !response ? ( @@ -239,11 +254,13 @@ const QuestionActions = ({ isValidVote, skippable, response, stringAnswerExpecte {response ? ( - {response.skipped ? 'Skipped' : stringAnswerExpected ? 'Submitted' : 'Voted'} + {response.skipped ? 'Skipped' : null} + {isQuiz && !response.skipped ? 'Answered' : null} + {!isQuiz && !response.skipped ? 'Voted' : null} ) : ( )} diff --git a/packages/roomkit-react/src/Prebuilt/components/Polls/Voting/StandardVoting.jsx b/packages/roomkit-react/src/Prebuilt/components/Polls/Voting/StandardVoting.jsx index 4721b90dc8..0a623fbb60 100644 --- a/packages/roomkit-react/src/Prebuilt/components/Polls/Voting/StandardVoting.jsx +++ b/packages/roomkit-react/src/Prebuilt/components/Polls/Voting/StandardVoting.jsx @@ -1,5 +1,6 @@ // @ts-check import React from 'react'; +import { PeerParticipationSummary } from './PeerParticipationSummary'; import { QuestionCard } from './QuestionCard'; /** @@ -11,12 +12,17 @@ export const StandardView = ({ poll }) => { if (!poll?.questions) { return null; } + + const isQuiz = poll.type === 'quiz'; + const isStopped = poll.state === 'stopped'; + return ( <> + {isQuiz && isStopped ? : null} {poll.questions?.map((question, index) => ( { const pollCreatorName = useHMSStore(selectPeerNameByID(poll?.createdBy)); const isLocalPeerCreator = useHMSStore(selectLocalPeerID) === poll?.createdBy; const { setPollView } = usePollViewState(); + const permissions = useHMSStore(selectPermissions); + + // const sharedLeaderboards = useHMSStore(selectSessionStore(SESSION_STORE_KEY.SHARED_LEADERBOARDS)); if (!poll) { return null; } + // const isLeaderboardShared = (sharedLeaderboards || []).includes(id); + const canViewLeaderboard = + poll.type === 'quiz' && poll.state === 'stopped' && !poll.anonymous && permissions?.pollWrite; + // Sets view - linear or vertical, toggles timer indicator const isTimed = (poll.duration || 0) > 0; const isLive = poll.state === 'started'; @@ -52,8 +60,8 @@ export const Voting = ({ id, toggleVoting }) => { > - {poll?.type?.toUpperCase()} - + {poll.title} + { {pollCreatorName || 'Participant'} started a {poll.type} - {poll.state === 'started' && isLocalPeerCreator && ( - - - - )} + {/* {poll.state === "stopped" && ( { isAdmin={isLocalPeerCreator} /> )} */} + {isTimed ? : } + + {poll.state === 'started' && isLocalPeerCreator && ( + + )} + + {canViewLeaderboard ? ( + + ) : null} ); diff --git a/packages/roomkit-react/src/Prebuilt/components/Polls/common/MultipleChoiceOptions.jsx b/packages/roomkit-react/src/Prebuilt/components/Polls/common/MultipleChoiceOptions.jsx index d109dcb300..0e218fc626 100644 --- a/packages/roomkit-react/src/Prebuilt/components/Polls/common/MultipleChoiceOptions.jsx +++ b/packages/roomkit-react/src/Prebuilt/components/Polls/common/MultipleChoiceOptions.jsx @@ -1,6 +1,6 @@ // @ts-check import React from 'react'; -import { CheckIcon } from '@100mslive/react-icons'; +import { CheckCircleIcon, CheckIcon } from '@100mslive/react-icons'; import { Checkbox, Flex, Label, Text } from '../../../../'; import { OptionInputWithDelete } from './OptionInputWithDelete'; import { VoteCount } from './VoteCount'; @@ -8,15 +8,17 @@ import { VoteProgress } from './VoteProgress'; export const MultipleChoiceOptions = ({ questionIndex, - isQuiz, options, - correctOptionIndexes, canRespond, response, totalResponses, selectedOptions, setSelectedOptions, showVoteCount, + isQuiz, + correctOptionIndexes, + localPeerResponse, + isStopped, }) => { const handleCheckedChange = (checked, index) => { const newSelected = new Set(selectedOptions); @@ -31,35 +33,45 @@ export const MultipleChoiceOptions = ({ return ( {options.map(option => { - const isCorrectAnswer = isQuiz && correctOptionIndexes?.includes(option.index); - return ( - handleCheckedChange(checked, option.index)} - css={{ - cursor: canRespond ? 'pointer' : 'not-allowed', - }} - > - - - - + {!isStopped || !isQuiz ? ( + handleCheckedChange(checked, option.index)} + css={{ + cursor: canRespond ? 'pointer' : 'not-allowed', + }} + > + + + + + ) : null} + + {isStopped && correctOptionIndexes.includes(option.index) ? ( + + + + ) : null} - {showVoteCount && ( - - )} + {showVoteCount && } {showVoteCount && } + + {isStopped && isQuiz && localPeerResponse?.options.includes(option.index) ? ( + + Your Answer + + ) : null} ); })} diff --git a/packages/roomkit-react/src/Prebuilt/components/Polls/common/SingleChoiceOptions.jsx b/packages/roomkit-react/src/Prebuilt/components/Polls/common/SingleChoiceOptions.jsx index 86ca58ea42..3a38350423 100644 --- a/packages/roomkit-react/src/Prebuilt/components/Polls/common/SingleChoiceOptions.jsx +++ b/packages/roomkit-react/src/Prebuilt/components/Polls/common/SingleChoiceOptions.jsx @@ -1,5 +1,6 @@ // @ts-check import React from 'react'; +import { CheckCircleIcon } from '@100mslive/react-icons'; import { Flex, Label, RadioGroup, Text } from '../../../../'; import { OptionInputWithDelete } from './OptionInputWithDelete'; import { VoteCount } from './VoteCount'; @@ -7,64 +8,75 @@ import { VoteProgress } from './VoteProgress'; export const SingleChoiceOptions = ({ questionIndex, - isQuiz, options, response, canRespond, - correctOptionIndex, setAnswer, totalResponses, showVoteCount, + correctOptionIndex, + isStopped, + isQuiz, + localPeerResponse, }) => { return ( setAnswer(value)}> {options.map(option => { - const isCorrectAnswer = isQuiz && option.index === correctOptionIndex; - return ( - - - + {!isStopped || !isQuiz ? ( + - + disabled={!canRespond} + value={option.index} + id={`${questionIndex}-${option.index}`} + > + + + ) : null} + + {isStopped && correctOptionIndex === option.index && isQuiz ? ( + + + + ) : null} - {showVoteCount && ( - - )} + {showVoteCount && } {showVoteCount && } + {isStopped && isQuiz && localPeerResponse?.option === option.index ? ( + + Your Answer + + ) : null} ); })} diff --git a/packages/roomkit-react/src/Prebuilt/components/Polls/common/StatusIndicator.jsx b/packages/roomkit-react/src/Prebuilt/components/Polls/common/StatusIndicator.jsx index 0996bc527f..5ba5e288f3 100644 --- a/packages/roomkit-react/src/Prebuilt/components/Polls/common/StatusIndicator.jsx +++ b/packages/roomkit-react/src/Prebuilt/components/Polls/common/StatusIndicator.jsx @@ -2,14 +2,14 @@ import React from 'react'; import { Flex, Text } from '../../../../'; -export const StatusIndicator = ({ isLive, shouldShowTimer }) => { +export const StatusIndicator = ({ isLive }) => { return ( { {isLive ? 'LIVE' : 'ENDED'} - - {shouldShowTimer ? ( - - - 0:32 - - - ) : null} ); }; diff --git a/packages/roomkit-react/src/Prebuilt/components/Polls/common/VoteCount.jsx b/packages/roomkit-react/src/Prebuilt/components/Polls/common/VoteCount.jsx index c6f2f8ebd1..42ec48c8ba 100644 --- a/packages/roomkit-react/src/Prebuilt/components/Polls/common/VoteCount.jsx +++ b/packages/roomkit-react/src/Prebuilt/components/Polls/common/VoteCount.jsx @@ -2,23 +2,9 @@ import React from 'react'; import { Flex, Text } from '../../../../'; -export const VoteCount = ({ isQuiz, voteCount, isCorrectAnswer }) => { +export const VoteCount = ({ voteCount }) => { return ( - {isQuiz && ( - - {isCorrectAnswer ? 'Correct' : 'Incorrect'} - - )} {voteCount ? ( {voteCount}  diff --git a/packages/roomkit-react/src/Prebuilt/components/VideoLayouts/EqualProminence.tsx b/packages/roomkit-react/src/Prebuilt/components/VideoLayouts/EqualProminence.tsx index 997b3477c0..d412d7295a 100644 --- a/packages/roomkit-react/src/Prebuilt/components/VideoLayouts/EqualProminence.tsx +++ b/packages/roomkit-react/src/Prebuilt/components/VideoLayouts/EqualProminence.tsx @@ -10,7 +10,6 @@ import { LayoutProps } from './interface'; // @ts-ignore: No implicit Any import { useUISettings } from '../AppData/useUISettings'; import { usePagesWithTiles, useTileLayout } from '../hooks/useTileLayout'; -// @ts-ignore: No implicit Any import { UI_SETTINGS } from '../../common/constants'; export function EqualProminence({ isInsetEnabled = false, peers, onPageChange, onPageSize, edgeToEdge }: LayoutProps) { @@ -23,10 +22,12 @@ export function EqualProminence({ isInsetEnabled = false, peers, onPageChange, o maxTileCount, }); // useMemo is needed to prevent recursion as new array is created for localPeer - const inputPeers = useMemo( - () => (pageList.length === 0 ? (localPeer ? [localPeer] : []) : peers), - [pageList.length, peers, localPeer], - ); + const inputPeers = useMemo(() => { + if (pageList.length === 0) { + return localPeer ? [localPeer] : []; + } + return peers; + }, [pageList.length, peers, localPeer]); // Pass local peer to main grid if no other peer has tiles pageList = usePagesWithTiles({ peers: inputPeers, diff --git a/packages/roomkit-react/src/Prebuilt/components/VideoLayouts/GridLayout.tsx b/packages/roomkit-react/src/Prebuilt/components/VideoLayouts/GridLayout.tsx index 4306bb744b..95856e9687 100644 --- a/packages/roomkit-react/src/Prebuilt/components/VideoLayouts/GridLayout.tsx +++ b/packages/roomkit-react/src/Prebuilt/components/VideoLayouts/GridLayout.tsx @@ -1,13 +1,20 @@ import React, { useEffect, useMemo, useState } from 'react'; import { GridVideoTileLayout } from '@100mslive/types-prebuilt/elements/video_tile_layout'; -import { selectPeers, selectPeerScreenSharing, useHMSStore, useHMSVanillaStore } from '@100mslive/react-sdk'; +import { + selectLocalPeerRoleName, + selectPeers, + selectPeerScreenSharing, + useHMSStore, + useHMSVanillaStore, +} from '@100mslive/react-sdk'; import { EqualProminence } from './EqualProminence'; import { RoleProminence } from './RoleProminence'; import { ScreenshareLayout } from './ScreenshareLayout'; // @ts-ignore: No implicit Any -import { usePinnedTrack } from '../AppData/useUISettings'; +import { usePinnedTrack, useSetAppDataByKey } from '../AppData/useUISettings'; import { VideoTileContext } from '../hooks/useVideoTileLayout'; import PeersSorter from '../../common/PeersSorter'; +import { APP_DATA } from '../../common/constants'; export type TileCustomisationProps = { hide_participant_name_on_tile: boolean; @@ -34,6 +41,8 @@ export const GridLayout = ({ const peerSharing = useHMSStore(selectPeerScreenSharing); const pinnedTrack = usePinnedTrack(); const peers = useHMSStore(selectPeers); + const localPeerRole = useHMSStore(selectLocalPeerRoleName); + const [activeScreensharePeerId] = useSetAppDataByKey(APP_DATA.activeScreensharePeerId); const isRoleProminence = (prominentRoles.length && peers.some( @@ -41,11 +50,20 @@ export const GridLayout = ({ )) || pinnedTrack; const updatedPeers = useMemo(() => { - if (isInsetEnabled && !isRoleProminence && !peerSharing) { - return peers.filter(peer => !peer.isLocal); + // remove screenshare peer from active speaker sorting + if (activeScreensharePeerId) { + return peers.filter(peer => peer.id !== activeScreensharePeerId); + } + if (isInsetEnabled) { + // if localPeer role is prominent role, it shows up in the center, so allow it in active speaker sorting + if (localPeerRole && prominentRoles.includes(localPeerRole)) { + return peers; + } else { + return peers.filter(peer => !peer.isLocal); + } } return peers; - }, [isInsetEnabled, isRoleProminence, peerSharing, peers]); + }, [isInsetEnabled, activeScreensharePeerId, localPeerRole, prominentRoles, peers]); const vanillaStore = useHMSVanillaStore(); const [sortedPeers, setSortedPeers] = useState(updatedPeers); const peersSorter = useMemo(() => new PeersSorter(vanillaStore), [vanillaStore]); @@ -61,7 +79,8 @@ export const GridLayout = ({ }; useEffect(() => { - if (mainPage !== 0) { + if (mainPage !== 0 || pageSize === 0) { + setSortedPeers(updatedPeers); return; } peersSorter.setPeersAndTilesPerPage({ diff --git a/packages/roomkit-react/src/Prebuilt/components/VideoLayouts/ScreenshareLayout.tsx b/packages/roomkit-react/src/Prebuilt/components/VideoLayouts/ScreenshareLayout.tsx index 83fc74484d..62a88aab7b 100644 --- a/packages/roomkit-react/src/Prebuilt/components/VideoLayouts/ScreenshareLayout.tsx +++ b/packages/roomkit-react/src/Prebuilt/components/VideoLayouts/ScreenshareLayout.tsx @@ -10,7 +10,6 @@ import { LayoutProps } from './interface'; import { ProminenceLayout } from './ProminenceLayout'; // @ts-ignore: No implicit Any import { useSetAppDataByKey } from '../AppData/useUISettings'; -// @ts-ignore: No implicit Any import { APP_DATA } from '../../common/constants'; export const ScreenshareLayout = ({ peers, onPageChange, onPageSize, edgeToEdge }: LayoutProps) => { diff --git a/packages/roomkit-react/src/Prebuilt/layouts/HLSView.jsx b/packages/roomkit-react/src/Prebuilt/layouts/HLSView.jsx index b6172a281c..9a0a6e0c3b 100644 --- a/packages/roomkit-react/src/Prebuilt/layouts/HLSView.jsx +++ b/packages/roomkit-react/src/Prebuilt/layouts/HLSView.jsx @@ -2,20 +2,31 @@ import React, { useCallback, useEffect, useRef, useState } from 'react'; import { useFullscreen, useMedia, usePrevious, useToggle } from 'react-use'; import { HLSPlaybackState, HMSHLSPlayer, HMSHLSPlayerEvents } from '@100mslive/hls-player'; import screenfull from 'screenfull'; -import { selectAppData, selectHLSState, useHMSActions, useHMSStore } from '@100mslive/react-sdk'; +import { + selectAppData, + selectHLSState, + selectPeerNameByID, + selectPollByID, + useHMSActions, + useHMSStore, + useHMSVanillaStore, +} from '@100mslive/react-sdk'; import { ColoredHandIcon, ExpandIcon, PlayIcon, RadioIcon, ShrinkIcon } from '@100mslive/react-icons'; import { HlsStatsOverlay } from '../components/HlsStatsOverlay'; import { HMSVideoPlayer } from '../components/HMSVideo'; import { FullScreenButton } from '../components/HMSVideo/FullscreenButton'; import { HLSAutoplayBlockedPrompt } from '../components/HMSVideo/HLSAutoplayBlockedPrompt'; +import { HLSCaptionSelector } from '../components/HMSVideo/HLSCaptionSelector'; import { HLSQualitySelector } from '../components/HMSVideo/HLSQualitySelector'; import { ToastManager } from '../components/Toast/ToastManager'; +import { Button } from '../../Button'; import { IconButton } from '../../IconButton'; import { Box, Flex } from '../../Layout'; import { Loading } from '../../Loading'; import { Text } from '../../Text'; import { config, useTheme } from '../../Theme'; import { Tooltip } from '../../Tooltip'; +import { usePollViewToggle } from '../components/AppData/useSidepane'; import { APP_DATA, EMOJI_REACTION_TYPE } from '../common/constants'; let hlsPlayer; @@ -33,6 +44,8 @@ const HLSView = () => { const [availableLayers, setAvailableLayers] = useState([]); const [isVideoLive, setIsVideoLive] = useState(true); const [isUserSelectedAuto, setIsUserSelectedAuto] = useState(true); + const [isCaptionEnabled, setIsCaptionEnabled] = useState(true); + const [hasCaptions, setHasCaptions] = useState(false); const [currentSelectedQuality, setCurrentSelectedQuality] = useState(null); const [isHlsAutoplayBlocked, setIsHlsAutoplayBlocked] = useState(false); const [isPaused, setIsPaused] = useState(false); @@ -43,13 +56,14 @@ const HLSView = () => { const controlsTimerRef = useRef(); const [qualityDropDownOpen, setQualityDropDownOpen] = useState(false); const lastHlsUrl = usePrevious(hlsUrl); + const togglePollView = usePollViewToggle(); + const vanillaStore = useHMSVanillaStore(); const isMobile = useMedia(config.media.md); const isFullScreen = useFullscreen(hlsViewRef, show, { onClose: () => toggle(false), }); const [showLoader, setShowLoader] = useState(false); - // FIXME: move this logic to player controller in next release useEffect(() => { /** @@ -91,6 +105,7 @@ const HLSView = () => { let videoEl = videoRef.current; const manifestLoadedHandler = ({ layers }) => { setAvailableLayers(layers); + setHasCaptions(hlsPlayer?.hasCaptions()); }; const layerUpdatedHandler = ({ layer }) => { setCurrentSelectedQuality(layer); @@ -103,9 +118,33 @@ const HLSView = () => { return str; } }; - // parse payload and extract start_time and payload const duration = rest.duration; const parsedPayload = parsePayload(payload); + // check if poll happened + if (parsedPayload.startsWith('poll:')) { + const pollId = parsedPayload.substr(parsedPayload.indexOf(':') + 1); + const poll = vanillaStore.getState(selectPollByID(pollId)); + const pollStartedBy = vanillaStore.getState(selectPeerNameByID(poll.startedBy)) || 'Participant'; + // launch poll + ToastManager.addToast({ + title: `${pollStartedBy} started a ${poll.type}: ${poll.title}`, + action: ( + + ), + }); + return; + } switch (parsedPayload.type) { case EMOJI_REACTION_TYPE: window.showFlyingEmoji?.({ emojiId: parsedPayload?.emojiId, senderId: parsedPayload?.senderId }); @@ -129,6 +168,9 @@ const HLSView = () => { }; const playbackEventHandler = data => setIsPaused(data.state === HLSPlaybackState.paused); + const captionEnabledEventHandler = isCaptionEnabled => { + setIsCaptionEnabled(isCaptionEnabled); + }; const handleAutoplayBlock = data => setIsHlsAutoplayBlocked(!!data); if (videoEl && hlsUrl) { @@ -137,6 +179,7 @@ const HLSView = () => { hlsPlayer.on(HMSHLSPlayerEvents.TIMED_METADATA_LOADED, metadataLoadedHandler); hlsPlayer.on(HMSHLSPlayerEvents.ERROR, handleError); hlsPlayer.on(HMSHLSPlayerEvents.PLAYBACK_STATE, playbackEventHandler); + hlsPlayer.on(HMSHLSPlayerEvents.CAPTION_ENABLED, captionEnabledEventHandler); hlsPlayer.on(HMSHLSPlayerEvents.AUTOPLAY_BLOCKED, handleAutoplayBlock); hlsPlayer.on(HMSHLSPlayerEvents.MANIFEST_LOADED, manifestLoadedHandler); @@ -146,6 +189,8 @@ const HLSView = () => { hlsPlayer.off(HMSHLSPlayerEvents.ERROR, handleError); hlsPlayer.off(HMSHLSPlayerEvents.TIMED_METADATA_LOADED, metadataLoadedHandler); hlsPlayer.off(HMSHLSPlayerEvents.PLAYBACK_STATE, playbackEventHandler); + hlsPlayer.off(HMSHLSPlayerEvents.CAPTION_ENABLED, captionEnabledEventHandler); + hlsPlayer.off(HMSHLSPlayerEvents.AUTOPLAY_BLOCKED, handleAutoplayBlock); hlsPlayer.off(HMSHLSPlayerEvents.MANIFEST_LOADED, manifestLoadedHandler); hlsPlayer.off(HMSHLSPlayerEvents.LAYER_UPDATED, layerUpdatedHandler); @@ -360,6 +405,9 @@ const HLSView = () => { + {hasCaptions && ( + hlsPlayer?.toggleCaption()} isEnabled={isCaptionEnabled} /> + )} {availableLayers.length > 0 ? (