@ai-matrx/meet 0.7.3 → 0.7.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.
- package/CHANGELOG.md +74 -0
- package/dist/index.cjs +544 -35
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +219 -24
- package/dist/index.d.ts +219 -24
- package/dist/index.js +544 -35
- package/dist/index.js.map +1 -1
- package/dist/react.cjs +1886 -162
- package/dist/react.cjs.map +1 -1
- package/dist/react.d.cts +136 -12
- package/dist/react.d.ts +136 -12
- package/dist/react.js +1887 -163
- package/dist/react.js.map +1 -1
- package/dist/styles.css +530 -0
- package/package.json +4 -3
package/dist/react.cjs
CHANGED
|
@@ -51,19 +51,23 @@ __export(react_exports, {
|
|
|
51
51
|
ControlBar: () => ControlBar,
|
|
52
52
|
ControlButton: () => ControlButton,
|
|
53
53
|
DATA_TOPICS: () => DATA_TOPICS,
|
|
54
|
+
DEFAULT_EFFECTS: () => DEFAULT_EFFECTS,
|
|
54
55
|
DEFAULT_PAGE_SIZE: () => DEFAULT_PAGE_SIZE,
|
|
55
56
|
END_MEETING_NOT_SERVED: () => END_MEETING_NOT_SERVED,
|
|
56
57
|
FILE_SESSION_PATH: () => FILE_SESSION_PATH,
|
|
58
|
+
FlagIcon: () => FlagIcon,
|
|
57
59
|
GUEST_RECORD_ACCESS_CHOICES: () => GUEST_RECORD_ACCESS_CHOICES,
|
|
58
60
|
GUEST_RECORD_ACCESS_LEVELS: () => GUEST_RECORD_ACCESS_LEVELS,
|
|
59
61
|
GUEST_RECORD_ACCESS_METADATA_KEY: () => GUEST_RECORD_ACCESS_METADATA_KEY,
|
|
60
62
|
GUEST_RECORD_NOT_SHARED: () => GUEST_RECORD_NOT_SHARED,
|
|
61
63
|
GuestRecordAccessControl: () => GuestRecordAccessControl,
|
|
62
64
|
HandIcon: () => HandIcon,
|
|
65
|
+
HostMenu: () => HostMenu,
|
|
63
66
|
IDLE_MEDIA: () => IDLE_MEDIA,
|
|
64
67
|
IDLE_TRACK: () => IDLE_TRACK,
|
|
65
68
|
IncomingCallCard: () => IncomingCallCard,
|
|
66
69
|
IncomingCallHost: () => IncomingCallHost,
|
|
70
|
+
KeyboardIcon: () => KeyboardIcon,
|
|
67
71
|
LISTEN_ONLY_NOTICE: () => LISTEN_ONLY_NOTICE,
|
|
68
72
|
LayoutIcon: () => LayoutIcon,
|
|
69
73
|
LobbyPanel: () => LobbyPanel,
|
|
@@ -73,15 +77,20 @@ __export(react_exports, {
|
|
|
73
77
|
MEET_ANSWER_CONTEXT_EVENT: () => MEET_ANSWER_CONTEXT_EVENT,
|
|
74
78
|
MEET_ANSWER_EVENTS: () => MEET_ANSWER_EVENTS,
|
|
75
79
|
MEET_EVENTS: () => MEET_EVENTS,
|
|
80
|
+
MEET_REACTIONS: () => MEET_REACTIONS,
|
|
76
81
|
MEET_ROUTES: () => MEET_ROUTES,
|
|
77
82
|
MEET_SCHEMA: () => MEET_SCHEMA,
|
|
83
|
+
MEET_SHORTCUTS: () => MEET_SHORTCUTS,
|
|
84
|
+
MODERATION_ACTIONS: () => MODERATION_ACTIONS,
|
|
78
85
|
MeetError: () => MeetError,
|
|
79
86
|
MeetProvider: () => MeetProvider,
|
|
80
87
|
MeetingList: () => MeetingList,
|
|
88
|
+
MeetingOverlays: () => MeetingOverlays,
|
|
81
89
|
MeetingRecordView: () => MeetingRecordView,
|
|
82
90
|
MeetingRoom: () => MeetingRoom,
|
|
83
91
|
MicIcon: () => MicIcon,
|
|
84
92
|
MicOffIcon: () => MicOffIcon,
|
|
93
|
+
MoreIcon: () => MoreIcon,
|
|
85
94
|
NOTE_TAKER_IDENTITY_PREFIX: () => NOTE_TAKER_IDENTITY_PREFIX,
|
|
86
95
|
NOTE_TAKER_METADATA_KEY: () => NOTE_TAKER_METADATA_KEY,
|
|
87
96
|
NO_GRANTS: () => NO_GRANTS,
|
|
@@ -89,12 +98,15 @@ __export(react_exports, {
|
|
|
89
98
|
PARTICIPANT_CAN_RECORD_ATTRIBUTE: () => PARTICIPANT_CAN_RECORD_ATTRIBUTE,
|
|
90
99
|
PARTICIPANT_ROLE_ATTRIBUTE: () => PARTICIPANT_ROLE_ATTRIBUTE,
|
|
91
100
|
ParticipantTile: () => ParticipantTile,
|
|
101
|
+
PencilIcon: () => PencilIcon,
|
|
92
102
|
PeopleIcon: () => PeopleIcon,
|
|
93
103
|
PeoplePanel: () => PeoplePanel,
|
|
104
|
+
PersonMenu: () => PersonMenu,
|
|
94
105
|
PhoneIcon: () => PhoneIcon,
|
|
95
106
|
PhoneOffIcon: () => PhoneOffIcon,
|
|
96
107
|
PinIcon: () => PinIcon,
|
|
97
108
|
PreJoin: () => PreJoin,
|
|
109
|
+
QualityBars: () => QualityBars,
|
|
98
110
|
REACTION_TTL_MS: () => REACTION_TTL_MS,
|
|
99
111
|
REFUSED_ABSENCE: () => REFUSED_ABSENCE,
|
|
100
112
|
ROLE_RANK: () => ROLE_RANK,
|
|
@@ -108,8 +120,15 @@ __export(react_exports, {
|
|
|
108
120
|
RecordingPlayer: () => RecordingPlayer,
|
|
109
121
|
ScheduleMeeting: () => ScheduleMeeting,
|
|
110
122
|
ScreenShareIcon: () => ScreenShareIcon,
|
|
123
|
+
SearchIcon: () => SearchIcon,
|
|
111
124
|
SettingsIcon: () => SettingsIcon,
|
|
125
|
+
SettingsPanel: () => SettingsPanel,
|
|
126
|
+
ShortcutsSheet: () => ShortcutsSheet,
|
|
127
|
+
SignalIcon: () => SignalIcon,
|
|
128
|
+
SmileIcon: () => SmileIcon,
|
|
112
129
|
SparklesIcon: () => SparklesIcon,
|
|
130
|
+
SpeakerIcon: () => SpeakerIcon,
|
|
131
|
+
SpotlightIcon: () => SpotlightIcon,
|
|
113
132
|
Stage: () => Stage,
|
|
114
133
|
TABLES: () => TABLES,
|
|
115
134
|
TRANSCRIPTION_ATTRIBUTES: () => TRANSCRIPTION_ATTRIBUTES,
|
|
@@ -137,6 +156,7 @@ __export(react_exports, {
|
|
|
137
156
|
createCallCenter: () => createCallCenter,
|
|
138
157
|
createCallInviteHandler: () => createCallInviteHandler,
|
|
139
158
|
createDeviceManager: () => createDeviceManager,
|
|
159
|
+
createInRoomClient: () => createInRoomClient,
|
|
140
160
|
createLiveKitDriver: () => createLiveKitDriver,
|
|
141
161
|
createMeetAi: () => createMeetAi,
|
|
142
162
|
createMeetApi: () => createMeetApi,
|
|
@@ -149,6 +169,7 @@ __export(react_exports, {
|
|
|
149
169
|
createRoomEngine: () => createRoomEngine,
|
|
150
170
|
createTokenClient: () => createTokenClient,
|
|
151
171
|
createWebDeviceStorage: () => createWebDeviceStorage,
|
|
172
|
+
createWebEffectsStorage: () => createWebEffectsStorage,
|
|
152
173
|
createWebRoomTokenStorage: () => createWebRoomTokenStorage,
|
|
153
174
|
displayNameFor: () => displayNameFor,
|
|
154
175
|
fileDownloadPath: () => fileDownloadPath,
|
|
@@ -212,6 +233,7 @@ __export(react_exports, {
|
|
|
212
233
|
settingsPatch: () => settingsPatch,
|
|
213
234
|
snapshotParticipant: () => snapshotParticipant,
|
|
214
235
|
unavailableFrom: () => unavailableFrom,
|
|
236
|
+
useAudioLevel: () => useAudioLevel,
|
|
215
237
|
useCalls: () => useCalls,
|
|
216
238
|
useCanModerate: () => useCanModerate,
|
|
217
239
|
useCanRecord: () => useCanRecord,
|
|
@@ -220,19 +242,23 @@ __export(react_exports, {
|
|
|
220
242
|
useElapsed: () => useElapsed,
|
|
221
243
|
useEndMeeting: () => useEndMeeting,
|
|
222
244
|
useGuestRecordAccess: () => useGuestRecordAccess,
|
|
245
|
+
useHandQueue: () => useHandQueue,
|
|
223
246
|
useHostControls: () => useHostControls,
|
|
224
247
|
useIsHost: () => useIsHost,
|
|
225
248
|
useListenOnlyNotice: () => useListenOnlyNotice,
|
|
226
249
|
useLobby: () => useLobby,
|
|
227
250
|
useLocalMedia: () => useLocalMedia,
|
|
228
251
|
useLocalUserId: () => useLocalUserId,
|
|
252
|
+
useMediaEffects: () => useMediaEffects,
|
|
229
253
|
useMeetAi: () => useMeetAi,
|
|
230
254
|
useMeetHost: () => useMeetHost,
|
|
255
|
+
useMeetShortcuts: () => useMeetShortcuts,
|
|
231
256
|
useMeetSnapshot: () => useMeetSnapshot,
|
|
232
257
|
useMeeting: () => useMeeting,
|
|
233
258
|
useMeetingId: () => useMeetingId,
|
|
234
259
|
useMeetingRecord: () => useMeetingRecord,
|
|
235
260
|
useMeetings: () => useMeetings,
|
|
261
|
+
useModerationNotice: () => useModerationNotice,
|
|
236
262
|
useReactions: () => useReactions,
|
|
237
263
|
useRecording: () => useRecording,
|
|
238
264
|
useRequiredMeetHost: () => useRequiredMeetHost,
|
|
@@ -442,7 +468,16 @@ var MEET_ROUTES = {
|
|
|
442
468
|
*/
|
|
443
469
|
meetingAnnounce: "/api/v1/meet/meetings/{meeting_id}/announce",
|
|
444
470
|
/** Put a one-off meeting on the caller's own Google Calendar (gated by the server). */
|
|
445
|
-
meetingGoogleCalendar: "/api/v1/meet/meetings/{meeting_id}/google-calendar"
|
|
471
|
+
meetingGoogleCalendar: "/api/v1/meet/meetings/{meeting_id}/google-calendar",
|
|
472
|
+
/**
|
|
473
|
+
* Meet wave 2: EVERY in-meeting host power (mute, stop video, mute all, ask to
|
|
474
|
+
* unmute, spotlight, lower hands, co-host, hand over, rename, remove, report).
|
|
475
|
+
* Decided by the server from the meeting itself; a guest's pass rides
|
|
476
|
+
* `x-meet-room-token`.
|
|
477
|
+
*/
|
|
478
|
+
moderate: "/api/v1/meet/moderate",
|
|
479
|
+
/** Meet wave 2: the SAVED chat — `POST` sends to everyone, `GET ?meeting_id=` reads history. */
|
|
480
|
+
chat: "/api/v1/meet/chat"
|
|
446
481
|
};
|
|
447
482
|
function meetingPath(template, meetingId) {
|
|
448
483
|
return template.replace("{meeting_id}", encodeURIComponent(meetingId));
|
|
@@ -1747,6 +1782,11 @@ function captionSegment(args) {
|
|
|
1747
1782
|
}
|
|
1748
1783
|
|
|
1749
1784
|
// src/core/store.ts
|
|
1785
|
+
var DEFAULT_EFFECTS = {
|
|
1786
|
+
noiseSuppression: true,
|
|
1787
|
+
background: "none",
|
|
1788
|
+
backgroundImage: null
|
|
1789
|
+
};
|
|
1750
1790
|
var NO_GRANTS = {
|
|
1751
1791
|
role: "participant",
|
|
1752
1792
|
canRecord: false,
|
|
@@ -1769,7 +1809,13 @@ var EMPTY = {
|
|
|
1769
1809
|
recordingSource: "local",
|
|
1770
1810
|
recordingId: null,
|
|
1771
1811
|
chat: [],
|
|
1812
|
+
chatLoaded: false,
|
|
1813
|
+
chatHistoryError: null,
|
|
1772
1814
|
reactions: [],
|
|
1815
|
+
spotlight: null,
|
|
1816
|
+
moderationNotice: null,
|
|
1817
|
+
actionNotice: null,
|
|
1818
|
+
effects: DEFAULT_EFFECTS,
|
|
1773
1819
|
transcript: [],
|
|
1774
1820
|
notes: [],
|
|
1775
1821
|
names: {},
|
|
@@ -1808,6 +1854,7 @@ function createMeetStore(initial = {}) {
|
|
|
1808
1854
|
setParticipants(participants) {
|
|
1809
1855
|
const names = learnNames(state.names, participants);
|
|
1810
1856
|
const hands = new Map(state.participants.map((p) => [p.identity, p.handRaised]));
|
|
1857
|
+
const handTimes = new Map(state.participants.map((p) => [p.identity, p.handRaisedAt ?? null]));
|
|
1811
1858
|
const quality = new Map(state.participants.map((p) => [p.identity, p.quality]));
|
|
1812
1859
|
commit({
|
|
1813
1860
|
...state,
|
|
@@ -1821,6 +1868,7 @@ function createMeetStore(initial = {}) {
|
|
|
1821
1868
|
// name book. `roster-names.test.ts` fails if a token ever gets past it.
|
|
1822
1869
|
displayName: displayNameFor(names, participant.identity, participant.displayName),
|
|
1823
1870
|
handRaised: hands.get(participant.identity) ?? participant.handRaised,
|
|
1871
|
+
handRaisedAt: handTimes.get(participant.identity) ?? participant.handRaisedAt ?? null,
|
|
1824
1872
|
quality: participant.quality === "unknown" ? quality.get(participant.identity) ?? "unknown" : participant.quality
|
|
1825
1873
|
}))
|
|
1826
1874
|
});
|
|
@@ -1843,11 +1891,17 @@ function createMeetStore(initial = {}) {
|
|
|
1843
1891
|
)
|
|
1844
1892
|
});
|
|
1845
1893
|
},
|
|
1846
|
-
setHand(identity, raised) {
|
|
1894
|
+
setHand(identity, raised, at) {
|
|
1847
1895
|
commit({
|
|
1848
1896
|
...state,
|
|
1849
1897
|
participants: state.participants.map(
|
|
1850
|
-
(participant) => participant.identity === identity ? {
|
|
1898
|
+
(participant) => participant.identity === identity ? {
|
|
1899
|
+
...participant,
|
|
1900
|
+
handRaised: raised,
|
|
1901
|
+
// A hand that is ALREADY up keeps its place in the queue when
|
|
1902
|
+
// it is re-announced (to a late joiner): the first time wins.
|
|
1903
|
+
handRaisedAt: raised ? participant.handRaised && participant.handRaisedAt != null ? participant.handRaisedAt : at ?? Date.now() : null
|
|
1904
|
+
} : participant
|
|
1851
1905
|
)
|
|
1852
1906
|
});
|
|
1853
1907
|
},
|
|
@@ -1857,6 +1911,31 @@ function createMeetStore(initial = {}) {
|
|
|
1857
1911
|
const names = learnName(state.names, message.identity, message.displayName);
|
|
1858
1912
|
commit({ ...state, chat: chat.slice(-CHAT_LIMIT), names });
|
|
1859
1913
|
},
|
|
1914
|
+
loadChat(messages) {
|
|
1915
|
+
const known = new Set(state.chat.map((message) => message.id));
|
|
1916
|
+
const merged = [...messages.filter((message) => !known.has(message.id)), ...state.chat].sort(
|
|
1917
|
+
(a, b) => a.at < b.at ? -1 : a.at > b.at ? 1 : 0
|
|
1918
|
+
);
|
|
1919
|
+
const names = learnNames(
|
|
1920
|
+
state.names,
|
|
1921
|
+
messages.map((message) => ({ identity: message.identity, displayName: message.displayName }))
|
|
1922
|
+
);
|
|
1923
|
+
commit({ ...state, chat: merged.slice(-CHAT_LIMIT), chatLoaded: true, chatHistoryError: null, names });
|
|
1924
|
+
},
|
|
1925
|
+
settleChat(localId, next) {
|
|
1926
|
+
const index = state.chat.findIndex((message) => message.id === localId);
|
|
1927
|
+
if (index === -1) return;
|
|
1928
|
+
const current = state.chat[index];
|
|
1929
|
+
let replacement;
|
|
1930
|
+
if ("failed" in next && typeof next.failed === "string" && !("id" in next)) {
|
|
1931
|
+
replacement = { ...current, failed: next.failed };
|
|
1932
|
+
} else {
|
|
1933
|
+
replacement = next;
|
|
1934
|
+
}
|
|
1935
|
+
const duplicate = replacement.id !== localId && state.chat.some((message) => message.id === replacement.id);
|
|
1936
|
+
const chat = duplicate ? state.chat.filter((message) => message.id !== localId) : state.chat.map((message, position) => position === index ? replacement : message);
|
|
1937
|
+
commit({ ...state, chat });
|
|
1938
|
+
},
|
|
1860
1939
|
addReaction(reaction) {
|
|
1861
1940
|
commit({ ...state, reactions: [...state.reactions, reaction] });
|
|
1862
1941
|
},
|
|
@@ -1947,7 +2026,7 @@ function createMeetStore(initial = {}) {
|
|
|
1947
2026
|
commit({ ...state, media: { ...state.media, [kind]: next } });
|
|
1948
2027
|
},
|
|
1949
2028
|
reset() {
|
|
1950
|
-
commit({ ...EMPTY });
|
|
2029
|
+
commit({ ...EMPTY, effects: state.effects });
|
|
1951
2030
|
}
|
|
1952
2031
|
};
|
|
1953
2032
|
}
|
|
@@ -3202,7 +3281,19 @@ var ROOM_EVENTS = {
|
|
|
3202
3281
|
* production 2026-09-09 the guest lane showed no indicator at all while the
|
|
3203
3282
|
* host recorded (MRI-C8).
|
|
3204
3283
|
*/
|
|
3205
|
-
recording: "matrx.meet.recording"
|
|
3284
|
+
recording: "matrx.meet.recording",
|
|
3285
|
+
/**
|
|
3286
|
+
* A chat message to everyone was SAVED (Meet wave 2). Payload
|
|
3287
|
+
* `{ message: { id, meeting_id, sender_identity, sender_name, body, created_at } }`.
|
|
3288
|
+
*/
|
|
3289
|
+
chat: "matrx.meet.chat",
|
|
3290
|
+
/**
|
|
3291
|
+
* A host or co-host acted (Meet wave 2). Payload `{ meeting_id, kind, … }`:
|
|
3292
|
+
* `muted {source, by}` · `ask-unmute {by}` · `removed {by}` — addressed to the
|
|
3293
|
+
* person concerned; `lower-hands {identities: [...] | "*"}` · `role {identity,
|
|
3294
|
+
* role}` — to everyone.
|
|
3295
|
+
*/
|
|
3296
|
+
moderation: "matrx.meet.moderation"
|
|
3206
3297
|
};
|
|
3207
3298
|
function isRoomEventTopic(topic) {
|
|
3208
3299
|
return topic.startsWith(ROOM_EVENT_PREFIX);
|
|
@@ -3238,6 +3329,10 @@ function applyRoomEvent(args) {
|
|
|
3238
3329
|
return applyEnded(store, payload);
|
|
3239
3330
|
case ROOM_EVENTS.recording:
|
|
3240
3331
|
return applyRecording(store, payload);
|
|
3332
|
+
case ROOM_EVENTS.chat:
|
|
3333
|
+
return applyChat(store, payload);
|
|
3334
|
+
case ROOM_EVENTS.moderation:
|
|
3335
|
+
return applyModeration(store, payload, now);
|
|
3241
3336
|
default:
|
|
3242
3337
|
return ignored(`${topic} is not known to this version of @ai-matrx/meet`);
|
|
3243
3338
|
}
|
|
@@ -3329,6 +3424,101 @@ function applyEnded(store, payload) {
|
|
|
3329
3424
|
store.patch({ meeting: { ...meeting, endedAt } });
|
|
3330
3425
|
return HANDLED;
|
|
3331
3426
|
}
|
|
3427
|
+
function chatFromRow(row) {
|
|
3428
|
+
const id = text2(row["id"]);
|
|
3429
|
+
const identity = text2(row["sender_identity"]);
|
|
3430
|
+
const body = text2(row["body"]);
|
|
3431
|
+
if (id === null || identity === null || body === null) return null;
|
|
3432
|
+
return {
|
|
3433
|
+
id,
|
|
3434
|
+
identity: asParticipantIdentity(identity),
|
|
3435
|
+
displayName: text2(row["sender_name"]) ?? "Participant",
|
|
3436
|
+
text: body,
|
|
3437
|
+
at: text2(row["created_at"]) ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
3438
|
+
// `false` only from a server whose chat table has not landed: the room heard
|
|
3439
|
+
// it, nobody will see it later — and the line says so.
|
|
3440
|
+
saved: row["saved"] !== false
|
|
3441
|
+
};
|
|
3442
|
+
}
|
|
3443
|
+
function applyChat(store, payload) {
|
|
3444
|
+
const row = object(payload["message"]);
|
|
3445
|
+
if (row === null) return ignored("a chat event with no `message` row");
|
|
3446
|
+
const meeting = store.snapshot().meeting;
|
|
3447
|
+
if (meeting !== null && text2(row["meeting_id"]) !== null && row["meeting_id"] !== meeting.id) {
|
|
3448
|
+
return ignored("a chat message for a different meeting");
|
|
3449
|
+
}
|
|
3450
|
+
const message = chatFromRow(row);
|
|
3451
|
+
if (message === null) return ignored("a chat row with no id, sender or body");
|
|
3452
|
+
store.addChat(message);
|
|
3453
|
+
return HANDLED;
|
|
3454
|
+
}
|
|
3455
|
+
function moderationText(kind, payload) {
|
|
3456
|
+
const by = text2(payload["by"]) ?? "The host";
|
|
3457
|
+
switch (kind) {
|
|
3458
|
+
case "muted": {
|
|
3459
|
+
const source = text2(payload["source"]) ?? "microphone";
|
|
3460
|
+
return source === "microphone" ? `${by} muted you.` : source === "camera" ? `${by} turned off your camera.` : `${by} stopped your screen share.`;
|
|
3461
|
+
}
|
|
3462
|
+
case "ask-unmute":
|
|
3463
|
+
return `${by} is asking you to unmute.`;
|
|
3464
|
+
case "removed":
|
|
3465
|
+
return `${by} removed you from this meeting.`;
|
|
3466
|
+
default:
|
|
3467
|
+
return null;
|
|
3468
|
+
}
|
|
3469
|
+
}
|
|
3470
|
+
function applyModeration(store, payload, now) {
|
|
3471
|
+
const snapshot = store.snapshot();
|
|
3472
|
+
const meetingId = text2(payload["meeting_id"]);
|
|
3473
|
+
if (snapshot.meeting !== null && meetingId !== null && meetingId !== snapshot.meeting.id) {
|
|
3474
|
+
return ignored("a moderation event for a different meeting");
|
|
3475
|
+
}
|
|
3476
|
+
const kind = text2(payload["kind"]);
|
|
3477
|
+
switch (kind) {
|
|
3478
|
+
case "lower-hands": {
|
|
3479
|
+
const raw = payload["identities"];
|
|
3480
|
+
const everyone = raw === "*";
|
|
3481
|
+
const named = new Set(
|
|
3482
|
+
Array.isArray(raw) ? raw.filter((value) => typeof value === "string") : []
|
|
3483
|
+
);
|
|
3484
|
+
for (const participant of snapshot.participants) {
|
|
3485
|
+
if (participant.handRaised && (everyone || named.has(participant.identity))) {
|
|
3486
|
+
store.setHand(participant.identity, false);
|
|
3487
|
+
}
|
|
3488
|
+
}
|
|
3489
|
+
return HANDLED;
|
|
3490
|
+
}
|
|
3491
|
+
case "role": {
|
|
3492
|
+
const identity = text2(payload["identity"]);
|
|
3493
|
+
const role = text2(payload["role"]);
|
|
3494
|
+
if (identity === null || role === null || !ROLES.includes(role)) {
|
|
3495
|
+
return ignored("a role event with no identity or an unknown role");
|
|
3496
|
+
}
|
|
3497
|
+
if (identity === snapshot.localIdentity) {
|
|
3498
|
+
store.patch({
|
|
3499
|
+
grants: { ...snapshot.grants, role },
|
|
3500
|
+
moderationNotice: {
|
|
3501
|
+
kind: "role",
|
|
3502
|
+
text: role === "host" ? "You are now the host." : role === "cohost" ? "You are now a co-host." : "You are no longer a co-host.",
|
|
3503
|
+
at: now()
|
|
3504
|
+
}
|
|
3505
|
+
});
|
|
3506
|
+
}
|
|
3507
|
+
return HANDLED;
|
|
3508
|
+
}
|
|
3509
|
+
case "muted":
|
|
3510
|
+
case "ask-unmute":
|
|
3511
|
+
case "removed": {
|
|
3512
|
+
const sentence = moderationText(kind, payload);
|
|
3513
|
+
if (sentence === null) return ignored("a moderation event with no sentence");
|
|
3514
|
+
store.patch({ moderationNotice: { kind, text: sentence, at: now() } });
|
|
3515
|
+
return HANDLED;
|
|
3516
|
+
}
|
|
3517
|
+
default:
|
|
3518
|
+
return ignored(`a moderation kind this version does not know: ${String(kind)}`);
|
|
3519
|
+
}
|
|
3520
|
+
}
|
|
3521
|
+
var ROLES = ["host", "cohost", "participant", "guest"];
|
|
3332
3522
|
|
|
3333
3523
|
// src/core/note-taker.ts
|
|
3334
3524
|
var NOTE_TAKER_IDENTITY_PREFIX = "agent:note_taker:";
|
|
@@ -3393,8 +3583,7 @@ function noteTakerLabel(status) {
|
|
|
3393
3583
|
var DATA_TOPICS = {
|
|
3394
3584
|
hand: "mx.hand",
|
|
3395
3585
|
reaction: "mx.reaction",
|
|
3396
|
-
chat: "mx.chat"
|
|
3397
|
-
hostAction: "mx.host"
|
|
3586
|
+
chat: "mx.chat"
|
|
3398
3587
|
};
|
|
3399
3588
|
var TRANSCRIPTION_TOPIC = "lk.transcription";
|
|
3400
3589
|
var TRANSCRIPTION_ATTRIBUTES = {
|
|
@@ -3530,7 +3719,37 @@ function createRoomEngine(options) {
|
|
|
3530
3719
|
const handlers = {
|
|
3531
3720
|
participants(list) {
|
|
3532
3721
|
const joinedAt = new Date(now()).toISOString();
|
|
3722
|
+
const before = new Set(store.snapshot().participants.map((p) => p.identity));
|
|
3533
3723
|
store.setParticipants(list.map((entry) => projectParticipant(entry, joinedAt)));
|
|
3724
|
+
const snapshot = store.snapshot();
|
|
3725
|
+
const newcomer = snapshot.participants.some(
|
|
3726
|
+
(participant) => !participant.isLocal && !before.has(participant.identity)
|
|
3727
|
+
);
|
|
3728
|
+
const me = snapshot.participants.find((p) => p.identity === snapshot.localIdentity);
|
|
3729
|
+
if (newcomer && before.size > 0 && me?.handRaised === true) {
|
|
3730
|
+
void publish(DATA_TOPICS.hand, { raised: true, raisedAt: me.handRaisedAt ?? now() }).catch(
|
|
3731
|
+
() => void 0
|
|
3732
|
+
);
|
|
3733
|
+
}
|
|
3734
|
+
},
|
|
3735
|
+
roomMetadata(metadata) {
|
|
3736
|
+
let spotlight = null;
|
|
3737
|
+
if (metadata.length > 0) {
|
|
3738
|
+
try {
|
|
3739
|
+
const parsed = JSON.parse(metadata);
|
|
3740
|
+
if (typeof parsed === "object" && parsed !== null) {
|
|
3741
|
+
const value = parsed.spotlight;
|
|
3742
|
+
if (typeof value === "string" && value.length > 0) spotlight = asParticipantIdentity(value);
|
|
3743
|
+
}
|
|
3744
|
+
} catch {
|
|
3745
|
+
report(
|
|
3746
|
+
"warn",
|
|
3747
|
+
"meet: the room's metadata is not JSON, so no spotlight was read from it",
|
|
3748
|
+
"Only aidream writes room metadata; check services/meet/moderation.py."
|
|
3749
|
+
);
|
|
3750
|
+
}
|
|
3751
|
+
}
|
|
3752
|
+
if (store.snapshot().spotlight !== spotlight) store.patch({ spotlight });
|
|
3534
3753
|
},
|
|
3535
3754
|
activeSpeakers(identities) {
|
|
3536
3755
|
store.setSpeaking(identities.map(asParticipantIdentity));
|
|
@@ -3634,7 +3853,11 @@ function createRoomEngine(options) {
|
|
|
3634
3853
|
const sender = asParticipantIdentity(from ?? "");
|
|
3635
3854
|
switch (topic) {
|
|
3636
3855
|
case DATA_TOPICS.hand:
|
|
3637
|
-
store.setHand(
|
|
3856
|
+
store.setHand(
|
|
3857
|
+
sender,
|
|
3858
|
+
envelope.raised === true,
|
|
3859
|
+
typeof envelope.raisedAt === "number" ? envelope.raisedAt : now()
|
|
3860
|
+
);
|
|
3638
3861
|
return;
|
|
3639
3862
|
case DATA_TOPICS.reaction:
|
|
3640
3863
|
if (typeof envelope.emoji === "string") {
|
|
@@ -3648,12 +3871,14 @@ function createRoomEngine(options) {
|
|
|
3648
3871
|
return;
|
|
3649
3872
|
case DATA_TOPICS.chat:
|
|
3650
3873
|
if (typeof envelope.text === "string") {
|
|
3874
|
+
const local = store.snapshot().localIdentity;
|
|
3651
3875
|
store.addChat({
|
|
3652
3876
|
id: envelope.id ?? `${sender}-${now()}`,
|
|
3653
3877
|
identity: sender,
|
|
3654
3878
|
displayName: envelope.name ?? sender,
|
|
3655
3879
|
text: envelope.text,
|
|
3656
|
-
at: envelope.at ?? new Date(now()).toISOString()
|
|
3880
|
+
at: envelope.at ?? new Date(now()).toISOString(),
|
|
3881
|
+
...typeof envelope.to === "string" && local !== null ? { to: local } : {}
|
|
3657
3882
|
});
|
|
3658
3883
|
}
|
|
3659
3884
|
return;
|
|
@@ -3699,6 +3924,24 @@ function createRoomEngine(options) {
|
|
|
3699
3924
|
store.patch({ phase: "connected", failure: null });
|
|
3700
3925
|
store.setLocalTrack("microphone", IDLE_TRACK);
|
|
3701
3926
|
store.setLocalTrack("camera", IDLE_TRACK);
|
|
3927
|
+
const effects = options.effectsStorage?.load() ?? store.snapshot().effects;
|
|
3928
|
+
store.patch({ effects });
|
|
3929
|
+
if (!effects.noiseSuppression || effects.background !== "none") {
|
|
3930
|
+
try {
|
|
3931
|
+
await applyEffects(created, effects);
|
|
3932
|
+
} catch {
|
|
3933
|
+
}
|
|
3934
|
+
}
|
|
3935
|
+
for (const kind of ["audioinput", "videoinput", "audiooutput"]) {
|
|
3936
|
+
const chosen = options.resolveDevice?.(kind) ?? null;
|
|
3937
|
+
if (chosen === null || chosen === "default") continue;
|
|
3938
|
+
try {
|
|
3939
|
+
await created.switchDevice(kind, chosen);
|
|
3940
|
+
} catch (error) {
|
|
3941
|
+
const normalized = normalizeDeviceError(error, `meet.switchDevice(${kind})`);
|
|
3942
|
+
report("warn", normalized.message, normalized.remedy);
|
|
3943
|
+
}
|
|
3944
|
+
}
|
|
3702
3945
|
if (request.mode === "video") {
|
|
3703
3946
|
await enableTrack(created, "camera", true);
|
|
3704
3947
|
}
|
|
@@ -3721,9 +3964,30 @@ function createRoomEngine(options) {
|
|
|
3721
3964
|
return state;
|
|
3722
3965
|
}
|
|
3723
3966
|
}
|
|
3724
|
-
async function publish(topic, body) {
|
|
3967
|
+
async function publish(topic, body, to) {
|
|
3725
3968
|
if (driver === null) return;
|
|
3726
|
-
await driver.publishData(topic, JSON.stringify(body));
|
|
3969
|
+
await driver.publishData(topic, JSON.stringify(body), to);
|
|
3970
|
+
}
|
|
3971
|
+
async function applyEffects(target, effects) {
|
|
3972
|
+
try {
|
|
3973
|
+
await target.setAudioProcessing?.({
|
|
3974
|
+
noiseSuppression: effects.noiseSuppression,
|
|
3975
|
+
echoCancellation: true,
|
|
3976
|
+
autoGainControl: true
|
|
3977
|
+
});
|
|
3978
|
+
} catch (error) {
|
|
3979
|
+
report("warn", `meet: noise suppression could not be applied: ${String(error)}`, "The microphone keeps working without it.");
|
|
3980
|
+
}
|
|
3981
|
+
if (target.setVideoEffect === void 0) return;
|
|
3982
|
+
const effect = effects.background === "blur" ? { kind: "blur" } : effects.background === "image" && effects.backgroundImage !== null ? { kind: "image", imageUrl: effects.backgroundImage } : { kind: "none" };
|
|
3983
|
+
if (effect.kind !== "none" && target.supportsVideoEffects?.() === false) return;
|
|
3984
|
+
try {
|
|
3985
|
+
await target.setVideoEffect(effect);
|
|
3986
|
+
} catch (error) {
|
|
3987
|
+
const normalized = normalizeMeetError(error, "meet.setVideoEffect");
|
|
3988
|
+
report("warn", normalized.message, normalized.remedy);
|
|
3989
|
+
throw normalized;
|
|
3990
|
+
}
|
|
3727
3991
|
}
|
|
3728
3992
|
return {
|
|
3729
3993
|
join(request) {
|
|
@@ -3779,7 +4043,7 @@ function createRoomEngine(options) {
|
|
|
3779
4043
|
async raiseHand(raised) {
|
|
3780
4044
|
const local = store.snapshot().localIdentity;
|
|
3781
4045
|
if (local !== null) store.setHand(local, raised);
|
|
3782
|
-
await publish(DATA_TOPICS.hand, { raised });
|
|
4046
|
+
await publish(DATA_TOPICS.hand, { raised, raisedAt: now() });
|
|
3783
4047
|
},
|
|
3784
4048
|
async react(emoji) {
|
|
3785
4049
|
const local = store.snapshot().localIdentity;
|
|
@@ -3789,32 +4053,41 @@ function createRoomEngine(options) {
|
|
|
3789
4053
|
}
|
|
3790
4054
|
await publish(DATA_TOPICS.reaction, { id, emoji });
|
|
3791
4055
|
},
|
|
3792
|
-
async
|
|
4056
|
+
async sendPrivateChat(to, text3) {
|
|
3793
4057
|
const trimmed = text3.trim();
|
|
3794
4058
|
if (trimmed.length === 0) return;
|
|
3795
4059
|
const local = store.snapshot().localIdentity;
|
|
3796
4060
|
const id = `${local ?? "local"}-${now()}`;
|
|
3797
4061
|
const at = new Date(now()).toISOString();
|
|
3798
4062
|
if (local !== null) {
|
|
3799
|
-
store.addChat({
|
|
3800
|
-
id,
|
|
3801
|
-
identity: local,
|
|
3802
|
-
displayName: identity.displayName,
|
|
3803
|
-
text: trimmed,
|
|
3804
|
-
at
|
|
3805
|
-
});
|
|
4063
|
+
store.addChat({ id, identity: local, displayName: identity.displayName, text: trimmed, at, to });
|
|
3806
4064
|
}
|
|
3807
|
-
await publish(
|
|
4065
|
+
await publish(
|
|
4066
|
+
DATA_TOPICS.chat,
|
|
4067
|
+
{ id, text: trimmed, at, name: identity.displayName, to },
|
|
4068
|
+
[to]
|
|
4069
|
+
);
|
|
3808
4070
|
},
|
|
3809
|
-
async
|
|
3810
|
-
|
|
3811
|
-
|
|
3812
|
-
|
|
3813
|
-
|
|
3814
|
-
|
|
3815
|
-
|
|
4071
|
+
async setEffects(next) {
|
|
4072
|
+
const previous = store.snapshot().effects;
|
|
4073
|
+
const merged = { ...previous, ...next };
|
|
4074
|
+
store.patch({ effects: merged });
|
|
4075
|
+
options.effectsStorage?.save(merged);
|
|
4076
|
+
if (driver === null) return;
|
|
4077
|
+
try {
|
|
4078
|
+
await applyEffects(driver, merged);
|
|
4079
|
+
} catch {
|
|
4080
|
+
const reverted = { ...merged, background: previous.background, backgroundImage: previous.backgroundImage };
|
|
4081
|
+
store.patch({ effects: reverted });
|
|
4082
|
+
options.effectsStorage?.save(reverted);
|
|
3816
4083
|
}
|
|
3817
|
-
|
|
4084
|
+
},
|
|
4085
|
+
supportsVideoEffects() {
|
|
4086
|
+
return driver?.supportsVideoEffects?.() ?? false;
|
|
4087
|
+
},
|
|
4088
|
+
async switchDevice(kind, deviceId2) {
|
|
4089
|
+
if (driver === null) return;
|
|
4090
|
+
await driver.switchDevice(kind, deviceId2);
|
|
3818
4091
|
},
|
|
3819
4092
|
pin(who) {
|
|
3820
4093
|
store.patch({ pinned: who, layout: who === null ? "grid" : "speaker" });
|
|
@@ -3845,6 +4118,192 @@ function hostUserIds(participants) {
|
|
|
3845
4118
|
return participants.filter((participant) => participant.role === "host" && participant.userId !== null).map((participant) => participant.userId);
|
|
3846
4119
|
}
|
|
3847
4120
|
|
|
4121
|
+
// src/core/effects.ts
|
|
4122
|
+
function coerce(value) {
|
|
4123
|
+
if (typeof value !== "object" || value === null) return DEFAULT_EFFECTS;
|
|
4124
|
+
const row = value;
|
|
4125
|
+
const background = row["background"] === "blur" || row["background"] === "image" ? row["background"] : "none";
|
|
4126
|
+
const backgroundImage = typeof row["backgroundImage"] === "string" ? row["backgroundImage"] : null;
|
|
4127
|
+
return {
|
|
4128
|
+
noiseSuppression: row["noiseSuppression"] !== false,
|
|
4129
|
+
background: background === "image" && backgroundImage === null ? "none" : background,
|
|
4130
|
+
backgroundImage
|
|
4131
|
+
};
|
|
4132
|
+
}
|
|
4133
|
+
function createWebEffectsStorage(key = "mx.meet.effects") {
|
|
4134
|
+
let memory = DEFAULT_EFFECTS;
|
|
4135
|
+
const storage = globalThis.localStorage;
|
|
4136
|
+
return {
|
|
4137
|
+
load() {
|
|
4138
|
+
if (storage === void 0) return memory;
|
|
4139
|
+
try {
|
|
4140
|
+
const raw = storage.getItem(key);
|
|
4141
|
+
return raw === null ? memory : coerce(JSON.parse(raw));
|
|
4142
|
+
} catch {
|
|
4143
|
+
return memory;
|
|
4144
|
+
}
|
|
4145
|
+
},
|
|
4146
|
+
save(effects) {
|
|
4147
|
+
memory = effects;
|
|
4148
|
+
try {
|
|
4149
|
+
storage?.setItem(key, JSON.stringify(effects));
|
|
4150
|
+
} catch {
|
|
4151
|
+
}
|
|
4152
|
+
}
|
|
4153
|
+
};
|
|
4154
|
+
}
|
|
4155
|
+
|
|
4156
|
+
// src/core/in-room.ts
|
|
4157
|
+
var MODERATION_ACTIONS = [
|
|
4158
|
+
"mute",
|
|
4159
|
+
"stop_video",
|
|
4160
|
+
"stop_screen",
|
|
4161
|
+
"mute_all",
|
|
4162
|
+
"ask_unmute",
|
|
4163
|
+
"ask_all_unmute",
|
|
4164
|
+
"spotlight",
|
|
4165
|
+
"lower_hand",
|
|
4166
|
+
"lower_all_hands",
|
|
4167
|
+
"make_cohost",
|
|
4168
|
+
"revoke_cohost",
|
|
4169
|
+
"transfer_host",
|
|
4170
|
+
"rename",
|
|
4171
|
+
"remove",
|
|
4172
|
+
"report"
|
|
4173
|
+
];
|
|
4174
|
+
function createInRoomClient(options) {
|
|
4175
|
+
const { api, store } = options;
|
|
4176
|
+
const now = options.now ?? (() => Date.now());
|
|
4177
|
+
function meetingId(operation) {
|
|
4178
|
+
const meeting = store.snapshot().meeting;
|
|
4179
|
+
if (meeting === null) {
|
|
4180
|
+
throw new MeetError(
|
|
4181
|
+
"not-found",
|
|
4182
|
+
`${operation}: this room has no meeting record`,
|
|
4183
|
+
"Moderation and saved chat belong to a meeting. An ad-hoc call has neither."
|
|
4184
|
+
);
|
|
4185
|
+
}
|
|
4186
|
+
return meeting.id;
|
|
4187
|
+
}
|
|
4188
|
+
function headers() {
|
|
4189
|
+
const token = options.resolveRoomToken();
|
|
4190
|
+
return token === null ? void 0 : { [ROOM_TOKEN_HEADER]: token };
|
|
4191
|
+
}
|
|
4192
|
+
async function refusal2(response, operation, remedy) {
|
|
4193
|
+
let body = null;
|
|
4194
|
+
try {
|
|
4195
|
+
body = await response.json();
|
|
4196
|
+
} catch {
|
|
4197
|
+
body = null;
|
|
4198
|
+
}
|
|
4199
|
+
const said = readServerRefusal(body);
|
|
4200
|
+
return new MeetError(
|
|
4201
|
+
response.status === 403 || response.status === 401 ? "forbidden" : response.status === 404 ? "not-found" : response.status === 409 ? "conflict" : "transport",
|
|
4202
|
+
said?.reason ?? `${operation}: the server answered ${response.status}`,
|
|
4203
|
+
said?.remedy !== void 0 && said.remedy.length > 0 ? said.remedy : remedy
|
|
4204
|
+
);
|
|
4205
|
+
}
|
|
4206
|
+
return {
|
|
4207
|
+
async moderate(request) {
|
|
4208
|
+
const operation = `meet.moderate(${request.action})`;
|
|
4209
|
+
const hdrs = headers();
|
|
4210
|
+
const response = await api.request({
|
|
4211
|
+
path: MEET_ROUTES.moderate,
|
|
4212
|
+
method: "POST",
|
|
4213
|
+
operation,
|
|
4214
|
+
body: {
|
|
4215
|
+
meeting_id: meetingId(operation),
|
|
4216
|
+
action: request.action,
|
|
4217
|
+
...request.identity != null ? { identity: request.identity } : {},
|
|
4218
|
+
...request.name !== void 0 ? { name: request.name } : {},
|
|
4219
|
+
...request.reason !== void 0 ? { reason: request.reason } : {}
|
|
4220
|
+
},
|
|
4221
|
+
...hdrs !== void 0 ? { headers: hdrs } : {}
|
|
4222
|
+
});
|
|
4223
|
+
if (!response.ok) {
|
|
4224
|
+
throw await refusal2(
|
|
4225
|
+
response,
|
|
4226
|
+
operation,
|
|
4227
|
+
"Nothing was changed. The server decides host powers from the meeting itself."
|
|
4228
|
+
);
|
|
4229
|
+
}
|
|
4230
|
+
const payload = await response.json();
|
|
4231
|
+
return {
|
|
4232
|
+
action: request.action,
|
|
4233
|
+
affected: Array.isArray(payload["affected"]) ? payload["affected"].filter((value) => typeof value === "string") : [],
|
|
4234
|
+
message: typeof payload["message"] === "string" ? payload["message"] : "Done."
|
|
4235
|
+
};
|
|
4236
|
+
},
|
|
4237
|
+
async sendChat(text3) {
|
|
4238
|
+
const body = text3.trim();
|
|
4239
|
+
if (body.length === 0) return;
|
|
4240
|
+
const operation = "meet.sendChat";
|
|
4241
|
+
const id = meetingId(operation);
|
|
4242
|
+
const snapshot = store.snapshot();
|
|
4243
|
+
const me = snapshot.participants.find((p) => p.identity === snapshot.localIdentity);
|
|
4244
|
+
const localId = `pending-${now()}`;
|
|
4245
|
+
const pending = {
|
|
4246
|
+
id: localId,
|
|
4247
|
+
identity: snapshot.localIdentity ?? "local",
|
|
4248
|
+
displayName: me?.displayName ?? "You",
|
|
4249
|
+
text: body,
|
|
4250
|
+
at: new Date(now()).toISOString(),
|
|
4251
|
+
saved: false
|
|
4252
|
+
};
|
|
4253
|
+
store.addChat(pending);
|
|
4254
|
+
const hdrs = headers();
|
|
4255
|
+
let response;
|
|
4256
|
+
try {
|
|
4257
|
+
response = await api.request({
|
|
4258
|
+
path: MEET_ROUTES.chat,
|
|
4259
|
+
method: "POST",
|
|
4260
|
+
operation,
|
|
4261
|
+
body: { meeting_id: id, body },
|
|
4262
|
+
...hdrs !== void 0 ? { headers: hdrs } : {}
|
|
4263
|
+
});
|
|
4264
|
+
} catch (error) {
|
|
4265
|
+
store.settleChat(localId, { failed: "Not sent \u2014 the server could not be reached." });
|
|
4266
|
+
throw error;
|
|
4267
|
+
}
|
|
4268
|
+
if (!response.ok) {
|
|
4269
|
+
const error = await refusal2(response, operation, "Your message was not sent.");
|
|
4270
|
+
store.settleChat(localId, { failed: error.message });
|
|
4271
|
+
throw error;
|
|
4272
|
+
}
|
|
4273
|
+
const saved = chatFromRow(await response.json());
|
|
4274
|
+
if (saved === null) {
|
|
4275
|
+
store.settleChat(localId, { failed: "The server's answer could not be read." });
|
|
4276
|
+
return;
|
|
4277
|
+
}
|
|
4278
|
+
store.settleChat(localId, saved);
|
|
4279
|
+
},
|
|
4280
|
+
async loadChat() {
|
|
4281
|
+
const operation = "meet.loadChat";
|
|
4282
|
+
const id = meetingId(operation);
|
|
4283
|
+
const hdrs = headers();
|
|
4284
|
+
const response = await api.request({
|
|
4285
|
+
path: `${MEET_ROUTES.chat}?meeting_id=${encodeURIComponent(id)}`,
|
|
4286
|
+
method: "GET",
|
|
4287
|
+
operation,
|
|
4288
|
+
...hdrs !== void 0 ? { headers: hdrs } : {}
|
|
4289
|
+
});
|
|
4290
|
+
if (!response.ok) {
|
|
4291
|
+
throw await refusal2(
|
|
4292
|
+
response,
|
|
4293
|
+
operation,
|
|
4294
|
+
"Messages sent before you joined could not be read. New messages still arrive."
|
|
4295
|
+
);
|
|
4296
|
+
}
|
|
4297
|
+
const payload = await response.json();
|
|
4298
|
+
const rows = Array.isArray(payload.messages) ? payload.messages : [];
|
|
4299
|
+
const messages = rows.map(
|
|
4300
|
+
(row) => typeof row === "object" && row !== null ? chatFromRow(row) : null
|
|
4301
|
+
).filter((message) => message !== null);
|
|
4302
|
+
store.loadChat(messages);
|
|
4303
|
+
}
|
|
4304
|
+
};
|
|
4305
|
+
}
|
|
4306
|
+
|
|
3848
4307
|
// src/core/livekit-driver.ts
|
|
3849
4308
|
function mapQuality(quality) {
|
|
3850
4309
|
switch (quality) {
|
|
@@ -3889,11 +4348,48 @@ function snapshotParticipant(participant, isLocal) {
|
|
|
3889
4348
|
screenSharing: participant.isScreenShareEnabled
|
|
3890
4349
|
};
|
|
3891
4350
|
}
|
|
4351
|
+
var DEFAULT_AUDIO_PROCESSING = {
|
|
4352
|
+
noiseSuppression: true,
|
|
4353
|
+
echoCancellation: true,
|
|
4354
|
+
autoGainControl: true
|
|
4355
|
+
};
|
|
4356
|
+
function videoEffectsSupported() {
|
|
4357
|
+
const scope = globalThis;
|
|
4358
|
+
if (typeof scope.OffscreenCanvas === "undefined" || scope.document === void 0) return false;
|
|
4359
|
+
try {
|
|
4360
|
+
return scope.document.createElement("canvas").getContext("webgl2") !== null;
|
|
4361
|
+
} catch {
|
|
4362
|
+
return false;
|
|
4363
|
+
}
|
|
4364
|
+
}
|
|
4365
|
+
async function applyVideoEffect(target, effect) {
|
|
4366
|
+
const { Track } = await import("livekit-client");
|
|
4367
|
+
const track = target.localParticipant.getTrackPublication(Track.Source.Camera)?.track;
|
|
4368
|
+
if (track === void 0) return;
|
|
4369
|
+
try {
|
|
4370
|
+
if (effect.kind === "none") {
|
|
4371
|
+
await track.stopProcessor();
|
|
4372
|
+
return;
|
|
4373
|
+
}
|
|
4374
|
+
const processors = await import("@livekit/track-processors");
|
|
4375
|
+
const processor = effect.kind === "blur" ? processors.BackgroundBlur(effect.radius ?? 12) : processors.VirtualBackground(effect.imageUrl);
|
|
4376
|
+
await track.setProcessor(processor);
|
|
4377
|
+
} catch (error) {
|
|
4378
|
+
throw new MeetError(
|
|
4379
|
+
"media-unsupported",
|
|
4380
|
+
"The background effect could not start on this camera",
|
|
4381
|
+
"Background effects need a browser with WebGL2 (current Chrome, Edge, Firefox or Safari). Your video continues without the effect.",
|
|
4382
|
+
error
|
|
4383
|
+
);
|
|
4384
|
+
}
|
|
4385
|
+
}
|
|
3892
4386
|
var encoder = new TextEncoder();
|
|
3893
4387
|
var decoder = new TextDecoder();
|
|
3894
4388
|
function createLiveKitDriver(handlers, options = {}) {
|
|
3895
4389
|
let room = null;
|
|
3896
4390
|
let projectRoster = null;
|
|
4391
|
+
let audioCapture = DEFAULT_AUDIO_PROCESSING;
|
|
4392
|
+
let videoEffect = { kind: "none" };
|
|
3897
4393
|
async function load() {
|
|
3898
4394
|
if (room !== null) return room;
|
|
3899
4395
|
let module2;
|
|
@@ -3922,7 +4418,9 @@ function createLiveKitDriver(handlers, options = {}) {
|
|
|
3922
4418
|
handlers.participants([local, ...remotes]);
|
|
3923
4419
|
};
|
|
3924
4420
|
projectRoster = roster;
|
|
3925
|
-
created.on(RoomEvent.ParticipantConnected, roster).on(RoomEvent.ParticipantDisconnected, roster).on(RoomEvent.TrackSubscribed, roster).on(RoomEvent.TrackUnsubscribed, roster).on(RoomEvent.TrackMuted, roster).on(RoomEvent.TrackUnmuted, roster).on(RoomEvent.LocalTrackPublished, roster).on(RoomEvent.LocalTrackUnpublished, roster).on(RoomEvent.
|
|
4421
|
+
created.on(RoomEvent.ParticipantConnected, roster).on(RoomEvent.ParticipantDisconnected, roster).on(RoomEvent.TrackSubscribed, roster).on(RoomEvent.TrackUnsubscribed, roster).on(RoomEvent.TrackMuted, roster).on(RoomEvent.TrackUnmuted, roster).on(RoomEvent.LocalTrackPublished, roster).on(RoomEvent.LocalTrackUnpublished, roster).on(RoomEvent.ParticipantNameChanged, roster).on(RoomEvent.ParticipantAttributesChanged, roster).on(RoomEvent.ParticipantMetadataChanged, roster).on(RoomEvent.RoomMetadataChanged, (metadata) => {
|
|
4422
|
+
handlers.roomMetadata?.(metadata ?? "");
|
|
4423
|
+
}).on(RoomEvent.ActiveSpeakersChanged, (speakers) => {
|
|
3926
4424
|
handlers.activeSpeakers(speakers.map((speaker) => speaker.identity));
|
|
3927
4425
|
}).on(RoomEvent.ConnectionQualityChanged, (quality, participant) => {
|
|
3928
4426
|
handlers.quality(participant.identity, mapQuality(quality));
|
|
@@ -3999,6 +4497,7 @@ function createLiveKitDriver(handlers, options = {}) {
|
|
|
3999
4497
|
throw normalizeDeviceError(error, "meet.connect");
|
|
4000
4498
|
}
|
|
4001
4499
|
projectRoster?.();
|
|
4500
|
+
handlers.roomMetadata?.(target.metadata ?? "");
|
|
4002
4501
|
},
|
|
4003
4502
|
async disconnect() {
|
|
4004
4503
|
if (room === null) return;
|
|
@@ -4013,14 +4512,19 @@ function createLiveKitDriver(handlers, options = {}) {
|
|
|
4013
4512
|
},
|
|
4014
4513
|
async setMicrophoneEnabled(enabled) {
|
|
4015
4514
|
try {
|
|
4016
|
-
await requireRoom("setMicrophoneEnabled").localParticipant.setMicrophoneEnabled(
|
|
4515
|
+
await requireRoom("setMicrophoneEnabled").localParticipant.setMicrophoneEnabled(
|
|
4516
|
+
enabled,
|
|
4517
|
+
audioCapture
|
|
4518
|
+
);
|
|
4017
4519
|
} catch (error) {
|
|
4018
4520
|
throw normalizeDeviceError(error, "meet.setMicrophoneEnabled");
|
|
4019
4521
|
}
|
|
4020
4522
|
},
|
|
4021
4523
|
async setCameraEnabled(enabled) {
|
|
4022
4524
|
try {
|
|
4023
|
-
|
|
4525
|
+
const target = requireRoom("setCameraEnabled");
|
|
4526
|
+
await target.localParticipant.setCameraEnabled(enabled);
|
|
4527
|
+
if (enabled && videoEffect.kind !== "none") await applyVideoEffect(target, videoEffect);
|
|
4024
4528
|
} catch (error) {
|
|
4025
4529
|
throw normalizeDeviceError(error, "meet.setCameraEnabled");
|
|
4026
4530
|
}
|
|
@@ -4032,14 +4536,40 @@ function createLiveKitDriver(handlers, options = {}) {
|
|
|
4032
4536
|
throw normalizeDeviceError(error, "meet.setScreenShareEnabled");
|
|
4033
4537
|
}
|
|
4034
4538
|
},
|
|
4035
|
-
async publishData(topic, payload) {
|
|
4539
|
+
async publishData(topic, payload, to) {
|
|
4036
4540
|
await requireRoom("publishData").localParticipant.publishData(
|
|
4037
4541
|
// `reliable` is not optional for a control plane: a dropped
|
|
4038
4542
|
// "hand lowered" leaves a hand up on every other screen forever.
|
|
4039
4543
|
encoder.encode(payload),
|
|
4040
|
-
{ reliable: true, topic }
|
|
4544
|
+
to === void 0 || to.length === 0 ? { reliable: true, topic } : { reliable: true, topic, destinationIdentities: [...to] }
|
|
4041
4545
|
);
|
|
4042
4546
|
},
|
|
4547
|
+
async setAudioProcessing(processing) {
|
|
4548
|
+
audioCapture = { ...audioCapture, ...processing };
|
|
4549
|
+
const target = room;
|
|
4550
|
+
if (target === null) return;
|
|
4551
|
+
const { Track } = await import("livekit-client");
|
|
4552
|
+
const publication = target.localParticipant.getTrackPublication(Track.Source.Microphone);
|
|
4553
|
+
const track = publication?.track;
|
|
4554
|
+
if (track === void 0 || publication?.isMuted === true) return;
|
|
4555
|
+
try {
|
|
4556
|
+
await track.restartTrack({
|
|
4557
|
+
...audioCapture,
|
|
4558
|
+
...target.getActiveDevice("audioinput") !== void 0 ? { deviceId: target.getActiveDevice("audioinput") } : {}
|
|
4559
|
+
});
|
|
4560
|
+
} catch (error) {
|
|
4561
|
+
throw normalizeDeviceError(error, "meet.setAudioProcessing");
|
|
4562
|
+
}
|
|
4563
|
+
},
|
|
4564
|
+
async setVideoEffect(effect) {
|
|
4565
|
+
videoEffect = effect;
|
|
4566
|
+
const target = room;
|
|
4567
|
+
if (target === null) return;
|
|
4568
|
+
await applyVideoEffect(target, effect);
|
|
4569
|
+
},
|
|
4570
|
+
supportsVideoEffects() {
|
|
4571
|
+
return videoEffectsSupported();
|
|
4572
|
+
},
|
|
4043
4573
|
attach(identity, kind, element) {
|
|
4044
4574
|
const target = requireRoom("attach");
|
|
4045
4575
|
const source = sources?.[kind];
|
|
@@ -4073,6 +4603,52 @@ function createLiveKitDriver(handlers, options = {}) {
|
|
|
4073
4603
|
}
|
|
4074
4604
|
};
|
|
4075
4605
|
}
|
|
4606
|
+
async function createCameraPreview(deviceId2, effect) {
|
|
4607
|
+
let module2;
|
|
4608
|
+
try {
|
|
4609
|
+
module2 = await import("livekit-client");
|
|
4610
|
+
} catch (error) {
|
|
4611
|
+
throw new MeetError(
|
|
4612
|
+
"misconfigured",
|
|
4613
|
+
"livekit-client could not be loaded for the camera preview",
|
|
4614
|
+
"The bundler excluded `livekit-client`; remove it from any `external` list.",
|
|
4615
|
+
error
|
|
4616
|
+
);
|
|
4617
|
+
}
|
|
4618
|
+
let track;
|
|
4619
|
+
try {
|
|
4620
|
+
track = await module2.createLocalVideoTrack(deviceId2 === null ? {} : { deviceId: deviceId2 });
|
|
4621
|
+
} catch (error) {
|
|
4622
|
+
throw normalizeDeviceError(error, "meet.cameraPreview");
|
|
4623
|
+
}
|
|
4624
|
+
const target = track;
|
|
4625
|
+
async function apply(next) {
|
|
4626
|
+
if (next.kind === "none") {
|
|
4627
|
+
await target.stopProcessor();
|
|
4628
|
+
return;
|
|
4629
|
+
}
|
|
4630
|
+
const processors = await import("@livekit/track-processors");
|
|
4631
|
+
await target.setProcessor(
|
|
4632
|
+
next.kind === "blur" ? processors.BackgroundBlur(next.radius ?? 12) : processors.VirtualBackground(next.imageUrl)
|
|
4633
|
+
);
|
|
4634
|
+
}
|
|
4635
|
+
if (effect.kind !== "none" && videoEffectsSupported()) {
|
|
4636
|
+
try {
|
|
4637
|
+
await apply(effect);
|
|
4638
|
+
} catch {
|
|
4639
|
+
}
|
|
4640
|
+
}
|
|
4641
|
+
return {
|
|
4642
|
+
attach(element) {
|
|
4643
|
+
track.attach(element);
|
|
4644
|
+
},
|
|
4645
|
+
setEffect: apply,
|
|
4646
|
+
stop() {
|
|
4647
|
+
track.stop();
|
|
4648
|
+
track.detach();
|
|
4649
|
+
}
|
|
4650
|
+
};
|
|
4651
|
+
}
|
|
4076
4652
|
|
|
4077
4653
|
// src/react/provider.tsx
|
|
4078
4654
|
var import_jsx_runtime = require("react/jsx-runtime");
|
|
@@ -4182,7 +4758,9 @@ function MeetRuntime(props) {
|
|
|
4182
4758
|
avatarUrl: props.avatarUrl ?? null,
|
|
4183
4759
|
deviceId: deviceId()
|
|
4184
4760
|
};
|
|
4185
|
-
const
|
|
4761
|
+
const effectsStorage = createWebEffectsStorage();
|
|
4762
|
+
const store = createMeetStore({ effects: effectsStorage.load() });
|
|
4763
|
+
const devicesHolder = { current: null };
|
|
4186
4764
|
const api = createMeetApi({
|
|
4187
4765
|
baseUrl,
|
|
4188
4766
|
organizationId: identity.organizationId,
|
|
@@ -4201,7 +4779,9 @@ function MeetRuntime(props) {
|
|
|
4201
4779
|
identity,
|
|
4202
4780
|
resolveMeeting: (slug) => repository.meetingBySlug(slug),
|
|
4203
4781
|
driverFactory: (handlers) => (driverRef.current ?? createLiveKitDriver)(handlers),
|
|
4204
|
-
onDiagnostic: report
|
|
4782
|
+
onDiagnostic: report,
|
|
4783
|
+
effectsStorage,
|
|
4784
|
+
resolveDevice: (kind) => devicesHolder.current?.resolve(kind) ?? null
|
|
4205
4785
|
});
|
|
4206
4786
|
const meetings = createMeetingSession({
|
|
4207
4787
|
manager,
|
|
@@ -4216,16 +4796,36 @@ function MeetRuntime(props) {
|
|
|
4216
4796
|
void engine.rejoin();
|
|
4217
4797
|
}
|
|
4218
4798
|
});
|
|
4219
|
-
const
|
|
4220
|
-
|
|
4221
|
-
|
|
4222
|
-
|
|
4223
|
-
|
|
4224
|
-
|
|
4225
|
-
|
|
4226
|
-
|
|
4227
|
-
|
|
4799
|
+
const resolveRoomToken = () => {
|
|
4800
|
+
const roomName = store.snapshot().meeting?.roomName;
|
|
4801
|
+
if (roomName === void 0) return null;
|
|
4802
|
+
const active = engine.token();
|
|
4803
|
+
if (active?.roomName === roomName && active.token.length > 0) return active.token;
|
|
4804
|
+
return tokens.remembered(roomName)?.token ?? null;
|
|
4805
|
+
};
|
|
4806
|
+
const ai = createMeetAi({ api, store, resolveRoomToken });
|
|
4807
|
+
const inRoom = createInRoomClient({ api, store, resolveRoomToken });
|
|
4808
|
+
let chatReadFor = null;
|
|
4809
|
+
store.subscribe(() => {
|
|
4810
|
+
const snapshot = store.snapshot();
|
|
4811
|
+
if (snapshot.phase !== "connected" || snapshot.meeting === null) {
|
|
4812
|
+
if (snapshot.phase === "left" || snapshot.phase === "idle") chatReadFor = null;
|
|
4813
|
+
return;
|
|
4228
4814
|
}
|
|
4815
|
+
if (chatReadFor === snapshot.meeting.id) return;
|
|
4816
|
+
chatReadFor = snapshot.meeting.id;
|
|
4817
|
+
void inRoom.loadChat().catch((error) => {
|
|
4818
|
+
const normalized = error;
|
|
4819
|
+
store.patch({
|
|
4820
|
+
chatLoaded: true,
|
|
4821
|
+
chatHistoryError: normalized.remedy ?? "Messages sent before you joined could not be read. New messages still arrive."
|
|
4822
|
+
});
|
|
4823
|
+
report({
|
|
4824
|
+
level: "warn",
|
|
4825
|
+
message: normalized.message ?? "meet: the saved chat could not be read",
|
|
4826
|
+
...normalized.remedy !== void 0 ? { remedy: normalized.remedy } : {}
|
|
4827
|
+
});
|
|
4828
|
+
});
|
|
4229
4829
|
});
|
|
4230
4830
|
const recording = createRecordingController({
|
|
4231
4831
|
api,
|
|
@@ -4247,6 +4847,7 @@ function MeetRuntime(props) {
|
|
|
4247
4847
|
},
|
|
4248
4848
|
storage: createWebDeviceStorage()
|
|
4249
4849
|
});
|
|
4850
|
+
devicesHolder.current = devices;
|
|
4250
4851
|
const calls = identity.userId === null ? null : createCallCenter({
|
|
4251
4852
|
manager,
|
|
4252
4853
|
repository,
|
|
@@ -4261,6 +4862,7 @@ function MeetRuntime(props) {
|
|
|
4261
4862
|
engine,
|
|
4262
4863
|
calls,
|
|
4263
4864
|
meetings,
|
|
4865
|
+
inRoom,
|
|
4264
4866
|
recording,
|
|
4265
4867
|
devices,
|
|
4266
4868
|
repository,
|
|
@@ -4382,7 +4984,8 @@ function computeStage(args) {
|
|
|
4382
4984
|
const ordered = orderParticipants(args.participants, args.activeSpeakers);
|
|
4383
4985
|
const sharer = ordered.find((participant) => participant.screenSharing) ?? null;
|
|
4384
4986
|
const pinnedParticipant = args.pinned === null ? null : ordered.find((participant) => participant.identity === args.pinned) ?? null;
|
|
4385
|
-
const
|
|
4987
|
+
const spotlit = args.spotlight == null ? null : ordered.find((participant) => participant.identity === args.spotlight) ?? null;
|
|
4988
|
+
const feature = sharer !== null ? sharer : pinnedParticipant !== null && args.layout !== "grid" ? pinnedParticipant : spotlit !== null ? spotlit : args.layout === "grid" ? null : ordered[0] ?? null;
|
|
4386
4989
|
const rest = feature === null ? ordered : ordered.filter((participant) => participant.identity !== feature.identity);
|
|
4387
4990
|
const pageCount = Math.max(1, Math.ceil(rest.length / pageSize));
|
|
4388
4991
|
const page = Math.min(Math.max(0, args.page ?? 0), pageCount - 1);
|
|
@@ -4861,7 +5464,14 @@ function useRoom() {
|
|
|
4861
5464
|
[engine]
|
|
4862
5465
|
),
|
|
4863
5466
|
sendChat: (0, import_react3.useCallback)(
|
|
4864
|
-
async (text3) =>
|
|
5467
|
+
async (text3) => {
|
|
5468
|
+
if (host === null) throw noRuntime("sendChat");
|
|
5469
|
+
await host.inRoom.sendChat(text3);
|
|
5470
|
+
},
|
|
5471
|
+
[host]
|
|
5472
|
+
),
|
|
5473
|
+
sendPrivateChat: (0, import_react3.useCallback)(
|
|
5474
|
+
async (to, text3) => requireEngine(engine, "sendPrivateChat").sendPrivateChat(to, text3),
|
|
4865
5475
|
[engine]
|
|
4866
5476
|
),
|
|
4867
5477
|
pin: (0, import_react3.useCallback)(
|
|
@@ -4882,6 +5492,7 @@ function useStage(pageSize = DEFAULT_PAGE_SIZE) {
|
|
|
4882
5492
|
participants: snapshot?.participants ?? [],
|
|
4883
5493
|
layout: snapshot?.layout ?? "grid",
|
|
4884
5494
|
pinned: snapshot?.pinned ?? null,
|
|
5495
|
+
spotlight: snapshot?.spotlight ?? null,
|
|
4885
5496
|
activeSpeakers: snapshot?.activeSpeakers ?? [],
|
|
4886
5497
|
pageSize,
|
|
4887
5498
|
page
|
|
@@ -5214,33 +5825,160 @@ function useHostControls() {
|
|
|
5214
5825
|
const host = useMeetHost();
|
|
5215
5826
|
const snapshot = useMeetSnapshot();
|
|
5216
5827
|
const role = useRole();
|
|
5217
|
-
const engine = host?.engine ?? null;
|
|
5218
5828
|
const me = snapshot?.participants.find(
|
|
5219
5829
|
(participant) => participant.identity === snapshot.localIdentity
|
|
5220
5830
|
);
|
|
5221
5831
|
const actor = me === void 0 ? void 0 : { ...me, role };
|
|
5832
|
+
const [busy, setBusy] = (0, import_react3.useState)(false);
|
|
5833
|
+
const inRoom = host?.inRoom ?? null;
|
|
5834
|
+
const store = host?.store ?? null;
|
|
5835
|
+
const notice = snapshot?.actionNotice ?? null;
|
|
5836
|
+
const setNotice = (0, import_react3.useCallback)(
|
|
5837
|
+
(next) => store?.patch({ actionNotice: next === null ? null : { ...next, at: Date.now() } }),
|
|
5838
|
+
[store]
|
|
5839
|
+
);
|
|
5840
|
+
const run = (0, import_react3.useCallback)(
|
|
5841
|
+
async (request) => {
|
|
5842
|
+
if (inRoom === null) throw noRuntime(`moderate(${request.action})`);
|
|
5843
|
+
setBusy(true);
|
|
5844
|
+
try {
|
|
5845
|
+
const result = await inRoom.moderate(request);
|
|
5846
|
+
setNotice({ text: result.message, failed: false });
|
|
5847
|
+
return result;
|
|
5848
|
+
} catch (thrown) {
|
|
5849
|
+
const error = thrown;
|
|
5850
|
+
setNotice({
|
|
5851
|
+
text: [error.message, error.remedy].filter(Boolean).join(" "),
|
|
5852
|
+
failed: true
|
|
5853
|
+
});
|
|
5854
|
+
return null;
|
|
5855
|
+
} finally {
|
|
5856
|
+
setBusy(false);
|
|
5857
|
+
}
|
|
5858
|
+
},
|
|
5859
|
+
[inRoom, setNotice]
|
|
5860
|
+
);
|
|
5222
5861
|
return {
|
|
5223
5862
|
isHost: isHostRole(role),
|
|
5863
|
+
isOwner: role === "host",
|
|
5864
|
+
notice,
|
|
5865
|
+
busy,
|
|
5866
|
+
clearNotice: (0, import_react3.useCallback)(() => setNotice(null), [setNotice]),
|
|
5224
5867
|
canModerate: (0, import_react3.useCallback)(
|
|
5225
5868
|
(target) => canModerateParticipant(actor, target),
|
|
5226
5869
|
[actor]
|
|
5227
5870
|
),
|
|
5871
|
+
run,
|
|
5228
5872
|
mute: (0, import_react3.useCallback)(
|
|
5229
|
-
async (target) =>
|
|
5230
|
-
|
|
5231
|
-
|
|
5232
|
-
|
|
5233
|
-
[engine]
|
|
5873
|
+
async (target) => {
|
|
5874
|
+
await run({ action: "mute", identity: target.identity });
|
|
5875
|
+
},
|
|
5876
|
+
[run]
|
|
5234
5877
|
),
|
|
5235
5878
|
remove: (0, import_react3.useCallback)(
|
|
5236
|
-
async (target) =>
|
|
5237
|
-
|
|
5238
|
-
|
|
5239
|
-
|
|
5879
|
+
async (target) => {
|
|
5880
|
+
await run({ action: "remove", identity: target.identity });
|
|
5881
|
+
},
|
|
5882
|
+
[run]
|
|
5883
|
+
)
|
|
5884
|
+
};
|
|
5885
|
+
}
|
|
5886
|
+
function useHandQueue() {
|
|
5887
|
+
const snapshot = useMeetSnapshot();
|
|
5888
|
+
return (0, import_react3.useMemo)(
|
|
5889
|
+
() => (snapshot?.participants ?? []).filter((participant) => participant.handRaised).sort((a, b) => (a.handRaisedAt ?? 0) - (b.handRaisedAt ?? 0)),
|
|
5890
|
+
[snapshot?.participants]
|
|
5891
|
+
);
|
|
5892
|
+
}
|
|
5893
|
+
function useMediaEffects() {
|
|
5894
|
+
const host = useMeetHost();
|
|
5895
|
+
const snapshot = useMeetSnapshot();
|
|
5896
|
+
const engine = host?.engine ?? null;
|
|
5897
|
+
return {
|
|
5898
|
+
effects: snapshot?.effects ?? DEFAULT_EFFECTS,
|
|
5899
|
+
backgroundsSupported: engine?.supportsVideoEffects() ?? false,
|
|
5900
|
+
set: (0, import_react3.useCallback)(
|
|
5901
|
+
async (next) => requireEngine(engine, "setEffects").setEffects(next),
|
|
5240
5902
|
[engine]
|
|
5241
5903
|
)
|
|
5242
5904
|
};
|
|
5243
5905
|
}
|
|
5906
|
+
function useModerationNotice() {
|
|
5907
|
+
const host = useMeetHost();
|
|
5908
|
+
const snapshot = useMeetSnapshot();
|
|
5909
|
+
return {
|
|
5910
|
+
notice: snapshot?.moderationNotice ?? null,
|
|
5911
|
+
dismiss: (0, import_react3.useCallback)(() => host?.store.patch({ moderationNotice: null }), [host])
|
|
5912
|
+
};
|
|
5913
|
+
}
|
|
5914
|
+
function useAudioLevel(stream) {
|
|
5915
|
+
const [level, setLevel] = (0, import_react3.useState)(0);
|
|
5916
|
+
(0, import_react3.useEffect)(() => {
|
|
5917
|
+
if (stream === null || stream.getAudioTracks().length === 0) {
|
|
5918
|
+
setLevel(0);
|
|
5919
|
+
return void 0;
|
|
5920
|
+
}
|
|
5921
|
+
const Context = globalThis.AudioContext ?? globalThis.webkitAudioContext;
|
|
5922
|
+
if (Context === void 0) return void 0;
|
|
5923
|
+
const context = new Context();
|
|
5924
|
+
const source = context.createMediaStreamSource(stream);
|
|
5925
|
+
const analyser = context.createAnalyser();
|
|
5926
|
+
analyser.fftSize = 512;
|
|
5927
|
+
source.connect(analyser);
|
|
5928
|
+
const samples = new Uint8Array(analyser.fftSize);
|
|
5929
|
+
let frame = 0;
|
|
5930
|
+
const tick = () => {
|
|
5931
|
+
analyser.getByteTimeDomainData(samples);
|
|
5932
|
+
let peak = 0;
|
|
5933
|
+
for (const sample of samples) peak = Math.max(peak, Math.abs(sample - 128) / 128);
|
|
5934
|
+
setLevel((previous) => Math.abs(previous - peak) > 0.02 ? peak : previous);
|
|
5935
|
+
frame = requestAnimationFrame(tick);
|
|
5936
|
+
};
|
|
5937
|
+
frame = requestAnimationFrame(tick);
|
|
5938
|
+
return () => {
|
|
5939
|
+
cancelAnimationFrame(frame);
|
|
5940
|
+
source.disconnect();
|
|
5941
|
+
void context.close();
|
|
5942
|
+
};
|
|
5943
|
+
}, [stream]);
|
|
5944
|
+
return level;
|
|
5945
|
+
}
|
|
5946
|
+
var MEET_SHORTCUTS = [
|
|
5947
|
+
{ id: "mic", keys: "\u2318/Ctrl + D", label: "Mute or unmute your microphone" },
|
|
5948
|
+
{ id: "camera", keys: "\u2318/Ctrl + E", label: "Turn your camera on or off" },
|
|
5949
|
+
{ id: "hand", keys: "\u2318/Ctrl + Alt + H", label: "Raise or lower your hand" },
|
|
5950
|
+
{ id: "chat", keys: "\u2318/Ctrl + Alt + C", label: "Open or close chat" },
|
|
5951
|
+
{ id: "people", keys: "\u2318/Ctrl + Alt + P", label: "Open or close people" },
|
|
5952
|
+
{ id: "help", keys: "Shift + ?", label: "Show keyboard shortcuts" }
|
|
5953
|
+
];
|
|
5954
|
+
function useMeetShortcuts(handlers) {
|
|
5955
|
+
const ref = (0, import_react3.useRef)(handlers);
|
|
5956
|
+
ref.current = handlers;
|
|
5957
|
+
(0, import_react3.useEffect)(() => {
|
|
5958
|
+
const target = globalThis.window;
|
|
5959
|
+
if (target === void 0) return void 0;
|
|
5960
|
+
const onKey = (event) => {
|
|
5961
|
+
const element = event.target;
|
|
5962
|
+
const typing = element !== null && (element.isContentEditable || element.tagName === "INPUT" || element.tagName === "TEXTAREA" || element.tagName === "SELECT");
|
|
5963
|
+
const mod = event.metaKey || event.ctrlKey;
|
|
5964
|
+
const key = event.key.toLowerCase();
|
|
5965
|
+
let id = null;
|
|
5966
|
+
if (mod && !event.altKey && !event.shiftKey && key === "d") id = "mic";
|
|
5967
|
+
else if (mod && !event.altKey && !event.shiftKey && key === "e") id = "camera";
|
|
5968
|
+
else if (mod && event.altKey && (event.code === "KeyH" || key === "h")) id = "hand";
|
|
5969
|
+
else if (mod && event.altKey && (event.code === "KeyC" || key === "c")) id = "chat";
|
|
5970
|
+
else if (mod && event.altKey && (event.code === "KeyP" || key === "p")) id = "people";
|
|
5971
|
+
else if (!mod && !typing && event.key === "?") id = "help";
|
|
5972
|
+
if (id === null) return;
|
|
5973
|
+
const handler = ref.current[id];
|
|
5974
|
+
if (handler === void 0) return;
|
|
5975
|
+
event.preventDefault();
|
|
5976
|
+
handler();
|
|
5977
|
+
};
|
|
5978
|
+
target.addEventListener("keydown", onKey);
|
|
5979
|
+
return () => target.removeEventListener("keydown", onKey);
|
|
5980
|
+
}, []);
|
|
5981
|
+
}
|
|
5244
5982
|
function useMeeting() {
|
|
5245
5983
|
return useMeetSnapshot()?.meeting ?? null;
|
|
5246
5984
|
}
|
|
@@ -5587,12 +6325,66 @@ function WifiLowIcon(props) {
|
|
|
5587
6325
|
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M8.5 16.5a5 5 0 0 1 7 0" })
|
|
5588
6326
|
] });
|
|
5589
6327
|
}
|
|
5590
|
-
|
|
5591
|
-
|
|
5592
|
-
|
|
5593
|
-
|
|
5594
|
-
|
|
5595
|
-
/* @__PURE__ */ (0,
|
|
6328
|
+
function SmileIcon(props) {
|
|
6329
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(Icon, { ...props, children: [
|
|
6330
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("circle", { cx: "12", cy: "12", r: "10" }),
|
|
6331
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M8 14s1.5 2 4 2 4-2 4-2" }),
|
|
6332
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("line", { x1: "9", x2: "9.01", y1: "9", y2: "9" }),
|
|
6333
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("line", { x1: "15", x2: "15.01", y1: "9", y2: "9" })
|
|
6334
|
+
] });
|
|
6335
|
+
}
|
|
6336
|
+
function MoreIcon(props) {
|
|
6337
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(Icon, { ...props, children: [
|
|
6338
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("circle", { cx: "12", cy: "12", r: "1" }),
|
|
6339
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("circle", { cx: "19", cy: "12", r: "1" }),
|
|
6340
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("circle", { cx: "5", cy: "12", r: "1" })
|
|
6341
|
+
] });
|
|
6342
|
+
}
|
|
6343
|
+
function SearchIcon(props) {
|
|
6344
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(Icon, { ...props, children: [
|
|
6345
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("circle", { cx: "11", cy: "11", r: "8" }),
|
|
6346
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "m21 21-4.3-4.3" })
|
|
6347
|
+
] });
|
|
6348
|
+
}
|
|
6349
|
+
function KeyboardIcon(props) {
|
|
6350
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(Icon, { ...props, children: [
|
|
6351
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M10 8h.01M12 12h.01M14 8h.01M16 12h.01M18 8h.01M6 8h.01M7 16h10M8 12h.01" }),
|
|
6352
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("rect", { width: "20", height: "16", x: "2", y: "4", rx: "2" })
|
|
6353
|
+
] });
|
|
6354
|
+
}
|
|
6355
|
+
function SpotlightIcon(props) {
|
|
6356
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Icon, { ...props, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M12 2 15 8.5 22 9.3 17 14 18.2 21 12 17.6 5.8 21 7 14 2 9.3 9 8.5Z" }) });
|
|
6357
|
+
}
|
|
6358
|
+
function SignalIcon({ bars, ...props }) {
|
|
6359
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(Icon, { ...props, children: [
|
|
6360
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M4 20v-2", opacity: bars >= 1 ? 1 : 0.3 }),
|
|
6361
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M10 20v-6", opacity: bars >= 2 ? 1 : 0.3 }),
|
|
6362
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M16 20V9", opacity: bars >= 3 ? 1 : 0.3 }),
|
|
6363
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M22 20V4", opacity: bars >= 3 ? 1 : 0.3 })
|
|
6364
|
+
] });
|
|
6365
|
+
}
|
|
6366
|
+
function SpeakerIcon(props) {
|
|
6367
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(Icon, { ...props, children: [
|
|
6368
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M11 5 6 9H2v6h4l5 4V5Z" }),
|
|
6369
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M15.54 8.46a5 5 0 0 1 0 7.07" }),
|
|
6370
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M19.07 4.93a10 10 0 0 1 0 14.14" })
|
|
6371
|
+
] });
|
|
6372
|
+
}
|
|
6373
|
+
function FlagIcon(props) {
|
|
6374
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(Icon, { ...props, children: [
|
|
6375
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M4 15s1-1 4-1 5 2 8 2 4-1 4-1V3s-1 1-4 1-5-2-8-2-4 1-4 1z" }),
|
|
6376
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("line", { x1: "4", x2: "4", y1: "22", y2: "15" })
|
|
6377
|
+
] });
|
|
6378
|
+
}
|
|
6379
|
+
function PencilIcon(props) {
|
|
6380
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Icon, { ...props, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M17 3a2.85 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z" }) });
|
|
6381
|
+
}
|
|
6382
|
+
|
|
6383
|
+
// src/react/components.tsx
|
|
6384
|
+
var import_jsx_runtime3 = require("react/jsx-runtime");
|
|
6385
|
+
function InertNotice({ what }) {
|
|
6386
|
+
return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "mx-meet__empty", role: "status", children: [
|
|
6387
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("p", { className: "mx-meet__empty-title", children: [
|
|
5596
6388
|
what,
|
|
5597
6389
|
" is not ready yet"
|
|
5598
6390
|
] }),
|
|
@@ -5687,12 +6479,17 @@ function ParticipantTile({
|
|
|
5687
6479
|
featured = false
|
|
5688
6480
|
}) {
|
|
5689
6481
|
const host = useMeetHost();
|
|
5690
|
-
const
|
|
6482
|
+
const snapshot = useMeetSnapshot();
|
|
6483
|
+
const queue = useHandQueue();
|
|
6484
|
+
const [menu, setMenu] = (0, import_react4.useState)(false);
|
|
5691
6485
|
const videoRef = useTrack(
|
|
5692
6486
|
participant.identity,
|
|
5693
6487
|
participant.screenSharing ? "screen" : "camera"
|
|
5694
6488
|
);
|
|
5695
6489
|
const showVideo = participant.cameraEnabled || participant.screenSharing;
|
|
6490
|
+
const pinnedHere = snapshot?.pinned === participant.identity;
|
|
6491
|
+
const spotlit = snapshot?.spotlight === participant.identity;
|
|
6492
|
+
const handPosition = participant.handRaised ? queue.findIndex((entry) => entry.identity === participant.identity) + 1 : 0;
|
|
5696
6493
|
return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
|
|
5697
6494
|
"div",
|
|
5698
6495
|
{
|
|
@@ -5713,44 +6510,59 @@ function ParticipantTile({
|
|
|
5713
6510
|
muted: participant.isLocal
|
|
5714
6511
|
}
|
|
5715
6512
|
) : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "mx-meet__tile-placeholder", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Avatar, { name: participant.displayName, url: participant.avatarUrl, size: featured ? 96 : 56 }) }),
|
|
6513
|
+
participant.handRaised ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { className: "mx-meet__tile-hand", title: `Hand raised \xB7 ${ordinal(handPosition)} in line`, children: [
|
|
6514
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(HandIcon, { width: 14, height: 14 }),
|
|
6515
|
+
handPosition > 0 ? handPosition : null
|
|
6516
|
+
] }) : null,
|
|
5716
6517
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "mx-meet__tile-bar", children: [
|
|
5717
6518
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { className: "mx-meet__tile-name", children: [
|
|
6519
|
+
spotlit ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { title: "Spotlighted for everyone", className: "mx-meet__tile-flag", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(SpotlightIcon, { width: 12, height: 12 }) }) : null,
|
|
5718
6520
|
participant.displayName,
|
|
5719
6521
|
participant.isLocal ? " (you)" : "",
|
|
5720
6522
|
participant.isAgent ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "mx-meet__badge", children: "AI" }) : null,
|
|
5721
6523
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(RoleBadge, { role: participant.role })
|
|
5722
6524
|
] }),
|
|
5723
6525
|
!participant.micEnabled ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(MicOffIcon, { width: 14, height: 14 }) : null,
|
|
5724
|
-
|
|
5725
|
-
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(ConnectionDot, { quality: participant.quality })
|
|
6526
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(QualityBars, { quality: participant.quality })
|
|
5726
6527
|
] }),
|
|
5727
|
-
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "mx-meet__tile-actions", children: [
|
|
6528
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: menu ? "mx-meet__tile-actions mx-meet__tile-actions--open" : "mx-meet__tile-actions", children: [
|
|
5728
6529
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
5729
6530
|
ControlButton,
|
|
5730
6531
|
{
|
|
5731
|
-
label:
|
|
5732
|
-
|
|
6532
|
+
label: pinnedHere ? "Unpin" : `Pin ${participant.displayName} for me`,
|
|
6533
|
+
active: pinnedHere,
|
|
6534
|
+
onClick: () => host?.engine.pin(pinnedHere ? null : participant.identity),
|
|
5733
6535
|
children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(PinIcon, { width: 16, height: 16 })
|
|
5734
6536
|
}
|
|
5735
6537
|
),
|
|
5736
|
-
|
|
5737
|
-
|
|
5738
|
-
|
|
5739
|
-
|
|
5740
|
-
|
|
5741
|
-
|
|
5742
|
-
|
|
5743
|
-
|
|
5744
|
-
|
|
5745
|
-
|
|
5746
|
-
|
|
5747
|
-
|
|
5748
|
-
) : null
|
|
6538
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { className: "mx-meet__anchor", children: [
|
|
6539
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
6540
|
+
ControlButton,
|
|
6541
|
+
{
|
|
6542
|
+
label: `More for ${participant.displayName}`,
|
|
6543
|
+
active: menu,
|
|
6544
|
+
onClick: () => setMenu(!menu),
|
|
6545
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(MoreIcon, { width: 16, height: 16 })
|
|
6546
|
+
}
|
|
6547
|
+
),
|
|
6548
|
+
menu ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(PersonMenu, { participant, onClose: () => setMenu(false), placement: "down" }) : null
|
|
6549
|
+
] })
|
|
5749
6550
|
] })
|
|
5750
6551
|
]
|
|
5751
6552
|
}
|
|
5752
6553
|
);
|
|
5753
6554
|
}
|
|
6555
|
+
function ordinal(position) {
|
|
6556
|
+
if (position <= 0) return "";
|
|
6557
|
+
const suffix = position % 100 >= 11 && position % 100 <= 13 ? "th" : { 1: "st", 2: "nd", 3: "rd" }[position % 10] ?? "th";
|
|
6558
|
+
return `${position}${suffix}`;
|
|
6559
|
+
}
|
|
6560
|
+
function QualityBars({ quality }) {
|
|
6561
|
+
if (quality === "unknown") return null;
|
|
6562
|
+
const bars = quality === "excellent" ? 3 : quality === "good" ? 2 : quality === "poor" ? 1 : 0;
|
|
6563
|
+
const label = quality === "lost" ? "Connection lost" : quality === "poor" ? "Poor connection" : `Connection ${quality}`;
|
|
6564
|
+
return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: `mx-meet__signal mx-meet__signal--${quality}`, title: label, "aria-label": label, role: "img", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(SignalIcon, { bars, width: 14, height: 14 }) });
|
|
6565
|
+
}
|
|
5754
6566
|
function Stage({ pageSize }) {
|
|
5755
6567
|
const stage = useStage(pageSize);
|
|
5756
6568
|
const columns = gridColumns(stage.tiles.length);
|
|
@@ -5779,22 +6591,52 @@ function Stage({ pageSize }) {
|
|
|
5779
6591
|
] }) : null
|
|
5780
6592
|
] });
|
|
5781
6593
|
}
|
|
6594
|
+
var MEET_REACTIONS = ["\u{1F496}", "\u{1F44D}", "\u{1F389}", "\u{1F44F}", "\u{1F602}", "\u{1F62E}", "\u{1F622}", "\u{1F914}", "\u{1F44E}"];
|
|
5782
6595
|
function ControlBar({ onLeave }) {
|
|
5783
6596
|
const room = useRoom();
|
|
5784
6597
|
const recording = useRecording();
|
|
5785
6598
|
const [panel, setPanel] = (0, import_react4.useState)("none");
|
|
6599
|
+
const [chatTo, setChatTo] = (0, import_react4.useState)(null);
|
|
6600
|
+
const [popover, setPopover] = (0, import_react4.useState)("none");
|
|
6601
|
+
const [shortcuts, setShortcuts] = (0, import_react4.useState)(false);
|
|
5786
6602
|
const snapshot = useMeetSnapshot();
|
|
5787
6603
|
const lobby = useLobby();
|
|
6604
|
+
const controls = useHostControls();
|
|
6605
|
+
const queue = useHandQueue();
|
|
5788
6606
|
const micUnavailable = useLocalMedia().microphone.unavailable;
|
|
6607
|
+
const unread = useUnreadChat(panel === "chat");
|
|
6608
|
+
const toggle = (next) => setPanel(panel === next ? "none" : next);
|
|
6609
|
+
useMeetShortcuts({
|
|
6610
|
+
mic: () => void room.toggleMicrophone(),
|
|
6611
|
+
camera: () => void room.toggleCamera(),
|
|
6612
|
+
hand: () => void room.toggleHand(),
|
|
6613
|
+
chat: () => toggle("chat"),
|
|
6614
|
+
people: () => toggle("people"),
|
|
6615
|
+
help: () => setShortcuts(true)
|
|
6616
|
+
});
|
|
6617
|
+
const hasSpotlight = snapshot?.spotlight !== null && snapshot?.spotlight !== void 0;
|
|
5789
6618
|
return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
|
|
5790
|
-
|
|
5791
|
-
|
|
6619
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(MeetingOverlays, {}),
|
|
6620
|
+
shortcuts ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(ShortcutsSheet, { onClose: () => setShortcuts(false) }) : null,
|
|
6621
|
+
panel === "chat" ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(ChatPanel, { onClose: () => setPanel("none"), to: chatTo, onTo: setChatTo }) : null,
|
|
6622
|
+
panel === "people" ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
6623
|
+
PeoplePanel,
|
|
6624
|
+
{
|
|
6625
|
+
onClose: () => setPanel("none"),
|
|
6626
|
+
onMessage: (identity) => {
|
|
6627
|
+
setChatTo(identity);
|
|
6628
|
+
setPanel("chat");
|
|
6629
|
+
}
|
|
6630
|
+
}
|
|
6631
|
+
) : null,
|
|
5792
6632
|
panel === "ai" ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(AiPanel, { onClose: () => setPanel("none") }) : null,
|
|
6633
|
+
panel === "settings" ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(SettingsPanel, { onClose: () => setPanel("none") }) : null,
|
|
6634
|
+
controls.notice !== null && panel !== "people" ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Toast, { text: controls.notice.text, failed: controls.notice.failed, onDone: controls.clearNotice }) : null,
|
|
5793
6635
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "mx-meet__controls", children: [
|
|
5794
6636
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
5795
6637
|
ControlButton,
|
|
5796
6638
|
{
|
|
5797
|
-
label: micUnavailable !== null ? `Unmute \u2014 ${micUnavailable.remedy}` : room.local?.micEnabled === true ? "Mute" : "Unmute",
|
|
6639
|
+
label: micUnavailable !== null ? `Unmute \u2014 ${micUnavailable.remedy}` : room.local?.micEnabled === true ? "Mute (\u2318D)" : "Unmute (\u2318D)",
|
|
5798
6640
|
active: room.local?.micEnabled === true,
|
|
5799
6641
|
onClick: () => void room.toggleMicrophone(),
|
|
5800
6642
|
children: room.local?.micEnabled === true ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(MicIcon, {}) : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(MicOffIcon, {})
|
|
@@ -5803,7 +6645,7 @@ function ControlBar({ onLeave }) {
|
|
|
5803
6645
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
5804
6646
|
ControlButton,
|
|
5805
6647
|
{
|
|
5806
|
-
label: room.local?.cameraEnabled === true ? "Stop video" : "Start video",
|
|
6648
|
+
label: room.local?.cameraEnabled === true ? "Stop video (\u2318E)" : "Start video (\u2318E)",
|
|
5807
6649
|
active: room.local?.cameraEnabled === true,
|
|
5808
6650
|
onClick: () => void room.toggleCamera(),
|
|
5809
6651
|
children: room.local?.cameraEnabled === true ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(CameraIcon, {}) : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(CameraOffIcon, {})
|
|
@@ -5818,31 +6660,152 @@ function ControlBar({ onLeave }) {
|
|
|
5818
6660
|
children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(ScreenShareIcon, {})
|
|
5819
6661
|
}
|
|
5820
6662
|
),
|
|
6663
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { className: "mx-meet__anchor", children: [
|
|
6664
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
6665
|
+
ControlButton,
|
|
6666
|
+
{
|
|
6667
|
+
label: "Send a reaction",
|
|
6668
|
+
active: popover === "reactions",
|
|
6669
|
+
onClick: () => setPopover(popover === "reactions" ? "none" : "reactions"),
|
|
6670
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(SmileIcon, {})
|
|
6671
|
+
}
|
|
6672
|
+
),
|
|
6673
|
+
popover === "reactions" ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Popover, { label: "Reactions", onClose: () => setPopover("none"), children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "mx-meet__emoji-bar", children: MEET_REACTIONS.map((emoji) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
6674
|
+
"button",
|
|
6675
|
+
{
|
|
6676
|
+
type: "button",
|
|
6677
|
+
className: "mx-meet__emoji",
|
|
6678
|
+
"aria-label": `React ${emoji}`,
|
|
6679
|
+
onClick: () => void room.react(emoji),
|
|
6680
|
+
children: emoji
|
|
6681
|
+
},
|
|
6682
|
+
emoji
|
|
6683
|
+
)) }) }) : null
|
|
6684
|
+
] }),
|
|
5821
6685
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
5822
6686
|
ControlButton,
|
|
5823
6687
|
{
|
|
5824
|
-
label: room.local?.handRaised === true ? "Lower hand" : "Raise hand",
|
|
6688
|
+
label: room.local?.handRaised === true ? "Lower hand (\u2318\u2325H)" : "Raise hand (\u2318\u2325H)",
|
|
5825
6689
|
active: room.local?.handRaised === true,
|
|
5826
6690
|
onClick: () => void room.toggleHand(),
|
|
5827
6691
|
children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(HandIcon, {})
|
|
5828
6692
|
}
|
|
5829
6693
|
),
|
|
5830
|
-
/* @__PURE__ */ (0, import_jsx_runtime3.
|
|
6694
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(ControlButton, { label: "Chat (\u2318\u2325C)", active: panel === "chat", onClick: () => toggle("chat"), children: [
|
|
6695
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(ChatIcon, {}),
|
|
6696
|
+
unread > 0 ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "mx-meet__dot", "aria-hidden": "true" }) : null
|
|
6697
|
+
] }),
|
|
5831
6698
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
|
|
5832
6699
|
ControlButton,
|
|
5833
6700
|
{
|
|
5834
6701
|
label: lobby.waiting.length > 0 ? `People \u2014 ${lobby.waiting.length} waiting` : `People \u2014 ${(snapshot?.participants ?? []).length} in the meeting`,
|
|
5835
6702
|
active: panel === "people",
|
|
5836
|
-
onClick: () =>
|
|
6703
|
+
onClick: () => toggle("people"),
|
|
5837
6704
|
children: [
|
|
5838
6705
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(PeopleIcon, {}),
|
|
5839
|
-
lobby.waiting.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "mx-meet__dot", "aria-hidden": "true" }) : null
|
|
6706
|
+
lobby.waiting.length > 0 || controls.isHost && queue.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "mx-meet__dot", "aria-hidden": "true" }) : null
|
|
5840
6707
|
]
|
|
5841
6708
|
}
|
|
5842
6709
|
),
|
|
5843
|
-
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(AiControl, { active: panel === "ai", onToggle: () =>
|
|
6710
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(AiControl, { active: panel === "ai", onToggle: () => toggle("ai") }),
|
|
5844
6711
|
recording.canStart ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(ControlButton, { label: "Start recording", onClick: () => void recording.start(), children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(RecordIcon, {}) }) : null,
|
|
5845
6712
|
recording.canStop ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(ControlButton, { label: "Stop recording", active: true, onClick: () => void recording.stop(), children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(RecordIcon, {}) }) : null,
|
|
6713
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { className: "mx-meet__anchor", children: [
|
|
6714
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
6715
|
+
ControlButton,
|
|
6716
|
+
{
|
|
6717
|
+
label: "More options",
|
|
6718
|
+
active: popover === "more" || panel === "settings",
|
|
6719
|
+
onClick: () => setPopover(popover === "more" ? "none" : "more"),
|
|
6720
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(MoreIcon, {})
|
|
6721
|
+
}
|
|
6722
|
+
),
|
|
6723
|
+
popover === "more" ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Popover, { label: "More options", onClose: () => setPopover("none"), children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "mx-meet__menu", role: "menu", children: [
|
|
6724
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
|
|
6725
|
+
MenuItem,
|
|
6726
|
+
{
|
|
6727
|
+
onSelect: () => {
|
|
6728
|
+
setPopover("none");
|
|
6729
|
+
setPanel("settings");
|
|
6730
|
+
},
|
|
6731
|
+
children: [
|
|
6732
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(SettingsIcon, { width: 16, height: 16 }),
|
|
6733
|
+
" Audio, video and effects"
|
|
6734
|
+
]
|
|
6735
|
+
}
|
|
6736
|
+
),
|
|
6737
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
|
|
6738
|
+
MenuItem,
|
|
6739
|
+
{
|
|
6740
|
+
onSelect: () => {
|
|
6741
|
+
setPopover("none");
|
|
6742
|
+
setShortcuts(true);
|
|
6743
|
+
},
|
|
6744
|
+
children: [
|
|
6745
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(KeyboardIcon, { width: 16, height: 16 }),
|
|
6746
|
+
" Keyboard shortcuts"
|
|
6747
|
+
]
|
|
6748
|
+
}
|
|
6749
|
+
),
|
|
6750
|
+
controls.isHost ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
|
|
6751
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "mx-meet__menu-sep", role: "separator" }),
|
|
6752
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
|
|
6753
|
+
MenuItem,
|
|
6754
|
+
{
|
|
6755
|
+
onSelect: () => {
|
|
6756
|
+
setPopover("none");
|
|
6757
|
+
void controls.run({ action: "mute_all" });
|
|
6758
|
+
},
|
|
6759
|
+
children: [
|
|
6760
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(MicOffIcon, { width: 16, height: 16 }),
|
|
6761
|
+
" Mute everyone"
|
|
6762
|
+
]
|
|
6763
|
+
}
|
|
6764
|
+
),
|
|
6765
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
|
|
6766
|
+
MenuItem,
|
|
6767
|
+
{
|
|
6768
|
+
onSelect: () => {
|
|
6769
|
+
setPopover("none");
|
|
6770
|
+
void controls.run({ action: "ask_all_unmute" });
|
|
6771
|
+
},
|
|
6772
|
+
children: [
|
|
6773
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(MicIcon, { width: 16, height: 16 }),
|
|
6774
|
+
" Ask everyone to unmute"
|
|
6775
|
+
]
|
|
6776
|
+
}
|
|
6777
|
+
),
|
|
6778
|
+
queue.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
|
|
6779
|
+
MenuItem,
|
|
6780
|
+
{
|
|
6781
|
+
onSelect: () => {
|
|
6782
|
+
setPopover("none");
|
|
6783
|
+
void controls.run({ action: "lower_all_hands" });
|
|
6784
|
+
},
|
|
6785
|
+
children: [
|
|
6786
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(HandIcon, { width: 16, height: 16 }),
|
|
6787
|
+
" Lower all hands (",
|
|
6788
|
+
queue.length,
|
|
6789
|
+
")"
|
|
6790
|
+
]
|
|
6791
|
+
}
|
|
6792
|
+
) : null,
|
|
6793
|
+
hasSpotlight ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
|
|
6794
|
+
MenuItem,
|
|
6795
|
+
{
|
|
6796
|
+
onSelect: () => {
|
|
6797
|
+
setPopover("none");
|
|
6798
|
+
void controls.run({ action: "spotlight", identity: null });
|
|
6799
|
+
},
|
|
6800
|
+
children: [
|
|
6801
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(SpotlightIcon, { width: 16, height: 16 }),
|
|
6802
|
+
" Remove spotlight"
|
|
6803
|
+
]
|
|
6804
|
+
}
|
|
6805
|
+
) : null
|
|
6806
|
+
] }) : null
|
|
6807
|
+
] }) }) : null
|
|
6808
|
+
] }),
|
|
5846
6809
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
5847
6810
|
ControlButton,
|
|
5848
6811
|
{
|
|
@@ -5858,6 +6821,150 @@ function ControlBar({ onLeave }) {
|
|
|
5858
6821
|
] })
|
|
5859
6822
|
] });
|
|
5860
6823
|
}
|
|
6824
|
+
function useUnreadChat(open) {
|
|
6825
|
+
const snapshot = useMeetSnapshot();
|
|
6826
|
+
const chat = snapshot?.chat ?? [];
|
|
6827
|
+
const local = snapshot?.localIdentity ?? null;
|
|
6828
|
+
const [seen, setSeen] = (0, import_react4.useState)(0);
|
|
6829
|
+
(0, import_react4.useEffect)(() => {
|
|
6830
|
+
if (open) setSeen(chat.length);
|
|
6831
|
+
}, [open, chat.length]);
|
|
6832
|
+
const loaded = snapshot?.chatLoaded === true;
|
|
6833
|
+
const [baseline, setBaseline] = (0, import_react4.useState)(null);
|
|
6834
|
+
(0, import_react4.useEffect)(() => {
|
|
6835
|
+
if (loaded && baseline === null) setBaseline(chat.length);
|
|
6836
|
+
}, [loaded, baseline, chat.length]);
|
|
6837
|
+
if (open) return 0;
|
|
6838
|
+
const from = Math.max(seen, baseline ?? chat.length);
|
|
6839
|
+
return chat.slice(from).filter((message) => message.identity !== local).length;
|
|
6840
|
+
}
|
|
6841
|
+
function Popover({
|
|
6842
|
+
label,
|
|
6843
|
+
onClose,
|
|
6844
|
+
children,
|
|
6845
|
+
placement = "up"
|
|
6846
|
+
}) {
|
|
6847
|
+
const ref = (0, import_react4.useRef)(null);
|
|
6848
|
+
const [style, setStyle] = (0, import_react4.useState)({ visibility: "hidden" });
|
|
6849
|
+
(0, import_react4.useLayoutEffect)(() => {
|
|
6850
|
+
const element = ref.current;
|
|
6851
|
+
const anchor = element?.parentElement ?? null;
|
|
6852
|
+
if (element === null || anchor === null) return;
|
|
6853
|
+
const place = () => {
|
|
6854
|
+
const rect = anchor.getBoundingClientRect();
|
|
6855
|
+
const width = element.offsetWidth;
|
|
6856
|
+
const height = element.offsetHeight;
|
|
6857
|
+
const gutter = 8;
|
|
6858
|
+
const left = Math.min(
|
|
6859
|
+
Math.max(gutter, rect.left + rect.width / 2 - width / 2),
|
|
6860
|
+
window.innerWidth - width - gutter
|
|
6861
|
+
);
|
|
6862
|
+
const fitsBelow = rect.bottom + gutter + height <= window.innerHeight;
|
|
6863
|
+
const fitsAbove = rect.top - gutter - height >= 0;
|
|
6864
|
+
const down = placement === "down" ? fitsBelow || !fitsAbove : !fitsAbove && fitsBelow;
|
|
6865
|
+
setStyle(
|
|
6866
|
+
down ? { position: "fixed", left, top: rect.bottom + gutter } : { position: "fixed", left, bottom: window.innerHeight - rect.top + gutter }
|
|
6867
|
+
);
|
|
6868
|
+
};
|
|
6869
|
+
place();
|
|
6870
|
+
window.addEventListener("resize", place);
|
|
6871
|
+
return () => window.removeEventListener("resize", place);
|
|
6872
|
+
}, [placement]);
|
|
6873
|
+
(0, import_react4.useEffect)(() => {
|
|
6874
|
+
const onKey = (event) => {
|
|
6875
|
+
if (event.key === "Escape") onClose();
|
|
6876
|
+
};
|
|
6877
|
+
const onPointer = (event) => {
|
|
6878
|
+
const anchor = ref.current?.parentElement ?? null;
|
|
6879
|
+
if (anchor !== null && !anchor.contains(event.target)) onClose();
|
|
6880
|
+
};
|
|
6881
|
+
window.addEventListener("keydown", onKey);
|
|
6882
|
+
window.addEventListener("pointerdown", onPointer);
|
|
6883
|
+
return () => {
|
|
6884
|
+
window.removeEventListener("keydown", onKey);
|
|
6885
|
+
window.removeEventListener("pointerdown", onPointer);
|
|
6886
|
+
};
|
|
6887
|
+
}, [onClose]);
|
|
6888
|
+
return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { ref, className: "mx-meet__popover", style, role: "dialog", "aria-label": label, children });
|
|
6889
|
+
}
|
|
6890
|
+
function MenuItem({
|
|
6891
|
+
onSelect,
|
|
6892
|
+
danger = false,
|
|
6893
|
+
children
|
|
6894
|
+
}) {
|
|
6895
|
+
return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
6896
|
+
"button",
|
|
6897
|
+
{
|
|
6898
|
+
type: "button",
|
|
6899
|
+
role: "menuitem",
|
|
6900
|
+
className: danger ? "mx-meet__menu-item mx-meet__menu-item--danger" : "mx-meet__menu-item",
|
|
6901
|
+
onClick: onSelect,
|
|
6902
|
+
children
|
|
6903
|
+
}
|
|
6904
|
+
);
|
|
6905
|
+
}
|
|
6906
|
+
function Toast({ text: text3, failed, onDone }) {
|
|
6907
|
+
(0, import_react4.useEffect)(() => {
|
|
6908
|
+
const timer = setTimeout(onDone, failed ? 7e3 : 4e3);
|
|
6909
|
+
return () => clearTimeout(timer);
|
|
6910
|
+
}, [text3, failed, onDone]);
|
|
6911
|
+
return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: failed ? "mx-meet__toast mx-meet__toast--failed" : "mx-meet__toast", role: "status", children: [
|
|
6912
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { children: text3 }),
|
|
6913
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { type: "button", className: "mx-meet__toast-close", "aria-label": "Dismiss", onClick: onDone, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(CloseIcon, { width: 14, height: 14 }) })
|
|
6914
|
+
] });
|
|
6915
|
+
}
|
|
6916
|
+
function MeetingOverlays() {
|
|
6917
|
+
const reactions = useReactions();
|
|
6918
|
+
const { notice, dismiss } = useModerationNotice();
|
|
6919
|
+
const room = useRoom();
|
|
6920
|
+
return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
|
|
6921
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(ReactionOverlay, { reactions }),
|
|
6922
|
+
notice !== null && notice.kind !== "removed" ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "mx-meet__notice-bar", role: "alert", children: [
|
|
6923
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { children: notice.text }),
|
|
6924
|
+
notice.kind === "ask-unmute" && room.local?.micEnabled !== true ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
6925
|
+
"button",
|
|
6926
|
+
{
|
|
6927
|
+
type: "button",
|
|
6928
|
+
className: "mx-meet__primary mx-meet__primary--small",
|
|
6929
|
+
onClick: () => {
|
|
6930
|
+
dismiss();
|
|
6931
|
+
void room.toggleMicrophone();
|
|
6932
|
+
},
|
|
6933
|
+
children: "Unmute"
|
|
6934
|
+
}
|
|
6935
|
+
) : null,
|
|
6936
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { type: "button", className: "mx-meet__toast-close", "aria-label": "Dismiss", onClick: dismiss, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(CloseIcon, { width: 14, height: 14 }) })
|
|
6937
|
+
] }) : null
|
|
6938
|
+
] });
|
|
6939
|
+
}
|
|
6940
|
+
function ShortcutsSheet({ onClose }) {
|
|
6941
|
+
(0, import_react4.useEffect)(() => {
|
|
6942
|
+
const onKey = (event) => {
|
|
6943
|
+
if (event.key === "Escape") onClose();
|
|
6944
|
+
};
|
|
6945
|
+
window.addEventListener("keydown", onKey);
|
|
6946
|
+
return () => window.removeEventListener("keydown", onKey);
|
|
6947
|
+
}, [onClose]);
|
|
6948
|
+
return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "mx-meet__sheet-backdrop", onClick: onClose, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
|
|
6949
|
+
"div",
|
|
6950
|
+
{
|
|
6951
|
+
className: "mx-meet__sheet",
|
|
6952
|
+
role: "dialog",
|
|
6953
|
+
"aria-label": "Keyboard shortcuts",
|
|
6954
|
+
onClick: (event) => event.stopPropagation(),
|
|
6955
|
+
children: [
|
|
6956
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("header", { className: "mx-meet__panel-head", children: [
|
|
6957
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("h2", { className: "mx-meet__panel-title", children: "Keyboard shortcuts" }),
|
|
6958
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(ControlButton, { label: "Close", onClick: onClose, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(CloseIcon, { width: 16, height: 16 }) })
|
|
6959
|
+
] }),
|
|
6960
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("dl", { className: "mx-meet__shortcuts", children: MEET_SHORTCUTS.map((shortcut) => /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "mx-meet__shortcut", children: [
|
|
6961
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("dt", { children: shortcut.label }),
|
|
6962
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("dd", { children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("kbd", { className: "mx-meet__kbd", children: shortcut.keys }) })
|
|
6963
|
+
] }, shortcut.id)) })
|
|
6964
|
+
]
|
|
6965
|
+
}
|
|
6966
|
+
) });
|
|
6967
|
+
}
|
|
5861
6968
|
function AiControl({ active, onToggle }) {
|
|
5862
6969
|
const snapshot = useMeetSnapshot();
|
|
5863
6970
|
const meeting = snapshot?.meeting ?? null;
|
|
@@ -5877,117 +6984,641 @@ function PanelShell({
|
|
|
5877
6984
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "mx-meet__panel-body", children })
|
|
5878
6985
|
] });
|
|
5879
6986
|
}
|
|
5880
|
-
function ChatPanel({
|
|
6987
|
+
function ChatPanel({
|
|
6988
|
+
onClose,
|
|
6989
|
+
to = null,
|
|
6990
|
+
onTo
|
|
6991
|
+
}) {
|
|
5881
6992
|
const snapshot = useMeetSnapshot();
|
|
5882
6993
|
const room = useRoom();
|
|
5883
6994
|
const [draft, setDraft] = (0, import_react4.useState)("");
|
|
6995
|
+
const [recipient, setRecipient] = (0, import_react4.useState)(to);
|
|
6996
|
+
const listRef = (0, import_react4.useRef)(null);
|
|
5884
6997
|
const messages = snapshot?.chat ?? [];
|
|
5885
|
-
|
|
5886
|
-
|
|
5887
|
-
|
|
5888
|
-
|
|
5889
|
-
|
|
5890
|
-
|
|
6998
|
+
const local = snapshot?.localIdentity ?? null;
|
|
6999
|
+
const people = (snapshot?.participants ?? []).filter(
|
|
7000
|
+
(participant) => !participant.isLocal && !participant.isAgent
|
|
7001
|
+
);
|
|
7002
|
+
const recipientName = recipient === null ? null : people.find((p) => p.identity === recipient)?.displayName ?? null;
|
|
7003
|
+
(0, import_react4.useEffect)(() => setRecipient(to), [to]);
|
|
7004
|
+
(0, import_react4.useEffect)(() => {
|
|
7005
|
+
const list = listRef.current;
|
|
7006
|
+
if (list !== null) list.scrollTop = list.scrollHeight;
|
|
7007
|
+
}, [messages.length]);
|
|
7008
|
+
const choose = (next) => {
|
|
7009
|
+
setRecipient(next);
|
|
7010
|
+
onTo?.(next);
|
|
7011
|
+
};
|
|
7012
|
+
return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(PanelShell, { title: "Chat", onClose, children: [
|
|
7013
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("ul", { className: "mx-meet__chat", ref: listRef, children: [
|
|
7014
|
+
snapshot?.chatHistoryError != null ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("li", { className: "mx-meet__panel-note", children: snapshot.chatHistoryError }) : null,
|
|
7015
|
+
snapshot?.chatLoaded !== true && messages.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("li", { className: "mx-meet__panel-note", children: "Reading earlier messages\u2026" }) : messages.length === 0 && snapshot?.chatHistoryError == null ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("li", { className: "mx-meet__panel-note", children: "No messages yet. Messages to everyone stay with the meeting, so people who join later see them." }) : null,
|
|
7016
|
+
messages.map((message) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(ChatLine, { message, local, people }, message.id))
|
|
7017
|
+
] }),
|
|
5891
7018
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
|
|
5892
7019
|
"form",
|
|
5893
7020
|
{
|
|
5894
|
-
className: "mx-meet__composer",
|
|
7021
|
+
className: "mx-meet__composer mx-meet__composer--chat",
|
|
5895
7022
|
onSubmit: (event) => {
|
|
5896
7023
|
event.preventDefault();
|
|
5897
7024
|
const text3 = draft;
|
|
7025
|
+
if (text3.trim().length === 0) return;
|
|
5898
7026
|
setDraft("");
|
|
5899
|
-
void room.sendChat(text3);
|
|
7027
|
+
if (recipient === null) void room.sendChat(text3).catch(() => void 0);
|
|
7028
|
+
else void room.sendPrivateChat(recipient, text3);
|
|
5900
7029
|
},
|
|
5901
7030
|
children: [
|
|
5902
|
-
/* @__PURE__ */ (0, import_jsx_runtime3.
|
|
5903
|
-
"
|
|
7031
|
+
people.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
|
|
7032
|
+
"select",
|
|
5904
7033
|
{
|
|
5905
|
-
className: "mx-
|
|
5906
|
-
|
|
5907
|
-
|
|
5908
|
-
|
|
5909
|
-
|
|
7034
|
+
className: "mx-meet__select mx-meet__select--compact",
|
|
7035
|
+
"aria-label": "Send to",
|
|
7036
|
+
value: recipient ?? "",
|
|
7037
|
+
onChange: (event) => choose(event.target.value === "" ? null : event.target.value),
|
|
7038
|
+
children: [
|
|
7039
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("option", { value: "", children: "Everyone" }),
|
|
7040
|
+
people.map((participant) => /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("option", { value: participant.identity, children: [
|
|
7041
|
+
participant.displayName,
|
|
7042
|
+
" (private)"
|
|
7043
|
+
] }, participant.identity))
|
|
7044
|
+
]
|
|
5910
7045
|
}
|
|
5911
|
-
),
|
|
5912
|
-
/* @__PURE__ */ (0, import_jsx_runtime3.
|
|
7046
|
+
) : null,
|
|
7047
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "mx-meet__composer-row", children: [
|
|
7048
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
7049
|
+
"input",
|
|
7050
|
+
{
|
|
7051
|
+
className: "mx-meet__input",
|
|
7052
|
+
value: draft,
|
|
7053
|
+
onChange: (event) => setDraft(event.target.value),
|
|
7054
|
+
placeholder: recipientName === null ? "Message everyone" : `Message ${recipientName} privately`,
|
|
7055
|
+
"aria-label": recipientName === null ? "Message everyone" : `Message ${recipientName} privately`,
|
|
7056
|
+
maxLength: 4e3
|
|
7057
|
+
}
|
|
7058
|
+
),
|
|
7059
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { type: "submit", className: "mx-meet__send", disabled: draft.trim().length === 0, children: "Send" })
|
|
7060
|
+
] }),
|
|
7061
|
+
recipientName !== null ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("p", { className: "mx-meet__panel-note", children: [
|
|
7062
|
+
"Only ",
|
|
7063
|
+
recipientName,
|
|
7064
|
+
" sees this. Private messages are not saved."
|
|
7065
|
+
] }) : null
|
|
5913
7066
|
]
|
|
5914
7067
|
}
|
|
5915
7068
|
)
|
|
5916
7069
|
] });
|
|
5917
7070
|
}
|
|
5918
|
-
function
|
|
7071
|
+
function ChatLine({
|
|
7072
|
+
message,
|
|
7073
|
+
local,
|
|
7074
|
+
people
|
|
7075
|
+
}) {
|
|
7076
|
+
const mine = message.identity === local;
|
|
7077
|
+
const privateTo = message.to ?? null;
|
|
7078
|
+
const counterpart = privateTo === null ? null : mine ? people.find((p) => p.identity === privateTo)?.displayName ?? "them" : "you";
|
|
7079
|
+
return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
|
|
7080
|
+
"li",
|
|
7081
|
+
{
|
|
7082
|
+
className: [
|
|
7083
|
+
"mx-meet__chat-line",
|
|
7084
|
+
privateTo !== null ? "mx-meet__chat-line--private" : "",
|
|
7085
|
+
message.failed !== void 0 ? "mx-meet__chat-line--failed" : ""
|
|
7086
|
+
].filter(Boolean).join(" "),
|
|
7087
|
+
children: [
|
|
7088
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { className: "mx-meet__chat-meta", children: [
|
|
7089
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("strong", { children: mine ? "You" : message.displayName }),
|
|
7090
|
+
counterpart !== null ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { className: "mx-meet__chat-private", children: [
|
|
7091
|
+
"to ",
|
|
7092
|
+
counterpart,
|
|
7093
|
+
" \xB7 private"
|
|
7094
|
+
] }) : null,
|
|
7095
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("time", { dateTime: message.at, children: clockOf(message.at) })
|
|
7096
|
+
] }),
|
|
7097
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "mx-meet__chat-text", children: message.text }),
|
|
7098
|
+
message.failed !== void 0 ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "mx-meet__chat-failed", children: message.failed }) : privateTo === null && message.saved === false ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "mx-meet__chat-pending", children: message.id.startsWith("pending-") ? "Sending\u2026" : "Not saved \u2014 people who join later will not see this" }) : null
|
|
7099
|
+
]
|
|
7100
|
+
}
|
|
7101
|
+
);
|
|
7102
|
+
}
|
|
7103
|
+
function PeoplePanel({
|
|
7104
|
+
onClose,
|
|
7105
|
+
onMessage
|
|
7106
|
+
}) {
|
|
5919
7107
|
const snapshot = useMeetSnapshot();
|
|
5920
7108
|
const controls = useHostControls();
|
|
7109
|
+
const queue = useHandQueue();
|
|
7110
|
+
const [query, setQuery] = (0, import_react4.useState)("");
|
|
7111
|
+
const [menuFor, setMenuFor] = (0, import_react4.useState)(null);
|
|
5921
7112
|
const myRole = useRole();
|
|
5922
7113
|
const local = snapshot?.localIdentity ?? null;
|
|
5923
|
-
|
|
7114
|
+
const everyone = snapshot?.participants ?? [];
|
|
7115
|
+
const needle = query.trim().toLowerCase();
|
|
7116
|
+
const shown = needle.length === 0 ? everyone : everyone.filter((participant) => participant.displayName.toLowerCase().includes(needle));
|
|
7117
|
+
return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(PanelShell, { title: `People (${everyone.length})`, onClose, children: [
|
|
5924
7118
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(LobbyPanel, {}),
|
|
5925
|
-
|
|
5926
|
-
|
|
5927
|
-
|
|
5928
|
-
|
|
5929
|
-
|
|
5930
|
-
|
|
5931
|
-
|
|
7119
|
+
queue.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("section", { className: "mx-meet__section", "aria-label": "Raised hands", children: [
|
|
7120
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("header", { className: "mx-meet__section-head", children: [
|
|
7121
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("h3", { className: "mx-meet__section-title", children: [
|
|
7122
|
+
"Raised hands \xB7 ",
|
|
7123
|
+
queue.length
|
|
7124
|
+
] }),
|
|
7125
|
+
controls.isHost ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
7126
|
+
"button",
|
|
5932
7127
|
{
|
|
5933
|
-
|
|
7128
|
+
type: "button",
|
|
7129
|
+
className: "mx-meet__link",
|
|
7130
|
+
onClick: () => void controls.run({ action: "lower_all_hands" }),
|
|
7131
|
+
children: "Lower all"
|
|
5934
7132
|
}
|
|
5935
|
-
)
|
|
5936
|
-
isNoteTaker(participant) ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "mx-meet__badge", children: "Meeting assistant" }) : null
|
|
7133
|
+
) : null
|
|
5937
7134
|
] }),
|
|
5938
|
-
|
|
5939
|
-
|
|
5940
|
-
|
|
5941
|
-
|
|
7135
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("ol", { className: "mx-meet__queue", children: queue.map((participant) => /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("li", { className: "mx-meet__person", children: [
|
|
7136
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Avatar, { name: participant.displayName, url: participant.avatarUrl, size: 28 }),
|
|
7137
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { className: "mx-meet__person-name", children: [
|
|
7138
|
+
participant.displayName,
|
|
7139
|
+
participant.identity === local ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "mx-meet__you", children: " (you)" }) : null
|
|
7140
|
+
] }),
|
|
7141
|
+
controls.isHost || participant.identity === local ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
7142
|
+
"button",
|
|
7143
|
+
{
|
|
7144
|
+
type: "button",
|
|
7145
|
+
className: "mx-meet__link",
|
|
7146
|
+
onClick: () => void controls.run({ action: "lower_hand", identity: participant.identity }),
|
|
7147
|
+
children: "Lower"
|
|
7148
|
+
}
|
|
7149
|
+
) : null
|
|
7150
|
+
] }, participant.identity)) })
|
|
7151
|
+
] }) : null,
|
|
7152
|
+
controls.isHost && everyone.length > 1 ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "mx-meet__bulk", children: [
|
|
7153
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("button", { type: "button", className: "mx-meet__chip", onClick: () => void controls.run({ action: "mute_all" }), children: [
|
|
7154
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(MicOffIcon, { width: 14, height: 14 }),
|
|
7155
|
+
" Mute all"
|
|
7156
|
+
] }),
|
|
7157
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
|
|
7158
|
+
"button",
|
|
7159
|
+
{
|
|
7160
|
+
type: "button",
|
|
7161
|
+
className: "mx-meet__chip",
|
|
7162
|
+
onClick: () => void controls.run({ action: "ask_all_unmute" }),
|
|
7163
|
+
children: [
|
|
7164
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(MicIcon, { width: 14, height: 14 }),
|
|
7165
|
+
" Ask all to unmute"
|
|
7166
|
+
]
|
|
7167
|
+
}
|
|
7168
|
+
)
|
|
7169
|
+
] }) : null,
|
|
7170
|
+
everyone.length > 4 ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("label", { className: "mx-meet__search", children: [
|
|
7171
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(SearchIcon, { width: 16, height: 16 }),
|
|
7172
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
7173
|
+
"input",
|
|
7174
|
+
{
|
|
7175
|
+
className: "mx-meet__input mx-meet__input--bare",
|
|
7176
|
+
value: query,
|
|
7177
|
+
onChange: (event) => setQuery(event.target.value),
|
|
7178
|
+
placeholder: "Search people",
|
|
7179
|
+
"aria-label": "Search people"
|
|
7180
|
+
}
|
|
7181
|
+
)
|
|
7182
|
+
] }) : null,
|
|
7183
|
+
controls.notice !== null ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: controls.notice.failed ? "mx-meet__error" : "mx-meet__notice", role: "status", children: controls.notice.text }) : null,
|
|
7184
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("ul", { className: "mx-meet__people", children: [
|
|
7185
|
+
shown.map((participant) => /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("li", { className: "mx-meet__person", children: [
|
|
7186
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Avatar, { name: participant.displayName, url: participant.avatarUrl, size: 32 }),
|
|
7187
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { className: "mx-meet__person-name", children: [
|
|
7188
|
+
participant.displayName,
|
|
7189
|
+
participant.identity === local ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "mx-meet__you", children: " (you)" }) : null,
|
|
7190
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(RoleBadge, { role: participant.identity === local ? myRole : participant.role }),
|
|
7191
|
+
isNoteTaker(participant) ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "mx-meet__badge", children: "Meeting assistant" }) : null,
|
|
7192
|
+
snapshot?.spotlight === participant.identity ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "mx-meet__badge", children: "Spotlight" }) : null
|
|
7193
|
+
] }),
|
|
7194
|
+
participant.handRaised ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(HandIcon, { width: 14, height: 14 }) : null,
|
|
7195
|
+
!participant.micEnabled && !isNoteTaker(participant) ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(MicOffIcon, { width: 14, height: 14 }) : null,
|
|
7196
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(QualityBars, { quality: participant.quality }),
|
|
7197
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { className: "mx-meet__anchor", children: [
|
|
7198
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
7199
|
+
ControlButton,
|
|
7200
|
+
{
|
|
7201
|
+
label: `More for ${participant.displayName}`,
|
|
7202
|
+
active: menuFor === participant.identity,
|
|
7203
|
+
onClick: () => setMenuFor(menuFor === participant.identity ? null : participant.identity),
|
|
7204
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(MoreIcon, { width: 16, height: 16 })
|
|
7205
|
+
}
|
|
7206
|
+
),
|
|
7207
|
+
menuFor === participant.identity ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
7208
|
+
PersonMenu,
|
|
7209
|
+
{
|
|
7210
|
+
participant,
|
|
7211
|
+
placement: "down",
|
|
7212
|
+
onClose: () => setMenuFor(null),
|
|
7213
|
+
...onMessage !== void 0 ? { onMessage } : {}
|
|
7214
|
+
}
|
|
7215
|
+
) : null
|
|
7216
|
+
] })
|
|
7217
|
+
] }, participant.identity)),
|
|
7218
|
+
shown.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("li", { className: "mx-meet__panel-note", children: [
|
|
7219
|
+
"Nobody matches \u201C",
|
|
7220
|
+
query,
|
|
7221
|
+
"\u201D."
|
|
7222
|
+
] }) : null
|
|
7223
|
+
] }),
|
|
7224
|
+
snapshot?.meeting !== null && snapshot?.meeting !== void 0 ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(GuestRecordAccessControl, { meeting: snapshot.meeting }) : null
|
|
5942
7225
|
] });
|
|
5943
7226
|
}
|
|
5944
|
-
function
|
|
7227
|
+
function PersonMenu({
|
|
5945
7228
|
participant,
|
|
5946
|
-
|
|
7229
|
+
onClose,
|
|
7230
|
+
onMessage,
|
|
7231
|
+
placement = "down"
|
|
5947
7232
|
}) {
|
|
5948
|
-
const
|
|
5949
|
-
|
|
5950
|
-
|
|
5951
|
-
|
|
5952
|
-
|
|
5953
|
-
|
|
5954
|
-
|
|
5955
|
-
|
|
5956
|
-
|
|
5957
|
-
|
|
7233
|
+
const host = useMeetHost();
|
|
7234
|
+
const snapshot = useMeetSnapshot();
|
|
7235
|
+
const controls = useHostControls();
|
|
7236
|
+
const [mode, setMode] = (0, import_react4.useState)("menu");
|
|
7237
|
+
const [text3, setText] = (0, import_react4.useState)("");
|
|
7238
|
+
const self = participant.identity === snapshot?.localIdentity;
|
|
7239
|
+
const moderate = controls.canModerate(participant);
|
|
7240
|
+
const pinned = snapshot?.pinned === participant.identity;
|
|
7241
|
+
const spotlit = snapshot?.spotlight === participant.identity;
|
|
7242
|
+
const hasAccount = participant.userId !== null;
|
|
7243
|
+
const act = (request) => {
|
|
7244
|
+
void controls.run(request).then((result) => {
|
|
7245
|
+
if (result !== null) onClose();
|
|
7246
|
+
});
|
|
7247
|
+
};
|
|
7248
|
+
if (mode === "rename" || mode === "report") {
|
|
7249
|
+
const renaming = mode === "rename";
|
|
7250
|
+
return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Popover, { label: renaming ? "Rename" : "Report", onClose, placement, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
|
|
7251
|
+
"form",
|
|
7252
|
+
{
|
|
7253
|
+
className: "mx-meet__menu-form",
|
|
7254
|
+
onSubmit: (event) => {
|
|
7255
|
+
event.preventDefault();
|
|
7256
|
+
if (text3.trim().length === 0) return;
|
|
7257
|
+
act(
|
|
7258
|
+
renaming ? { action: "rename", identity: participant.identity, name: text3.trim() } : { action: "report", identity: participant.identity, reason: text3.trim() }
|
|
7259
|
+
);
|
|
7260
|
+
},
|
|
7261
|
+
children: [
|
|
7262
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("label", { className: "mx-meet__field", children: [
|
|
7263
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "mx-meet__field-label", children: renaming ? "Name shown in this meeting" : `What did ${participant.displayName} do?` }),
|
|
7264
|
+
renaming ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
7265
|
+
"input",
|
|
7266
|
+
{
|
|
7267
|
+
className: "mx-meet__input",
|
|
7268
|
+
autoFocus: true,
|
|
7269
|
+
maxLength: 80,
|
|
7270
|
+
value: text3,
|
|
7271
|
+
onChange: (event) => setText(event.target.value)
|
|
7272
|
+
}
|
|
7273
|
+
) : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
7274
|
+
"textarea",
|
|
7275
|
+
{
|
|
7276
|
+
className: "mx-meet__input",
|
|
7277
|
+
autoFocus: true,
|
|
7278
|
+
rows: 3,
|
|
7279
|
+
maxLength: 1e3,
|
|
7280
|
+
value: text3,
|
|
7281
|
+
onChange: (event) => setText(event.target.value)
|
|
7282
|
+
}
|
|
7283
|
+
)
|
|
7284
|
+
] }),
|
|
7285
|
+
controls.notice?.failed === true ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: "mx-meet__error", children: controls.notice.text }) : null,
|
|
7286
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "mx-meet__menu-actions", children: [
|
|
7287
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { type: "button", className: "mx-meet__link", onClick: () => setMode("menu"), children: "Back" }),
|
|
7288
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { type: "submit", className: "mx-meet__send", disabled: text3.trim().length === 0 || controls.busy, children: renaming ? "Rename" : "Report" })
|
|
7289
|
+
] })
|
|
7290
|
+
]
|
|
7291
|
+
}
|
|
7292
|
+
) });
|
|
7293
|
+
}
|
|
7294
|
+
if (mode === "remove" || mode === "transfer") {
|
|
7295
|
+
const removing = mode === "remove";
|
|
7296
|
+
return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Popover, { label: removing ? "Remove" : "Make host", onClose, placement, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "mx-meet__menu-form", children: [
|
|
7297
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: "mx-meet__confirm-text", children: removing ? `Remove ${participant.displayName}? They leave immediately and cannot rejoin this meeting.` : `Make ${participant.displayName} the host? They get every host control and own this meeting. You stay on as a co-host.` }),
|
|
7298
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "mx-meet__menu-actions", children: [
|
|
7299
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { type: "button", className: "mx-meet__link", onClick: () => setMode("menu"), children: "Cancel" }),
|
|
7300
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
7301
|
+
"button",
|
|
7302
|
+
{
|
|
7303
|
+
type: "button",
|
|
7304
|
+
className: removing ? "mx-meet__send mx-meet__send--danger" : "mx-meet__send",
|
|
7305
|
+
disabled: controls.busy,
|
|
7306
|
+
onClick: () => act({ action: removing ? "remove" : "transfer_host", identity: participant.identity }),
|
|
7307
|
+
children: removing ? "Remove" : "Make host"
|
|
7308
|
+
}
|
|
7309
|
+
)
|
|
7310
|
+
] })
|
|
7311
|
+
] }) });
|
|
7312
|
+
}
|
|
7313
|
+
return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Popover, { label: `Options for ${participant.displayName}`, onClose, placement, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "mx-meet__menu", role: "menu", children: [
|
|
7314
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
|
|
7315
|
+
MenuItem,
|
|
7316
|
+
{
|
|
7317
|
+
onSelect: () => {
|
|
7318
|
+
host?.engine.pin(pinned ? null : participant.identity);
|
|
7319
|
+
onClose();
|
|
7320
|
+
},
|
|
7321
|
+
children: [
|
|
7322
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(PinIcon, { width: 16, height: 16 }),
|
|
7323
|
+
" ",
|
|
7324
|
+
pinned ? "Unpin" : "Pin for me"
|
|
7325
|
+
]
|
|
7326
|
+
}
|
|
7327
|
+
),
|
|
7328
|
+
!self && !participant.isAgent && onMessage !== void 0 ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
|
|
7329
|
+
MenuItem,
|
|
7330
|
+
{
|
|
7331
|
+
onSelect: () => {
|
|
7332
|
+
onMessage(participant.identity);
|
|
7333
|
+
onClose();
|
|
7334
|
+
},
|
|
7335
|
+
children: [
|
|
7336
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(ChatIcon, { width: 16, height: 16 }),
|
|
7337
|
+
" Message privately"
|
|
7338
|
+
]
|
|
7339
|
+
}
|
|
7340
|
+
) : null,
|
|
7341
|
+
controls.isHost ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(MenuItem, { onSelect: () => act({ action: "spotlight", identity: spotlit ? null : participant.identity }), children: [
|
|
7342
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(SpotlightIcon, { width: 16, height: 16 }),
|
|
7343
|
+
" ",
|
|
7344
|
+
spotlit ? "Remove spotlight" : "Spotlight for everyone"
|
|
7345
|
+
] }) : null,
|
|
7346
|
+
moderate ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
|
|
7347
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "mx-meet__menu-sep", role: "separator" }),
|
|
7348
|
+
participant.micEnabled ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(MenuItem, { onSelect: () => act({ action: "mute", identity: participant.identity }), children: [
|
|
7349
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(MicOffIcon, { width: 16, height: 16 }),
|
|
7350
|
+
" Mute"
|
|
7351
|
+
] }) : !participant.isAgent ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(MenuItem, { onSelect: () => act({ action: "ask_unmute", identity: participant.identity }), children: [
|
|
7352
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(MicIcon, { width: 16, height: 16 }),
|
|
7353
|
+
" Ask to unmute"
|
|
7354
|
+
] }) : null,
|
|
7355
|
+
participant.cameraEnabled ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(MenuItem, { onSelect: () => act({ action: "stop_video", identity: participant.identity }), children: [
|
|
7356
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(CameraOffIcon, { width: 16, height: 16 }),
|
|
7357
|
+
" Stop video"
|
|
7358
|
+
] }) : null,
|
|
7359
|
+
participant.screenSharing ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(MenuItem, { onSelect: () => act({ action: "stop_screen", identity: participant.identity }), children: [
|
|
7360
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(ScreenShareIcon, { width: 16, height: 16 }),
|
|
7361
|
+
" Stop screen share"
|
|
7362
|
+
] }) : null,
|
|
7363
|
+
participant.handRaised ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(MenuItem, { onSelect: () => act({ action: "lower_hand", identity: participant.identity }), children: [
|
|
7364
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(HandIcon, { width: 16, height: 16 }),
|
|
7365
|
+
" Lower hand"
|
|
7366
|
+
] }) : null,
|
|
7367
|
+
hasAccount && !participant.isAgent && participant.role !== "cohost" ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(MenuItem, { onSelect: () => act({ action: "make_cohost", identity: participant.identity }), children: [
|
|
7368
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(PeopleIcon, { width: 16, height: 16 }),
|
|
7369
|
+
" Make co-host"
|
|
7370
|
+
] }) : null,
|
|
7371
|
+
!participant.isAgent ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
|
|
7372
|
+
MenuItem,
|
|
5958
7373
|
{
|
|
5959
|
-
|
|
5960
|
-
|
|
5961
|
-
|
|
5962
|
-
setConfirming(false);
|
|
5963
|
-
void controls.remove(participant);
|
|
7374
|
+
onSelect: () => {
|
|
7375
|
+
setText(participant.displayName);
|
|
7376
|
+
setMode("rename");
|
|
5964
7377
|
},
|
|
5965
|
-
children:
|
|
7378
|
+
children: [
|
|
7379
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(PencilIcon, { width: 16, height: 16 }),
|
|
7380
|
+
" Rename"
|
|
7381
|
+
]
|
|
5966
7382
|
}
|
|
5967
|
-
)
|
|
5968
|
-
|
|
5969
|
-
|
|
5970
|
-
|
|
5971
|
-
|
|
5972
|
-
|
|
5973
|
-
|
|
7383
|
+
) : null
|
|
7384
|
+
] }) : null,
|
|
7385
|
+
controls.isOwner && participant.role === "cohost" ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(MenuItem, { onSelect: () => act({ action: "revoke_cohost", identity: participant.identity }), children: [
|
|
7386
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(PeopleIcon, { width: 16, height: 16 }),
|
|
7387
|
+
" Remove co-host"
|
|
7388
|
+
] }) : null,
|
|
7389
|
+
controls.isOwner && !self && hasAccount && !participant.isAgent ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(MenuItem, { onSelect: () => setMode("transfer"), children: [
|
|
7390
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(SpotlightIcon, { width: 16, height: 16 }),
|
|
7391
|
+
" Make host"
|
|
7392
|
+
] }) : null,
|
|
7393
|
+
self ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
|
|
7394
|
+
MenuItem,
|
|
5974
7395
|
{
|
|
5975
|
-
|
|
5976
|
-
|
|
5977
|
-
|
|
7396
|
+
onSelect: () => {
|
|
7397
|
+
setText(participant.displayName);
|
|
7398
|
+
setMode("rename");
|
|
7399
|
+
},
|
|
7400
|
+
children: [
|
|
7401
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(PencilIcon, { width: 16, height: 16 }),
|
|
7402
|
+
" Rename yourself"
|
|
7403
|
+
]
|
|
5978
7404
|
}
|
|
5979
7405
|
) : null,
|
|
7406
|
+
!self && !participant.isAgent ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
|
|
7407
|
+
MenuItem,
|
|
7408
|
+
{
|
|
7409
|
+
onSelect: () => {
|
|
7410
|
+
setText("");
|
|
7411
|
+
setMode("report");
|
|
7412
|
+
},
|
|
7413
|
+
children: [
|
|
7414
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(FlagIcon, { width: 16, height: 16 }),
|
|
7415
|
+
" Report"
|
|
7416
|
+
]
|
|
7417
|
+
}
|
|
7418
|
+
) : null,
|
|
7419
|
+
moderate && !participant.isAgent ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(MenuItem, { danger: true, onSelect: () => setMode("remove"), children: [
|
|
7420
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(CloseIcon, { width: 16, height: 16 }),
|
|
7421
|
+
" Remove from meeting"
|
|
7422
|
+
] }) : null
|
|
7423
|
+
] }) });
|
|
7424
|
+
}
|
|
7425
|
+
function SettingsPanel({ onClose }) {
|
|
7426
|
+
const host = useRequiredMeetHost();
|
|
7427
|
+
return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(PanelShell, { title: "Audio, video and effects", onClose, children: [
|
|
5980
7428
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
5981
|
-
|
|
7429
|
+
DeviceSettings,
|
|
5982
7430
|
{
|
|
5983
|
-
|
|
5984
|
-
|
|
5985
|
-
|
|
5986
|
-
|
|
7431
|
+
onPick: (kind, deviceId2) => {
|
|
7432
|
+
host.devices.select(kind, deviceId2);
|
|
7433
|
+
void host.engine.switchDevice(kind, deviceId2);
|
|
7434
|
+
}
|
|
5987
7435
|
}
|
|
5988
|
-
)
|
|
7436
|
+
),
|
|
7437
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(EffectsSettings, {})
|
|
7438
|
+
] });
|
|
7439
|
+
}
|
|
7440
|
+
function DeviceSettings({
|
|
7441
|
+
onPick
|
|
7442
|
+
}) {
|
|
7443
|
+
const host = useRequiredMeetHost();
|
|
7444
|
+
const devices = useDeviceSnapshot();
|
|
7445
|
+
const microphone = devices?.selected.audioinput ?? host.devices.resolve("audioinput");
|
|
7446
|
+
const speaker = devices?.selected.audiooutput ?? host.devices.resolve("audiooutput");
|
|
7447
|
+
const stream = useMicrophoneStream(microphone);
|
|
7448
|
+
const level = useAudioLevel(stream);
|
|
7449
|
+
const [testing, setTesting] = (0, import_react4.useState)(false);
|
|
7450
|
+
(0, import_react4.useEffect)(() => {
|
|
7451
|
+
void host.devices.refresh();
|
|
7452
|
+
}, [host]);
|
|
7453
|
+
return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("section", { className: "mx-meet__section", "aria-label": "Devices", children: [
|
|
7454
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(DevicePicker, { kind: "audioinput", label: "Microphone", onPick }),
|
|
7455
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "mx-meet__meter", role: "meter", "aria-label": "Microphone level", "aria-valuemin": 0, "aria-valuemax": 100, "aria-valuenow": Math.round(level * 100), children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { style: { width: `${Math.min(100, Math.round(level * 160))}%` } }) }),
|
|
7456
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(DevicePicker, { kind: "audiooutput", label: "Speaker", onPick }),
|
|
7457
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
|
|
7458
|
+
"button",
|
|
7459
|
+
{
|
|
7460
|
+
type: "button",
|
|
7461
|
+
className: "mx-meet__chip",
|
|
7462
|
+
disabled: testing,
|
|
7463
|
+
onClick: () => {
|
|
7464
|
+
setTesting(true);
|
|
7465
|
+
void playTestSound(speaker).finally(() => setTesting(false));
|
|
7466
|
+
},
|
|
7467
|
+
children: [
|
|
7468
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(SpeakerIcon, { width: 14, height: 14 }),
|
|
7469
|
+
" ",
|
|
7470
|
+
testing ? "Playing\u2026" : "Test speaker"
|
|
7471
|
+
]
|
|
7472
|
+
}
|
|
7473
|
+
),
|
|
7474
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(DevicePicker, { kind: "videoinput", label: "Camera", onPick })
|
|
5989
7475
|
] });
|
|
5990
7476
|
}
|
|
7477
|
+
function EffectsSettings({ preview }) {
|
|
7478
|
+
const effects = useMediaEffects();
|
|
7479
|
+
const [error, setError] = (0, import_react4.useState)(null);
|
|
7480
|
+
const fileRef = (0, import_react4.useRef)(null);
|
|
7481
|
+
const supported = effects.backgroundsSupported || preview != null || videoEffectsSupported();
|
|
7482
|
+
const apply = (next) => {
|
|
7483
|
+
setError(null);
|
|
7484
|
+
void effects.set(next);
|
|
7485
|
+
if (preview != null) {
|
|
7486
|
+
const merged = { ...effects.effects, ...next };
|
|
7487
|
+
void preview.setEffect(toVideoEffect(merged)).catch(
|
|
7488
|
+
() => setError("This browser could not run the background effect.")
|
|
7489
|
+
);
|
|
7490
|
+
}
|
|
7491
|
+
};
|
|
7492
|
+
return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("section", { className: "mx-meet__section", "aria-label": "Effects", children: [
|
|
7493
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("label", { className: "mx-meet__toggle", children: [
|
|
7494
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
7495
|
+
"input",
|
|
7496
|
+
{
|
|
7497
|
+
type: "checkbox",
|
|
7498
|
+
checked: effects.effects.noiseSuppression,
|
|
7499
|
+
onChange: (event) => apply({ noiseSuppression: event.target.checked })
|
|
7500
|
+
}
|
|
7501
|
+
),
|
|
7502
|
+
"Noise suppression",
|
|
7503
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "mx-meet__hint", children: "Filters keyboards, fans and background chatter." })
|
|
7504
|
+
] }),
|
|
7505
|
+
supported ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "mx-meet__field", children: [
|
|
7506
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "mx-meet__field-label", children: "Background" }),
|
|
7507
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "mx-meet__pills", role: "radiogroup", "aria-label": "Background", children: ["none", "blur", "image"].map((kind) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
7508
|
+
"button",
|
|
7509
|
+
{
|
|
7510
|
+
type: "button",
|
|
7511
|
+
role: "radio",
|
|
7512
|
+
"aria-checked": effects.effects.background === kind,
|
|
7513
|
+
className: effects.effects.background === kind ? "mx-meet__pill mx-meet__pill--on" : "mx-meet__pill",
|
|
7514
|
+
onClick: () => {
|
|
7515
|
+
if (kind === "image") {
|
|
7516
|
+
fileRef.current?.click();
|
|
7517
|
+
return;
|
|
7518
|
+
}
|
|
7519
|
+
apply({ background: kind });
|
|
7520
|
+
},
|
|
7521
|
+
children: kind === "none" ? "None" : kind === "blur" ? "Blur" : "Picture\u2026"
|
|
7522
|
+
},
|
|
7523
|
+
kind
|
|
7524
|
+
)) }),
|
|
7525
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
7526
|
+
"input",
|
|
7527
|
+
{
|
|
7528
|
+
ref: fileRef,
|
|
7529
|
+
type: "file",
|
|
7530
|
+
accept: "image/*",
|
|
7531
|
+
hidden: true,
|
|
7532
|
+
onChange: (event) => {
|
|
7533
|
+
const file = event.target.files?.[0];
|
|
7534
|
+
event.target.value = "";
|
|
7535
|
+
if (file === void 0) return;
|
|
7536
|
+
void shrinkImage(file).then((url) => apply({ background: "image", backgroundImage: url })).catch(() => setError("That picture could not be read. Try a JPEG or PNG."));
|
|
7537
|
+
}
|
|
7538
|
+
}
|
|
7539
|
+
),
|
|
7540
|
+
error !== null ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: "mx-meet__error", children: error }) : null
|
|
7541
|
+
] }) : null
|
|
7542
|
+
] });
|
|
7543
|
+
}
|
|
7544
|
+
function toVideoEffect(effects) {
|
|
7545
|
+
if (effects.background === "blur") return { kind: "blur" };
|
|
7546
|
+
if (effects.background === "image" && effects.backgroundImage !== null) {
|
|
7547
|
+
return { kind: "image", imageUrl: effects.backgroundImage };
|
|
7548
|
+
}
|
|
7549
|
+
return { kind: "none" };
|
|
7550
|
+
}
|
|
7551
|
+
async function shrinkImage(file) {
|
|
7552
|
+
const url = URL.createObjectURL(file);
|
|
7553
|
+
try {
|
|
7554
|
+
const image = new Image();
|
|
7555
|
+
image.src = url;
|
|
7556
|
+
await image.decode();
|
|
7557
|
+
const scale = Math.min(1, 1280 / image.naturalWidth);
|
|
7558
|
+
const canvas = document.createElement("canvas");
|
|
7559
|
+
canvas.width = Math.round(image.naturalWidth * scale);
|
|
7560
|
+
canvas.height = Math.round(image.naturalHeight * scale);
|
|
7561
|
+
const context = canvas.getContext("2d");
|
|
7562
|
+
if (context === null) throw new Error("no 2d context");
|
|
7563
|
+
context.drawImage(image, 0, 0, canvas.width, canvas.height);
|
|
7564
|
+
return canvas.toDataURL("image/jpeg", 0.85);
|
|
7565
|
+
} finally {
|
|
7566
|
+
URL.revokeObjectURL(url);
|
|
7567
|
+
}
|
|
7568
|
+
}
|
|
7569
|
+
async function playTestSound(sinkId) {
|
|
7570
|
+
const Context = globalThis.AudioContext;
|
|
7571
|
+
if (Context === void 0) return;
|
|
7572
|
+
const context = new Context();
|
|
7573
|
+
const destination = context.createMediaStreamDestination();
|
|
7574
|
+
const gain = context.createGain();
|
|
7575
|
+
gain.gain.value = 0.2;
|
|
7576
|
+
gain.connect(destination);
|
|
7577
|
+
for (const [frequency, start] of [
|
|
7578
|
+
[660, 0],
|
|
7579
|
+
[880, 0.25]
|
|
7580
|
+
]) {
|
|
7581
|
+
const oscillator = context.createOscillator();
|
|
7582
|
+
oscillator.frequency.value = frequency;
|
|
7583
|
+
oscillator.connect(gain);
|
|
7584
|
+
oscillator.start(context.currentTime + start);
|
|
7585
|
+
oscillator.stop(context.currentTime + start + 0.22);
|
|
7586
|
+
}
|
|
7587
|
+
const audio = new Audio();
|
|
7588
|
+
audio.srcObject = destination.stream;
|
|
7589
|
+
const sinkable = audio;
|
|
7590
|
+
if (sinkId !== null && sinkId !== "default" && sinkable.setSinkId !== void 0) {
|
|
7591
|
+
await sinkable.setSinkId(sinkId).catch(() => void 0);
|
|
7592
|
+
}
|
|
7593
|
+
await audio.play().catch(() => void 0);
|
|
7594
|
+
await new Promise((resolve) => setTimeout(resolve, 700));
|
|
7595
|
+
audio.pause();
|
|
7596
|
+
await context.close();
|
|
7597
|
+
}
|
|
7598
|
+
function useMicrophoneStream(deviceId2) {
|
|
7599
|
+
const [stream, setStream] = (0, import_react4.useState)(null);
|
|
7600
|
+
(0, import_react4.useEffect)(() => {
|
|
7601
|
+
if (deviceId2 === "off") return void 0;
|
|
7602
|
+
let live = true;
|
|
7603
|
+
let opened = null;
|
|
7604
|
+
const media = globalThis.navigator?.mediaDevices;
|
|
7605
|
+
if (media === void 0) return void 0;
|
|
7606
|
+
void media.getUserMedia({ audio: deviceId2 === null ? true : { deviceId: { exact: deviceId2 } } }).then((next) => {
|
|
7607
|
+
if (!live) {
|
|
7608
|
+
next.getTracks().forEach((track) => track.stop());
|
|
7609
|
+
return;
|
|
7610
|
+
}
|
|
7611
|
+
opened = next;
|
|
7612
|
+
setStream(next);
|
|
7613
|
+
}).catch(() => setStream(null));
|
|
7614
|
+
return () => {
|
|
7615
|
+
live = false;
|
|
7616
|
+
opened?.getTracks().forEach((track) => track.stop());
|
|
7617
|
+
setStream(null);
|
|
7618
|
+
};
|
|
7619
|
+
}, [deviceId2]);
|
|
7620
|
+
return stream;
|
|
7621
|
+
}
|
|
5991
7622
|
function LobbyPanel() {
|
|
5992
7623
|
const lobby = useLobby();
|
|
5993
7624
|
if (!lobby.canAdmit || lobby.waiting.length === 0) return null;
|
|
@@ -6213,16 +7844,40 @@ function PreJoinBody({
|
|
|
6213
7844
|
const room = useRoom();
|
|
6214
7845
|
const devices = useDeviceSnapshot();
|
|
6215
7846
|
const recording = useRecording();
|
|
7847
|
+
const effects = useMediaEffects();
|
|
7848
|
+
const { notice } = useModerationNotice();
|
|
6216
7849
|
const [mode, setMode] = (0, import_react4.useState)("video");
|
|
6217
7850
|
const [microphone, setMicrophone] = (0, import_react4.useState)(true);
|
|
6218
7851
|
const [joining, setJoining] = (0, import_react4.useState)(false);
|
|
7852
|
+
const camera = devices?.selected.videoinput ?? host.devices.resolve("videoinput");
|
|
7853
|
+
const mic = devices?.selected.audioinput ?? host.devices.resolve("audioinput");
|
|
7854
|
+
const preview = useCameraPreview(mode === "video" ? camera : void 0, effects.effects);
|
|
7855
|
+
const micStream = useMicrophoneStream(microphone ? mic : "off");
|
|
7856
|
+
const level = useAudioLevel(micStream);
|
|
7857
|
+
const [speakerTesting, setSpeakerTesting] = (0, import_react4.useState)(false);
|
|
6219
7858
|
(0, import_react4.useEffect)(() => {
|
|
6220
7859
|
void host.devices.refresh();
|
|
6221
7860
|
}, [host]);
|
|
6222
7861
|
return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "mx-meet__prejoin", children: [
|
|
6223
|
-
/* @__PURE__ */ (0, import_jsx_runtime3.
|
|
7862
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "mx-meet__preview", children: [
|
|
7863
|
+
mode === "video" && preview.preview !== null ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
7864
|
+
"video",
|
|
7865
|
+
{
|
|
7866
|
+
className: "mx-meet__video mx-meet__video--mirror",
|
|
7867
|
+
autoPlay: true,
|
|
7868
|
+
playsInline: true,
|
|
7869
|
+
muted: true,
|
|
7870
|
+
ref: (element) => {
|
|
7871
|
+
if (element !== null) preview.preview?.attach(element);
|
|
7872
|
+
}
|
|
7873
|
+
}
|
|
7874
|
+
) : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "mx-meet__tile-placeholder", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Avatar, { name: host.identity.displayName, url: host.identity.avatarUrl, size: 96 }) }),
|
|
7875
|
+
mode === "video" && preview.error !== null ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: "mx-meet__preview-note", children: preview.error }) : null,
|
|
7876
|
+
microphone && micStream !== null ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "mx-meet__meter mx-meet__meter--preview", role: "meter", "aria-label": "Microphone level", "aria-valuemin": 0, "aria-valuemax": 100, "aria-valuenow": Math.round(level * 100), children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { style: { width: `${Math.min(100, Math.round(level * 160))}%` } }) }) : null
|
|
7877
|
+
] }),
|
|
6224
7878
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "mx-meet__prejoin-panel", children: [
|
|
6225
7879
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("h1", { className: "mx-meet__prejoin-title", children: "Ready to join?" }),
|
|
7880
|
+
notice?.kind === "removed" ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: "mx-meet__warning", children: notice.text }) : null,
|
|
6226
7881
|
devices?.labelsHidden === true ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("p", { className: "mx-meet__panel-note", children: [
|
|
6227
7882
|
"Your browser is hiding device names until you allow camera and microphone access.",
|
|
6228
7883
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
@@ -6239,7 +7894,30 @@ function PreJoinBody({
|
|
|
6239
7894
|
] }) : null,
|
|
6240
7895
|
devices?.fallbackNotice != null ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: "mx-meet__warning", children: devices.fallbackNotice }) : null,
|
|
6241
7896
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(DevicePicker, { kind: "audioinput", label: "Microphone" }),
|
|
7897
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "mx-meet__field-row", children: [
|
|
7898
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(DevicePicker, { kind: "audiooutput", label: "Speaker" }),
|
|
7899
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
|
|
7900
|
+
"button",
|
|
7901
|
+
{
|
|
7902
|
+
type: "button",
|
|
7903
|
+
className: "mx-meet__chip",
|
|
7904
|
+
disabled: speakerTesting,
|
|
7905
|
+
onClick: () => {
|
|
7906
|
+
setSpeakerTesting(true);
|
|
7907
|
+
void playTestSound(devices?.selected.audiooutput ?? host.devices.resolve("audiooutput")).finally(
|
|
7908
|
+
() => setSpeakerTesting(false)
|
|
7909
|
+
);
|
|
7910
|
+
},
|
|
7911
|
+
children: [
|
|
7912
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(SpeakerIcon, { width: 14, height: 14 }),
|
|
7913
|
+
" ",
|
|
7914
|
+
speakerTesting ? "Playing\u2026" : "Test"
|
|
7915
|
+
]
|
|
7916
|
+
}
|
|
7917
|
+
)
|
|
7918
|
+
] }),
|
|
6242
7919
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(DevicePicker, { kind: "videoinput", label: "Camera" }),
|
|
7920
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(EffectsSettings, { preview: preview.preview }),
|
|
6243
7921
|
recording.consentNotice !== null ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: "mx-meet__consent", children: recording.consentNotice }) : null,
|
|
6244
7922
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "mx-meet__prejoin-actions", children: [
|
|
6245
7923
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("label", { className: "mx-meet__toggle", children: [
|
|
@@ -6271,6 +7949,7 @@ function PreJoinBody({
|
|
|
6271
7949
|
className: "mx-meet__primary",
|
|
6272
7950
|
onClick: () => {
|
|
6273
7951
|
setJoining(true);
|
|
7952
|
+
preview.release();
|
|
6274
7953
|
void room.join({
|
|
6275
7954
|
roomName,
|
|
6276
7955
|
mode,
|
|
@@ -6290,9 +7969,51 @@ function PreJoinBody({
|
|
|
6290
7969
|
] })
|
|
6291
7970
|
] });
|
|
6292
7971
|
}
|
|
7972
|
+
function useCameraPreview(deviceId2, effects) {
|
|
7973
|
+
const [preview, setPreview] = (0, import_react4.useState)(null);
|
|
7974
|
+
const [error, setError] = (0, import_react4.useState)(null);
|
|
7975
|
+
const current = (0, import_react4.useRef)(null);
|
|
7976
|
+
const initialEffect = (0, import_react4.useRef)(toVideoEffect(effects));
|
|
7977
|
+
initialEffect.current = toVideoEffect(effects);
|
|
7978
|
+
(0, import_react4.useEffect)(() => {
|
|
7979
|
+
if (deviceId2 === void 0) return void 0;
|
|
7980
|
+
let live = true;
|
|
7981
|
+
setError(null);
|
|
7982
|
+
void createCameraPreview(deviceId2, initialEffect.current).then((created) => {
|
|
7983
|
+
if (!live) {
|
|
7984
|
+
created.stop();
|
|
7985
|
+
return;
|
|
7986
|
+
}
|
|
7987
|
+
current.current = created;
|
|
7988
|
+
setPreview(created);
|
|
7989
|
+
}).catch((thrown) => {
|
|
7990
|
+
if (!live) return;
|
|
7991
|
+
const failure = thrown;
|
|
7992
|
+
const message = (failure.message ?? "").replace(/^meet\.[A-Za-z.()]+:\s*/, "");
|
|
7993
|
+
const sentence = message.length > 0 ? message.charAt(0).toUpperCase() + message.slice(1) : "";
|
|
7994
|
+
setError([sentence, failure.remedy].filter(Boolean).join(". ") || "The camera did not start.");
|
|
7995
|
+
});
|
|
7996
|
+
return () => {
|
|
7997
|
+
live = false;
|
|
7998
|
+
current.current?.stop();
|
|
7999
|
+
current.current = null;
|
|
8000
|
+
setPreview(null);
|
|
8001
|
+
};
|
|
8002
|
+
}, [deviceId2]);
|
|
8003
|
+
return {
|
|
8004
|
+
preview,
|
|
8005
|
+
error,
|
|
8006
|
+
release: () => {
|
|
8007
|
+
current.current?.stop();
|
|
8008
|
+
current.current = null;
|
|
8009
|
+
setPreview(null);
|
|
8010
|
+
}
|
|
8011
|
+
};
|
|
8012
|
+
}
|
|
6293
8013
|
function DevicePicker({
|
|
6294
8014
|
kind,
|
|
6295
|
-
label
|
|
8015
|
+
label,
|
|
8016
|
+
onPick
|
|
6296
8017
|
}) {
|
|
6297
8018
|
const host = useRequiredMeetHost();
|
|
6298
8019
|
const devices = useDeviceSnapshot();
|
|
@@ -6305,7 +8026,10 @@ function DevicePicker({
|
|
|
6305
8026
|
{
|
|
6306
8027
|
className: "mx-meet__select",
|
|
6307
8028
|
value: devices?.selected[kind] ?? host.devices.resolve(kind) ?? "",
|
|
6308
|
-
onChange: (event) =>
|
|
8029
|
+
onChange: (event) => {
|
|
8030
|
+
if (onPick !== void 0) onPick(kind, event.target.value);
|
|
8031
|
+
else host.devices.select(kind, event.target.value);
|
|
8032
|
+
},
|
|
6309
8033
|
children: list.map((device) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("option", { value: device.deviceId, children: device.label.length > 0 ? device.label : `${label} (name hidden)` }, device.deviceId))
|
|
6310
8034
|
}
|
|
6311
8035
|
)
|