@100mslive/roomkit-react 0.3.15 → 0.3.16-alpha.1

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 (43) hide show
  1. package/dist/{HLSView-ICCDFOHV.js → HLSView-ERPYXVOY.js} +2 -2
  2. package/dist/{HLSView-SFAI4O64.css → HLSView-T2PXKR6I.css} +3 -3
  3. package/dist/{HLSView-SFAI4O64.css.map → HLSView-T2PXKR6I.css.map} +1 -1
  4. package/dist/Prebuilt/components/Chat/ChatBody.d.ts +887 -0
  5. package/dist/Prebuilt/components/Chat/ChatFooter.d.ts +1 -1
  6. package/dist/Prebuilt/components/Chat/utils.d.ts +2 -0
  7. package/dist/Prebuilt/components/PIP/PIPChat.d.ts +2 -0
  8. package/dist/Prebuilt/components/PIP/PIPChatOption.d.ts +5 -0
  9. package/dist/Prebuilt/components/PIP/PIPProvider.d.ts +6 -0
  10. package/dist/Prebuilt/components/PIP/PIPWindow.d.ts +7 -0
  11. package/dist/Prebuilt/components/PIP/context.d.ts +8 -0
  12. package/dist/Prebuilt/components/PIP/usePIPChat.d.ts +5 -0
  13. package/dist/Prebuilt/components/PIP/usePIPWindow.d.ts +2 -0
  14. package/dist/{chunk-WKJZMVEX.js → chunk-NKC36NL4.js} +8533 -8150
  15. package/dist/chunk-NKC36NL4.js.map +7 -0
  16. package/dist/index.cjs.css +2 -2
  17. package/dist/index.cjs.css.map +1 -1
  18. package/dist/index.cjs.js +3921 -3481
  19. package/dist/index.cjs.js.map +4 -4
  20. package/dist/index.css +2 -2
  21. package/dist/index.css.map +1 -1
  22. package/dist/index.js +1 -1
  23. package/dist/meta.cjs.json +1790 -1438
  24. package/dist/meta.esbuild.json +2330 -1978
  25. package/package.json +8 -7
  26. package/src/Prebuilt/components/AudioVideoToggle.tsx +14 -7
  27. package/src/Prebuilt/components/Chat/ChatBody.tsx +4 -12
  28. package/src/Prebuilt/components/Chat/ChatFooter.tsx +2 -3
  29. package/src/Prebuilt/components/Chat/utils.ts +11 -0
  30. package/src/Prebuilt/components/Footer/RoleAccordion.tsx +15 -1
  31. package/src/Prebuilt/components/Header/HeaderComponents.jsx +2 -3
  32. package/src/Prebuilt/components/MoreSettings/MoreSettings.tsx +4 -1
  33. package/src/Prebuilt/components/MoreSettings/SplitComponents/DesktopOptions.tsx +13 -5
  34. package/src/Prebuilt/components/PIP/PIPChat.tsx +225 -0
  35. package/src/Prebuilt/components/PIP/PIPChatOption.tsx +18 -0
  36. package/src/Prebuilt/components/PIP/PIPProvider.tsx +56 -0
  37. package/src/Prebuilt/components/PIP/PIPWindow.tsx +13 -0
  38. package/src/Prebuilt/components/PIP/context.ts +10 -0
  39. package/src/Prebuilt/components/PIP/usePIPChat.tsx +103 -0
  40. package/src/Prebuilt/components/PIP/usePIPWindow.tsx +12 -0
  41. package/src/Prebuilt/components/hooks/useMetadata.tsx +2 -1
  42. package/dist/chunk-WKJZMVEX.js.map +0 -7
  43. /package/dist/{HLSView-ICCDFOHV.js.map → HLSView-ERPYXVOY.js.map} +0 -0
package/package.json CHANGED
@@ -10,7 +10,7 @@
10
10
  "prebuilt",
11
11
  "roomkit"
12
12
  ],
13
- "version": "0.3.15",
13
+ "version": "0.3.16-alpha.1",
14
14
  "author": "100ms",
15
15
  "license": "MIT",
16
16
  "repository": {
@@ -75,12 +75,12 @@
75
75
  "react": ">=17.0.2 <19.0.0"
76
76
  },
77
77
  "dependencies": {
78
- "@100mslive/hls-player": "0.3.15",
78
+ "@100mslive/hls-player": "0.3.16-alpha.1",
79
79
  "@100mslive/hms-noise-cancellation": "0.0.1",
80
- "@100mslive/hms-virtual-background": "1.13.15",
81
- "@100mslive/hms-whiteboard": "0.0.5",
82
- "@100mslive/react-icons": "0.10.15",
83
- "@100mslive/react-sdk": "0.10.15",
80
+ "@100mslive/hms-virtual-background": "1.13.16-alpha.1",
81
+ "@100mslive/hms-whiteboard": "0.0.6-alpha.1",
82
+ "@100mslive/react-icons": "0.10.16-alpha.1",
83
+ "@100mslive/react-sdk": "0.10.16-alpha.1",
84
84
  "@100mslive/types-prebuilt": "0.12.9",
85
85
  "@emoji-mart/data": "^1.0.6",
86
86
  "@emoji-mart/react": "^1.0.1",
@@ -104,6 +104,7 @@
104
104
  "eventemitter2": "^6.4.9",
105
105
  "lodash.merge": "^4.6.2",
106
106
  "qrcode.react": "^3.1.0",
107
+ "react-dom": "^18.2.0",
107
108
  "react-draggable": "^4.4.5",
108
109
  "react-intersection-observer": "^9.4.3",
109
110
  "react-swipeable": "^7.0.1",
@@ -116,5 +117,5 @@
116
117
  "uuid": "^8.3.2",
117
118
  "worker-timers": "^7.0.40"
118
119
  },
119
- "gitHead": "deb50b4115cbc0457e9ab3418fa7710895020664"
120
+ "gitHead": "2ae950c4b6a1aed199e246ce1b8f9d2c1c2ebe7c"
120
121
  }
@@ -109,6 +109,9 @@ const useNoiseCancellationWithPlugin = () => {
109
109
  if (inProgress) {
110
110
  return;
111
111
  }
112
+ if (!krispPlugin.checkSupport().isSupported) {
113
+ throw Error('Krisp plugin is not supported');
114
+ }
112
115
  setInProgress(true);
113
116
  if (enabled) {
114
117
  await actions.addPluginToAudioTrack(krispPlugin);
@@ -281,13 +284,17 @@ export const AudioVideoToggle = ({ hideOptions = false }: { hideOptions?: boolea
281
284
  useEffect(() => {
282
285
  (async () => {
283
286
  if (isNoiseCancellationEnabled && !isKrispPluginAdded && !inProgress && localPeer?.audioTrack) {
284
- await setNoiseCancellationWithPlugin(true);
285
- ToastManager.addToast({
286
- title: `Noise Reduction Enabled`,
287
- variant: 'standard',
288
- duration: 2000,
289
- icon: <AudioLevelIcon />,
290
- });
287
+ try {
288
+ await setNoiseCancellationWithPlugin(true);
289
+ ToastManager.addToast({
290
+ title: `Noise Reduction Enabled`,
291
+ variant: 'standard',
292
+ duration: 2000,
293
+ icon: <AudioLevelIcon />,
294
+ });
295
+ } catch (error) {
296
+ console.error(error);
297
+ }
291
298
  }
292
299
  })();
293
300
  // eslint-disable-next-line react-hooks/exhaustive-deps
@@ -28,18 +28,9 @@ import { useRoomLayoutConferencingScreen } from '../../provider/roomLayoutProvid
28
28
  // @ts-ignore: No implicit Any
29
29
  import { useSetSubscribedChatSelector } from '../AppData/useUISettings';
30
30
  import { usePinnedBy } from '../hooks/usePinnedBy';
31
+ import { formatTime } from './utils';
31
32
  import { CHAT_SELECTOR, SESSION_STORE_KEY } from '../../common/constants';
32
33
 
33
- const formatTime = (date: Date) => {
34
- if (!(date instanceof Date)) {
35
- return '';
36
- }
37
- const hours = date.getHours();
38
- const minutes = date.getMinutes();
39
- const suffix = hours > 11 ? 'PM' : 'AM';
40
- return `${hours < 10 ? '0' : ''}${hours}:${minutes < 10 ? '0' : ''}${minutes} ${suffix}`;
41
- };
42
-
43
34
  const rowHeights: Record<number, { size: number; id: string }> = {};
44
35
  let listInstance: VariableSizeList | null = null; //eslint-disable-line
45
36
  function getRowHeight(index: number) {
@@ -109,7 +100,7 @@ const MessageTypeContainer = ({ left, right }: { left?: string; right?: string }
109
100
  );
110
101
  };
111
102
 
112
- const MessageType = ({
103
+ export const MessageType = ({
113
104
  roles,
114
105
  hasCurrentUserSent,
115
106
  receiver,
@@ -172,7 +163,8 @@ const getMessageType = ({ roles, receiver }: { roles?: HMSRoleName[]; receiver?:
172
163
  }
173
164
  return receiver ? 'private' : '';
174
165
  };
175
- const SenderName = styled(Text, {
166
+
167
+ export const SenderName = styled(Text, {
176
168
  overflow: 'hidden',
177
169
  textOverflow: 'ellipsis',
178
170
  whiteSpace: 'nowrap',
@@ -20,10 +20,9 @@ import { useIsPeerBlacklisted } from '../hooks/useChatBlacklist';
20
20
  // @ts-ignore: No implicit any
21
21
  import { useEmojiPickerStyles } from './useEmojiPickerStyles';
22
22
  import { useDefaultChatSelection, useLandscapeHLSStream, useMobileHLSStream } from '../../common/hooks';
23
+ import { CHAT_MESSAGE_LIMIT } from './utils';
23
24
  import { CHAT_SELECTOR, SESSION_STORE_KEY } from '../../common/constants';
24
25
 
25
- const CHAT_MESSAGE_LIMIT = 2000;
26
-
27
26
  const TextArea = styled('textarea', {
28
27
  width: '100%',
29
28
  bg: 'transparent',
@@ -76,7 +75,7 @@ function EmojiPicker({ onSelect }: { onSelect: (emoji: any) => void }) {
76
75
  );
77
76
  }
78
77
 
79
- export const ChatFooter = ({ onSend, children }: { onSend: (count: number) => void; children: ReactNode }) => {
78
+ export const ChatFooter = ({ onSend, children }: { onSend: (count: number) => void; children?: ReactNode }) => {
80
79
  const hmsActions = useHMSActions();
81
80
  const inputRef = useRef<HTMLTextAreaElement>(null);
82
81
  const [draftMessage, setDraftMessage] = useChatDraftMessage();
@@ -0,0 +1,11 @@
1
+ export const formatTime = (date: Date) => {
2
+ if (!(date instanceof Date)) {
3
+ return '';
4
+ }
5
+ const hours = date.getHours();
6
+ const minutes = date.getMinutes();
7
+ const suffix = hours > 11 ? 'PM' : 'AM';
8
+ return `${hours < 10 ? '0' : ''}${hours}:${minutes < 10 ? '0' : ''}${minutes} ${suffix}`;
9
+ };
10
+
11
+ export const CHAT_MESSAGE_LIMIT = 2000;
@@ -122,7 +122,21 @@ export const RoleAccordion = ({
122
122
  <Accordion.Content contentStyles={{ border: '1px solid $border_default', borderTop: 'none' }}>
123
123
  <FixedSizeList
124
124
  itemSize={ROW_HEIGHT}
125
- itemData={{ peerList: peersInAccordion, isConnected, isHandRaisedAccordion }}
125
+ itemData={{
126
+ peerList: isHandRaisedAccordion
127
+ ? peersInAccordion.sort((a, b) => {
128
+ try {
129
+ const aHandRaisedAt = JSON.parse(a.metadata || '{}').handRaisedAt;
130
+ const bHandRaisedAt = JSON.parse(b.metadata || '{}').handRaisedAt;
131
+ return aHandRaisedAt - bHandRaisedAt;
132
+ } catch (err) {
133
+ return 0;
134
+ }
135
+ })
136
+ : peersInAccordion,
137
+ isConnected,
138
+ isHandRaisedAccordion,
139
+ }}
126
140
  itemKey={itemKey}
127
141
  itemCount={peersInAccordion.length}
128
142
  width={width}
@@ -1,7 +1,7 @@
1
1
  import React, { useEffect, useState } from 'react';
2
2
  import { selectDominantSpeaker, useHMSStore } from '@100mslive/react-sdk';
3
3
  import { VolumeOneIcon } from '@100mslive/react-icons';
4
- import { Flex, styled, Text, textEllipsis, VerticalDivider } from '../../../';
4
+ import { Flex, styled, Text, textEllipsis } from '../../../';
5
5
  import { useRoomLayout } from '../../provider/roomLayoutProvider';
6
6
 
7
7
  export const SpeakerTag = () => {
@@ -14,9 +14,8 @@ export const SpeakerTag = () => {
14
14
  justify="center"
15
15
  css={{ flex: '1 1 0', color: '$on_surface_high', '@md': { display: 'none' } }}
16
16
  >
17
- <VerticalDivider css={{ ml: '$8' }} />
18
17
  <VolumeOneIcon />
19
- <Text variant="md" css={{ ...textEllipsis(200), ml: '$2' }} title={dominantSpeaker.name}>
18
+ <Text variant="sm" css={{ ...textEllipsis(200), ml: '$2' }} title={dominantSpeaker.name}>
20
19
  {dominantSpeaker.name}
21
20
  </Text>
22
21
  </Flex>
@@ -10,6 +10,7 @@ import { DesktopOptions } from './SplitComponents/DesktopOptions';
10
10
  // @ts-ignore: No implicit Any
11
11
  import { MwebOptions } from './SplitComponents/MwebOptions';
12
12
  import { config as cssConfig } from '../../..';
13
+ import { PIPProvider } from '../PIP/PIPProvider';
13
14
  import { useLandscapeHLSStream } from '../../common/hooks';
14
15
 
15
16
  export const MoreSettings = ({
@@ -24,6 +25,8 @@ export const MoreSettings = ({
24
25
  return isMobile || isLandscapeHLSStream ? (
25
26
  <MwebOptions elements={elements} screenType={screenType} />
26
27
  ) : (
27
- <DesktopOptions elements={elements} screenType={screenType} />
28
+ <PIPProvider>
29
+ <DesktopOptions elements={elements} screenType={screenType} />
30
+ </PIPProvider>
28
31
  );
29
32
  };
@@ -28,8 +28,11 @@ import { Checkbox, Dropdown, Flex, Switch, Text, Tooltip } from '../../../..';
28
28
  import IconButton from '../../../IconButton';
29
29
  // @ts-ignore: No implicit any
30
30
  import { PIP } from '../../PIP';
31
+ import { PIPChat } from '../../PIP/PIPChat';
31
32
  // @ts-ignore: No implicit any
33
+ import { PIPChatOption } from '../../PIP/PIPChatOption';
32
34
  import { PictureInPicture } from '../../PIP/PIPManager';
35
+ import { PIPWindow } from '../../PIP/PIPWindow';
33
36
  // @ts-ignore: No implicit any
34
37
  import { RoleChangeModal } from '../../RoleChangeModal';
35
38
  // @ts-ignore: No implicit any
@@ -48,6 +51,8 @@ import { MuteAllModal } from '../MuteAllModal';
48
51
  import { useDropdownList } from '../../hooks/useDropdownList';
49
52
  import { useMyMetadata } from '../../hooks/useMetadata';
50
53
  // @ts-ignore: No implicit any
54
+ import { usePIPChat } from '../../PIP/usePIPChat';
55
+ // @ts-ignore: No implicit any
51
56
  import { APP_DATA, isMacOS } from '../../../common/constants';
52
57
 
53
58
  const MODALS = {
@@ -79,6 +84,8 @@ export const DesktopOptions = ({
79
84
  const isBRBEnabled = !!elements?.brb;
80
85
  const isTranscriptionAllowed = useHMSStore(selectIsTranscriptionAllowedByMode(HMSTranscriptionMode.CAPTION));
81
86
  const isTranscriptionEnabled = useHMSStore(selectIsTranscriptionEnabled);
87
+ const { isSupported, pipWindow, requestPipWindow } = usePIPChat();
88
+ const showPipChatOption = !!elements?.chat && isSupported;
82
89
 
83
90
  useDropdownList({ open: openModals.size > 0, name: 'MoreSettings' });
84
91
 
@@ -98,6 +105,11 @@ export const DesktopOptions = ({
98
105
 
99
106
  return (
100
107
  <Fragment>
108
+ {isSupported && pipWindow ? (
109
+ <PIPWindow pipWindow={pipWindow}>
110
+ <PIPChat />
111
+ </PIPWindow>
112
+ ) : null}
101
113
  <Dropdown.Root
102
114
  open={openModals.has(MODALS.MORE_SETTINGS)}
103
115
  onOpenChange={value => updateState(MODALS.MORE_SETTINGS, value)}
@@ -168,13 +180,9 @@ export const DesktopOptions = ({
168
180
  </Dropdown.Item>
169
181
  ) : null}
170
182
 
183
+ <PIPChatOption showPIPChat={showPipChatOption} openChat={async () => await requestPipWindow(350, 500)} />
171
184
  <FullScreenItem />
172
- {/* {isAllowedToPublish.screen && isEmbedEnabled && (
173
- <EmbedUrl setShowOpenUrl={() => updateState(MODALS.EMBED_URL, true)} />
174
- )} */}
175
-
176
185
  <Dropdown.ItemSeparator css={{ mx: 0 }} />
177
-
178
186
  <Dropdown.Item onClick={() => updateState(MODALS.DEVICE_SETTINGS, true)} data-testid="device_settings_btn">
179
187
  <SettingsIcon />
180
188
  <Text variant="sm" css={{ ml: '$4' }}>
@@ -0,0 +1,225 @@
1
+ import React, { useCallback, useEffect, useMemo, useState } from 'react';
2
+ import {
3
+ selectHMSMessages,
4
+ selectLocalPeerID,
5
+ selectSessionStore,
6
+ selectUnreadHMSMessagesCount,
7
+ useHMSStore,
8
+ } from '@100mslive/react-sdk';
9
+ import { SendIcon } from '@100mslive/react-icons';
10
+ import { Box, Flex } from '../../../Layout';
11
+ import { Text } from '../../../Text';
12
+ import { TextArea } from '../../../TextArea';
13
+ import { Tooltip } from '../../../Tooltip';
14
+ import IconButton from '../../IconButton';
15
+ import { AnnotisedMessage } from '../Chat/ChatBody';
16
+ import { useRoomLayoutConferencingScreen } from '../../provider/roomLayoutProvider/hooks/useRoomLayoutScreen';
17
+ import { CHAT_MESSAGE_LIMIT, formatTime } from '../Chat/utils';
18
+ import { SESSION_STORE_KEY } from '../../common/constants';
19
+
20
+ export const PIPChat = () => {
21
+ const messages = useHMSStore(selectHMSMessages);
22
+ const localPeerID = useHMSStore(selectLocalPeerID);
23
+ const count = useHMSStore(selectUnreadHMSMessagesCount);
24
+ const [unreadMessageCount, setUnreadMessageCount] = useState(0);
25
+
26
+ const getSenderName = useCallback(
27
+ (senderName: string, senderID?: string) => {
28
+ const slicedName = senderName.length > 10 ? senderName.slice(0, 10) + '...' : senderName;
29
+ return slicedName + (senderID === localPeerID ? ' (You)' : '');
30
+ },
31
+ [localPeerID],
32
+ );
33
+
34
+ useEffect(() => {
35
+ const timeoutId = setTimeout(() => {
36
+ setUnreadMessageCount(count);
37
+ }, 100);
38
+ return () => clearTimeout(timeoutId);
39
+ }, [count]);
40
+
41
+ const blacklistedMessageIDs = useHMSStore(selectSessionStore(SESSION_STORE_KEY.CHAT_MESSAGE_BLACKLIST));
42
+ const filteredMessages = useMemo(() => {
43
+ const blacklistedMessageIDSet = new Set(blacklistedMessageIDs || []);
44
+ return messages?.filter(message => message.type === 'chat' && !blacklistedMessageIDSet.has(message.id)) || [];
45
+ }, [blacklistedMessageIDs, messages]);
46
+ const { elements } = useRoomLayoutConferencingScreen();
47
+ const message_placeholder = elements?.chat?.message_placeholder || 'Send a message';
48
+ const canSendChatMessages = !!elements?.chat?.public_chat_enabled || !!elements?.chat?.roles_whitelist?.length;
49
+
50
+ return (
51
+ <div style={{ height: '100%' }}>
52
+ <Box
53
+ id="chat-container"
54
+ css={{
55
+ bg: '$surface_dim',
56
+ overflowY: 'auto',
57
+ // Subtracting height of footer
58
+ h: canSendChatMessages ? 'calc(100% - 90px)' : '100%',
59
+ position: 'relative',
60
+ }}
61
+ >
62
+ {unreadMessageCount ? (
63
+ <Box
64
+ id="new-message-notif"
65
+ style={{
66
+ position: 'fixed',
67
+ bottom: '76px',
68
+ right: '4px',
69
+ }}
70
+ >
71
+ <Text
72
+ variant="xs"
73
+ css={{ cursor: 'pointer' }}
74
+ style={{ color: 'white', background: 'gray', padding: '4px', borderRadius: '4px' }}
75
+ >
76
+ {unreadMessageCount === 1 ? 'New message' : `${unreadMessageCount} new messages`}
77
+ </Text>
78
+ </Box>
79
+ ) : (
80
+ ''
81
+ )}
82
+ {filteredMessages.length === 0 ? (
83
+ <div
84
+ style={{ display: 'flex', height: '100%', width: '100%', alignItems: 'center', justifyContent: 'center' }}
85
+ >
86
+ <Text>No messages here yet</Text>
87
+ </div>
88
+ ) : (
89
+ filteredMessages.map(message => (
90
+ <Box className="pip-message" key={message.id} id={message.id} style={{ padding: '8px 0.75rem' }}>
91
+ <Flex style={{ width: '100%', alignItems: 'center', justifyContent: 'between' }}>
92
+ <Text
93
+ style={{ display: 'flex', justifyContent: 'between', width: '100%', alignItems: 'center' }}
94
+ css={{
95
+ color: '$on_surface_high',
96
+ fontWeight: '$semiBold',
97
+ }}
98
+ >
99
+ <Flex style={{ flexGrow: 1, gap: '2px', alignItems: 'center' }}>
100
+ {message.senderName === 'You' || !message.senderName ? (
101
+ <Text as="span" variant="sub2" css={{ color: '$on_surface_high', fontWeight: '$semiBold' }}>
102
+ {message.senderName || 'Anonymous'}
103
+ </Text>
104
+ ) : (
105
+ <Tooltip title={message.senderName} side="top" align="start">
106
+ <Text as="span" variant="sub2" css={{ color: '$on_surface_high', fontWeight: '$semiBold' }}>
107
+ {getSenderName(message.senderName, message?.sender)}
108
+ </Text>
109
+ </Tooltip>
110
+ )}
111
+ {message.recipientRoles ? (
112
+ <Text as="span" variant="sub2" css={{ color: '$on_surface_high', fontWeight: '$semiBold' }}>
113
+ to {message.recipientRoles} (Group)
114
+ </Text>
115
+ ) : null}
116
+ {message.recipientPeer ? (
117
+ <Text as="span" variant="sub2" css={{ color: '$on_surface_high', fontWeight: '$semiBold' }}>
118
+ (DM)
119
+ </Text>
120
+ ) : null}
121
+ </Flex>
122
+
123
+ <Text
124
+ variant="xs"
125
+ css={{
126
+ color: '$on_surface_medium',
127
+ flexShrink: 0,
128
+ p: '$2',
129
+ whitespace: 'nowrap',
130
+ }}
131
+ >
132
+ {formatTime(message.time)}
133
+ </Text>
134
+ </Text>
135
+ </Flex>
136
+ <Text
137
+ variant="sm"
138
+ css={{
139
+ w: '100%',
140
+ mt: '$2',
141
+ wordBreak: 'break-word',
142
+ whiteSpace: 'pre-wrap',
143
+ userSelect: 'all',
144
+ color: '$on_surface_high',
145
+ }}
146
+ >
147
+ <AnnotisedMessage message={message.message} />
148
+ </Text>
149
+ </Box>
150
+ ))
151
+ )}
152
+ <div id="marker" style={{ height: filteredMessages.length ? '1px' : 0 }} />
153
+ </Box>
154
+ {canSendChatMessages && (
155
+ <Box css={{ bg: '$surface_dim' }}>
156
+ <Flex css={{ px: '$4', pb: '3px', gap: '$2', alignItems: 'center' }}>
157
+ <Text variant="caption">To:</Text>
158
+ <Flex css={{ bg: '$primary_bright', color: '$on_primary_high', r: '$2' }}>
159
+ <select
160
+ id="selector"
161
+ style={{
162
+ background: 'inherit',
163
+ color: 'inherit',
164
+ border: 'none',
165
+ outline: 'none',
166
+ borderRadius: '4px',
167
+ padding: '0 2px',
168
+ }}
169
+ defaultValue={elements.chat?.public_chat_enabled ? 'Everyone' : elements.chat?.roles_whitelist?.[0]}
170
+ >
171
+ {elements.chat?.roles_whitelist?.map(role => (
172
+ <option key={role} value={role}>
173
+ {role}
174
+ </option>
175
+ ))}
176
+ {elements.chat?.public_chat_enabled ? <option value="Everyone">Everyone</option> : ''}
177
+ </select>
178
+ </Flex>
179
+ </Flex>
180
+ <Flex
181
+ align="center"
182
+ css={{
183
+ bg: '$surface_default',
184
+ minHeight: '$16',
185
+ width: '100%',
186
+ py: '$6',
187
+ pl: '$4',
188
+ boxSizing: 'border-box',
189
+ gap: '$2',
190
+ r: '$2',
191
+ }}
192
+ >
193
+ <TextArea
194
+ id="chat-input"
195
+ maxLength={CHAT_MESSAGE_LIMIT}
196
+ style={{ border: 'none', resize: 'none' }}
197
+ css={{
198
+ w: '100%',
199
+ c: '$on_surface_high',
200
+ padding: '0.25rem !important',
201
+ }}
202
+ placeholder={message_placeholder}
203
+ required
204
+ autoComplete="off"
205
+ aria-autocomplete="none"
206
+ />
207
+
208
+ <IconButton
209
+ id="send-btn"
210
+ css={{
211
+ ml: 'auto',
212
+ height: 'max-content',
213
+ mr: '$4',
214
+ '&:hover': { c: '$on_surface_medium' },
215
+ }}
216
+ data-testid="send_msg_btn"
217
+ >
218
+ <SendIcon />
219
+ </IconButton>
220
+ </Flex>
221
+ </Box>
222
+ )}
223
+ </div>
224
+ );
225
+ };
@@ -0,0 +1,18 @@
1
+ import React from 'react';
2
+ import { ExternalLinkIcon } from '@100mslive/react-icons';
3
+ import { Dropdown } from '../../../Dropdown';
4
+ import { Text } from '../../../Text';
5
+
6
+ export const PIPChatOption = ({ openChat, showPIPChat }: { openChat: () => void; showPIPChat: boolean }) => {
7
+ if (!showPIPChat) {
8
+ return <></>;
9
+ }
10
+ return (
11
+ <Dropdown.Item onClick={openChat} data-testid="brb_btn">
12
+ <ExternalLinkIcon height={18} width={18} style={{ padding: '0 $2' }} />
13
+ <Text variant="sm" css={{ ml: '$4', color: '$on_surface_high' }}>
14
+ Pop out Chat
15
+ </Text>
16
+ </Dropdown.Item>
17
+ );
18
+ };
@@ -0,0 +1,56 @@
1
+ import React, { useCallback, useMemo, useState } from 'react';
2
+ import { PIPContext } from './context';
3
+
4
+ type PIPProviderProps = {
5
+ children: React.ReactNode;
6
+ };
7
+
8
+ export const PIPProvider = ({ children }: PIPProviderProps) => {
9
+ // Detect if the feature is available.
10
+ const isSupported = 'documentPictureInPicture' in window;
11
+
12
+ // Expose pipWindow that is currently active
13
+ const [pipWindow, setPipWindow] = useState<Window | null>(null);
14
+
15
+ // Close pipWidnow programmatically
16
+ const closePipWindow = useCallback(() => {
17
+ if (pipWindow != null) {
18
+ pipWindow.close();
19
+ setPipWindow(null);
20
+ }
21
+ }, [pipWindow]);
22
+
23
+ // Open new pipWindow
24
+ const requestPipWindow = useCallback(
25
+ async (width: number, height: number) => {
26
+ // We don't want to allow multiple requests.
27
+ if (pipWindow != null) {
28
+ return;
29
+ }
30
+ // @ts-ignore for documentPIP
31
+ const pip = await window.documentPictureInPicture.requestWindow({
32
+ width,
33
+ height,
34
+ });
35
+
36
+ // Detect when window is closed by user
37
+ pip.addEventListener('pagehide', () => {
38
+ setPipWindow(null);
39
+ });
40
+
41
+ setPipWindow(pip);
42
+ },
43
+ [pipWindow],
44
+ );
45
+
46
+ const value = useMemo(() => {
47
+ return {
48
+ isSupported,
49
+ pipWindow,
50
+ requestPipWindow,
51
+ closePipWindow,
52
+ };
53
+ }, [closePipWindow, isSupported, pipWindow, requestPipWindow]);
54
+
55
+ return <PIPContext.Provider value={value}>{children}</PIPContext.Provider>;
56
+ };
@@ -0,0 +1,13 @@
1
+ import React from 'react';
2
+ import { createPortal } from 'react-dom';
3
+
4
+ type PIPWindowProps = {
5
+ pipWindow: Window;
6
+ children: React.ReactNode;
7
+ };
8
+
9
+ export const PIPWindow = ({ pipWindow, children }: PIPWindowProps) => {
10
+ pipWindow.document.body.style.margin = '0';
11
+ pipWindow.document.body.style.overflowX = 'hidden';
12
+ return createPortal(children, pipWindow.document.body);
13
+ };
@@ -0,0 +1,10 @@
1
+ import { createContext } from 'react';
2
+
3
+ export type PIPContextType = {
4
+ isSupported: boolean;
5
+ pipWindow: Window | null;
6
+ requestPipWindow: (width: number, height: number) => Promise<void>;
7
+ closePipWindow: () => void;
8
+ };
9
+
10
+ export const PIPContext = createContext<PIPContextType | undefined>(undefined);