@100mslive/roomkit-react 0.2.2-alpha.4 → 0.2.2-alpha.6

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.
@@ -189,7 +189,7 @@ const ChatMessage = React.memo(
189
189
  roles: message.recipientRoles,
190
190
  receiver: message.recipientPeer,
191
191
  });
192
- const [openSheet, setOpenSheet] = useState(false);
192
+ const [openSheet, setOpenSheetBare] = useState(false);
193
193
  const showPinAction = !!elements?.chat?.allow_pinning_messages;
194
194
  const showReply = message.sender !== selectedPeer.id && message.sender !== localPeerId && isPrivateChatEnabled;
195
195
  useLayoutEffect(() => {
@@ -198,6 +198,11 @@ const ChatMessage = React.memo(
198
198
  }
199
199
  }, [index, message.id]);
200
200
 
201
+ const setOpenSheet = (value: boolean, e?: React.MouseEvent<HTMLElement, MouseEvent>) => {
202
+ e?.stopPropagation();
203
+ setOpenSheetBare(value);
204
+ };
205
+
201
206
  return (
202
207
  <Box
203
208
  css={{
@@ -228,9 +233,9 @@ const ChatMessage = React.memo(
228
233
  },
229
234
  }}
230
235
  data-testid="chat_msg"
231
- onClick={() => {
236
+ onClick={e => {
232
237
  if (isMobile) {
233
- setOpenSheet(true);
238
+ setOpenSheet(true, e);
234
239
  }
235
240
  }}
236
241
  >
@@ -321,8 +326,7 @@ const ChatMessage = React.memo(
321
326
  color: isOverlay ? '#FFF' : '$on_surface_high',
322
327
  }}
323
328
  onClick={e => {
324
- e.stopPropagation();
325
- setOpenSheet(true);
329
+ setOpenSheet(true, e);
326
330
  }}
327
331
  >
328
332
  <AnnotisedMessage message={message.message} />
@@ -2,6 +2,7 @@ import React from 'react';
2
2
  import { CheckCircleIcon, ClockIcon, TrophyFilledIcon } from '@100mslive/react-icons';
3
3
  import { Box, Flex } from '../../../../Layout';
4
4
  import { Text } from '../../../../Text';
5
+ import { getFormattedTime } from '../common/utils';
5
6
 
6
7
  const positionColorMap: Record<number, string> = { 1: '#D69516', 2: '#3E3E3E', 3: '#583B0F' };
7
8
 
@@ -23,7 +24,7 @@ export const LeaderboardEntry = ({
23
24
  duration: number;
24
25
  }) => {
25
26
  return (
26
- <Flex align="center" justify="between" css={{ my: '$4' }}>
27
+ <Flex align="center" justify="between" css={{ my: '$8' }}>
27
28
  <Flex align="center" css={{ gap: '$6' }}>
28
29
  <Flex
29
30
  align="center"
@@ -66,7 +67,7 @@ export const LeaderboardEntry = ({
66
67
  {duration ? (
67
68
  <Flex align="center" css={{ gap: '$2', color: '$on_surface_medium' }}>
68
69
  <ClockIcon height={16} width={16} />
69
- <Text variant="xs">{(duration / 1000).toFixed(3)}s</Text>
70
+ <Text variant="xs">{getFormattedTime(duration)}</Text>
70
71
  </Flex>
71
72
  ) : null}
72
73
  </Flex>
@@ -4,6 +4,7 @@ import { Box } from '../../../../Layout';
4
4
  import { Text } from '../../../../Text';
5
5
  import { StatisticBox } from './StatisticBox';
6
6
  import { useQuizSummary } from './useQuizSummary';
7
+ import { getFormattedTime } from '../common/utils';
7
8
 
8
9
  export const PeerParticipationSummary = ({ quiz }: { quiz: HMSPoll }) => {
9
10
  const localPeerId = useHMSStore(selectLocalPeerID);
@@ -29,7 +30,7 @@ export const PeerParticipationSummary = ({ quiz }: { quiz: HMSPoll }) => {
29
30
  }/${summary.totalUsers})`,
30
31
  },
31
32
  // Time in ms
32
- { title: 'Avg. Time Taken', value: `${(summary.avgTime / 1000).toFixed(3)}s` },
33
+ { title: 'Avg. Time Taken', value: getFormattedTime(summary.avgTime) },
33
34
  {
34
35
  title: 'Avg. Score',
35
36
  value: Number.isInteger(summary.avgScore) ? summary.avgScore : summary.avgScore.toFixed(2),
@@ -37,9 +38,9 @@ export const PeerParticipationSummary = ({ quiz }: { quiz: HMSPoll }) => {
37
38
  ]
38
39
  : [
39
40
  { title: 'Your rank', value: peerEntry?.position || '-' },
40
- { title: 'Points', value: peerEntry?.score },
41
+ { title: 'Points', value: peerEntry?.score || 0 },
41
42
  // Time in ms
42
- { title: 'Time Taken', value: `${((peerEntry?.duration || 0) / 1000).toFixed(3)}s` },
43
+ { title: 'Time Taken', value: getFormattedTime(peerEntry?.duration) },
43
44
  {
44
45
  title: 'Correct Answers',
45
46
  value: peerEntry?.totalResponses ? `${peerEntry?.correctResponses}/${peerEntry.totalResponses}` : '-',
@@ -73,11 +73,6 @@ export const QuestionCard = ({
73
73
  },
74
74
  ]);
75
75
  startTime.current = Date.now();
76
-
77
- if (isQuiz && index !== totalQuestions) {
78
- setSingleOptionAnswer(undefined);
79
- setMultipleOptionAnswer(new Set());
80
- }
81
76
  }, [
82
77
  isValidVote,
83
78
  actions.interactivityCenter,
@@ -118,8 +113,8 @@ export const QuestionCard = ({
118
113
  gap: '$4',
119
114
  }}
120
115
  >
121
- {respondedToQuiz && isCorrectAnswer && pollEnded ? <CheckCircleIcon height={20} width={20} /> : null}
122
- {respondedToQuiz && !isCorrectAnswer && pollEnded ? <CrossCircleIcon height={20} width={20} /> : null}
116
+ {respondedToQuiz && isCorrectAnswer && pollEnded ? <CheckCircleIcon height={16} width={16} /> : null}
117
+ {respondedToQuiz && !isCorrectAnswer && pollEnded ? <CrossCircleIcon height={16} width={16} /> : null}
123
118
  QUESTION {index} OF {totalQuestions}: {type.toUpperCase()}
124
119
  </Text>
125
120
  </Flex>
@@ -136,7 +131,9 @@ export const QuestionCard = ({
136
131
  </Box>
137
132
  </Flex>
138
133
 
139
- <Box css={{ maxHeight: showOptions ? '$80' : '0', transition: 'max-height 0.3s ease', overflowY: 'hidden' }}>
134
+ <Box
135
+ css={{ maxHeight: showOptions ? '$80' : '0', transition: 'max-height 0.3s ease', overflowY: 'auto', mb: '$4' }}
136
+ >
140
137
  {type === QUESTION_TYPE.SINGLE_CHOICE ? (
141
138
  <SingleChoiceOptions
142
139
  key={index}
@@ -150,7 +147,6 @@ export const QuestionCard = ({
150
147
  showVoteCount={showVoteCount}
151
148
  localPeerResponse={localPeerResponse}
152
149
  isStopped={pollState === 'stopped'}
153
- answer={singleOptionAnswer}
154
150
  />
155
151
  ) : null}
156
152
 
@@ -169,19 +165,18 @@ export const QuestionCard = ({
169
165
  isStopped={pollState === 'stopped'}
170
166
  />
171
167
  ) : null}
172
-
173
- {isLive && (
174
- <QuestionActions
175
- isValidVote={isValidVote}
176
- onVote={handleVote}
177
- response={localPeerResponse}
178
- isQuiz={isQuiz}
179
- incrementIndex={() => {
180
- setCurrentIndex(curr => Math.min(totalQuestions, curr + 1));
181
- }}
182
- />
183
- )}
184
168
  </Box>
169
+ {isLive && (
170
+ <QuestionActions
171
+ isValidVote={isValidVote}
172
+ onVote={handleVote}
173
+ response={localPeerResponse}
174
+ isQuiz={isQuiz}
175
+ incrementIndex={() => {
176
+ setCurrentIndex(curr => Math.min(totalQuestions, curr + 1));
177
+ }}
178
+ />
179
+ )}
185
180
  </Box>
186
181
  );
187
182
  };
@@ -3,7 +3,7 @@ import { Box } from '../../../../Layout';
3
3
  import { Text } from '../../../../Text';
4
4
 
5
5
  export const StatisticBox = ({ title, value = 0 }: { title: string; value: string | number | undefined }) => {
6
- if (!value) {
6
+ if (!value && !(typeof value === 'number')) {
7
7
  return <></>;
8
8
  }
9
9
  return (
@@ -1,33 +1,44 @@
1
1
  import React, { useState } from 'react';
2
- import { HMSPoll } from '@100mslive/react-sdk';
2
+ import { HMSPoll, selectLocalPeerID, useHMSStore } from '@100mslive/react-sdk';
3
3
  // @ts-ignore
4
4
  import { QuestionCard } from './QuestionCard';
5
+ // @ts-ignore
6
+ import { getLastAttemptedIndex } from '../../../common/utils';
5
7
 
6
8
  export const TimedView = ({ poll }: { poll: HMSPoll }) => {
7
- // Backend question index starts at 1
8
- const [currentIndex, setCurrentIndex] = useState(1);
9
+ const localPeerId = useHMSStore(selectLocalPeerID);
10
+ const lastAttemptedIndex = getLastAttemptedIndex(poll.questions, localPeerId, '');
11
+ const [currentIndex, setCurrentIndex] = useState(lastAttemptedIndex);
9
12
  const activeQuestion = poll.questions?.find(question => question.index === currentIndex);
13
+ const attemptedAll = poll.questions?.length === lastAttemptedIndex - 1;
10
14
 
11
- if (!activeQuestion) {
15
+ if (!activeQuestion || !poll.questions?.length) {
12
16
  return null;
13
17
  }
14
18
 
15
19
  return (
16
- <QuestionCard
17
- pollID={poll.id}
18
- isQuiz={poll.type === 'quiz'}
19
- startedBy={poll.startedBy}
20
- pollState={poll.state}
21
- index={activeQuestion.index}
22
- text={activeQuestion.text}
23
- type={activeQuestion.type}
24
- result={activeQuestion?.result}
25
- totalQuestions={poll.questions?.length || 0}
26
- options={activeQuestion.options}
27
- responses={activeQuestion.responses}
28
- answer={activeQuestion.answer}
29
- setCurrentIndex={setCurrentIndex}
30
- rolesThatCanViewResponses={poll.rolesThatCanViewResponses}
31
- />
20
+ <>
21
+ {poll.questions.map(question => {
22
+ return attemptedAll || activeQuestion.index === question.index ? (
23
+ <QuestionCard
24
+ key={question.index}
25
+ pollID={poll.id}
26
+ isQuiz={poll.type === 'quiz'}
27
+ startedBy={poll.startedBy}
28
+ pollState={poll.state}
29
+ index={question.index}
30
+ text={question.text}
31
+ type={question.type}
32
+ result={question?.result}
33
+ totalQuestions={poll.questions?.length || 0}
34
+ options={question.options}
35
+ responses={question.responses}
36
+ answer={question.answer}
37
+ setCurrentIndex={setCurrentIndex}
38
+ rolesThatCanViewResponses={poll.rolesThatCanViewResponses}
39
+ />
40
+ ) : null;
41
+ })}
42
+ </>
32
43
  );
33
44
  };
@@ -68,7 +68,7 @@ export const Voting = ({ id, toggleVoting }: { id: string; toggleVoting: () => v
68
68
  </Box>
69
69
  </Flex>
70
70
 
71
- <Flex direction="column" css={{ p: '$8 $10', overflowY: 'auto' }}>
71
+ <Flex direction="column" css={{ p: '$8 $10', flex: '1 1 0', overflowY: 'auto' }}>
72
72
  {poll.state === 'started' ? (
73
73
  <Text css={{ color: '$on_surface_medium', fontWeight: '$semiBold' }}>
74
74
  {pollCreatorName || 'Participant'} started a {poll.type}
@@ -76,22 +76,21 @@ export const Voting = ({ id, toggleVoting }: { id: string; toggleVoting: () => v
76
76
  ) : null}
77
77
 
78
78
  {showSingleView ? <TimedView poll={poll} /> : <StandardView poll={poll} />}
79
-
79
+ </Flex>
80
+ <Flex
81
+ css={{ w: '100%', justifyContent: 'end', alignItems: 'center', p: '$8', borderTop: '1px solid $border_bright' }}
82
+ >
80
83
  {poll.state === 'started' && canEndActivity && (
81
84
  <Button
82
85
  variant="danger"
83
- css={{ fontWeight: '$semiBold', w: 'max-content', ml: 'auto', mt: '$8' }}
86
+ css={{ fontWeight: '$semiBold', w: 'max-content' }}
84
87
  onClick={() => actions.interactivityCenter.stopPoll(id)}
85
88
  >
86
89
  End {poll.type}
87
90
  </Button>
88
91
  )}
89
-
90
92
  {canViewLeaderboard ? (
91
- <Button
92
- css={{ fontWeight: '$semiBold', w: 'max-content', ml: 'auto', mt: '$8' }}
93
- onClick={() => setPollView(POLL_VIEWS.RESULTS)}
94
- >
93
+ <Button css={{ fontWeight: '$semiBold', w: 'max-content' }} onClick={() => setPollView(POLL_VIEWS.RESULTS)}>
95
94
  View Leaderboard
96
95
  </Button>
97
96
  ) : null}
@@ -52,7 +52,7 @@ export const MultipleChoiceOptions = ({
52
52
 
53
53
  {isStopped && correctOptionIndexes?.includes(option.index) ? (
54
54
  <Flex css={{ color: '$on_surface_high' }}>
55
- <CheckCircleIcon />
55
+ <CheckCircleIcon height={20} width={20} />
56
56
  </Flex>
57
57
  ) : null}
58
58
 
@@ -83,7 +83,7 @@ export const MultipleChoiceOptionInputs = ({ isQuiz, options, selectAnswer, hand
83
83
  <Flex direction="column" css={{ gap: '$md', w: '100%', mb: '$md' }}>
84
84
  {options.map((option, index) => {
85
85
  return (
86
- <Flex align="center" key={index} css={{ w: '100%', gap: '$5' }}>
86
+ <Flex align="center" key={index} css={{ w: '100%', gap: '$4' }}>
87
87
  {isQuiz && (
88
88
  <Checkbox.Root
89
89
  onCheckedChange={checked => selectAnswer(checked, index)}
@@ -17,14 +17,13 @@ export const SingleChoiceOptions = ({
17
17
  isStopped,
18
18
  isQuiz,
19
19
  localPeerResponse,
20
- answer,
21
20
  }) => {
22
21
  return (
23
- <RadioGroup.Root value={answer || null} onValueChange={value => setAnswer(value)}>
22
+ <RadioGroup.Root value={localPeerResponse?.option} onValueChange={value => setAnswer(value)}>
24
23
  <Flex direction="column" css={{ gap: '$md', w: '100%', mb: '$md' }}>
25
24
  {options.map(option => {
26
25
  return (
27
- <Flex align="start" key={`${questionIndex}-${option.index}`} css={{ w: '100%', gap: '$5' }}>
26
+ <Flex align="start" key={`${questionIndex}-${option.index}`} css={{ w: '100%', gap: '$4' }}>
28
27
  {!isStopped || !isQuiz ? (
29
28
  <RadioGroup.Item
30
29
  css={{
@@ -59,7 +58,7 @@ export const SingleChoiceOptions = ({
59
58
 
60
59
  {isStopped && correctOptionIndex === option.index && isQuiz ? (
61
60
  <Flex css={{ color: '$on_surface_high' }}>
62
- <CheckCircleIcon />
61
+ <CheckCircleIcon height={20} width={20} />
63
62
  </Flex>
64
63
  ) : null}
65
64
 
@@ -95,7 +94,7 @@ export const SingleChoiceOptionInputs = ({ isQuiz, options, selectAnswer, handle
95
94
  <Flex direction="column" css={{ gap: '$md', w: '100%', mb: '$md' }}>
96
95
  {options.map((option, index) => {
97
96
  return (
98
- <Flex align="center" key={`option-${index}`} css={{ w: '100%', gap: '$5' }}>
97
+ <Flex align="center" key={`option-${index}`} css={{ w: '100%', gap: '$4' }}>
99
98
  {isQuiz && (
100
99
  <RadioGroup.Item
101
100
  css={{
@@ -0,0 +1,16 @@
1
+ export const getFormattedTime = (milliseconds: number | undefined) => {
2
+ if (!milliseconds) return '-';
3
+
4
+ const totalSeconds = milliseconds / 1000;
5
+ const minutes = Math.floor(totalSeconds / 60);
6
+ const seconds = totalSeconds % 60;
7
+
8
+ let formattedSeconds = '';
9
+ if (Number.isInteger(seconds) || minutes) {
10
+ formattedSeconds = seconds.toFixed(0);
11
+ } else {
12
+ formattedSeconds = seconds.toFixed(1);
13
+ }
14
+
15
+ return `${minutes ? `${minutes}m ` : ''}${formattedSeconds}s`;
16
+ };