@100mslive/roomkit-react 0.1.14 → 0.1.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/dist/{HLSView-662T7R7H.js → HLSView-EMUOLCTM.js} +128 -39
  2. package/dist/HLSView-EMUOLCTM.js.map +7 -0
  3. package/dist/Prebuilt/common/PeersSorter.d.ts +1 -0
  4. package/dist/Prebuilt/common/constants.d.ts +9 -5
  5. package/dist/Prebuilt/common/hooks.d.ts +1 -0
  6. package/dist/Prebuilt/components/Footer/ParticipantList.d.ts +17 -0
  7. package/dist/Prebuilt/components/Footer/RoleAccordion.d.ts +3 -2
  8. package/dist/Prebuilt/components/Footer/WhiteboardToggle.d.ts +2 -0
  9. package/dist/Prebuilt/components/HMSVideo/HLSCaptionSelector.d.ts +5 -0
  10. package/dist/Prebuilt/components/Notifications/HandRaisedNotifications.d.ts +1 -0
  11. package/dist/Prebuilt/components/Polls/Voting/Leaderboard.d.ts +4 -0
  12. package/dist/Prebuilt/components/Polls/Voting/LeaderboardEntry.d.ts +9 -0
  13. package/dist/Prebuilt/components/Polls/Voting/PeerParticipationSummary.d.ts +5 -0
  14. package/dist/Prebuilt/components/PreviousRoleInMetadata.d.ts +1 -0
  15. package/dist/Prebuilt/components/RemoveParticipant.d.ts +5 -0
  16. package/dist/Prebuilt/components/hooks/useCloseScreenshareWhiteboard.d.ts +4 -0
  17. package/dist/Prebuilt/layouts/WhiteboardView.d.ts +2 -0
  18. package/dist/{chunk-2B7YYNHQ.js → chunk-ZYR4B4KQ.js} +2240 -1767
  19. package/dist/chunk-ZYR4B4KQ.js.map +7 -0
  20. package/dist/index.cjs.js +2805 -2172
  21. package/dist/index.cjs.js.map +4 -4
  22. package/dist/index.js +1 -1
  23. package/dist/meta.cjs.json +739 -177
  24. package/dist/meta.esbuild.json +749 -186
  25. package/package.json +7 -7
  26. package/src/Prebuilt/AppStateContext.tsx +1 -1
  27. package/src/Prebuilt/common/PeersSorter.ts +24 -8
  28. package/src/Prebuilt/common/constants.ts +6 -6
  29. package/src/Prebuilt/common/hooks.ts +16 -0
  30. package/src/Prebuilt/common/utils.js +33 -0
  31. package/src/Prebuilt/components/AppData/AppData.tsx +1 -16
  32. package/src/Prebuilt/components/Chat/Chat.jsx +10 -34
  33. package/src/Prebuilt/components/Chat/ChatBody.jsx +107 -66
  34. package/src/Prebuilt/components/Chat/ChatFooter.tsx +21 -12
  35. package/src/Prebuilt/components/Chat/ChatSelector.tsx +25 -25
  36. package/src/Prebuilt/components/Chat/ChatSelectorContainer.tsx +15 -16
  37. package/src/Prebuilt/components/Chat/PinnedMessage.tsx +7 -2
  38. package/src/Prebuilt/components/ConferenceScreen.tsx +2 -0
  39. package/src/Prebuilt/components/Footer/ChatToggle.tsx +30 -7
  40. package/src/Prebuilt/components/Footer/Footer.tsx +2 -1
  41. package/src/Prebuilt/components/Footer/PaginatedParticipants.tsx +0 -1
  42. package/src/Prebuilt/components/Footer/{ParticipantList.jsx → ParticipantList.tsx} +169 -127
  43. package/src/Prebuilt/components/Footer/RoleAccordion.tsx +23 -13
  44. package/src/Prebuilt/components/Footer/WhiteboardToggle.tsx +34 -0
  45. package/src/Prebuilt/components/HMSVideo/HLSCaptionSelector.tsx +13 -0
  46. package/src/Prebuilt/components/HMSVideo/HMSVideo.jsx +34 -2
  47. package/src/Prebuilt/components/Notifications/HandRaisedNotifications.tsx +35 -0
  48. package/src/Prebuilt/components/Notifications/Notifications.tsx +47 -14
  49. package/src/Prebuilt/components/Notifications/PeerNotifications.tsx +7 -2
  50. package/src/Prebuilt/components/Polls/CreatePollQuiz/PollsQuizMenu.jsx +3 -9
  51. package/src/Prebuilt/components/Polls/CreateQuestions/CreateQuestions.jsx +21 -1
  52. package/src/Prebuilt/components/Polls/CreateQuestions/QuestionForm.jsx +34 -7
  53. package/src/Prebuilt/components/Polls/CreateQuestions/SavedQuestion.jsx +2 -2
  54. package/src/Prebuilt/components/Polls/Polls.tsx +3 -0
  55. package/src/Prebuilt/components/Polls/Voting/Leaderboard.tsx +115 -0
  56. package/src/Prebuilt/components/Polls/Voting/LeaderboardEntry.tsx +63 -0
  57. package/src/Prebuilt/components/Polls/Voting/PeerParticipationSummary.tsx +38 -0
  58. package/src/Prebuilt/components/Polls/Voting/QuestionCard.jsx +33 -8
  59. package/src/Prebuilt/components/Polls/Voting/StandardVoting.jsx +7 -1
  60. package/src/Prebuilt/components/Polls/Voting/Voting.jsx +31 -13
  61. package/src/Prebuilt/components/Polls/common/MultipleChoiceOptions.jsx +33 -21
  62. package/src/Prebuilt/components/Polls/common/SingleChoiceOptions.jsx +47 -35
  63. package/src/Prebuilt/components/Polls/common/StatusIndicator.jsx +2 -22
  64. package/src/Prebuilt/components/Polls/common/VoteCount.jsx +1 -15
  65. package/src/Prebuilt/components/PreviousRoleInMetadata.tsx +21 -0
  66. package/src/Prebuilt/components/RemoveParticipant.tsx +35 -0
  67. package/src/Prebuilt/components/RoleChangeModal.jsx +1 -1
  68. package/src/Prebuilt/components/SidePaneTabs.tsx +0 -1
  69. package/src/Prebuilt/components/TileMenu/TileMenuContent.tsx +1 -1
  70. package/src/Prebuilt/components/Toast/ToastConfig.jsx +15 -3
  71. package/src/Prebuilt/components/VideoLayouts/EqualProminence.tsx +6 -5
  72. package/src/Prebuilt/components/VideoLayouts/GridLayout.tsx +27 -5
  73. package/src/Prebuilt/components/VideoLayouts/ScreenshareLayout.tsx +0 -1
  74. package/src/Prebuilt/components/hooks/useCloseScreenshareWhiteboard.tsx +24 -0
  75. package/src/Prebuilt/layouts/HLSView.jsx +51 -3
  76. package/src/Prebuilt/layouts/VideoStreamingSection.tsx +20 -3
  77. package/src/Prebuilt/layouts/WhiteboardView.tsx +66 -0
  78. package/dist/HLSView-662T7R7H.js.map +0 -7
  79. package/dist/chunk-2B7YYNHQ.js.map +0 -7
  80. package/src/Prebuilt/components/AppData/useAppLayout.js +0 -6
  81. package/src/Prebuilt/components/init/initUtils.js +0 -67
@@ -3,8 +3,40 @@ import { Flex } from '../../../';
3
3
 
4
4
  export const HMSVideo = forwardRef(({ children, ...props }, videoRef) => {
5
5
  return (
6
- <Flex data-testid="hms-video" css={{ size: '100%', position: 'relative' }} direction="column" {...props}>
7
- <video style={{ flex: '1 1 0', margin: '0 auto', minHeight: '0' }} ref={videoRef} playsInline />
6
+ <Flex
7
+ data-testid="hms-video"
8
+ css={{
9
+ size: '100%',
10
+ position: 'relative',
11
+ '& video::cue': {
12
+ color: 'white',
13
+ // textShadow: '0px 0px 4px #000',
14
+ whiteSpace: 'pre-line',
15
+ fontSize: '$lg',
16
+ fontStyle: 'normal',
17
+ fontWeight: '$semiBold',
18
+ lineHeight: '$sm',
19
+ letterSpacing: '0.5px',
20
+ },
21
+ '& video::-webkit-media-text-track-display': {
22
+ padding: '0 $4',
23
+ },
24
+ '& video::-webkit-media-text-track-container': {
25
+ fontSize: '$space$10 !important',
26
+ },
27
+ }}
28
+ direction="column"
29
+ {...props}
30
+ >
31
+ <video
32
+ style={{
33
+ flex: '1 1 0',
34
+ margin: '0 auto',
35
+ minHeight: '0',
36
+ }}
37
+ ref={videoRef}
38
+ playsInline
39
+ />
8
40
  {children}
9
41
  </Flex>
10
42
  );
@@ -0,0 +1,35 @@
1
+ import { useEffect } from 'react';
2
+ import {
3
+ HMSNotificationTypes,
4
+ HMSRoomState,
5
+ selectHasPeerHandRaised,
6
+ selectRoomState,
7
+ useHMSNotifications,
8
+ useHMSStore,
9
+ useHMSVanillaStore,
10
+ } from '@100mslive/react-sdk';
11
+ // @ts-ignore: No implicit Any
12
+ import { ToastBatcher } from '../Toast/ToastBatcher';
13
+ import { useRoomLayoutConferencingScreen } from '../../provider/roomLayoutProvider/hooks/useRoomLayoutScreen';
14
+
15
+ export const HandRaisedNotifications = () => {
16
+ const notification = useHMSNotifications(HMSNotificationTypes.HAND_RAISE_CHANGED);
17
+ const roomState = useHMSStore(selectRoomState);
18
+ const vanillaStore = useHMSVanillaStore();
19
+ const { on_stage_exp } = useRoomLayoutConferencingScreen().elements || {};
20
+
21
+ useEffect(() => {
22
+ if (!notification?.data) {
23
+ return;
24
+ }
25
+ if (roomState !== HMSRoomState.Connected || notification.data.isLocal || !on_stage_exp) {
26
+ return;
27
+ }
28
+ const hasPeerHandRaised = vanillaStore.getState(selectHasPeerHandRaised(notification.data.id));
29
+ if (hasPeerHandRaised) {
30
+ ToastBatcher.showToast({ notification, type: 'RAISE_HAND' });
31
+ }
32
+ }, [notification, on_stage_exp, roomState, vanillaStore]);
33
+
34
+ return null;
35
+ };
@@ -4,7 +4,6 @@ import {
4
4
  HMSNotificationTypes,
5
5
  HMSRoleChangeRequest,
6
6
  HMSRoomState,
7
- selectHasPeerHandRaised,
8
7
  selectLocalPeerID,
9
8
  selectPeerNameByID,
10
9
  selectRoomState,
@@ -13,7 +12,8 @@ import {
13
12
  useHMSStore,
14
13
  useHMSVanillaStore,
15
14
  } from '@100mslive/react-sdk';
16
- import { Button } from '../../..';
15
+ import { GroupIcon } from '@100mslive/react-icons';
16
+ import { Box, Button } from '../../..';
17
17
  import { useUpdateRoomLayout } from '../../provider/roomLayoutProvider';
18
18
  // @ts-ignore: No implicit Any
19
19
  import { ToastBatcher } from '../Toast/ToastBatcher';
@@ -21,6 +21,7 @@ import { ToastBatcher } from '../Toast/ToastBatcher';
21
21
  import { ToastManager } from '../Toast/ToastManager';
22
22
  import { AutoplayBlockedModal } from './AutoplayBlockedModal';
23
23
  import { ChatNotifications } from './ChatNotifications';
24
+ import { HandRaisedNotifications } from './HandRaisedNotifications';
24
25
  import { InitErrorModal } from './InitErrorModal';
25
26
  import { PeerNotifications } from './PeerNotifications';
26
27
  import { PermissionErrorModal } from './PermissionErrorModal';
@@ -28,6 +29,7 @@ import { ReconnectNotifications } from './ReconnectNotifications';
28
29
  import { TrackBulkUnmuteModal } from './TrackBulkUnmuteModal';
29
30
  import { TrackNotifications } from './TrackNotifications';
30
31
  import { TrackUnmuteModal } from './TrackUnmuteModal';
32
+ import { useRoomLayoutConferencingScreen } from '../../provider/roomLayoutProvider/hooks/useRoomLayoutScreen';
31
33
  // @ts-ignore: No implicit Any
32
34
  import { usePollViewToggle } from '../AppData/useSidepane';
33
35
  // @ts-ignore: No implicit Any
@@ -43,6 +45,7 @@ export function Notifications() {
43
45
  const roomState = useHMSStore(selectRoomState);
44
46
  const updateRoomLayoutForRole = useUpdateRoomLayout();
45
47
  const isNotificationDisabled = useIsNotificationDisabled();
48
+ const screenProps = useRoomLayoutConferencingScreen();
46
49
  const vanillaStore = useHMSVanillaStore();
47
50
  const togglePollView = usePollViewToggle();
48
51
 
@@ -53,23 +56,42 @@ export function Notifications() {
53
56
  });
54
57
  }, []);
55
58
 
59
+ /*
60
+ const leaderboardResultsShared = useCallback(
61
+ (stringifiedPollDetails: string) => {
62
+ const pollDetails = JSON.parse(stringifiedPollDetails);
63
+ if (pollDetails.startedBy !== localPeerID) {
64
+ const pollStartedBy = pollDetails.initiatorName;
65
+ ToastManager.addToast({
66
+ title: `${pollStartedBy} shared leaderboard for the quiz`,
67
+ action: (
68
+ <Button
69
+ onClick={() => togglePollView(pollDetails.id)}
70
+ variant="standard"
71
+ css={{
72
+ backgroundColor: '$surface_bright',
73
+ fontWeight: '$semiBold',
74
+ color: '$on_surface_high',
75
+ p: '$xs $md',
76
+ }}
77
+ >
78
+ View
79
+ </Button>
80
+ ),
81
+ });
82
+ }
83
+ },
84
+ [localPeerID, togglePollView],
85
+ ); */
86
+
56
87
  useCustomEvent({ type: ROLE_CHANGE_DECLINED, onEvent: handleRoleChangeDenied });
88
+ // useCustomEvent({ type: 'POLL_LEADERBOARD_SHARED', onEvent: leaderboardResultsShared });
57
89
 
58
90
  useEffect(() => {
59
91
  if (!notification || isNotificationDisabled) {
60
92
  return;
61
93
  }
62
94
  switch (notification.type) {
63
- case HMSNotificationTypes.HAND_RAISE_CHANGED: {
64
- if (roomState !== HMSRoomState.Connected || notification.data.isLocal) {
65
- return;
66
- }
67
- const hasPeerHandRaised = vanillaStore.getState(selectHasPeerHandRaised(notification.data.id));
68
- if (hasPeerHandRaised) {
69
- ToastBatcher.showToast({ notification, type: 'RAISE_HAND' });
70
- }
71
- break;
72
- }
73
95
  case HMSNotificationTypes.METADATA_UPDATED:
74
96
  if (roomState !== HMSRoomState.Connected) {
75
97
  return;
@@ -92,6 +114,16 @@ export function Notifications() {
92
114
  ToastManager.addToast({
93
115
  title: `Error: ${notification.data?.message}`,
94
116
  });
117
+ } else if (notification.data?.message === 'role limit reached') {
118
+ ToastManager.addToast({
119
+ title: 'The room is currently full, try joining later',
120
+ close: true,
121
+ icon: (
122
+ <Box css={{ color: '$alert_error_default' }}>
123
+ <GroupIcon />
124
+ </Box>
125
+ ),
126
+ });
95
127
  } else {
96
128
  ToastManager.addToast({
97
129
  title:
@@ -146,7 +178,7 @@ export function Notifications() {
146
178
  break;
147
179
 
148
180
  case HMSNotificationTypes.POLL_STARTED:
149
- if (notification.data.startedBy !== localPeerID) {
181
+ if (notification.data.startedBy !== localPeerID && screenProps.screenType !== 'hls_live_streaming') {
150
182
  const pollStartedBy = vanillaStore.getState(selectPeerNameByID(notification.data.startedBy)) || 'Participant';
151
183
  ToastManager.addToast({
152
184
  title: `${pollStartedBy} started a ${notification.data.type}: ${notification.data.title}`,
@@ -161,7 +193,7 @@ export function Notifications() {
161
193
  p: '$xs $md',
162
194
  }}
163
195
  >
164
- Vote
196
+ {notification.data.type === 'quiz' ? 'Answer' : 'Vote'}
165
197
  </Button>
166
198
  ),
167
199
  });
@@ -188,6 +220,7 @@ export function Notifications() {
188
220
  <PermissionErrorModal />
189
221
  <InitErrorModal />
190
222
  <ChatNotifications />
223
+ <HandRaisedNotifications />
191
224
  </>
192
225
  );
193
226
  }
@@ -3,9 +3,9 @@ import { HMSNotificationTypes, useHMSNotifications } from '@100mslive/react-sdk'
3
3
  // @ts-ignore: No implicit Any
4
4
  import { ToastBatcher } from '../Toast/ToastBatcher';
5
5
  // @ts-ignore: No implicit Any
6
- import { useSubscribedNotifications } from '../AppData/useUISettings';
6
+ import { useSetSubscribedChatSelector, useSubscribedNotifications } from '../AppData/useUISettings';
7
7
  // @ts-ignore: No implicit Any
8
- import { SUBSCRIBED_NOTIFICATIONS } from '../../common/constants';
8
+ import { CHAT_SELECTOR, SUBSCRIBED_NOTIFICATIONS } from '../../common/constants';
9
9
 
10
10
  const notificationTypes = [
11
11
  HMSNotificationTypes.PEER_LIST,
@@ -17,6 +17,8 @@ export const PeerNotifications = () => {
17
17
  const notification = useHMSNotifications(notificationTypes);
18
18
  const isPeerJoinSubscribed = useSubscribedNotifications(SUBSCRIBED_NOTIFICATIONS.PEER_JOINED);
19
19
  const isPeerLeftSubscribed = useSubscribedNotifications(SUBSCRIBED_NOTIFICATIONS.PEER_LEFT);
20
+ const [selectedPeer, setPeerSelector] = useSetSubscribedChatSelector(CHAT_SELECTOR.PEER);
21
+
20
22
  useEffect(() => {
21
23
  if (!notification?.data) {
22
24
  return;
@@ -35,6 +37,9 @@ export const PeerNotifications = () => {
35
37
  }
36
38
  break;
37
39
  case HMSNotificationTypes.PEER_LEFT:
40
+ if (selectedPeer.id === notification.data.id) {
41
+ setPeerSelector({});
42
+ }
38
43
  if (!isPeerLeftSubscribed) {
39
44
  return;
40
45
  }
@@ -191,13 +191,7 @@ const PrevMenu = () => {
191
191
  </Text>
192
192
  <Flex direction="column" css={{ gap: '$10', mt: '$8' }}>
193
193
  {polls.map(poll => (
194
- <InteractionCard
195
- key={poll.id}
196
- id={poll.id}
197
- title={poll.title}
198
- isLive={poll.state === 'started'}
199
- isTimed={(poll.duration || 0) > 0}
200
- />
194
+ <InteractionCard key={poll.id} id={poll.id} title={poll.title} isLive={poll.state === 'started'} />
201
195
  ))}
202
196
  </Flex>
203
197
  </Flex>
@@ -205,7 +199,7 @@ const PrevMenu = () => {
205
199
  ) : null;
206
200
  };
207
201
 
208
- const InteractionCard = ({ id, title, isLive, isTimed }) => {
202
+ const InteractionCard = ({ id, title, isLive }) => {
209
203
  const { setPollState } = usePollViewState();
210
204
 
211
205
  const goToVote = id => {
@@ -221,7 +215,7 @@ const InteractionCard = ({ id, title, isLive, isTimed }) => {
221
215
  <Text variant="sub1" css={{ c: '$on_surface_high', fontWeight: '$semiBold' }}>
222
216
  {title}
223
217
  </Text>
224
- <StatusIndicator isLive={isLive} shouldShowTimer={isLive && isTimed} />
218
+ <StatusIndicator isLive={isLive} />
225
219
  </Flex>
226
220
  <Flex css={{ w: '100%', gap: '$4' }} justify="end">
227
221
  <Button variant="primary" onClick={() => goToVote(id)}>
@@ -1,7 +1,7 @@
1
1
  // @ts-check
2
2
  import React, { useMemo, useState } from 'react';
3
3
  import { v4 as uuid } from 'uuid';
4
- import { selectPollByID, useHMSActions, useHMSStore } from '@100mslive/react-sdk';
4
+ import { selectPollByID, useHMSActions, useHMSStore, useRecordingStreaming } from '@100mslive/react-sdk';
5
5
  import { AddCircleIcon } from '@100mslive/react-icons';
6
6
  import { Button, Flex, Text } from '../../../../';
7
7
  import { Container, ContentHeader } from '../../Streaming/Common';
@@ -14,6 +14,7 @@ import { POLL_VIEWS } from '../../../common/constants';
14
14
  export function CreateQuestions() {
15
15
  const [questions, setQuestions] = useState([{ draftID: uuid() }]);
16
16
  const actions = useHMSActions();
17
+ const { isHLSRunning } = useRecordingStreaming();
17
18
  const togglePollView = usePollViewToggle();
18
19
  const { pollInView: id, setPollView } = usePollViewState();
19
20
  const interaction = useHMSStore(selectPollByID(id));
@@ -31,11 +32,30 @@ export function CreateQuestions() {
31
32
  type: question.type,
32
33
  options: question.options,
33
34
  skippable: question.skippable,
35
+ weight: question.weight,
34
36
  }));
35
37
  await actions.interactivityCenter.addQuestionsToPoll(id, validQuestions);
36
38
  await actions.interactivityCenter.startPoll(id);
39
+ await sendTimedMetadata(id);
37
40
  setPollView(POLL_VIEWS.VOTE);
38
41
  };
42
+
43
+ const sendTimedMetadata = async poll_id => {
44
+ // send hls timedmetadata when it is running
45
+ if (poll_id && isHLSRunning) {
46
+ try {
47
+ await actions.sendHLSTimedMetadata([
48
+ {
49
+ payload: `poll:${poll_id}`,
50
+ duration: 100,
51
+ },
52
+ ]);
53
+ } catch (e) {
54
+ console.error(e);
55
+ }
56
+ }
57
+ };
58
+
39
59
  const headingTitle = interaction?.type
40
60
  ? interaction?.type?.[0]?.toUpperCase() + interaction?.type?.slice(1)
41
61
  : 'Polls and Quizzes';
@@ -17,6 +17,7 @@ export const QuestionForm = ({ question, index, length, onSave, removeQuestion,
17
17
  const [open, setOpen] = useState(false);
18
18
  const [type, setType] = useState(question.type || QUESTION_TYPE.SINGLE_CHOICE);
19
19
  const [text, setText] = useState(question.text);
20
+ const [weight, setWeight] = useState(isQuiz ? 10 : 1);
20
21
  const [options, setOptions] = useState(
21
22
  question?.options || [
22
23
  { text: '', isCorrectAnswer: false },
@@ -28,6 +29,7 @@ export const QuestionForm = ({ question, index, length, onSave, removeQuestion,
28
29
  text,
29
30
  type,
30
31
  options,
32
+ weight,
31
33
  isQuiz,
32
34
  });
33
35
 
@@ -182,12 +184,31 @@ export const QuestionForm = ({ question, index, length, onSave, removeQuestion,
182
184
  </Flex>
183
185
  )}
184
186
  {isQuiz ? (
185
- <Flex css={{ mt: '$md', gap: '$6' }}>
186
- <Switch defaultChecked={skippable} onCheckedChange={checked => setSkippable(checked)} />
187
- <Text variant="sm" css={{ color: '$on_surface_medium' }}>
188
- Not required to answer
189
- </Text>
190
- </Flex>
187
+ <>
188
+ <Flex justify="between" align="center" css={{ mt: '$md', gap: '$6', w: '100%' }}>
189
+ <Text variant="sm" css={{ color: '$on_surface_medium' }}>
190
+ Point Weightage
191
+ </Text>
192
+ <Input
193
+ type="number"
194
+ value={weight}
195
+ min={1}
196
+ max={999}
197
+ onChange={e => setWeight(Math.min(e.target.value, 999))}
198
+ css={{
199
+ backgroundColor: '$surface_bright',
200
+ border: '1px solid $border_bright',
201
+ maxWidth: '$20',
202
+ }}
203
+ />
204
+ </Flex>
205
+ <Flex justify="between" css={{ mt: '$md', gap: '$6', w: '100%' }}>
206
+ <Text variant="sm" css={{ color: '$on_surface_medium' }}>
207
+ Allow to skip
208
+ </Text>
209
+ <Switch defaultChecked={skippable} onCheckedChange={checked => setSkippable(checked)} />
210
+ </Flex>
211
+ </>
191
212
  ) : null}
192
213
  </>
193
214
  ) : null}
@@ -222,6 +243,7 @@ export const QuestionForm = ({ question, index, length, onSave, removeQuestion,
222
243
  options,
223
244
  skippable,
224
245
  draftID: question.draftID,
246
+ weight,
225
247
  });
226
248
  }}
227
249
  >
@@ -235,7 +257,7 @@ export const QuestionForm = ({ question, index, length, onSave, removeQuestion,
235
257
  );
236
258
  };
237
259
 
238
- export const isValidQuestion = ({ text, type, options, isQuiz = false }) => {
260
+ export const isValidQuestion = ({ text, type, options, weight, isQuiz = false }) => {
239
261
  if (!isValidTextInput(text) || !type) {
240
262
  return false;
241
263
  }
@@ -251,5 +273,10 @@ export const isValidQuestion = ({ text, type, options, isQuiz = false }) => {
251
273
  return everyOptionHasText;
252
274
  }
253
275
 
276
+ // The minimum acceptable value of weight is 1
277
+ if (isQuiz && weight < 1) {
278
+ return false;
279
+ }
280
+
254
281
  return everyOptionHasText && hasCorrectAnswer;
255
282
  };
@@ -15,8 +15,8 @@ export const SavedQuestion = ({ question, index, length, convertToDraft, removeQ
15
15
  <Text variant="body2" css={{ mt: '$4', mb: '$md' }}>
16
16
  {question.text}
17
17
  </Text>
18
- {question.options.map(option => (
19
- <Flex css={{ alignItems: 'center', my: '$xs' }}>
18
+ {question.options.map((option, index) => (
19
+ <Flex key={`${option.text}-${index}`} css={{ alignItems: 'center', my: '$xs' }}>
20
20
  <Text variant="body2" css={{ c: '$on_surface_medium' }}>
21
21
  {option.text}
22
22
  </Text>
@@ -3,6 +3,7 @@ import React from 'react';
3
3
  import { PollsQuizMenu } from './CreatePollQuiz/PollsQuizMenu';
4
4
  // @ts-ignore: No implicit Any
5
5
  import { CreateQuestions } from './CreateQuestions/CreateQuestions';
6
+ import { Leaderboard } from './Voting/Leaderboard';
6
7
  // @ts-ignore: No implicit Any
7
8
  import { Voting } from './Voting/Voting';
8
9
  // @ts-ignore: No implicit Any
@@ -22,6 +23,8 @@ export const Polls = () => {
22
23
  return <CreateQuestions />;
23
24
  } else if (view === POLL_VIEWS.VOTE) {
24
25
  return <Voting toggleVoting={togglePollView} id={pollID} />;
26
+ } else if (view === POLL_VIEWS.RESULTS) {
27
+ return <Leaderboard pollID={pollID} />;
25
28
  } else {
26
29
  return null;
27
30
  }
@@ -0,0 +1,115 @@
1
+ import React, { useEffect, useState } from 'react';
2
+ import { HMSPollLeaderboardResponse, selectPollByID, useHMSActions, useHMSStore } from '@100mslive/react-sdk';
3
+ import { ChevronLeftIcon, CrossIcon } from '@100mslive/react-icons';
4
+ import { Box, Flex } from '../../../../Layout';
5
+ import { Loading } from '../../../../Loading';
6
+ import { Text } from '../../../../Text';
7
+ import { LeaderboardEntry } from './LeaderboardEntry';
8
+ // @ts-ignore
9
+ import { useSidepaneToggle } from '../../AppData/useSidepane';
10
+ // @ts-ignore
11
+ import { usePollViewState } from '../../AppData/useUISettings';
12
+ // @ts-ignore
13
+ import { StatusIndicator } from '../common/StatusIndicator';
14
+ import { POLL_VIEWS } from '../../../common/constants';
15
+
16
+ export const Leaderboard = ({ pollID }: { pollID: string }) => {
17
+ const hmsActions = useHMSActions();
18
+ const poll = useHMSStore(selectPollByID(pollID));
19
+ const [pollLeaderboard, setPollLeaderboard] = useState<HMSPollLeaderboardResponse | undefined>();
20
+ const { setPollView } = usePollViewState();
21
+ const toggleSidepane = useSidepaneToggle();
22
+
23
+ /*
24
+ const sharedLeaderboardRef = useRef(false);
25
+ const sharedLeaderboards = useHMSStore(selectSessionStore(SESSION_STORE_KEY.SHARED_LEADERBOARDS));
26
+ const { sendEvent } = useCustomEvent({
27
+ type: HMSNotificationTypes.POLL_LEADERBOARD_SHARED,
28
+ onEvent: () => {
29
+ return;
30
+ },
31
+ });
32
+ */
33
+
34
+ useEffect(() => {
35
+ const fetchLeaderboardData = async () => {
36
+ if (poll) {
37
+ const leaderboardData = await hmsActions.interactivityCenter.fetchLeaderboard(poll, 0, 50);
38
+ setPollLeaderboard(leaderboardData);
39
+ }
40
+ };
41
+ fetchLeaderboardData();
42
+ }, [poll, hmsActions.interactivityCenter]);
43
+
44
+ if (!poll || !pollLeaderboard)
45
+ return (
46
+ <Flex align="center" justify="center" css={{ size: '100%' }}>
47
+ <Loading />
48
+ </Flex>
49
+ );
50
+ const maxPossibleScore = poll.questions?.reduce((total, question) => (total += question.weight || 0), 0) || 0;
51
+ const questionCount = poll.questions?.length || 0;
52
+
53
+ return (
54
+ <Flex direction="column" css={{ size: '100%' }}>
55
+ <Flex justify="between" align="center" css={{ pb: '$6', borderBottom: '1px solid $border_bright', mb: '$8' }}>
56
+ <Flex align="center" css={{ gap: '$4' }}>
57
+ <Flex
58
+ css={{ color: '$on_surface_medium', '&:hover': { color: '$on_surface_high', cursor: 'pointer' } }}
59
+ onClick={() => setPollView(POLL_VIEWS.VOTE)}
60
+ >
61
+ <ChevronLeftIcon />
62
+ </Flex>
63
+ <Text variant="lg" css={{ fontWeight: '$semiBold' }}>
64
+ {poll.title}
65
+ </Text>
66
+ <StatusIndicator isLive={false} />
67
+ </Flex>
68
+ <Flex
69
+ css={{ color: '$on_surface_medium', '&:hover': { color: '$on_surface_high', cursor: 'pointer' } }}
70
+ onClick={toggleSidepane}
71
+ >
72
+ <CrossIcon />
73
+ </Flex>
74
+ </Flex>
75
+ <Text variant="sm" css={{ fontWeight: '$semiBold' }}>
76
+ Leaderboard
77
+ </Text>
78
+ <Text variant="xs" css={{ color: '$on_surface_medium' }}>
79
+ Based on score and time taken to cast the correct answer
80
+ </Text>
81
+ <Box css={{ mt: '$8', gap: '$4', overflowY: 'auto', flex: '1 1 0', mr: '-$6', pr: '$6' }}>
82
+ {pollLeaderboard?.entries &&
83
+ pollLeaderboard.entries.map(question => (
84
+ <LeaderboardEntry
85
+ key={question.position}
86
+ position={question.position}
87
+ score={question.score}
88
+ questionCount={questionCount}
89
+ correctResponses={question.correctResponses}
90
+ userName={question.peer.username || ''}
91
+ maxPossibleScore={maxPossibleScore}
92
+ />
93
+ ))}
94
+ </Box>
95
+
96
+ {/* {!sharedLeaderboardRef.current ? (
97
+ <Button
98
+ css={{ ml: 'auto', mt: '$8' }}
99
+ onClick={() => {
100
+ const currentlySharedLeaderboards = sharedLeaderboards || [];
101
+ hmsActions.sessionStore.set(SESSION_STORE_KEY.SHARED_LEADERBOARDS, [
102
+ ...currentlySharedLeaderboards,
103
+ pollID,
104
+ ]);
105
+ const pollDetails = { initiatorName: '', startedBy: poll.startedBy, id: pollID };
106
+ sendEvent();
107
+ sharedLeaderboardRef.current = true;
108
+ }}
109
+ >
110
+ Share Results
111
+ </Button>
112
+ ) : null} */}
113
+ </Flex>
114
+ );
115
+ };
@@ -0,0 +1,63 @@
1
+ import React from 'react';
2
+ import { CheckCircleIcon, TrophyFilledIcon } from '@100mslive/react-icons';
3
+ import { Box, Flex } from '../../../../Layout';
4
+ import { Text } from '../../../../Text';
5
+
6
+ const positionColorMap: Record<number, string> = { 1: '#D69516', 2: '#3E3E3E', 3: '#583B0F' };
7
+
8
+ export const LeaderboardEntry = ({
9
+ position,
10
+ score,
11
+ questionCount,
12
+ correctResponses,
13
+ userName,
14
+ maxPossibleScore,
15
+ }: {
16
+ position: number;
17
+ score: number;
18
+ questionCount: number;
19
+ correctResponses: number;
20
+ userName: string;
21
+ maxPossibleScore: number;
22
+ }) => {
23
+ return (
24
+ <Flex align="center" justify="between">
25
+ <Flex align="center" css={{ gap: '$6' }}>
26
+ <Flex
27
+ align="center"
28
+ justify="center"
29
+ css={{
30
+ backgroundColor: positionColorMap[position] || '',
31
+ h: '$10',
32
+ w: '$10',
33
+ borderRadius: '$round',
34
+ color: position > 3 ? '$on_surface_low' : '#FFF',
35
+ fontSize: '$xs',
36
+ fontWeight: '$semiBold',
37
+ }}
38
+ >
39
+ {position}
40
+ </Flex>
41
+
42
+ <Box>
43
+ <Text variant="sm" css={{ fontWeight: '$semiBold', color: '$on_surface_high' }}>
44
+ {userName}
45
+ </Text>
46
+
47
+ <Text variant="sm">
48
+ {score}/{maxPossibleScore} points
49
+ </Text>
50
+ </Box>
51
+ </Flex>
52
+ <Flex align="center" css={{ gap: '$6', color: '$on_surface_medium' }}>
53
+ {position === 1 ? <TrophyFilledIcon /> : null}
54
+ <CheckCircleIcon height={16} width={16} />
55
+ {questionCount ? (
56
+ <Text variant="xs">
57
+ {correctResponses}/{questionCount}
58
+ </Text>
59
+ ) : null}
60
+ </Flex>
61
+ </Flex>
62
+ );
63
+ };
@@ -0,0 +1,38 @@
1
+ import React from 'react';
2
+ import { HMSPoll, selectLocalPeer, useHMSStore } from '@100mslive/react-sdk';
3
+ import { Box } from '../../../../Layout';
4
+ import { Text } from '../../../../Text';
5
+ // @ts-ignore
6
+ import { getPeerParticipationSummary } from '../../../common/utils';
7
+
8
+ export const PeerParticipationSummary = ({ poll }: { poll: HMSPoll }) => {
9
+ const localPeer = useHMSStore(selectLocalPeer);
10
+ const { totalResponses, correctResponses, score } = getPeerParticipationSummary(
11
+ poll,
12
+ localPeer?.id,
13
+ localPeer?.customerUserId,
14
+ );
15
+
16
+ const boxes = [
17
+ { title: 'Points', value: score },
18
+ { title: 'Correct Answers', value: `${correctResponses}/${totalResponses}` },
19
+ ];
20
+ return (
21
+ <Box>
22
+ <Text css={{ fontWeight: '$semiBold', my: '$8' }}>Participation Summary</Text>
23
+ <Box css={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '$4' }}>
24
+ {boxes.map(box => (
25
+ <Box key={box.title} css={{ p: '$8', background: '$surface_default', borderRadius: '$1' }}>
26
+ <Text
27
+ variant="tiny"
28
+ css={{ textTransform: 'uppercase', color: '$on_surface_medium', fontWeight: '$semiBold', my: '$4' }}
29
+ >
30
+ {box.title}
31
+ </Text>
32
+ <Text css={{ fontWeight: '$semiBold' }}>{box.value}</Text>
33
+ </Box>
34
+ ))}
35
+ </Box>
36
+ </Box>
37
+ );
38
+ };