@cometchat/calls-sdk-react-native 5.0.0-beta.2 → 5.0.0-beta.4

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/dist/index.mjs CHANGED
@@ -5,6 +5,7 @@ import { combine, persist, subscribeWithSelector } from "zustand/middleware";
5
5
  import { shallow } from "zustand/shallow";
6
6
  import React, { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
7
7
  import JitsiMeetJS from "lib-jitsi-meet";
8
+ import AsyncStorage from "@react-native-async-storage/async-storage";
8
9
  import { ActivityIndicator, Animated, AppState, DeviceEventEmitter, Dimensions, FlatList, Image, Modal, NativeEventEmitter, NativeModules, PanResponder, Platform, Pressable, ScrollView, StyleSheet, Text, TextInput, TouchableOpacity, TouchableWithoutFeedback, View } from "react-native";
9
10
  import { RTCView, permissions } from "react-native-webrtc";
10
11
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
@@ -44,17 +45,24 @@ var EventBus = class {
44
45
  }
45
46
  }
46
47
  }
47
- subscribe(actionType, listener) {
48
+ subscribe(actionType, listener, options) {
49
+ if (options?.signal?.aborted) {
50
+ return () => {};
51
+ }
48
52
  if (!this.actionListenersMap.get(actionType)) {
49
53
  this.actionListenersMap.set(actionType, []);
50
54
  }
51
55
  this.actionListenersMap.get(actionType)?.push(listener);
52
- return () => {
56
+ const unsubscribe = () => {
53
57
  const listenersList = this.actionListenersMap.get(actionType);
54
58
  if (listenersList) {
55
59
  this.actionListenersMap.set(actionType, listenersList.filter((l) => l !== listener));
56
60
  }
57
61
  };
62
+ if (options?.signal) {
63
+ options.signal.addEventListener("abort", unsubscribe, { once: true });
64
+ }
65
+ return unsubscribe;
58
66
  }
59
67
  };
60
68
  const eventBus = new EventBus();
@@ -104,10 +112,12 @@ const VIDEO_QUALITY_LEVELS = {
104
112
  LOW: 180,
105
113
  NONE: 0
106
114
  };
107
- const PLATFORM = {
115
+ const SDK_PLATFORM = {
108
116
  WEB: "web",
109
117
  ANDROID: "android",
110
- IOS: "ios"
118
+ IOS: "ios",
119
+ REACT_NATIVE_ANDROID: "react-native-android",
120
+ REACT_NATIVE_IOS: "react-native-ios"
111
121
  };
112
122
  const EVENT_LISTENER_METHODS = {
113
123
  SessionStatusListener: {
@@ -222,7 +232,7 @@ function calculateTileLayout(containerWidth, containerHeight, numberOfTiles) {
222
232
  const tileArea = totalArea / numberOfTiles;
223
233
  const minArea = MIN_TILE_WIDTH * MIN_TILE_WIDTH * MIN_ASPECT_RATIO;
224
234
  if (tileArea < minArea) {
225
- const columnCount$1 = Math.floor(containerWidth / MIN_TILE_WIDTH);
235
+ const columnCount$1 = Math.max(2, Math.floor(containerWidth / MIN_TILE_WIDTH));
226
236
  const rowCount$1 = Math.ceil(numberOfTiles / columnCount$1);
227
237
  const totalHorizontalGap$1 = columnCount$1 * GRID_GAP;
228
238
  const tileWidth$1 = (containerWidth - totalHorizontalGap$1) / columnCount$1;
@@ -405,12 +415,37 @@ function isDeviceEqual(device1, device2) {
405
415
  function getDefaultDevice(devices) {
406
416
  return devices.find((device) => device.deviceId === "default") || devices[0];
407
417
  }
418
+ /**
419
+ * Returns a promise that resolves when the given Zustand store
420
+ * satisfies the provided predicate. Resolves immediately if the
421
+ * condition is already met. Includes a timeout to avoid hanging
422
+ * forever (defaults to 5 000 ms).
423
+ */
424
+ function waitForStoreState(store, predicate, timeoutMs = 5e3) {
425
+ return new Promise((resolve, reject) => {
426
+ if (predicate(store.getState())) {
427
+ resolve();
428
+ return;
429
+ }
430
+ const timer = setTimeout(() => {
431
+ unsubscribe();
432
+ reject(new Error("waitForStoreState timed out"));
433
+ }, timeoutMs);
434
+ const unsubscribe = store.subscribe((state) => {
435
+ if (predicate(state)) {
436
+ clearTimeout(timer);
437
+ unsubscribe();
438
+ resolve();
439
+ }
440
+ });
441
+ });
442
+ }
408
443
 
409
444
  //#endregion
410
445
  //#region calls-sdk-core/utils/try-catch.ts
411
- async function tryCatch(promise) {
446
+ async function tryCatch(promise, timeoutMs) {
412
447
  try {
413
- const data = await promise;
448
+ const data = timeoutMs != null ? await Promise.race([promise, new Promise((_, reject) => setTimeout(() => reject(new Error("timeout")), timeoutMs))]) : await promise;
414
449
  return {
415
450
  data,
416
451
  error: null
@@ -467,6 +502,13 @@ var SessionMethodsCore = class {
467
502
  unMuteAudioTrack();
468
503
  }
469
504
  /**
505
+ * Toggles the local user's audio mute state.
506
+ * If audio is muted, it will be unmuted, and vice versa.
507
+ */
508
+ static toggleAudio() {
509
+ toggleAudioTrack();
510
+ }
511
+ /**
470
512
  * Pauses the local user's video stream.
471
513
  */
472
514
  static pauseVideo() {
@@ -479,22 +521,17 @@ var SessionMethodsCore = class {
479
521
  resumeVideoTrack();
480
522
  }
481
523
  /**
482
- * Local user leaves the current session.
483
- */
484
- static leaveSession() {
485
- leaveSession();
486
- }
487
- /**
488
- * Starts sharing the user's screen with other participants.
524
+ * Toggles the local user's video stream.
525
+ * If video is paused, it will be resumed, and vice versa.
489
526
  */
490
- static startScreenSharing() {
491
- startScreenSharing();
527
+ static toggleVideo() {
528
+ toggleVideoTrack();
492
529
  }
493
530
  /**
494
- * Stops the ongoing screen sharing session.
531
+ * Local user leaves the current session.
495
532
  */
496
- static stopScreenSharing() {
497
- stopScreenSharing();
533
+ static leaveSession() {
534
+ leaveSession();
498
535
  }
499
536
  /**
500
537
  * Raises the user's virtual hand in the call.
@@ -509,6 +546,13 @@ var SessionMethodsCore = class {
509
546
  lowerHandLocal();
510
547
  }
511
548
  /**
549
+ * Toggles the user's virtual hand state.
550
+ * If the hand is raised, it will be lowered, and vice versa.
551
+ */
552
+ static toggleHand() {
553
+ toggleRaiseHand();
554
+ }
555
+ /**
512
556
  * Switches between the front and rear camera.
513
557
  */
514
558
  static switchCamera() {
@@ -524,22 +568,21 @@ var SessionMethodsCore = class {
524
568
  /**
525
569
  * Starts recording the call.
526
570
  */
527
- static startRecording() {}
571
+ static startRecording() {
572
+ startRecording();
573
+ }
528
574
  /**
529
575
  * Stops the ongoing call recording.
530
576
  */
531
- static stopRecording() {}
532
- /**
533
- * Enables Picture-in-Picture (PIP) layout during the call.
534
- */
535
- static enablePictureInPictureLayout() {
536
- enablePictureInPictureLayout();
577
+ static stopRecording() {
578
+ stopRecording();
537
579
  }
538
580
  /**
539
- * Disables Picture-in-Picture (PIP) layout.
581
+ * Toggles the call recording state.
582
+ * If recording is active, it will be stopped, and vice versa.
540
583
  */
541
- static disablePictureInPictureLayout() {
542
- disablePictureInPictureLayout();
584
+ static toggleRecording() {
585
+ toggleRecording();
543
586
  }
544
587
  /**
545
588
  * Pins a participant's video to focus on them.
@@ -577,16 +620,22 @@ var SessionMethodsCore = class {
577
620
  setChatButtonUnreadCount(count);
578
621
  }
579
622
  /**
580
- * @deprecated use startScreenSharing() instead
623
+ * Toggles the visibility of the participant list panel.
624
+ */
625
+ static toggleParticipantList() {
626
+ toggleParticipantList();
627
+ }
628
+ /**
629
+ * Shows the participant list panel.
581
630
  */
582
- static startScreenShare() {
583
- this.startScreenSharing();
631
+ static showParticipantList() {
632
+ showParticipantList();
584
633
  }
585
634
  /**
586
- * @deprecated use stopScreenSharing() instead
635
+ * Hides the participant list panel.
587
636
  */
588
- static stopScreenShare() {
589
- this.stopScreenSharing();
637
+ static hideParticipantList() {
638
+ hideParticipantList();
590
639
  }
591
640
  /**
592
641
  * @deprecated switchToVideoCall is deprecated and not supported.
@@ -679,6 +728,27 @@ async function createLocalTrack(type, deviceId = null, cameraFacing = CAMERA_FAC
679
728
  }
680
729
  }
681
730
  }
731
+ function createLocalTracks() {
732
+ const enableCompanionMode = useConfigStore.getState().enableCompanionMode;
733
+ if (!enableCompanionMode) {
734
+ const audioInputDeviceId = useConfigStore.getState().audioInputDeviceId ?? useBaseStore.getState().audioInputDevice?.deviceId;
735
+ createLocalTrack("audio", audioInputDeviceId);
736
+ }
737
+ const sessionType = useConfigStore.getState().sessionType;
738
+ if (sessionType === SESSION_TYPE.VIDEO) {
739
+ const videoInputDeviceIdP1 = useConfigStore.getState().videoInputDeviceId;
740
+ const videoInputDeviceIdP2 = useBaseStore.getState().videoInputDevice?.deviceId;
741
+ const initialCameraFacingP1 = useConfigStore.getState().initialCameraFacing;
742
+ const initialCameraFacingP2 = useBaseStore.getState().cameraFacing;
743
+ if (videoInputDeviceIdP1) {
744
+ createLocalTrack("video", videoInputDeviceIdP1);
745
+ } else if (initialCameraFacingP1) {
746
+ createLocalTrack("video", null, initialCameraFacingP2);
747
+ } else {
748
+ createLocalTrack("video", videoInputDeviceIdP2, initialCameraFacingP2);
749
+ }
750
+ }
751
+ }
682
752
  function updateAudioInputDevice(deviceId) {
683
753
  const audioInputDevices = useBaseStore.getState().audioInputDevices.filter((device) => device.deviceId !== "");
684
754
  if (audioInputDevices.length > 0) {
@@ -878,7 +948,6 @@ const initialState$7 = {
878
948
  hideLeaveSessionButton: false,
879
949
  hideToggleAudioButton: false,
880
950
  hideParticipantListButton: false,
881
- hideSwitchLayoutButton: false,
882
951
  hideChatButton: true,
883
952
  hideToggleVideoButton: false,
884
953
  hideScreenSharingButton: false,
@@ -895,7 +964,9 @@ const initialState$7 = {
895
964
  idleTimeoutPeriodAfterPrompt: 18e4,
896
965
  enableSpotlightDrag: true,
897
966
  enableSpotlightSwap: true,
898
- showFrameRate: false
967
+ showFrameRate: false,
968
+ enableCompanionMode: false,
969
+ isPeerCall: false
899
970
  };
900
971
  const useConfigStore = create()(subscribeWithSelector(combine(initialState$7, (set) => ({ reset: () => set(initialState$7) }))));
901
972
  const setConfig = (config) => {
@@ -923,10 +994,16 @@ const initialState$6 = {
923
994
  };
924
995
  const useParticipantStore = create()(subscribeWithSelector(combine(initialState$6, (set, get$1) => ({
925
996
  addParticipant: (participant) => {
926
- set((state) => ({ participants: [...state.participants, participant] }));
997
+ set((state) => ({ participants: state.participants.some((p) => p.pid === participant.pid) ? state.participants.map((p) => p.pid === participant.pid ? {
998
+ ...p,
999
+ ...participant
1000
+ } : p) : [...state.participants, participant] }));
927
1001
  },
928
1002
  addVirtualParticipant: (participant) => {
929
- set((state) => ({ virtualParticipants: [...state.virtualParticipants, participant] }));
1003
+ set((state) => ({ virtualParticipants: state.virtualParticipants.some((p) => p.pid === participant.pid && p.type === participant.type) ? state.virtualParticipants.map((p) => p.pid === participant.pid && p.type === participant.type ? {
1004
+ ...p,
1005
+ ...participant
1006
+ } : p) : [...state.virtualParticipants, participant] }));
930
1007
  },
931
1008
  clearParticipants: () => set({
932
1009
  participants: [],
@@ -1163,6 +1240,8 @@ const useConferenceStore = create()(subscribeWithSelector(combine(initialState$5
1163
1240
  sendParticipantEvent(EVENT_LISTENER_METHODS.ParticipantEventsListner.onParticipantHandRaised, participantId);
1164
1241
  },
1165
1242
  lowerHand: (participantId) => {
1243
+ const hasRaisedHand = useConferenceStore.getState().raiseHandMap.has(participantId);
1244
+ if (!hasRaisedHand) return;
1166
1245
  set((state) => {
1167
1246
  const raiseHandMap = new Map(state.raiseHandMap);
1168
1247
  raiseHandMap.delete(participantId);
@@ -1173,13 +1252,22 @@ const useConferenceStore = create()(subscribeWithSelector(combine(initialState$5
1173
1252
  leaveConference: async () => {
1174
1253
  const conference = useConferenceStore.getState().conference;
1175
1254
  if (conference) {
1176
- const { error } = await tryCatch(conference.leave());
1255
+ const { error } = await tryCatch(conference.leave(), 500);
1177
1256
  if (error) {
1178
1257
  console.warn("Error leaving conference:", error);
1179
1258
  eventBus.publish({ type: EVENT_LISTENER_METHODS.SessionStatusListener.onSessionLeft });
1180
1259
  }
1181
1260
  }
1182
1261
  },
1262
+ endConference: async () => {
1263
+ const conference = useConferenceStore.getState().conference;
1264
+ if (conference) {
1265
+ const { error } = await tryCatch(conference.end());
1266
+ if (error) {
1267
+ console.warn("Error ending conference:", error);
1268
+ }
1269
+ }
1270
+ },
1183
1271
  stopRecording: async () => {
1184
1272
  const conference = useConferenceStore.getState().conference;
1185
1273
  if (conference) {
@@ -1351,6 +1439,12 @@ const useTracksStore = create()(subscribeWithSelector(combine(initialState$4, (s
1351
1439
  muted: originalTrack.isMuted() ? 1 : 0,
1352
1440
  originalTrack
1353
1441
  };
1442
+ const existingIdx = state.tracks.findIndex((t) => t.pid === participantId && t.mediaType === track.mediaType && t.local === isLocal);
1443
+ if (existingIdx !== -1) {
1444
+ const tracks = [...state.tracks];
1445
+ tracks[existingIdx] = track;
1446
+ return { tracks };
1447
+ }
1354
1448
  return { tracks: [...state.tracks, track] };
1355
1449
  }),
1356
1450
  removeTrack: (originalTrack) => set((state) => ({ tracks: state.tracks.filter((track) => track.originalTrack !== originalTrack) })),
@@ -1552,8 +1646,13 @@ useTracksStore.subscribe((state) => state.tracks.find((t) => t.mediaType === MED
1552
1646
  }
1553
1647
  if (track) {
1554
1648
  const deviceId = track.getDeviceId();
1555
- const device = useBaseStore.getState().audioInputDevices.find((d) => d.deviceId === deviceId);
1556
- updateAudioInputDeviceState(device, true);
1649
+ waitForStoreState(useBaseStore, (state) => state.audioInputDevices.length > 0).then(() => {
1650
+ const audioInputDevices = useBaseStore.getState().audioInputDevices;
1651
+ const device = audioInputDevices.find((d) => d.deviceId === deviceId);
1652
+ updateAudioInputDeviceState(device, true);
1653
+ }).catch(() => {
1654
+ updateAudioInputDeviceState(undefined, true);
1655
+ });
1557
1656
  }
1558
1657
  });
1559
1658
  useTracksStore.subscribe((state) => state.tracks.find((t) => t.mediaType === MEDIA_TYPE.VIDEO && t.local)?.originalTrack, (track, prevTrack) => {
@@ -1735,7 +1834,7 @@ const initialState$3 = {
1735
1834
  desktopSharingFrameRate: 5,
1736
1835
  chatButtonUnreadCount: 0,
1737
1836
  enableNoiseReduction: true,
1738
- sdkPlatform: PLATFORM.WEB,
1837
+ sdkPlatform: SDK_PLATFORM.WEB,
1739
1838
  webOSName: "unknown",
1740
1839
  isMobileBrowser: false,
1741
1840
  visibleParticipants: {
@@ -1759,11 +1858,11 @@ const useBaseStore = create()(subscribeWithSelector(persist(combine(initialState
1759
1858
  toggleParticipantListVisible: () => set((state) => ({ participantListVisible: !state.participantListVisible })),
1760
1859
  incrementConnectionRetryCount: () => set((state) => ({ connectionRetryCount: state.connectionRetryCount + 1 })),
1761
1860
  isMobileSDK: () => {
1762
- const isMobileSDK = get$1().sdkPlatform === "android" || get$1().sdkPlatform === "ios";
1861
+ const isMobileSDK = get$1().sdkPlatform !== "web";
1763
1862
  return isMobileSDK;
1764
1863
  },
1765
1864
  isMobile: () => {
1766
- const isMobileSDK = get$1().sdkPlatform === "android" || get$1().sdkPlatform === "ios";
1865
+ const isMobileSDK = get$1().sdkPlatform !== "web";
1767
1866
  const isMobileBrowser = get$1().isMobileBrowser;
1768
1867
  return isMobileSDK || isMobileBrowser;
1769
1868
  },
@@ -1809,6 +1908,7 @@ const useBaseStore = create()(subscribeWithSelector(persist(combine(initialState
1809
1908
  const toggleParticipantListVisible = useBaseStore.getState().toggleParticipantListVisible;
1810
1909
  const hideParticipantList = () => useBaseStore.setState({ participantListVisible: false });
1811
1910
  const showParticipantList = () => useBaseStore.setState({ participantListVisible: true });
1911
+ const toggleParticipantList = () => useBaseStore.setState((state) => ({ participantListVisible: !state.participantListVisible }));
1812
1912
  const toggleMoreMenuVisible = useBaseStore.getState().toggleMoreMenuVisible;
1813
1913
  const toggleAudioModeMenuVisible = () => {
1814
1914
  useBaseStore.setState((state) => ({ audioModeMenuVisible: !state.audioModeMenuVisible }));
@@ -1857,6 +1957,9 @@ const toggleEnableNoiseReduction = () => {
1857
1957
  const setChatButtonUnreadCount = (count) => {
1858
1958
  useBaseStore.setState({ chatButtonUnreadCount: count });
1859
1959
  };
1960
+ const setAudioMode = (mode) => {
1961
+ useBaseStore.setState({ selectedAudioModeType: mode });
1962
+ };
1860
1963
  const getLayout = () => {
1861
1964
  return useBaseStore.getState().layout;
1862
1965
  };
@@ -2023,12 +2126,34 @@ const useConnectionStore = create()(subscribeWithSelector(combine(initialState$2
2023
2126
  },
2024
2127
  reset: () => set(initialState$2)
2025
2128
  }))));
2129
+ function waitForConnection() {
2130
+ const { connectionStatus } = useConnectionStore.getState();
2131
+ if (connectionStatus === "connected") return Promise.resolve();
2132
+ return new Promise((resolve, reject) => {
2133
+ const timeout = setTimeout(() => {
2134
+ unsub();
2135
+ reject(new Error("Connection timed out after 3 seconds"));
2136
+ }, 3e3);
2137
+ const unsub = useConnectionStore.subscribe((s) => s.connectionStatus, (status) => {
2138
+ if (status === "connected") {
2139
+ clearTimeout(timeout);
2140
+ unsub();
2141
+ resolve();
2142
+ } else if (status === "error") {
2143
+ clearTimeout(timeout);
2144
+ unsub();
2145
+ reject(useConnectionStore.getState().error);
2146
+ }
2147
+ });
2148
+ });
2149
+ }
2026
2150
 
2027
2151
  //#endregion
2028
2152
  //#region calls-sdk-core/store/utils/hooks.ts
2029
2153
  const useHideMuteAudioButton = () => {
2030
2154
  const hideMuteAudioButton = useConfigStore((state) => state.hideToggleAudioButton);
2031
- return hideMuteAudioButton;
2155
+ const enableCompanionMode = useConfigStore((state) => state.enableCompanionMode);
2156
+ return hideMuteAudioButton || enableCompanionMode;
2032
2157
  };
2033
2158
  const useHideToggleVideoButton = () => {
2034
2159
  const hideToggleVideoButton = useConfigStore((state) => state.hideToggleVideoButton);
@@ -2095,8 +2220,21 @@ const getMainParticipant = () => {
2095
2220
  const useIsReconnecting = () => {
2096
2221
  const connectionStatus = useConnectionStore((state) => state.connectionStatus);
2097
2222
  const conferenceStatus = useConferenceStore((state) => state.conferenceStatus);
2098
- const reconnecting = connectionStatus === "connected" && conferenceStatus === "interrupted";
2099
- return reconnecting;
2223
+ const isP2P = useConferenceStore((state) => state.p2p);
2224
+ const [isOnline, setIsOnline] = useState(true);
2225
+ useEffect(() => {
2226
+ if (typeof window === "undefined") return;
2227
+ const controller = new AbortController();
2228
+ const { signal } = controller;
2229
+ window.addEventListener("online", () => setIsOnline(true), { signal });
2230
+ window.addEventListener("offline", () => setIsOnline(false), { signal });
2231
+ return () => controller.abort();
2232
+ }, []);
2233
+ const interrupted = connectionStatus === "connected" && conferenceStatus === "interrupted";
2234
+ if (isP2P && interrupted && isOnline) {
2235
+ return false;
2236
+ }
2237
+ return interrupted;
2100
2238
  };
2101
2239
  const useHideRecordingButton = () => {
2102
2240
  const hideRecordingButton = useConfigStore((state) => state.hideRecordingButton);
@@ -2174,6 +2312,11 @@ const useIsVideoInputSelectionSupported = () => {
2174
2312
  const isVideoInputSelectionSupported = hasVideoPermission && videoInputDevices.length > 0;
2175
2313
  return isVideoInputSelectionSupported;
2176
2314
  };
2315
+ const useShouldMirrorLocalVideo = () => {
2316
+ const mirrorLocalVideo = useBaseStore((state) => state.mirrorLocalVideo);
2317
+ const cameraFacing = useBaseStore((state) => state.cameraFacing);
2318
+ return cameraFacing === "user" && mirrorLocalVideo;
2319
+ };
2177
2320
 
2178
2321
  //#endregion
2179
2322
  //#region calls-sdk-core/store/utils/switch-camera.ts
@@ -2346,8 +2489,13 @@ var ConferenceListener = class {
2346
2489
  track.removeAllListeners(JitsiMeetJS.events.track.NO_DATA_FROM_SOURCE);
2347
2490
  }
2348
2491
  onConferenceJoinInProgress() {}
2349
- onConferenceFailed(_conference, error, message) {
2350
- console.error("Conference failed:", error, message);
2492
+ onConferenceFailed(errorName, error, message) {
2493
+ console.log();
2494
+ if (errorName === JitsiMeetJS.errors.conference.CONFERENCE_DESTROYED) {
2495
+ leaveSession({ forceLeave: true });
2496
+ return;
2497
+ }
2498
+ console.error("Conference failed:", errorName, error, message);
2351
2499
  useConferenceStore.setState({
2352
2500
  conferenceStatus: "error",
2353
2501
  conferenceJoined: false,
@@ -2393,17 +2541,19 @@ var ConferenceListener = class {
2393
2541
  useConferenceStore.setState({ p2p });
2394
2542
  }
2395
2543
  onTrackMuteChanged(track, participantThatMutedUs) {
2396
- if (participantThatMutedUs) {
2397
- useTracksStore.getState().updateTrack(track, { muted: track.isMuted() ? 1 : 0 });
2398
- const displayName = participantThatMutedUs.getDisplayName();
2399
- if (displayName) {
2400
- eventBus.publish({
2401
- type: INTERNAL_EVENTS.notification,
2402
- payload: {
2403
- type: "info",
2404
- message: `${displayName} has muted you.`
2405
- }
2406
- });
2544
+ if (track.isLocal()) {
2545
+ useTracksStore.getState().updateLocalTrack(track.getType(), { muted: track.isMuted() ? 1 : 0 });
2546
+ if (participantThatMutedUs) {
2547
+ const displayName = participantThatMutedUs.getDisplayName();
2548
+ if (displayName) {
2549
+ eventBus.publish({
2550
+ type: INTERNAL_EVENTS.notification,
2551
+ payload: {
2552
+ type: "info",
2553
+ message: `${displayName} has muted you.`
2554
+ }
2555
+ });
2556
+ }
2407
2557
  }
2408
2558
  }
2409
2559
  }
@@ -2458,6 +2608,24 @@ var ConferenceListener = class {
2458
2608
  useParticipantStore.getState().updateParticipant(participantId, { role: newRole });
2459
2609
  }
2460
2610
  }
2611
+ onTrackUnmuteRejected(track) {
2612
+ if (!track.isLocal()) {
2613
+ return;
2614
+ }
2615
+ const mediaType = track.getType();
2616
+ track.dispose().catch(() => {});
2617
+ useTracksStore.getState().updateLocalTrack(mediaType, {
2618
+ originalTrack: undefined,
2619
+ muted: 1
2620
+ });
2621
+ eventBus.publish({
2622
+ type: INTERNAL_EVENTS.notification,
2623
+ payload: {
2624
+ type: "info",
2625
+ message: `Your ${mediaType} unmute was rejected.`
2626
+ }
2627
+ });
2628
+ }
2461
2629
  onTalkWhileMuted() {}
2462
2630
  onConferenceError(error) {
2463
2631
  console.error("Conference error:", error);
@@ -2536,6 +2704,7 @@ function addConferenceListeners(conference) {
2536
2704
  conference.on(JitsiMeetJS.events.conference.PARTICIPANT_PROPERTY_CHANGED, conferenceListener.onParticipantPropertyChanged);
2537
2705
  conference.on(JitsiMeetJS.events.conference.USER_ROLE_CHANGED, conferenceListener.onUserRoleChanged);
2538
2706
  conference.on(JitsiMeetJS.events.conference.TALK_WHILE_MUTED, conferenceListener.onTalkWhileMuted);
2707
+ conference.on(JitsiMeetJS.events.conference.TRACK_UNMUTE_REJECTED, conferenceListener.onTrackUnmuteRejected);
2539
2708
  conference.on(JitsiMeetJS.events.conference.TRACK_AUDIO_LEVEL_CHANGED, conferenceListener.onTrackAudioLevelChanged);
2540
2709
  conference.addCommandListener(CONFERENCE_COMMANDS.userInfo, (data, id) => {
2541
2710
  const vData = v.safeParse(UserInfoCommandSchema, safeParseJson(data.value));
@@ -2547,17 +2716,23 @@ function addConferenceListeners(conference) {
2547
2716
  }
2548
2717
  });
2549
2718
  }
2550
- async function createConference(connection, roomName) {
2719
+ async function _createConference() {
2720
+ const sessionId = useConfigStore.getState().sessionId;
2721
+ const connection = useConnectionStore.getState().connection;
2551
2722
  if (!connection) {
2552
2723
  throw new Error("No connection available");
2553
2724
  }
2725
+ const connectionStatus = useConnectionStore.getState().connectionStatus;
2726
+ if (connectionStatus !== "connected") {
2727
+ await waitForConnection();
2728
+ }
2554
2729
  const existingConference = useConferenceStore.getState().conference;
2555
2730
  if (existingConference) {
2556
2731
  console.log("Conference already exists, skipping creation");
2557
2732
  return;
2558
2733
  }
2559
2734
  const connectionConfig = useConnectionStore.getState().connectionConfig;
2560
- const conference = connection.initJitsiConference(roomName, connectionConfig);
2735
+ const conference = connection.initJitsiConference(sessionId, connectionConfig);
2561
2736
  const localAudioTrack = getLocalTrack(MEDIA_TYPE.AUDIO)?.originalTrack;
2562
2737
  const localVideoTrack = getLocalTrack(MEDIA_TYPE.VIDEO)?.originalTrack;
2563
2738
  if (localAudioTrack) {
@@ -2572,6 +2747,20 @@ async function createConference(connection, roomName) {
2572
2747
  conference.setDisplayName(useParticipantStore.getState().localParticipant.name);
2573
2748
  conference.join();
2574
2749
  }
2750
+ async function createConference() {
2751
+ const conference = useConferenceStore.getState().conference;
2752
+ if (!conference) {
2753
+ const result = await tryCatch(_createConference());
2754
+ if (result.error) {
2755
+ console.error("Error creating conference", result.error);
2756
+ useConferenceStore.setState({
2757
+ conferenceStatus: "error",
2758
+ conferenceJoined: false,
2759
+ conferenceError: result.error.message
2760
+ });
2761
+ }
2762
+ }
2763
+ }
2575
2764
  function muteParticipant(participantId) {
2576
2765
  const conference = useConferenceStore.getState().conference;
2577
2766
  conference?.muteParticipant(participantId, "audio");
@@ -2583,7 +2772,12 @@ function pauseParticipantVideo(participantId) {
2583
2772
 
2584
2773
  //#endregion
2585
2774
  //#region calls-sdk-core/handlers/connection.ts
2586
- function connect(roomName) {
2775
+ async function connect(autoJoinConference = true) {
2776
+ const existingConnection = useConnectionStore.getState().connection;
2777
+ if (existingConnection) {
2778
+ createConference();
2779
+ return;
2780
+ }
2587
2781
  const options = useConnectionStore.getState().connectionConfig;
2588
2782
  const jwt = useConnectionStore.getState().jwt;
2589
2783
  const iAmRecorder = useConfigStore.getState().iAmRecorder;
@@ -2599,15 +2793,8 @@ function connect(roomName) {
2599
2793
  async function onConnectionEstablished() {
2600
2794
  useConnectionStore.getState().connectionEstablished(connection);
2601
2795
  eventBus.publish({ type: INTERNAL_EVENTS.onConnectionEstablished });
2602
- const result = await tryCatch(createConference(connection, roomName));
2603
- if (result.error) {
2604
- console.error("Error creating conference", result.error);
2605
- useConferenceStore.setState({
2606
- conferenceStatus: "error",
2607
- conferenceJoined: false,
2608
- conferenceError: result.error.message
2609
- });
2610
- }
2796
+ if (!autoJoinConference) return;
2797
+ createConference();
2611
2798
  }
2612
2799
  function onConnectionFailed(err, message, ...args) {
2613
2800
  unsubscribe();
@@ -2805,6 +2992,7 @@ var Mutex = class {
2805
2992
  //#region calls-sdk-core/handlers/init.ts
2806
2993
  function initializeLib() {
2807
2994
  JitsiMeetJS.init();
2995
+ JitsiMeetJS.setLogLevel(JitsiMeetJS.logLevels.ERROR);
2808
2996
  console.log("JitsiMeetJS initialized successfully.");
2809
2997
  }
2810
2998
 
@@ -2813,6 +3001,7 @@ function initializeLib() {
2813
3001
  let isSessionStarted = false;
2814
3002
  let reconnectTimeoutId = null;
2815
3003
  const RECONNECT_DEBOUNCE_DELAY = 3e3;
3004
+ initializeLib();
2816
3005
  function startSession() {
2817
3006
  const sessionId = useConfigStore.getState().sessionId;
2818
3007
  if (!sessionId) {
@@ -2825,29 +3014,13 @@ function startSession() {
2825
3014
  }
2826
3015
  isSessionStarted = true;
2827
3016
  console.log(`Session started in room: ${sessionId}`);
2828
- initializeLib();
2829
- const audioInputDeviceId = useConfigStore.getState().audioInputDeviceId ?? useBaseStore.getState().audioInputDevice?.deviceId;
2830
- createLocalTrack("audio", audioInputDeviceId);
2831
- const sessionType = useConfigStore.getState().sessionType;
2832
- if (sessionType === SESSION_TYPE.VIDEO) {
2833
- const videoInputDeviceIdP1 = useConfigStore.getState().videoInputDeviceId;
2834
- const videoInputDeviceIdP2 = useBaseStore.getState().videoInputDevice?.deviceId;
2835
- const initialCameraFacingP1 = useConfigStore.getState().initialCameraFacing;
2836
- const initialCameraFacingP2 = useBaseStore.getState().cameraFacing;
2837
- if (videoInputDeviceIdP1) {
2838
- createLocalTrack("video", videoInputDeviceIdP1);
2839
- } else if (initialCameraFacingP1) {
2840
- createLocalTrack("video", null, initialCameraFacingP2);
2841
- } else {
2842
- createLocalTrack("video", videoInputDeviceIdP2, initialCameraFacingP2);
2843
- }
2844
- }
3017
+ createLocalTracks();
2845
3018
  const audioOutputDeviceId = useConfigStore.getState().audioOutputDeviceId ?? useBaseStore.getState().audioOutputDevice?.deviceId;
2846
3019
  if (audioOutputDeviceId) {
2847
3020
  updateAudioOutputDevice(audioOutputDeviceId);
2848
3021
  }
2849
3022
  eventBus.startEmitting();
2850
- const test = tryCatchSync(() => connect(sessionId));
3023
+ const test = tryCatchSync(() => connect());
2851
3024
  if (test.error) {
2852
3025
  console.error("Error connecting to session:", test.error);
2853
3026
  useConnectionStore.getState().connectionFailed(test.error.message);
@@ -2864,8 +3037,14 @@ async function _leaveSession() {
2864
3037
  await useConnectionStore.getState().disconnect();
2865
3038
  }
2866
3039
  const sessionMutex = new Mutex();
2867
- function leaveSession() {
3040
+ function leaveSession(options = {}) {
2868
3041
  return sessionMutex.run(async () => {
3042
+ const isPeerCall = useConfigStore.getState().isPeerCall;
3043
+ const shouldEnd = isPeerCall && !options.forceLeave;
3044
+ if (shouldEnd) {
3045
+ useConferenceStore.getState().endConference();
3046
+ return;
3047
+ }
2869
3048
  useBaseStore.getState().clearIdealTimeoutTimer();
2870
3049
  cancelPendingReconnect();
2871
3050
  await _leaveSession();
@@ -3962,10 +4141,16 @@ const PopupMenu = ({ visible, onClose, options, anchorLayout }) => {
3962
4141
  return /* @__PURE__ */ jsx(Modal, {
3963
4142
  transparent: true,
3964
4143
  animationType: "none",
4144
+ supportedOrientations: [
4145
+ "portrait",
4146
+ "landscape-left",
4147
+ "landscape-right"
4148
+ ],
3965
4149
  onRequestClose: onClose,
3966
4150
  children: /* @__PURE__ */ jsx(Pressable, {
3967
4151
  style: styles$27.backdrop,
3968
4152
  onPress: onClose,
4153
+ testID: "cometchat-popup-menu-backdrop",
3969
4154
  children: /* @__PURE__ */ jsx(Animated.View, {
3970
4155
  style: [styles$27.menu, {
3971
4156
  [isBelowMiddle ? "bottom" : "top"]: isBelowMiddle ? callContainerDimension.height - (y - 4) : y + height + 4,
@@ -3986,6 +4171,7 @@ const PopupMenu = ({ visible, onClose, options, anchorLayout }) => {
3986
4171
  },
3987
4172
  activeOpacity: option.selected ? DISABLED_OPTION_OPACITY : .2,
3988
4173
  style: [styles$27.menuItem, option.selected ? styles$27.menuItemSelected : {}],
4174
+ testID: `cometchat-popup-menu-option-${index}`,
3989
4175
  children: [option.iconName && /* @__PURE__ */ jsx(Icon_native_default, {
3990
4176
  name: option.iconName,
3991
4177
  size: 24,
@@ -4147,6 +4333,7 @@ const MoreOptionButton = ({ ruid }) => {
4147
4333
  ref: buttonRef,
4148
4334
  style: styles$4.moreButton,
4149
4335
  onPress: showMenu,
4336
+ testID: "cometchat-participant-more-options-button",
4150
4337
  children: /* @__PURE__ */ jsx(Icon_native_default, {
4151
4338
  name: "more",
4152
4339
  size: 20,
@@ -4316,6 +4503,7 @@ const Tile = ({ participant, style, zOrder, showLabel = true, disablePress = fal
4316
4503
  const videoTrack = useTrackByParticipantId(pid, type === "screen-share" ? MEDIA_TYPE.SCREENSHARE : MEDIA_TYPE.VIDEO)?.originalTrack;
4317
4504
  const videoMuted = useTrackMuted(type === "screen-share" ? MEDIA_TYPE.SCREENSHARE : MEDIA_TYPE.VIDEO, pid);
4318
4505
  const enableParticipantContextMenu = useEnableParticipantContextMenu();
4506
+ const shouldMirror = useShouldMirrorLocalVideo();
4319
4507
  const [size, fontSize] = React.useMemo(() => {
4320
4508
  const flatStyle = StyleSheet.flatten(style);
4321
4509
  const width$1 = flatStyle?.width;
@@ -4333,6 +4521,7 @@ const Tile = ({ participant, style, zOrder, showLabel = true, disablePress = fal
4333
4521
  activeOpacity: 1,
4334
4522
  disabled: disablePress,
4335
4523
  style: [styles$24.callScreen, style],
4524
+ testID: `cometchat-tile-${pid}`,
4336
4525
  children: [
4337
4526
  /* @__PURE__ */ jsx(View, {
4338
4527
  style: styles$24.tileAvatar,
@@ -4348,7 +4537,7 @@ const Tile = ({ participant, style, zOrder, showLabel = true, disablePress = fal
4348
4537
  objectFit: type === "screen-share" ? "contain" : "cover",
4349
4538
  muted: videoMuted,
4350
4539
  zOrder,
4351
- mirror: isLocal
4540
+ mirror: isLocal && type !== "screen-share" && shouldMirror
4352
4541
  }),
4353
4542
  showLabel && /* @__PURE__ */ jsx(Label_native_default, { participant }),
4354
4543
  enableParticipantContextMenu && /* @__PURE__ */ jsx(View, {
@@ -4407,6 +4596,7 @@ const GroupAvatarTile = ({ startIndex = 4, style }) => {
4407
4596
  const overflowCount = Math.max(0, participantCount - startIndex - 3);
4408
4597
  const visible = participants.slice(startIndex, startIndex + (overflowCount === 1 ? 4 : 3));
4409
4598
  return /* @__PURE__ */ jsxs(TouchableOpacity, {
4599
+ testID: "cometchat-group-avatar-tile",
4410
4600
  style: [styles$23.container, style],
4411
4601
  onPress: toggleParticipantListVisible,
4412
4602
  children: [/* @__PURE__ */ jsx(View, {
@@ -4514,6 +4704,7 @@ function SidebarLayout() {
4514
4704
  const mainParticipant = useMainParticipant();
4515
4705
  const participants = allParticipants.length > 1 ? [mainParticipant].concat(allParticipants) : [mainParticipant];
4516
4706
  return /* @__PURE__ */ jsxs(View, {
4707
+ testID: "cometchat-sidebar-layout",
4517
4708
  style: styles$22.container,
4518
4709
  children: [
4519
4710
  /* @__PURE__ */ jsx(Tile_native_default, {
@@ -4650,23 +4841,26 @@ const Spotlight = () => {
4650
4841
  if (otherParticipant) {
4651
4842
  spotlightParticipants.push(otherParticipant);
4652
4843
  }
4653
- return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(Tile_native_default, {
4654
- participant: spotlightParticipants[0],
4655
- disablePress: true
4656
- }, spotlightParticipants[0].ruid), spotlightParticipants[1] && /* @__PURE__ */ jsx(Pan, {
4657
- disableDrag: !enableSpotlightDrag,
4658
- layout: {
4659
- width: mainAreaDimension.width,
4660
- height: mainAreaDimension.height
4661
- },
4662
- children: /* @__PURE__ */ jsx(Tile_native_default, {
4663
- showLabel: false,
4664
- disablePress: !enableSpotlightSwap,
4665
- participant: spotlightParticipants[1],
4666
- style: styles$20.panTile,
4667
- zOrder: 1
4668
- }, spotlightParticipants[1].ruid)
4669
- })] });
4844
+ return /* @__PURE__ */ jsxs(View, {
4845
+ testID: "cometchat-spotlight-layout",
4846
+ children: [/* @__PURE__ */ jsx(Tile_native_default, {
4847
+ participant: spotlightParticipants[0],
4848
+ disablePress: true
4849
+ }, spotlightParticipants[0].ruid), spotlightParticipants[1] && /* @__PURE__ */ jsx(Pan, {
4850
+ disableDrag: !enableSpotlightDrag,
4851
+ layout: {
4852
+ width: mainAreaDimension.width,
4853
+ height: mainAreaDimension.height
4854
+ },
4855
+ children: /* @__PURE__ */ jsx(Tile_native_default, {
4856
+ showLabel: false,
4857
+ disablePress: !enableSpotlightSwap,
4858
+ participant: spotlightParticipants[1],
4859
+ style: styles$20.panTile,
4860
+ zOrder: 1
4861
+ }, spotlightParticipants[1].ruid)
4862
+ })]
4863
+ });
4670
4864
  };
4671
4865
  const styles$20 = StyleSheet.create({ panTile: {
4672
4866
  borderColor: "#1A1A1A",
@@ -4699,6 +4893,7 @@ function TileLayout() {
4699
4893
  participantCount: isPIPLayoutEnabled ? 1 : participants.length
4700
4894
  });
4701
4895
  return /* @__PURE__ */ jsx(FlatList, {
4896
+ testID: "cometchat-tile-layout",
4702
4897
  data: isPIPLayoutEnabled ? [mainParticipant] : participants,
4703
4898
  renderItem: ({ item }) => /* @__PURE__ */ jsx(Tile_native_default, {
4704
4899
  participant: item,
@@ -5048,10 +5243,16 @@ function ConfirmationDialog() {
5048
5243
  visible,
5049
5244
  transparent: true,
5050
5245
  animationType: "fade",
5246
+ supportedOrientations: [
5247
+ "portrait",
5248
+ "landscape-left",
5249
+ "landscape-right"
5250
+ ],
5051
5251
  onRequestClose: handleBackdropPress,
5052
5252
  children: /* @__PURE__ */ jsx(Pressable, {
5053
5253
  style: styles$15.backdrop,
5054
5254
  onPress: handleBackdropPress,
5255
+ testID: "cometchat-confirmation-dialog-backdrop",
5055
5256
  children: /* @__PURE__ */ jsx(View, {
5056
5257
  style: styles$15.dialog,
5057
5258
  children: /* @__PURE__ */ jsxs(View, {
@@ -5094,7 +5295,7 @@ function ConfirmationDialog() {
5094
5295
  //#endregion
5095
5296
  //#region src/ui/bottom-sheet/BottomSheet.native.tsx
5096
5297
  const SCREEN_HEIGHT = Dimensions.get("window").height;
5097
- const BottomSheet = ({ children, isVisible, onClose, maxHeight = SCREEN_HEIGHT * .4 }) => {
5298
+ const BottomSheet = ({ children, isVisible, onClose, maxHeight = SCREEN_HEIGHT * .4, testID }) => {
5098
5299
  const visibleTranslateY = SCREEN_HEIGHT - maxHeight;
5099
5300
  const hiddenTranslateY = useRef(SCREEN_HEIGHT).current;
5100
5301
  const animatedValue = useRef(new Animated.Value(hiddenTranslateY)).current;
@@ -5120,8 +5321,10 @@ const BottomSheet = ({ children, isVisible, onClose, maxHeight = SCREEN_HEIGHT *
5120
5321
  const bottomSheetAnimation = { transform: [{ translateY: animatedValue }] };
5121
5322
  return /* @__PURE__ */ jsxs(Fragment, { children: [isVisible && /* @__PURE__ */ jsx(TouchableWithoutFeedback, {
5122
5323
  onPress: onClose,
5324
+ testID: "cometchat-bottom-sheet-backdrop",
5123
5325
  children: /* @__PURE__ */ jsx(View, { style: styles$14.backdrop })
5124
5326
  }), /* @__PURE__ */ jsxs(Animated.View, {
5327
+ testID,
5125
5328
  style: [
5126
5329
  styles$14.bottomSheet,
5127
5330
  bottomSheetAnimation,
@@ -5179,8 +5382,9 @@ var BottomSheet_native_default = BottomSheet;
5179
5382
 
5180
5383
  //#endregion
5181
5384
  //#region src/ui/control-pane/MenuItem.native.tsx
5182
- const MenuItem = ({ iconName, label, onPress, selected = false }) => {
5385
+ const MenuItem = ({ iconName, label, onPress, selected = false, testID }) => {
5183
5386
  return /* @__PURE__ */ jsxs(TouchableOpacity, {
5387
+ testID,
5184
5388
  onPress: () => {
5185
5389
  hideAllBottomSheets();
5186
5390
  onPress();
@@ -5233,6 +5437,7 @@ const AudioModesMenu = ({ isVisible, onClose }) => {
5233
5437
  style: [commonStyles.bodyRegular, styles$12.noItemsText],
5234
5438
  children: "No audio modes available"
5235
5439
  }), audioModes.map((mode, index) => /* @__PURE__ */ jsx(MenuItem_native_default, {
5440
+ testID: `cometchat-menu-item-audio-${mode.type.toLowerCase()}`,
5236
5441
  iconName: AUDIO_MODE_TYPE_ICON_MAP[mode.type],
5237
5442
  label: mode.type,
5238
5443
  selected: mode.selected,
@@ -5284,6 +5489,7 @@ const AudioModeButton = () => {
5284
5489
  style: controlPaneStyles.controlButton,
5285
5490
  onPress: toggleAudioModeMenuVisible,
5286
5491
  activeOpacity: .7,
5492
+ testID: "cometchat-audio-mode-button",
5287
5493
  children: /* @__PURE__ */ jsx(Icon_native_default, {
5288
5494
  name: "speaker-fill",
5289
5495
  fill: "#FFF",
@@ -5309,6 +5515,7 @@ const AudioControl = () => {
5309
5515
  style: [controlPaneStyles.controlButton, muted && controlPaneStyles.toggledButton],
5310
5516
  onPress,
5311
5517
  activeOpacity: .7,
5518
+ testID: "cometchat-audio-toggle-button",
5312
5519
  children: /* @__PURE__ */ jsx(Icon_native_default, {
5313
5520
  name: muted ? "mic-off-fill" : "mic-fill",
5314
5521
  fill: muted ? "#9F3032" : "#FFF",
@@ -5334,6 +5541,7 @@ const VideoControl = () => {
5334
5541
  style: [controlPaneStyles.controlButton, videoMuted && controlPaneStyles.toggledButton],
5335
5542
  onPress,
5336
5543
  activeOpacity: .7,
5544
+ testID: "cometchat-video-toggle-button",
5337
5545
  children: /* @__PURE__ */ jsx(Icon_native_default, {
5338
5546
  name: videoMuted ? "video-off-fill" : "video-fill",
5339
5547
  fill: videoMuted ? "#9F3032" : "#FFF",
@@ -5358,6 +5566,7 @@ const LeaveSessionButton = () => {
5358
5566
  style: [controlPaneStyles.controlButton, controlPaneStyles.leaveSessionButton],
5359
5567
  onPress,
5360
5568
  activeOpacity: .7,
5569
+ testID: "cometchat-leave-session-button",
5361
5570
  children: /* @__PURE__ */ jsx(Icon_native_default, {
5362
5571
  name: "call-end",
5363
5572
  fill: "#FFF",
@@ -5374,6 +5583,7 @@ const MoreMenuButton = () => {
5374
5583
  style: controlPaneStyles.controlButton,
5375
5584
  onPress: toggleMoreMenuVisible,
5376
5585
  activeOpacity: .7,
5586
+ testID: "cometchat-more-menu-button",
5377
5587
  children: /* @__PURE__ */ jsx(Icon_native_default, {
5378
5588
  name: "more",
5379
5589
  fill: "#FFF",
@@ -5449,22 +5659,26 @@ const MoreMenu = ({ isVisible, onClose }) => {
5449
5659
  toggleParticipantListVisible();
5450
5660
  }, []);
5451
5661
  return /* @__PURE__ */ jsx(BottomSheet_native_default, {
5662
+ testID: "cometchat-more-menu-bottom-sheet",
5452
5663
  maxHeight: bottomSheetMaxHeight,
5453
5664
  isVisible,
5454
5665
  onClose,
5455
5666
  children: /* @__PURE__ */ jsxs(ScrollView, { children: [
5456
5667
  numberOfVisibleItems === 0 && /* @__PURE__ */ jsx(Text, {
5668
+ testID: "cometchat-more-menu-empty-state",
5457
5669
  style: [commonStyles.bodyRegular, styles$11.noItemsText],
5458
5670
  children: "No options available"
5459
5671
  }),
5460
5672
  !hideScreenSharingButton && /* @__PURE__ */ jsx(ScreenShareButton_default, {}),
5461
5673
  !hideRaiseHandButton && /* @__PURE__ */ jsx(MenuItem_native_default, {
5674
+ testID: "cometchat-menu-item-raise-hand",
5462
5675
  iconName: "raise-hand-fill",
5463
5676
  label: raiseHandTimestamp ? "Lower Hand" : "Raise Hand",
5464
5677
  onPress: onRaiseHandPress,
5465
5678
  selected: Boolean(raiseHandTimestamp)
5466
5679
  }),
5467
5680
  !hideRecordingButton && /* @__PURE__ */ jsx(MenuItem_native_default, {
5681
+ testID: isRecording ? "cometchat-menu-item-stop-recording" : "cometchat-menu-item-start-recording",
5468
5682
  iconName: isRecording ? "record-stop-fill" : "record-fill",
5469
5683
  label: isRecording ? "Stop Recording" : "Start Recording",
5470
5684
  onPress: () => {
@@ -5475,6 +5689,7 @@ const MoreMenu = ({ isVisible, onClose }) => {
5475
5689
  }
5476
5690
  }),
5477
5691
  !hideParticipantListButton && /* @__PURE__ */ jsx(MenuItem_native_default, {
5692
+ testID: "cometchat-menu-item-participants",
5478
5693
  iconName: "participants",
5479
5694
  label: "Participants",
5480
5695
  onPress: onParticipantListPress
@@ -5601,6 +5816,7 @@ const ChangeLayout = () => {
5601
5816
  eventBus.publish({ type: EVENT_LISTENER_METHODS.ButtonClickListener.onChangeLayoutButtonClicked });
5602
5817
  showMenu();
5603
5818
  },
5819
+ testID: "cometchat-change-layout-button",
5604
5820
  children: /* @__PURE__ */ jsx(Icon_native_default, {
5605
5821
  name: "tile-fill",
5606
5822
  fill: "#FFFFFF"
@@ -5653,6 +5869,7 @@ const ChatButton = () => {
5653
5869
  return /* @__PURE__ */ jsxs(TouchableOpacity, {
5654
5870
  style: [styles$9.iconButton],
5655
5871
  onPress,
5872
+ testID: "cometchat-chat-button",
5656
5873
  children: [/* @__PURE__ */ jsx(Icon_native_default, {
5657
5874
  name: "chat",
5658
5875
  fill: "#FFFFFF"
@@ -5709,6 +5926,7 @@ const SwitchCamera = () => {
5709
5926
  return null;
5710
5927
  }
5711
5928
  return /* @__PURE__ */ jsx(TouchableOpacity, {
5929
+ testID: "cometchat-switch-camera-button",
5712
5930
  disabled,
5713
5931
  style: [styles$8.iconButton, disabled && styles$8.iconButtonDisabled],
5714
5932
  onPress,
@@ -5741,6 +5959,7 @@ const SessionTimer = () => {
5741
5959
  return null;
5742
5960
  }
5743
5961
  return /* @__PURE__ */ jsx(Text, {
5962
+ testID: "cometchat-session-timer",
5744
5963
  style: [commonStyles.caption1Regular, styles$7.meetingTime],
5745
5964
  children: miliSecondsToMMSS(conferenceElapsedTime)
5746
5965
  });
@@ -5852,69 +6071,80 @@ const IdealTimeoutModal = ({ style = {} }) => {
5852
6071
  transparent: true,
5853
6072
  visible: idleTimeoutModalVisible,
5854
6073
  animationType: "none",
6074
+ supportedOrientations: [
6075
+ "portrait",
6076
+ "landscape-left",
6077
+ "landscape-right"
6078
+ ],
5855
6079
  statusBarTranslucent: true,
5856
6080
  children: /* @__PURE__ */ jsx(TouchableWithoutFeedback, {
5857
6081
  onPress: handleOverlayPress,
6082
+ testID: "cometchat-idle-timeout-overlay",
5858
6083
  children: /* @__PURE__ */ jsx(Animated.View, {
5859
6084
  style: [styles$6.overlay, { opacity: fadeAnim }],
5860
- children: /* @__PURE__ */ jsx(TouchableWithoutFeedback, { children: /* @__PURE__ */ jsx(Animated.View, {
5861
- style: [
5862
- styles$6.modal,
5863
- style,
5864
- {
5865
- opacity: fadeAnim,
5866
- transform: [{ scale: scaleAnim }]
5867
- }
5868
- ],
5869
- children: /* @__PURE__ */ jsxs(View, {
5870
- style: styles$6.content,
5871
- children: [
5872
- /* @__PURE__ */ jsx(View, {
5873
- style: styles$6.timerIcon,
5874
- children: /* @__PURE__ */ jsx(Text, {
5875
- style: [commonStyles.heading3Bold, styles$6.timerText],
5876
- children: formattedTime
5877
- })
5878
- }),
5879
- /* @__PURE__ */ jsxs(View, {
5880
- style: styles$6.textContent,
5881
- children: [/* @__PURE__ */ jsx(Text, {
5882
- style: [commonStyles.heading2Medium, styles$6.title],
5883
- children: "Are you still there?"
5884
- }), /* @__PURE__ */ jsxs(Text, {
5885
- style: [commonStyles.bodyRegular, styles$6.subtitle],
5886
- children: [
5887
- "You are the only one here, so this call will end in less than ",
5888
- ceilMinutes,
5889
- " minute",
5890
- ceilMinutes > 1 ? "s" : "",
5891
- ". Do you want to stay in this call?"
5892
- ]
5893
- })]
5894
- }),
5895
- /* @__PURE__ */ jsxs(View, {
5896
- style: styles$6.actions,
5897
- children: [/* @__PURE__ */ jsx(TouchableOpacity, {
5898
- style: [styles$6.button, styles$6.buttonSecondary],
5899
- onPress: onStayInCall,
5900
- activeOpacity: .8,
6085
+ children: /* @__PURE__ */ jsx(TouchableWithoutFeedback, {
6086
+ testID: "cometchat-idle-timeout-modal",
6087
+ children: /* @__PURE__ */ jsx(Animated.View, {
6088
+ style: [
6089
+ styles$6.modal,
6090
+ style,
6091
+ {
6092
+ opacity: fadeAnim,
6093
+ transform: [{ scale: scaleAnim }]
6094
+ }
6095
+ ],
6096
+ children: /* @__PURE__ */ jsxs(View, {
6097
+ style: styles$6.content,
6098
+ children: [
6099
+ /* @__PURE__ */ jsx(View, {
6100
+ style: styles$6.timerIcon,
5901
6101
  children: /* @__PURE__ */ jsx(Text, {
5902
- style: [commonStyles.bodyMedium, styles$6.buttonSecondaryText],
5903
- children: "Stay on the call"
6102
+ style: [commonStyles.heading3Bold, styles$6.timerText],
6103
+ children: formattedTime
5904
6104
  })
5905
- }), /* @__PURE__ */ jsx(TouchableOpacity, {
5906
- style: [styles$6.button, styles$6.buttonPrimary],
5907
- onPress: leaveSession,
5908
- activeOpacity: .8,
5909
- children: /* @__PURE__ */ jsx(Text, {
5910
- style: [commonStyles.bodyMedium, styles$6.buttonPrimaryText],
5911
- children: "Leave now"
5912
- })
5913
- })]
5914
- })
5915
- ]
6105
+ }),
6106
+ /* @__PURE__ */ jsxs(View, {
6107
+ style: styles$6.textContent,
6108
+ children: [/* @__PURE__ */ jsx(Text, {
6109
+ style: [commonStyles.heading2Medium, styles$6.title],
6110
+ children: "Are you still there?"
6111
+ }), /* @__PURE__ */ jsxs(Text, {
6112
+ style: [commonStyles.bodyRegular, styles$6.subtitle],
6113
+ children: [
6114
+ "You are the only one here, so this call will end in less than ",
6115
+ ceilMinutes,
6116
+ " minute",
6117
+ ceilMinutes > 1 ? "s" : "",
6118
+ ". Do you want to stay in this call?"
6119
+ ]
6120
+ })]
6121
+ }),
6122
+ /* @__PURE__ */ jsxs(View, {
6123
+ style: styles$6.actions,
6124
+ children: [/* @__PURE__ */ jsx(TouchableOpacity, {
6125
+ style: [styles$6.button, styles$6.buttonSecondary],
6126
+ onPress: onStayInCall,
6127
+ activeOpacity: .8,
6128
+ testID: "cometchat-idle-timeout-stay-button",
6129
+ children: /* @__PURE__ */ jsx(Text, {
6130
+ style: [commonStyles.bodyMedium, styles$6.buttonSecondaryText],
6131
+ children: "Stay on the call"
6132
+ })
6133
+ }), /* @__PURE__ */ jsx(TouchableOpacity, {
6134
+ style: [styles$6.button, styles$6.buttonPrimary],
6135
+ onPress: () => leaveSession(),
6136
+ activeOpacity: .8,
6137
+ testID: "cometchat-idle-timeout-leave-button",
6138
+ children: /* @__PURE__ */ jsx(Text, {
6139
+ style: [commonStyles.bodyMedium, styles$6.buttonPrimaryText],
6140
+ children: "Leave now"
6141
+ })
6142
+ })]
6143
+ })
6144
+ ]
6145
+ })
5916
6146
  })
5917
- }) })
6147
+ })
5918
6148
  })
5919
6149
  })
5920
6150
  });
@@ -5933,6 +6163,7 @@ const styles$6 = StyleSheet.create({
5933
6163
  borderWidth: 1,
5934
6164
  borderColor: "#383838",
5935
6165
  width: "100%",
6166
+ maxWidth: 372,
5936
6167
  paddingTop: 32,
5937
6168
  paddingHorizontal: 20,
5938
6169
  paddingBottom: 20,
@@ -6011,6 +6242,7 @@ const ShareInviteButton = () => {
6011
6242
  style: styles$5.shareButtonContainer,
6012
6243
  children: /* @__PURE__ */ jsxs(TouchableOpacity, {
6013
6244
  style: styles$5.shareButton,
6245
+ testID: "cometchat-share-invite-button",
6014
6246
  onPress: () => {
6015
6247
  eventBus.publish({ type: "onShareInviteButtonClicked" });
6016
6248
  },
@@ -6147,6 +6379,7 @@ const ParticipantList = () => {
6147
6379
  }), /* @__PURE__ */ jsx(TouchableOpacity, {
6148
6380
  onPress: toggleParticipantListVisible,
6149
6381
  accessibilityLabel: "Close participants list",
6382
+ testID: "cometchat-participant-list-close-button",
6150
6383
  children: /* @__PURE__ */ jsx(Icon_native_default, {
6151
6384
  name: "close",
6152
6385
  size: 24,
@@ -6168,7 +6401,8 @@ const ParticipantList = () => {
6168
6401
  style: styles$3.searchInput,
6169
6402
  value: searchTerm,
6170
6403
  onChangeText: setSearchTerm,
6171
- placeholderTextColor: "#858585"
6404
+ placeholderTextColor: "#858585",
6405
+ testID: "cometchat-participant-search-input"
6172
6406
  })]
6173
6407
  })
6174
6408
  }),
@@ -6422,6 +6656,7 @@ function ToastItemView({ toast, onDismiss }) {
6422
6656
  }), toast.action && /* @__PURE__ */ jsx(Pressable, {
6423
6657
  style: styles$2.actionButton,
6424
6658
  onPress: handleActionPress,
6659
+ testID: "cometchat-toast-action-button",
6425
6660
  children: /* @__PURE__ */ jsx(Text, {
6426
6661
  style: styles$2.actionText,
6427
6662
  children: toast.action.label
@@ -6474,13 +6709,17 @@ function CallUI(props) {
6474
6709
  const isConferenceJoined = useIsConferenceJoined();
6475
6710
  useLayoutEffect(() => {
6476
6711
  eventBus.publish({ type: INTERNAL_EVENTS.lifecycle.componentDidMount });
6477
- updateConfig(props.callSettings);
6712
+ updateConfig(props.sessionSettings);
6478
6713
  return () => {
6479
6714
  eventBus.publish({ type: INTERNAL_EVENTS.lifecycle.componentWillUnmount }, true);
6480
6715
  };
6481
- }, [props.callSettings]);
6716
+ }, [props.sessionSettings]);
6482
6717
  useEffect(() => {
6483
- useBaseStore.setState({ sdkPlatform: Platform.OS });
6718
+ if (props.sessionSettings.sdkPlatform) {
6719
+ useBaseStore.setState({ sdkPlatform: props.sessionSettings.sdkPlatform });
6720
+ } else {
6721
+ useBaseStore.setState({ sdkPlatform: Platform.OS === "ios" ? "react-native-ios" : "react-native-android" });
6722
+ }
6484
6723
  startSession();
6485
6724
  }, []);
6486
6725
  useEffect(() => {
@@ -6495,13 +6734,13 @@ function CallUI(props) {
6495
6734
  useEffect(() => {
6496
6735
  if (Platform.OS === "android") {
6497
6736
  AudioModeModule_default.setMode(type === SESSION_TYPE.VOICE ? AudioModeModule_default.AUDIO_CALL : AudioModeModule_default.VIDEO_CALL);
6498
- if (props.callSettings.audioMode) {
6499
- AudioModeModule_default.setAudioDevice(props.callSettings.audioMode);
6737
+ if (props.sessionSettings.audioMode) {
6738
+ AudioModeModule_default.setAudioDevice(props.sessionSettings.audioMode);
6500
6739
  }
6501
6740
  } else if (Platform.OS === "ios") {
6502
6741
  AudioModeModule_default.updateDeviceList();
6503
6742
  }
6504
- }, [props.callSettings.audioMode, type]);
6743
+ }, [props.sessionSettings.audioMode, type]);
6505
6744
  if (isPIPLayoutEnabled) {
6506
6745
  return /* @__PURE__ */ jsx(PiPTile_default, {});
6507
6746
  }
@@ -6630,10 +6869,10 @@ const convertLegacyCallSettingsToV5Props = (callSettings) => {
6630
6869
  if (cs.defaultAudioMode === "BLUETOOTH" || cs.defaultAudioMode === "EARPIECE" || cs.defaultAudioMode === "HEADPHONES" || cs.defaultAudioMode === "SPEAKER") {
6631
6870
  v5Props.audioMode = cs.defaultAudioMode;
6632
6871
  }
6633
- if (cs.mode === "SPOTLIGHT") {
6634
- v5Props.layout = "SPOTLIGHT";
6635
- } else {
6636
- v5Props.layout = "SIDEBAR";
6872
+ if (typeof cs.layout === "string") {
6873
+ v5Props.layout = cs.layout;
6874
+ } else if (cs.mode === "SPOTLIGHT" || cs.mode === "SIDEBAR") {
6875
+ v5Props.layout = cs.mode;
6637
6876
  }
6638
6877
  if (cs.idleTimeoutPeriod) {
6639
6878
  v5Props.idleTimeoutPeriodAfterPrompt = 6e4;
@@ -6793,8 +7032,8 @@ async function callVerifyTokenAPI({ appId, region, calltoken, baseURL }) {
6793
7032
  }
6794
7033
 
6795
7034
  //#endregion
6796
- //#region src/AppRN.tsx
6797
- function App(props) {
7035
+ //#region src/AppReactNativeSDK.tsx
7036
+ function AppReactNativeSDK(props) {
6798
7037
  const [internalSettings, setInternalSettings] = React.useState(null);
6799
7038
  const [infoMessage, setInfoMessage] = React.useState(null);
6800
7039
  useEffect(() => {
@@ -6806,7 +7045,7 @@ function App(props) {
6806
7045
  }, []);
6807
7046
  useEffect(() => {
6808
7047
  const listeners = [];
6809
- const cs = props.callSettings ?? {};
7048
+ const cs = props.sessionSettings ?? {};
6810
7049
  if (cs.listener?.onUserJoined) {
6811
7050
  listeners.push(CometChatCalls.addEventListener("onParticipantJoined", cs.listener.onUserJoined));
6812
7051
  }
@@ -6844,7 +7083,7 @@ function App(props) {
6844
7083
  listener();
6845
7084
  });
6846
7085
  };
6847
- }, [props.callSettings]);
7086
+ }, [props.sessionSettings]);
6848
7087
  useEffect(() => {
6849
7088
  callVerifyTokenAPI({
6850
7089
  appId: CometChatCalls.appSettings?.appId || "",
@@ -6873,14 +7112,12 @@ function App(props) {
6873
7112
  visible: true
6874
7113
  });
6875
7114
  }
6876
- return /* @__PURE__ */ jsx(index_native_default, { callSettings: {
6877
- ...props.callSettings,
6878
- ...convertLegacyCallSettingsToV5Props(props?.callSettings ?? {}),
7115
+ return /* @__PURE__ */ jsx(index_native_default, { sessionSettings: {
7116
+ ...props.sessionSettings,
7117
+ ...convertLegacyCallSettingsToV5Props(props?.sessionSettings ?? {}),
6879
7118
  internalSettings
6880
7119
  } });
6881
7120
  }
6882
- var AppRN_default = App;
6883
- const AppComponent = App;
6884
7121
 
6885
7122
  //#endregion
6886
7123
  //#region src/v4/Constants.ts
@@ -10613,7 +10850,7 @@ var CometChatCalls = class extends SessionMethodsCore {
10613
10850
  static OngoingCallListener = OngoingCallListener;
10614
10851
  static CallSettingsBuilder = CallSettingsBuilder;
10615
10852
  static CallAppSettingsBuilder = CallAppSettingsBuilder;
10616
- static Component = AppComponent;
10853
+ static Component = AppReactNativeSDK;
10617
10854
  /**
10618
10855
  * Initializes the CometChat Calls SDK with the provided app settings.
10619
10856
  * Must be called before any other SDK methods.
@@ -10635,7 +10872,7 @@ var CometChatCalls = class extends SessionMethodsCore {
10635
10872
  }
10636
10873
  this.appSettings = parsedAppSettings.output;
10637
10874
  this.isInitialized = true;
10638
- const savedUser = this.getSavedUser();
10875
+ const savedUser = await this.getSavedUser();
10639
10876
  if (savedUser) {
10640
10877
  let parsedUser;
10641
10878
  if (typeof savedUser === "string") {
@@ -10704,12 +10941,11 @@ var CometChatCalls = class extends SessionMethodsCore {
10704
10941
  if (this.loggedInUser && this.loggedInUser.uid !== uid) {
10705
10942
  await this.logoutInternal();
10706
10943
  }
10707
- console.log("Logging in user with UID:", uid);
10708
10944
  const authToken = await this.loginWithUID(uid, resolvedAuthKey);
10709
10945
  const user = await this.authenticateWithToken(authToken);
10710
10946
  this.loginInProgress = false;
10711
10947
  this.loggedInUser = user;
10712
- this.saveUser(user);
10948
+ await this.saveUser(user);
10713
10949
  this.notifyLoginSuccess(user);
10714
10950
  return user;
10715
10951
  } catch (error) {
@@ -10757,7 +10993,7 @@ var CometChatCalls = class extends SessionMethodsCore {
10757
10993
  const user = await this.authenticateWithToken(authToken);
10758
10994
  this.loginInProgress = false;
10759
10995
  this.loggedInUser = user;
10760
- this.saveUser(user);
10996
+ await this.saveUser(user);
10761
10997
  this.notifyLoginSuccess(user);
10762
10998
  return user;
10763
10999
  } catch (error) {
@@ -10890,7 +11126,7 @@ var CometChatCalls = class extends SessionMethodsCore {
10890
11126
  appId
10891
11127
  },
10892
11128
  body: {
10893
- platform: "web",
11129
+ platform: "react-native",
10894
11130
  deviceId: this.generateDeviceId()
10895
11131
  }
10896
11132
  });
@@ -10911,7 +11147,7 @@ var CometChatCalls = class extends SessionMethodsCore {
10911
11147
  appId
10912
11148
  },
10913
11149
  body: {
10914
- platform: "web",
11150
+ platform: "react-native",
10915
11151
  deviceId: this.generateDeviceId()
10916
11152
  }
10917
11153
  });
@@ -10951,7 +11187,7 @@ var CometChatCalls = class extends SessionMethodsCore {
10951
11187
  }
10952
11188
  }
10953
11189
  this.loggedInUser = null;
10954
- this.clearSavedUser();
11190
+ await this.clearSavedUser();
10955
11191
  }
10956
11192
  static async callGenerateTokenAPI(sessionId, authToken) {
10957
11193
  const appId = this.appSettings?.appId || "";
@@ -10979,9 +11215,11 @@ var CometChatCalls = class extends SessionMethodsCore {
10979
11215
  baseURL: this.getBaseURL()
10980
11216
  });
10981
11217
  }
10982
- static saveUser(user) {
11218
+ static getStorageKey() {
11219
+ return `${this.appSettings?.appId}:common_store/user`;
11220
+ }
11221
+ static async saveUser(user) {
10983
11222
  try {
10984
- const key = `${this.appSettings?.appId}:common_store/user`;
10985
11223
  const userWithDefaults = {
10986
11224
  hasBlockedMe: false,
10987
11225
  blockedByMe: false,
@@ -10990,31 +11228,28 @@ var CometChatCalls = class extends SessionMethodsCore {
10990
11228
  role: user.role || "default",
10991
11229
  wsChannel: user.wsChannel || { identity: `[${this.appSettings?.appId}]${user.uid}` }
10992
11230
  };
10993
- localStorage.setItem(key, JSON.stringify(userWithDefaults));
11231
+ await AsyncStorage.setItem(this.getStorageKey(), JSON.stringify(userWithDefaults));
10994
11232
  } catch (error) {
10995
- console.warn("Failed to save user to localStorage:", error);
11233
+ console.warn("Failed to save user to AsyncStorage:", error);
10996
11234
  }
10997
11235
  }
10998
- static getSavedUser() {
11236
+ static async getSavedUser() {
10999
11237
  try {
11000
- const key = `${this.appSettings?.appId}:common_store/user`;
11001
- const savedUser = localStorage.getItem(key);
11002
- return savedUser ? savedUser : null;
11238
+ return await AsyncStorage.getItem(this.getStorageKey());
11003
11239
  } catch (error) {
11004
- console.warn("Failed to get saved user from localStorage:", error);
11240
+ console.warn("Failed to get saved user from AsyncStorage:", error);
11005
11241
  return null;
11006
11242
  }
11007
11243
  }
11008
- static clearSavedUser() {
11244
+ static async clearSavedUser() {
11009
11245
  try {
11010
- const key = `${this.appSettings?.appId}:common_store/user`;
11011
- localStorage.removeItem(key);
11246
+ await AsyncStorage.removeItem(this.getStorageKey());
11012
11247
  } catch (error) {
11013
- console.warn("Failed to clear saved user from localStorage:", error);
11248
+ console.warn("Failed to clear saved user from AsyncStorage:", error);
11014
11249
  }
11015
11250
  }
11016
11251
  static generateDeviceId() {
11017
- return "web_" + Math.random().toString(36).substr(2, 9);
11252
+ return "rn_" + Math.random().toString(36).substring(2, 11);
11018
11253
  }
11019
11254
  static createError(error) {
11020
11255
  if (error.errorCode && error.errorDescription) {
@@ -11073,13 +11308,32 @@ var CometChatCalls = class extends SessionMethodsCore {
11073
11308
  * Adds an event listener for SDK events.
11074
11309
  * @param eventType - The type of event to listen for.
11075
11310
  * @param listener - The callback function to invoke when the event fires.
11311
+ * @param options - Optional configuration including an AbortSignal for automatic cleanup.
11076
11312
  * @returns An unsubscribe function to remove the listener.
11077
11313
  */
11078
- static addEventListener(eventType, listener) {
11079
- return eventBus.subscribe(eventType, listener);
11314
+ static addEventListener(eventType, listener, options) {
11315
+ return eventBus.subscribe(eventType, listener, options);
11316
+ }
11317
+ /**
11318
+ * Sets the audio output mode (mobile only).
11319
+ * @param mode - The audio mode to set (e.g., 'SPEAKER', 'EARPIECE', 'BLUETOOTH', 'HEADPHONES').
11320
+ */
11321
+ static setAudioMode(mode) {
11322
+ setAudioMode(mode);
11323
+ }
11324
+ /**
11325
+ * Enables Picture-in-Picture (PIP) layout during the call.
11326
+ */
11327
+ static enablePictureInPictureLayout() {
11328
+ enablePictureInPictureLayout();
11329
+ }
11330
+ /**
11331
+ * Disables Picture-in-Picture (PIP) layout.
11332
+ */
11333
+ static disablePictureInPictureLayout() {
11334
+ disablePictureInPictureLayout();
11080
11335
  }
11081
11336
  };
11082
11337
 
11083
11338
  //#endregion
11084
- export { CometChatCalls };
11085
- //# sourceMappingURL=index.mjs.map
11339
+ export { CometChatCalls };