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

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
+ };