@ethora/chat-component-rn 26.10.9 → 26.10.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (154) hide show
  1. package/CHANGELOG.md +1 -0
  2. package/README.md +1 -1
  3. package/lib/commonjs/components/MainComponents/AlbumGrid.js +6 -2
  4. package/lib/commonjs/components/MainComponents/AlbumGrid.js.map +1 -1
  5. package/lib/commonjs/components/MainComponents/ChatHeader.js +16 -11
  6. package/lib/commonjs/components/MainComponents/ChatHeader.js.map +1 -1
  7. package/lib/commonjs/components/MainComponents/MediaMessage.js +13 -0
  8. package/lib/commonjs/components/MainComponents/MediaMessage.js.map +1 -1
  9. package/lib/commonjs/components/MainComponents/RoomList.js +7 -16
  10. package/lib/commonjs/components/MainComponents/RoomList.js.map +1 -1
  11. package/lib/commonjs/components/MessageBubble/Message.js +27 -1
  12. package/lib/commonjs/components/MessageBubble/Message.js.map +1 -1
  13. package/lib/commonjs/components/Modals/ChatProfileModal/ChatMediaGrid.js +159 -0
  14. package/lib/commonjs/components/Modals/ChatProfileModal/ChatMediaGrid.js.map +1 -0
  15. package/lib/commonjs/components/Modals/ChatProfileModal/ChatProfileModal.js +203 -134
  16. package/lib/commonjs/components/Modals/ChatProfileModal/ChatProfileModal.js.map +1 -1
  17. package/lib/commonjs/components/Modals/ChatProfileModal/LeaveChatModal.js +180 -0
  18. package/lib/commonjs/components/Modals/ChatProfileModal/LeaveChatModal.js.map +1 -0
  19. package/lib/commonjs/components/Modals/FilePreviewModal/FilePreviewModal.js +100 -162
  20. package/lib/commonjs/components/Modals/FilePreviewModal/FilePreviewModal.js.map +1 -1
  21. package/lib/commonjs/components/Modals/FilePreviewModal/MediaViewer.js +824 -0
  22. package/lib/commonjs/components/Modals/FilePreviewModal/MediaViewer.js.map +1 -0
  23. package/lib/commonjs/components/Modals/FilePreviewModal/useSaveFile.js +179 -0
  24. package/lib/commonjs/components/Modals/FilePreviewModal/useSaveFile.js.map +1 -0
  25. package/lib/commonjs/components/Modals/MessageSearchModal/MessageSearchModal.js +22 -5
  26. package/lib/commonjs/components/Modals/MessageSearchModal/MessageSearchModal.js.map +1 -1
  27. package/lib/commonjs/components/styled/MessageImage.js +4 -1
  28. package/lib/commonjs/components/styled/MessageImage.js.map +1 -1
  29. package/lib/commonjs/components/styled/VideoMessage.js +5 -1
  30. package/lib/commonjs/components/styled/VideoMessage.js.map +1 -1
  31. package/lib/commonjs/context/MediaSourceContext.js +18 -0
  32. package/lib/commonjs/context/MediaSourceContext.js.map +1 -0
  33. package/lib/commonjs/helpers/constants/MODAL_COMPONENTS.js +3 -1
  34. package/lib/commonjs/helpers/constants/MODAL_COMPONENTS.js.map +1 -1
  35. package/lib/commonjs/helpers/constants/MODAL_TYPES.js +2 -0
  36. package/lib/commonjs/helpers/constants/MODAL_TYPES.js.map +1 -1
  37. package/lib/commonjs/helpers/publicChatsEntry.js +8 -6
  38. package/lib/commonjs/helpers/publicChatsEntry.js.map +1 -1
  39. package/lib/commonjs/helpers/roomMedia.js +118 -0
  40. package/lib/commonjs/helpers/roomMedia.js.map +1 -0
  41. package/lib/commonjs/i18n/strings.js +36 -0
  42. package/lib/commonjs/i18n/strings.js.map +1 -1
  43. package/lib/commonjs/networking/api-requests/rooms.api.js +9 -13
  44. package/lib/commonjs/networking/api-requests/rooms.api.js.map +1 -1
  45. package/lib/commonjs/version.js +1 -1
  46. package/lib/commonjs/version.js.map +1 -1
  47. package/lib/module/components/MainComponents/AlbumGrid.js +6 -2
  48. package/lib/module/components/MainComponents/AlbumGrid.js.map +1 -1
  49. package/lib/module/components/MainComponents/ChatHeader.js +18 -13
  50. package/lib/module/components/MainComponents/ChatHeader.js.map +1 -1
  51. package/lib/module/components/MainComponents/MediaMessage.js +13 -0
  52. package/lib/module/components/MainComponents/MediaMessage.js.map +1 -1
  53. package/lib/module/components/MainComponents/RoomList.js +7 -16
  54. package/lib/module/components/MainComponents/RoomList.js.map +1 -1
  55. package/lib/module/components/MessageBubble/Message.js +27 -1
  56. package/lib/module/components/MessageBubble/Message.js.map +1 -1
  57. package/lib/module/components/Modals/ChatProfileModal/ChatMediaGrid.js +154 -0
  58. package/lib/module/components/Modals/ChatProfileModal/ChatMediaGrid.js.map +1 -0
  59. package/lib/module/components/Modals/ChatProfileModal/ChatProfileModal.js +204 -136
  60. package/lib/module/components/Modals/ChatProfileModal/ChatProfileModal.js.map +1 -1
  61. package/lib/module/components/Modals/ChatProfileModal/LeaveChatModal.js +175 -0
  62. package/lib/module/components/Modals/ChatProfileModal/LeaveChatModal.js.map +1 -0
  63. package/lib/module/components/Modals/FilePreviewModal/FilePreviewModal.js +100 -163
  64. package/lib/module/components/Modals/FilePreviewModal/FilePreviewModal.js.map +1 -1
  65. package/lib/module/components/Modals/FilePreviewModal/MediaViewer.js +819 -0
  66. package/lib/module/components/Modals/FilePreviewModal/MediaViewer.js.map +1 -0
  67. package/lib/module/components/Modals/FilePreviewModal/useSaveFile.js +172 -0
  68. package/lib/module/components/Modals/FilePreviewModal/useSaveFile.js.map +1 -0
  69. package/lib/module/components/Modals/MessageSearchModal/MessageSearchModal.js +20 -4
  70. package/lib/module/components/Modals/MessageSearchModal/MessageSearchModal.js.map +1 -1
  71. package/lib/module/components/styled/MessageImage.js +4 -1
  72. package/lib/module/components/styled/MessageImage.js.map +1 -1
  73. package/lib/module/components/styled/VideoMessage.js +5 -1
  74. package/lib/module/components/styled/VideoMessage.js.map +1 -1
  75. package/lib/module/context/MediaSourceContext.js +13 -0
  76. package/lib/module/context/MediaSourceContext.js.map +1 -0
  77. package/lib/module/helpers/constants/MODAL_COMPONENTS.js +2 -1
  78. package/lib/module/helpers/constants/MODAL_COMPONENTS.js.map +1 -1
  79. package/lib/module/helpers/constants/MODAL_TYPES.js +2 -0
  80. package/lib/module/helpers/constants/MODAL_TYPES.js.map +1 -1
  81. package/lib/module/helpers/publicChatsEntry.js +8 -6
  82. package/lib/module/helpers/publicChatsEntry.js.map +1 -1
  83. package/lib/module/helpers/roomMedia.js +112 -0
  84. package/lib/module/helpers/roomMedia.js.map +1 -0
  85. package/lib/module/i18n/strings.js +36 -0
  86. package/lib/module/i18n/strings.js.map +1 -1
  87. package/lib/module/networking/api-requests/rooms.api.js +9 -13
  88. package/lib/module/networking/api-requests/rooms.api.js.map +1 -1
  89. package/lib/module/version.js +1 -1
  90. package/lib/module/version.js.map +1 -1
  91. package/lib/typescript/components/MainComponents/AlbumGrid.d.ts.map +1 -1
  92. package/lib/typescript/components/MainComponents/ChatHeader.d.ts.map +1 -1
  93. package/lib/typescript/components/MainComponents/MediaMessage.d.ts +3 -1
  94. package/lib/typescript/components/MainComponents/MediaMessage.d.ts.map +1 -1
  95. package/lib/typescript/components/MainComponents/RoomList.d.ts.map +1 -1
  96. package/lib/typescript/components/MessageBubble/Message.d.ts.map +1 -1
  97. package/lib/typescript/components/Modals/ChatProfileModal/ChatMediaGrid.d.ts +16 -0
  98. package/lib/typescript/components/Modals/ChatProfileModal/ChatMediaGrid.d.ts.map +1 -0
  99. package/lib/typescript/components/Modals/ChatProfileModal/ChatProfileModal.d.ts.map +1 -1
  100. package/lib/typescript/components/Modals/ChatProfileModal/LeaveChatModal.d.ts +18 -0
  101. package/lib/typescript/components/Modals/ChatProfileModal/LeaveChatModal.d.ts.map +1 -0
  102. package/lib/typescript/components/Modals/FilePreviewModal/FilePreviewModal.d.ts +16 -0
  103. package/lib/typescript/components/Modals/FilePreviewModal/FilePreviewModal.d.ts.map +1 -1
  104. package/lib/typescript/components/Modals/FilePreviewModal/MediaViewer.d.ts +27 -0
  105. package/lib/typescript/components/Modals/FilePreviewModal/MediaViewer.d.ts.map +1 -0
  106. package/lib/typescript/components/Modals/FilePreviewModal/useSaveFile.d.ts +9 -0
  107. package/lib/typescript/components/Modals/FilePreviewModal/useSaveFile.d.ts.map +1 -0
  108. package/lib/typescript/components/Modals/MessageSearchModal/MessageSearchModal.d.ts +8 -0
  109. package/lib/typescript/components/Modals/MessageSearchModal/MessageSearchModal.d.ts.map +1 -1
  110. package/lib/typescript/components/styled/MessageImage.d.ts.map +1 -1
  111. package/lib/typescript/components/styled/VideoMessage.d.ts.map +1 -1
  112. package/lib/typescript/context/MediaSourceContext.d.ts +4 -0
  113. package/lib/typescript/context/MediaSourceContext.d.ts.map +1 -0
  114. package/lib/typescript/helpers/constants/MODAL_COMPONENTS.d.ts.map +1 -1
  115. package/lib/typescript/helpers/constants/MODAL_TYPES.d.ts +2 -0
  116. package/lib/typescript/helpers/constants/MODAL_TYPES.d.ts.map +1 -1
  117. package/lib/typescript/helpers/publicChatsEntry.d.ts +7 -5
  118. package/lib/typescript/helpers/publicChatsEntry.d.ts.map +1 -1
  119. package/lib/typescript/helpers/roomMedia.d.ts +26 -0
  120. package/lib/typescript/helpers/roomMedia.d.ts.map +1 -0
  121. package/lib/typescript/i18n/strings.d.ts.map +1 -1
  122. package/lib/typescript/networking/api-requests/rooms.api.d.ts +6 -5
  123. package/lib/typescript/networking/api-requests/rooms.api.d.ts.map +1 -1
  124. package/lib/typescript/types/models/modal.model.d.ts +18 -0
  125. package/lib/typescript/types/models/modal.model.d.ts.map +1 -1
  126. package/lib/typescript/types/types.d.ts +18 -0
  127. package/lib/typescript/types/types.d.ts.map +1 -1
  128. package/lib/typescript/version.d.ts +1 -1
  129. package/lib/typescript/version.d.ts.map +1 -1
  130. package/package.json +9 -2
  131. package/src/components/MainComponents/AlbumGrid.tsx +5 -1
  132. package/src/components/MainComponents/ChatHeader.tsx +16 -11
  133. package/src/components/MainComponents/MediaMessage.tsx +13 -2
  134. package/src/components/MainComponents/RoomList.tsx +8 -19
  135. package/src/components/MessageBubble/Message.tsx +24 -1
  136. package/src/components/Modals/ChatProfileModal/ChatMediaGrid.tsx +174 -0
  137. package/src/components/Modals/ChatProfileModal/ChatProfileModal.tsx +119 -48
  138. package/src/components/Modals/ChatProfileModal/LeaveChatModal.tsx +191 -0
  139. package/src/components/Modals/FilePreviewModal/FilePreviewModal.tsx +115 -185
  140. package/src/components/Modals/FilePreviewModal/MediaViewer.tsx +880 -0
  141. package/src/components/Modals/FilePreviewModal/useSaveFile.ts +207 -0
  142. package/src/components/Modals/MessageSearchModal/MessageSearchModal.tsx +56 -30
  143. package/src/components/styled/MessageImage.tsx +3 -1
  144. package/src/components/styled/VideoMessage.tsx +5 -1
  145. package/src/context/MediaSourceContext.tsx +14 -0
  146. package/src/helpers/constants/MODAL_COMPONENTS.tsx +4 -1
  147. package/src/helpers/constants/MODAL_TYPES.ts +2 -0
  148. package/src/helpers/publicChatsEntry.ts +8 -7
  149. package/src/helpers/roomMedia.ts +126 -0
  150. package/src/i18n/strings.ts +36 -0
  151. package/src/networking/api-requests/rooms.api.ts +13 -12
  152. package/src/types/models/modal.model.ts +16 -0
  153. package/src/types/types.ts +16 -0
  154. package/src/version.ts +1 -1
@@ -49,7 +49,7 @@ import { MODAL_TYPES } from '../../../helpers/constants/MODAL_TYPES';
49
49
  import SelectUsersModal from '../SelectUsersModal/SelectUsersModal';
50
50
  import { useToast } from '../../../context/ToastContext';
51
51
  import { useRoomMute } from '../../../hooks/useRoomMute';
52
- import { useLeaveRoom } from '../../../hooks/useLeaveRoom';
52
+ import LeaveChatModal from './LeaveChatModal';
53
53
  import { useT } from '../../../i18n/useT';
54
54
  import DeleteChatModal from './DeleteChatModal';
55
55
  import { isMessageSearchEnabled } from '../../../helpers/isMessageSearchEnabled';
@@ -67,6 +67,10 @@ import {
67
67
  isRoomMembersTruncated,
68
68
  } from '../../../helpers/roomUserCount';
69
69
  import { useRoomDirectory } from '../../../hooks/useRoomDirectory';
70
+ import ChatMediaGrid from './ChatMediaGrid';
71
+ import MediaViewer from '../FilePreviewModal/MediaViewer';
72
+ import { useSaveFile } from '../FilePreviewModal/useSaveFile';
73
+ import type { ModalFile } from '../../../types/types';
70
74
 
71
75
  /** Member rows rendered per step; the rest sits behind a 'Show more' row. */
72
76
  export const MEMBER_PAGE_SIZE = 50;
@@ -103,6 +107,11 @@ const ChatProfileModal: React.FC<ChatProfileModalProps> = ({
103
107
  const [loading, setLoading] = useState<boolean>(false);
104
108
  const [isDeleteOpen, setIsDeleteOpen] = useState(false);
105
109
  const [isReportOpen, setIsReportOpen] = useState(false);
110
+ const [isLeaveOpen, setIsLeaveOpen] = useState(false);
111
+ const [activeTab, setActiveTab] = useState<'members' | 'media'>('members');
112
+ // A picture or video opened from the Media tab, shown over this screen.
113
+ const [viewerFile, setViewerFile] = useState<ModalFile | null>(null);
114
+ const saveFile = useSaveFile();
106
115
  const [memberQuery, setMemberQuery] = useState('');
107
116
  const [isSearchOpen, setIsSearchOpen] = useState(false);
108
117
  // Measured, not guessed: this screen renders inside whatever frame the
@@ -113,7 +122,6 @@ const ChatProfileModal: React.FC<ChatProfileModalProps> = ({
113
122
  const { showToast } = useToast();
114
123
  const dispatch = useDispatch();
115
124
  const { client } = useXmppClient();
116
- const leaveRoom = useLeaveRoom();
117
125
  const { user: stateUser, config } = useChatSettingState();
118
126
  const theme = useTheme();
119
127
  const styles = useMemo(() => createStyles(theme), [theme]);
@@ -343,27 +351,13 @@ const ChatProfileModal: React.FC<ChatProfileModalProps> = ({
343
351
  // Leaving takes us out of the members on the server, then out of the MUC;
344
352
  // dropping the room from the store is what takes the user out of the chat
345
353
  // in the UI, and the `!activeRoom` effect above then closes this modal.
346
- const handleLeave = () => {
347
- Alert.alert(t('modal.leaveChat.title'), t('modal.leaveChat.description'), [
348
- { text: t('action.cancel'), style: 'cancel' },
349
- {
350
- text: t('action.leave'),
351
- style: 'destructive',
352
- onPress: async () => {
353
- if (!(await leaveRoom(activeRoom?.jid || ''))) {
354
- Alert.alert(t('toast.error'), t('toast.leaveFailed'));
355
- return;
356
- }
357
- handleCloseModal?.();
358
- },
359
- },
360
- ]);
361
- };
354
+ const handleLeave = () => setIsLeaveOpen(true);
362
355
 
363
356
  const isModerator = activeRoom?.role === 'moderator';
364
357
  const canEditIcon =
365
358
  !config?.disableChatInfo?.disableIconEdit && activeRoom?.role !== 'participant';
366
359
  const canAddMembers = isModerator && activeRoom?.type === 'group';
360
+ const showMembers = !config?.disableChatInfo?.hideMembers;
367
361
  const canDeleteChat = isModerator && activeRoom?.type !== 'private';
368
362
  const hasIcon = !!activeRoom?.icon;
369
363
 
@@ -379,10 +373,19 @@ const ChatProfileModal: React.FC<ChatProfileModalProps> = ({
379
373
  collapsedForSearchRef.current = true;
380
374
  scrollHeaderTo(target);
381
375
  }
376
+ setActiveTab('members');
382
377
  setIsSearchOpen(true);
383
378
  setTimeout(() => searchInputRef.current?.focus(), 50);
384
379
  };
385
380
 
381
+ // Search looks through this chat's messages, like the room list's search
382
+ // does across chats. Where message search is off it falls back to the
383
+ // member search it used to be; members keep their own magnifier either way.
384
+ const openSearch = () =>
385
+ isMessageSearchEnabled(config)
386
+ ? dispatch(setActiveModal(MODAL_TYPES.CHAT_MESSAGE_SEARCH))
387
+ : openMemberSearch();
388
+
386
389
  const restoreHeader = () => {
387
390
  if (!collapsedForSearchRef.current) {return;}
388
391
  collapsedForSearchRef.current = false;
@@ -432,7 +435,7 @@ const ChatProfileModal: React.FC<ChatProfileModalProps> = ({
432
435
  key: 'search',
433
436
  label: t('action.search'),
434
437
  icon: (color: string) => <SearchIcon color={color} />,
435
- onPress: openMemberSearch,
438
+ onPress: openSearch,
436
439
  },
437
440
  {
438
441
  key: 'leave',
@@ -478,7 +481,7 @@ const ChatProfileModal: React.FC<ChatProfileModalProps> = ({
478
481
  key: 'search',
479
482
  label: t('action.search'),
480
483
  icon: <SearchIcon color={theme.text} width={18} height={18} />,
481
- onPress: openMemberSearch,
484
+ onPress: openSearch,
482
485
  });
483
486
  }
484
487
  items.push({
@@ -593,24 +596,61 @@ const ChatProfileModal: React.FC<ChatProfileModalProps> = ({
593
596
  </View>
594
597
  )}
595
598
 
596
- {isMessageSearchEnabled(config) && (
597
- <View style={styles.card}>
598
- <TouchableOpacity
599
- testID="chat-profile-search-messages"
600
- activeOpacity={0.7}
601
- style={styles.addMembersRow}
602
- onPress={() => dispatch(setActiveModal(MODAL_TYPES.MESSAGE_SEARCH))}
603
- >
604
- <SearchIcon color={getIconColor(config)} />
605
- <Text style={styles.addMembersLabel}>
606
- {t('search.messages.title')}
607
- </Text>
608
- </TouchableOpacity>
609
- </View>
610
- )}
611
599
 
612
- {!config?.disableChatInfo?.hideMembers && (
613
- <View style={styles.card}>
600
+ <View style={styles.card}>
601
+ {showMembers ? (
602
+ <View style={styles.tabsRow}>
603
+ {(['members', 'media'] as const).map((tab) => {
604
+ const active = tab === activeTab;
605
+ return (
606
+ <TouchableOpacity
607
+ key={tab}
608
+ testID={`chat-profile-tab-${tab}`}
609
+ activeOpacity={0.7}
610
+ style={[
611
+ styles.tab,
612
+ active && { borderBottomColor: getIconColor(config) },
613
+ ]}
614
+ onPress={() => setActiveTab(tab)}
615
+ >
616
+ <Text
617
+ style={[
618
+ styles.tabLabel,
619
+ getElementFont(config, 'profileSectionLabel'),
620
+ active && { color: getIconColor(config) },
621
+ ]}
622
+ >
623
+ {tab === 'members'
624
+ ? t('modal.chatProfile.membersLabel', { count: memberCount })
625
+ : t('modal.chatProfile.media')}
626
+ </Text>
627
+ </TouchableOpacity>
628
+ );
629
+ })}
630
+ {activeTab === 'members' && !isSearchOpen && (
631
+ <TouchableOpacity
632
+ testID="chat-profile-member-search-open"
633
+ accessibilityRole="button"
634
+ accessibilityLabel={t('search.members')}
635
+ activeOpacity={0.7}
636
+ hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}
637
+ style={styles.tabsSearch}
638
+ onPress={openMemberSearch}
639
+ >
640
+ <SearchIcon color={theme.textSecondary} width={18} height={18} />
641
+ </TouchableOpacity>
642
+ )}
643
+ </View>
644
+ ) : (
645
+ <Text style={[styles.cardLabel, getElementFont(config, 'profileSectionLabel')]}>
646
+ {t('modal.chatProfile.media')}
647
+ </Text>
648
+ )}
649
+
650
+ {!showMembers || activeTab === 'media' ? (
651
+ <ChatMediaGrid roomJid={activeRoom.jid} onOpen={setViewerFile} />
652
+ ) : (
653
+ <>
614
654
  {canAddMembers && (
615
655
  <SelectUsersModal
616
656
  trigger={(open) => (
@@ -629,15 +669,6 @@ const ChatProfileModal: React.FC<ChatProfileModalProps> = ({
629
669
  />
630
670
  )}
631
671
 
632
- <Text
633
- style={[
634
- styles.cardLabel,
635
- getElementFont(config, 'profileSectionLabel'),
636
- ]}
637
- >
638
- {t('modal.chatProfile.membersLabel', { count: memberCount })}
639
- </Text>
640
-
641
672
  {isSearchOpen && (
642
673
  <View style={styles.searchRow}>
643
674
  <SearchIcon color={theme.textSecondary} width={18} height={18} />
@@ -773,8 +804,9 @@ const ChatProfileModal: React.FC<ChatProfileModalProps> = ({
773
804
  </Text>
774
805
  </TouchableOpacity>
775
806
  )}
776
- </View>
777
- )}
807
+ </>
808
+ )}
809
+ </View>
778
810
  </View>
779
811
  </Animated.ScrollView>
780
812
 
@@ -798,6 +830,24 @@ const ChatProfileModal: React.FC<ChatProfileModalProps> = ({
798
830
  visible={isReportOpen}
799
831
  onClose={() => setIsReportOpen(false)}
800
832
  />
833
+ <LeaveChatModal
834
+ roomJid={isLeaveOpen && activeRoom?.jid ? activeRoom.jid : null}
835
+ onClose={() => setIsLeaveOpen(false)}
836
+ onLeft={() => handleCloseModal?.()}
837
+ />
838
+ {viewerFile && (
839
+ <MediaViewer
840
+ file={viewerFile}
841
+ onClose={() => setViewerFile(null)}
842
+ onPage={(index) => {
843
+ const next = viewerFile.siblings?.[index];
844
+ if (next) {setViewerFile({ ...next, siblings: viewerFile.siblings, index });}
845
+ }}
846
+ onSave={() => saveFile(viewerFile)}
847
+ // Reply opens the thread in the chat underneath this screen.
848
+ onReplyOpened={() => handleCloseModal?.()}
849
+ />
850
+ )}
801
851
  </ModalContainerFullScreen>
802
852
  );
803
853
  };
@@ -818,6 +868,27 @@ const createStyles = (theme: ChatTheme) => StyleSheet.create({
818
868
  padding: 12,
819
869
  gap: 12,
820
870
  },
871
+ tabsRow: {
872
+ flexDirection: 'row',
873
+ gap: 20,
874
+ borderBottomWidth: StyleSheet.hairlineWidth,
875
+ borderBottomColor: theme.border,
876
+ marginBottom: 12,
877
+ },
878
+ tabsSearch: {
879
+ marginLeft: 'auto',
880
+ paddingBottom: 10,
881
+ },
882
+ tab: {
883
+ paddingBottom: 10,
884
+ borderBottomWidth: 2,
885
+ borderBottomColor: 'transparent',
886
+ },
887
+ tabLabel: {
888
+ fontSize: 15,
889
+ fontWeight: '500',
890
+ color: theme.textSecondary,
891
+ },
821
892
  card: {
822
893
  backgroundColor: theme.surface,
823
894
  borderRadius: 14,
@@ -0,0 +1,191 @@
1
+ /** @format */
2
+
3
+ import React, { useMemo, useState } from 'react';
4
+ import {
5
+ ActivityIndicator,
6
+ Alert,
7
+ Modal,
8
+ StyleSheet,
9
+ Text,
10
+ TouchableOpacity,
11
+ View,
12
+ } from 'react-native';
13
+ import { useSelector } from 'react-redux';
14
+ import type { RootState } from '../../../roomStore';
15
+ import { ProfileImagePlaceholder } from '../../MainComponents/ProfileImagePlaceholder';
16
+ import { useLeaveRoom } from '../../../hooks/useLeaveRoom';
17
+ import { useFileToken } from '../../../hooks/useFileToken';
18
+ import { appendFileToken } from '../../../helpers/secureFileUrl';
19
+ import { useTheme } from '../../../hooks/useTheme';
20
+ import { useT } from '../../../i18n/useT';
21
+ import type { ChatTheme } from '../../../theme/theme';
22
+
23
+ interface LeaveChatModalProps {
24
+ /** The room to leave; the modal is shown while this is set. */
25
+ roomJid: string | null;
26
+ onClose: () => void;
27
+ /** Called after the server took us out, before the modal closes. */
28
+ onLeft?: (jid: string) => void;
29
+ }
30
+
31
+ /**
32
+ * Telegram-style confirmation in front of every Leave: the chat's avatar and
33
+ * name, the question, Cancel and a red Leave. The leave itself runs here, so
34
+ * the card stays up with a spinner until the server answers and a failure is
35
+ * reported over it instead of after it has gone.
36
+ */
37
+ const LeaveChatModal: React.FC<LeaveChatModalProps> = ({
38
+ roomJid,
39
+ onClose,
40
+ onLeft,
41
+ }) => {
42
+ const t = useT();
43
+ const theme = useTheme();
44
+ const styles = useMemo(() => createStyles(theme), [theme]);
45
+ const fileToken = useFileToken();
46
+ const leaveRoom = useLeaveRoom();
47
+ const room = useSelector((state: RootState) =>
48
+ roomJid ? state.rooms.rooms?.[roomJid] : undefined
49
+ );
50
+ const [leaving, setLeaving] = useState(false);
51
+
52
+ const name = room?.title || room?.name || '';
53
+
54
+ const close = () => {
55
+ if (!leaving) {onClose();}
56
+ };
57
+
58
+ const confirm = async () => {
59
+ if (!roomJid || leaving) {return;}
60
+ setLeaving(true);
61
+ const ok = await leaveRoom(roomJid);
62
+ setLeaving(false);
63
+ if (!ok) {
64
+ Alert.alert(t('toast.error'), t('toast.leaveFailed'));
65
+ return;
66
+ }
67
+ onLeft?.(roomJid);
68
+ onClose();
69
+ };
70
+
71
+ return (
72
+ <Modal
73
+ transparent
74
+ visible={roomJid !== null}
75
+ animationType="fade"
76
+ onRequestClose={close}
77
+ >
78
+ <View style={styles.backdrop}>
79
+ <TouchableOpacity
80
+ testID="leave-backdrop"
81
+ style={StyleSheet.absoluteFill}
82
+ activeOpacity={1}
83
+ onPress={close}
84
+ />
85
+ <View style={styles.card}>
86
+ <View style={styles.header}>
87
+ <ProfileImagePlaceholder
88
+ name={name}
89
+ size={40}
90
+ icon={appendFileToken(room?.icon, fileToken)}
91
+ disableOverlay
92
+ />
93
+ <Text style={styles.title} numberOfLines={1}>
94
+ {t('modal.leaveChat.title')}
95
+ </Text>
96
+ </View>
97
+ <Text style={styles.description}>
98
+ {name
99
+ ? t('modal.leaveChat.descriptionNamed', { name })
100
+ : t('modal.leaveChat.description')}
101
+ </Text>
102
+ <View style={styles.buttons}>
103
+ <TouchableOpacity
104
+ testID="leave-cancel"
105
+ activeOpacity={0.7}
106
+ disabled={leaving}
107
+ style={[styles.button, styles.cancel]}
108
+ onPress={close}
109
+ >
110
+ <Text style={styles.cancelLabel}>{t('action.cancel')}</Text>
111
+ </TouchableOpacity>
112
+ <TouchableOpacity
113
+ testID="leave-confirm"
114
+ activeOpacity={0.7}
115
+ disabled={leaving}
116
+ style={[styles.button, { backgroundColor: theme.danger }]}
117
+ onPress={confirm}
118
+ >
119
+ {leaving ? (
120
+ <ActivityIndicator color={theme.textOnPrimary} />
121
+ ) : (
122
+ <Text style={styles.leaveLabel}>{t('action.leave')}</Text>
123
+ )}
124
+ </TouchableOpacity>
125
+ </View>
126
+ </View>
127
+ </View>
128
+ </Modal>
129
+ );
130
+ };
131
+
132
+ const createStyles = (theme: ChatTheme) => StyleSheet.create({
133
+ backdrop: {
134
+ flex: 1,
135
+ backgroundColor: theme.overlay,
136
+ alignItems: 'center',
137
+ justifyContent: 'center',
138
+ padding: 24,
139
+ },
140
+ card: {
141
+ width: '100%',
142
+ maxWidth: 400,
143
+ borderRadius: 16,
144
+ backgroundColor: theme.surface,
145
+ padding: 20,
146
+ },
147
+ header: {
148
+ flexDirection: 'row',
149
+ alignItems: 'center',
150
+ gap: 12,
151
+ marginBottom: 12,
152
+ },
153
+ title: {
154
+ flex: 1,
155
+ fontSize: 18,
156
+ fontWeight: '600',
157
+ color: theme.text,
158
+ },
159
+ description: {
160
+ fontSize: 15,
161
+ lineHeight: 21,
162
+ color: theme.text,
163
+ },
164
+ buttons: {
165
+ flexDirection: 'row',
166
+ gap: 12,
167
+ marginTop: 20,
168
+ },
169
+ button: {
170
+ flex: 1,
171
+ height: 44,
172
+ borderRadius: 12,
173
+ alignItems: 'center',
174
+ justifyContent: 'center',
175
+ },
176
+ cancel: {
177
+ backgroundColor: theme.surfaceSecondary,
178
+ },
179
+ cancelLabel: {
180
+ fontSize: 16,
181
+ fontWeight: '500',
182
+ color: theme.text,
183
+ },
184
+ leaveLabel: {
185
+ fontSize: 16,
186
+ fontWeight: '600',
187
+ color: theme.textOnPrimary,
188
+ },
189
+ });
190
+
191
+ export default LeaveChatModal;