@ethora/chat-component-rn 26.10.8 → 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.
- package/CHANGELOG.md +7 -0
- package/README.md +1 -1
- package/lib/commonjs/components/MainComponents/AlbumGrid.js +6 -2
- package/lib/commonjs/components/MainComponents/AlbumGrid.js.map +1 -1
- package/lib/commonjs/components/MainComponents/ChatHeader.js +16 -11
- package/lib/commonjs/components/MainComponents/ChatHeader.js.map +1 -1
- package/lib/commonjs/components/MainComponents/MediaMessage.js +13 -0
- package/lib/commonjs/components/MainComponents/MediaMessage.js.map +1 -1
- package/lib/commonjs/components/MainComponents/RoomList.js +7 -16
- package/lib/commonjs/components/MainComponents/RoomList.js.map +1 -1
- package/lib/commonjs/components/Menu/HeaderRoomListMenu.js +3 -3
- package/lib/commonjs/components/Menu/HeaderRoomListMenu.js.map +1 -1
- package/lib/commonjs/components/MessageBubble/Message.js +27 -1
- package/lib/commonjs/components/MessageBubble/Message.js.map +1 -1
- package/lib/commonjs/components/Modals/ChatProfileModal/ChatMediaGrid.js +159 -0
- package/lib/commonjs/components/Modals/ChatProfileModal/ChatMediaGrid.js.map +1 -0
- package/lib/commonjs/components/Modals/ChatProfileModal/ChatProfileModal.js +203 -134
- package/lib/commonjs/components/Modals/ChatProfileModal/ChatProfileModal.js.map +1 -1
- package/lib/commonjs/components/Modals/ChatProfileModal/LeaveChatModal.js +180 -0
- package/lib/commonjs/components/Modals/ChatProfileModal/LeaveChatModal.js.map +1 -0
- package/lib/commonjs/components/Modals/FilePreviewModal/FilePreviewModal.js +100 -162
- package/lib/commonjs/components/Modals/FilePreviewModal/FilePreviewModal.js.map +1 -1
- package/lib/commonjs/components/Modals/FilePreviewModal/MediaViewer.js +824 -0
- package/lib/commonjs/components/Modals/FilePreviewModal/MediaViewer.js.map +1 -0
- package/lib/commonjs/components/Modals/FilePreviewModal/useSaveFile.js +179 -0
- package/lib/commonjs/components/Modals/FilePreviewModal/useSaveFile.js.map +1 -0
- package/lib/commonjs/components/Modals/MessageSearchModal/MessageSearchModal.js +22 -5
- package/lib/commonjs/components/Modals/MessageSearchModal/MessageSearchModal.js.map +1 -1
- package/lib/commonjs/components/VideoCalls/useCallKeep.js +1 -3
- package/lib/commonjs/components/VideoCalls/useCallKeep.js.map +1 -1
- package/lib/commonjs/components/styled/MessageImage.js +4 -1
- package/lib/commonjs/components/styled/MessageImage.js.map +1 -1
- package/lib/commonjs/components/styled/VideoMessage.js +5 -1
- package/lib/commonjs/components/styled/VideoMessage.js.map +1 -1
- package/lib/commonjs/context/MediaSourceContext.js +18 -0
- package/lib/commonjs/context/MediaSourceContext.js.map +1 -0
- package/lib/commonjs/helpers/constants/MODAL_COMPONENTS.js +3 -1
- package/lib/commonjs/helpers/constants/MODAL_COMPONENTS.js.map +1 -1
- package/lib/commonjs/helpers/constants/MODAL_TYPES.js +2 -0
- package/lib/commonjs/helpers/constants/MODAL_TYPES.js.map +1 -1
- package/lib/commonjs/helpers/publicChatsEntry.js +8 -6
- package/lib/commonjs/helpers/publicChatsEntry.js.map +1 -1
- package/lib/commonjs/helpers/roomMedia.js +118 -0
- package/lib/commonjs/helpers/roomMedia.js.map +1 -0
- package/lib/commonjs/i18n/strings.js +36 -0
- package/lib/commonjs/i18n/strings.js.map +1 -1
- package/lib/commonjs/networking/api-requests/rooms.api.js +9 -13
- package/lib/commonjs/networking/api-requests/rooms.api.js.map +1 -1
- package/lib/commonjs/networking/callTokenStanza.js +13 -1
- package/lib/commonjs/networking/callTokenStanza.js.map +1 -1
- package/lib/commonjs/push/sealedPushTask.js +20 -1
- package/lib/commonjs/push/sealedPushTask.js.map +1 -1
- package/lib/commonjs/version.js +1 -1
- package/lib/commonjs/version.js.map +1 -1
- package/lib/module/components/MainComponents/AlbumGrid.js +6 -2
- package/lib/module/components/MainComponents/AlbumGrid.js.map +1 -1
- package/lib/module/components/MainComponents/ChatHeader.js +18 -13
- package/lib/module/components/MainComponents/ChatHeader.js.map +1 -1
- package/lib/module/components/MainComponents/MediaMessage.js +13 -0
- package/lib/module/components/MainComponents/MediaMessage.js.map +1 -1
- package/lib/module/components/MainComponents/RoomList.js +7 -16
- package/lib/module/components/MainComponents/RoomList.js.map +1 -1
- package/lib/module/components/Menu/HeaderRoomListMenu.js +3 -3
- package/lib/module/components/Menu/HeaderRoomListMenu.js.map +1 -1
- package/lib/module/components/MessageBubble/Message.js +27 -1
- package/lib/module/components/MessageBubble/Message.js.map +1 -1
- package/lib/module/components/Modals/ChatProfileModal/ChatMediaGrid.js +154 -0
- package/lib/module/components/Modals/ChatProfileModal/ChatMediaGrid.js.map +1 -0
- package/lib/module/components/Modals/ChatProfileModal/ChatProfileModal.js +204 -136
- package/lib/module/components/Modals/ChatProfileModal/ChatProfileModal.js.map +1 -1
- package/lib/module/components/Modals/ChatProfileModal/LeaveChatModal.js +175 -0
- package/lib/module/components/Modals/ChatProfileModal/LeaveChatModal.js.map +1 -0
- package/lib/module/components/Modals/FilePreviewModal/FilePreviewModal.js +100 -163
- package/lib/module/components/Modals/FilePreviewModal/FilePreviewModal.js.map +1 -1
- package/lib/module/components/Modals/FilePreviewModal/MediaViewer.js +819 -0
- package/lib/module/components/Modals/FilePreviewModal/MediaViewer.js.map +1 -0
- package/lib/module/components/Modals/FilePreviewModal/useSaveFile.js +172 -0
- package/lib/module/components/Modals/FilePreviewModal/useSaveFile.js.map +1 -0
- package/lib/module/components/Modals/MessageSearchModal/MessageSearchModal.js +20 -4
- package/lib/module/components/Modals/MessageSearchModal/MessageSearchModal.js.map +1 -1
- package/lib/module/components/VideoCalls/useCallKeep.js +2 -4
- package/lib/module/components/VideoCalls/useCallKeep.js.map +1 -1
- package/lib/module/components/styled/MessageImage.js +4 -1
- package/lib/module/components/styled/MessageImage.js.map +1 -1
- package/lib/module/components/styled/VideoMessage.js +5 -1
- package/lib/module/components/styled/VideoMessage.js.map +1 -1
- package/lib/module/context/MediaSourceContext.js +13 -0
- package/lib/module/context/MediaSourceContext.js.map +1 -0
- package/lib/module/helpers/constants/MODAL_COMPONENTS.js +2 -1
- package/lib/module/helpers/constants/MODAL_COMPONENTS.js.map +1 -1
- package/lib/module/helpers/constants/MODAL_TYPES.js +2 -0
- package/lib/module/helpers/constants/MODAL_TYPES.js.map +1 -1
- package/lib/module/helpers/publicChatsEntry.js +8 -6
- package/lib/module/helpers/publicChatsEntry.js.map +1 -1
- package/lib/module/helpers/roomMedia.js +112 -0
- package/lib/module/helpers/roomMedia.js.map +1 -0
- package/lib/module/i18n/strings.js +36 -0
- package/lib/module/i18n/strings.js.map +1 -1
- package/lib/module/networking/api-requests/rooms.api.js +9 -13
- package/lib/module/networking/api-requests/rooms.api.js.map +1 -1
- package/lib/module/networking/callTokenStanza.js +13 -1
- package/lib/module/networking/callTokenStanza.js.map +1 -1
- package/lib/module/push/sealedPushTask.js +20 -1
- package/lib/module/push/sealedPushTask.js.map +1 -1
- package/lib/module/version.js +1 -1
- package/lib/module/version.js.map +1 -1
- package/lib/typescript/components/MainComponents/AlbumGrid.d.ts.map +1 -1
- package/lib/typescript/components/MainComponents/ChatHeader.d.ts.map +1 -1
- package/lib/typescript/components/MainComponents/MediaMessage.d.ts +3 -1
- package/lib/typescript/components/MainComponents/MediaMessage.d.ts.map +1 -1
- package/lib/typescript/components/MainComponents/RoomList.d.ts.map +1 -1
- package/lib/typescript/components/Menu/HeaderRoomListMenu.d.ts.map +1 -1
- package/lib/typescript/components/MessageBubble/Message.d.ts.map +1 -1
- package/lib/typescript/components/Modals/ChatProfileModal/ChatMediaGrid.d.ts +16 -0
- package/lib/typescript/components/Modals/ChatProfileModal/ChatMediaGrid.d.ts.map +1 -0
- package/lib/typescript/components/Modals/ChatProfileModal/ChatProfileModal.d.ts.map +1 -1
- package/lib/typescript/components/Modals/ChatProfileModal/LeaveChatModal.d.ts +18 -0
- package/lib/typescript/components/Modals/ChatProfileModal/LeaveChatModal.d.ts.map +1 -0
- package/lib/typescript/components/Modals/FilePreviewModal/FilePreviewModal.d.ts +16 -0
- package/lib/typescript/components/Modals/FilePreviewModal/FilePreviewModal.d.ts.map +1 -1
- package/lib/typescript/components/Modals/FilePreviewModal/MediaViewer.d.ts +27 -0
- package/lib/typescript/components/Modals/FilePreviewModal/MediaViewer.d.ts.map +1 -0
- package/lib/typescript/components/Modals/FilePreviewModal/useSaveFile.d.ts +9 -0
- package/lib/typescript/components/Modals/FilePreviewModal/useSaveFile.d.ts.map +1 -0
- package/lib/typescript/components/Modals/MessageSearchModal/MessageSearchModal.d.ts +8 -0
- package/lib/typescript/components/Modals/MessageSearchModal/MessageSearchModal.d.ts.map +1 -1
- package/lib/typescript/components/VideoCalls/useCallKeep.d.ts.map +1 -1
- package/lib/typescript/components/styled/MessageImage.d.ts.map +1 -1
- package/lib/typescript/components/styled/VideoMessage.d.ts.map +1 -1
- package/lib/typescript/context/MediaSourceContext.d.ts +4 -0
- package/lib/typescript/context/MediaSourceContext.d.ts.map +1 -0
- package/lib/typescript/helpers/constants/MODAL_COMPONENTS.d.ts.map +1 -1
- package/lib/typescript/helpers/constants/MODAL_TYPES.d.ts +2 -0
- package/lib/typescript/helpers/constants/MODAL_TYPES.d.ts.map +1 -1
- package/lib/typescript/helpers/publicChatsEntry.d.ts +7 -5
- package/lib/typescript/helpers/publicChatsEntry.d.ts.map +1 -1
- package/lib/typescript/helpers/roomMedia.d.ts +26 -0
- package/lib/typescript/helpers/roomMedia.d.ts.map +1 -0
- package/lib/typescript/i18n/strings.d.ts.map +1 -1
- package/lib/typescript/main.d.ts +1 -1
- package/lib/typescript/main.d.ts.map +1 -1
- package/lib/typescript/networking/api-requests/rooms.api.d.ts +6 -5
- package/lib/typescript/networking/api-requests/rooms.api.d.ts.map +1 -1
- package/lib/typescript/networking/callTokenStanza.d.ts.map +1 -1
- package/lib/typescript/push/sealedPushTask.d.ts +13 -0
- package/lib/typescript/push/sealedPushTask.d.ts.map +1 -1
- package/lib/typescript/types/models/modal.model.d.ts +18 -0
- package/lib/typescript/types/models/modal.model.d.ts.map +1 -1
- package/lib/typescript/types/types.d.ts +18 -0
- package/lib/typescript/types/types.d.ts.map +1 -1
- package/lib/typescript/version.d.ts +1 -1
- package/lib/typescript/version.d.ts.map +1 -1
- package/package.json +9 -2
- package/plugins/notification-service/NotificationService.swift +105 -19
- package/plugins/withNotificationServiceExtension.js +44 -4
- package/src/components/MainComponents/AlbumGrid.tsx +5 -1
- package/src/components/MainComponents/ChatHeader.tsx +16 -11
- package/src/components/MainComponents/MediaMessage.tsx +13 -2
- package/src/components/MainComponents/RoomList.tsx +8 -19
- package/src/components/Menu/HeaderRoomListMenu.tsx +13 -8
- package/src/components/MessageBubble/Message.tsx +24 -1
- package/src/components/Modals/ChatProfileModal/ChatMediaGrid.tsx +174 -0
- package/src/components/Modals/ChatProfileModal/ChatProfileModal.tsx +119 -48
- package/src/components/Modals/ChatProfileModal/LeaveChatModal.tsx +191 -0
- package/src/components/Modals/FilePreviewModal/FilePreviewModal.tsx +115 -185
- package/src/components/Modals/FilePreviewModal/MediaViewer.tsx +880 -0
- package/src/components/Modals/FilePreviewModal/useSaveFile.ts +207 -0
- package/src/components/Modals/MessageSearchModal/MessageSearchModal.tsx +56 -30
- package/src/components/VideoCalls/useCallKeep.ts +2 -4
- package/src/components/styled/MessageImage.tsx +3 -1
- package/src/components/styled/VideoMessage.tsx +5 -1
- package/src/context/MediaSourceContext.tsx +14 -0
- package/src/helpers/constants/MODAL_COMPONENTS.tsx +4 -1
- package/src/helpers/constants/MODAL_TYPES.ts +2 -0
- package/src/helpers/publicChatsEntry.ts +8 -7
- package/src/helpers/roomMedia.ts +126 -0
- package/src/i18n/strings.ts +36 -0
- package/src/main.ts +1 -1
- package/src/networking/api-requests/rooms.api.ts +13 -12
- package/src/networking/callTokenStanza.ts +10 -1
- package/src/push/sealedPushTask.ts +34 -1
- package/src/types/models/modal.model.ts +16 -0
- package/src/types/types.ts +16 -0
- package/src/version.ts +1 -1
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import { useCallback, useRef } from "react";
|
|
2
|
+
import { Alert, Platform, Share } from "react-native";
|
|
3
|
+
import * as FileSystem from "expo-file-system/legacy";
|
|
4
|
+
import type { ModalFile } from "../../../types/types";
|
|
5
|
+
import { withFileToken } from "../../../helpers/secureFileUrl";
|
|
6
|
+
import { getMediaLibrary } from "../../../helpers/mediaLibraryRuntime";
|
|
7
|
+
import { useToast } from "../../../context/ToastContext";
|
|
8
|
+
import { useT } from "../../../i18n/useT";
|
|
9
|
+
import {
|
|
10
|
+
getDisplayFileName,
|
|
11
|
+
getUniqueFileName,
|
|
12
|
+
sanitizeFileNameForPath,
|
|
13
|
+
} from "../../../helpers/getDisplayFileName";
|
|
14
|
+
|
|
15
|
+
export const isLocalFile = (url: string | undefined | null) =>
|
|
16
|
+
/^file:\/\//i.test(url || "");
|
|
17
|
+
|
|
18
|
+
const fetchToCache = async (url: string, cachePath: string) =>
|
|
19
|
+
isLocalFile(url)
|
|
20
|
+
? { status: 200, uri: url }
|
|
21
|
+
: FileSystem.downloadAsync(withFileToken(url), cachePath);
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Save button of the file viewers: pictures and videos to the gallery,
|
|
25
|
+
* anything else to Downloads (Android, through SAF) or the share sheet (iOS).
|
|
26
|
+
* Shared by the preview page and the media viewer opened from a chat profile.
|
|
27
|
+
*/
|
|
28
|
+
export const useSaveFile = () => {
|
|
29
|
+
const { showToast } = useToast();
|
|
30
|
+
const t = useT();
|
|
31
|
+
// Cache the SAF directory the user granted so saving several documents
|
|
32
|
+
// in a row doesn't re-prompt for a folder every time (Android only).
|
|
33
|
+
const safDirUriRef = useRef<string | null>(null);
|
|
34
|
+
|
|
35
|
+
return useCallback(
|
|
36
|
+
async (activeFile: ModalFile) => {
|
|
37
|
+
// Bug #40: prefer the sender's original name over the server's stored
|
|
38
|
+
// hash name when saving/sharing the file. Sanitized so it's safe to use
|
|
39
|
+
// as an actual path segment (strips path separators/control chars).
|
|
40
|
+
const displayFileName = sanitizeFileNameForPath(
|
|
41
|
+
getDisplayFileName({
|
|
42
|
+
originalName: activeFile.originalName,
|
|
43
|
+
fileName: activeFile.fileName,
|
|
44
|
+
location: activeFile.fileURL,
|
|
45
|
+
mimetype: activeFile.mimetype,
|
|
46
|
+
})
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
const requestStoragePermission = async () => {
|
|
50
|
+
const MediaLibrary = getMediaLibrary();
|
|
51
|
+
if (!MediaLibrary?.requestPermissionsAsync) {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
try {
|
|
55
|
+
const { status } = await MediaLibrary.requestPermissionsAsync();
|
|
56
|
+
return status === "granted";
|
|
57
|
+
} catch (error) {
|
|
58
|
+
console.error("Error requesting permission:", error);
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const saveToGallery = async () => {
|
|
64
|
+
const hasPermission = await requestStoragePermission();
|
|
65
|
+
if (!hasPermission) {
|
|
66
|
+
Alert.alert(
|
|
67
|
+
"Permission Denied",
|
|
68
|
+
"Storage permission is required to save files to the gallery."
|
|
69
|
+
);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
try {
|
|
74
|
+
const filePath = FileSystem.cacheDirectory + displayFileName;
|
|
75
|
+
const download = await fetchToCache(activeFile.fileURL, filePath);
|
|
76
|
+
|
|
77
|
+
if (download.status === 200) {
|
|
78
|
+
await getMediaLibrary()?.saveToLibraryAsync(download.uri);
|
|
79
|
+
showToast({
|
|
80
|
+
id: Date.now().toString(),
|
|
81
|
+
title: t("toast.successTitle"),
|
|
82
|
+
message: t("toast.saveSuccessful"),
|
|
83
|
+
type: "success",
|
|
84
|
+
});
|
|
85
|
+
} else {
|
|
86
|
+
Alert.alert("Error", "Failed to save the file.");
|
|
87
|
+
}
|
|
88
|
+
} catch (err) {
|
|
89
|
+
Alert.alert("Error", `Failed to save the file: ${displayFileName}`);
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
// Best-effort existing-name lookup for the Android SAF branch below: SAF
|
|
94
|
+
// hands back content URIs, not plain names, so this decodes each one and
|
|
95
|
+
// takes its last path segment. If the provider's URI shape doesn't match
|
|
96
|
+
// what we expect, we fail open (empty set) rather than block the save -
|
|
97
|
+
// worst case we skip de-duplication, we never crash the save flow.
|
|
98
|
+
const listSafDirectoryNames = async (
|
|
99
|
+
saf: any,
|
|
100
|
+
dirUri: string
|
|
101
|
+
): Promise<Set<string>> => {
|
|
102
|
+
try {
|
|
103
|
+
const uris: string[] = await saf.readDirectoryAsync(dirUri);
|
|
104
|
+
const names = uris.map((uri) => {
|
|
105
|
+
try {
|
|
106
|
+
const decoded = decodeURIComponent(uri);
|
|
107
|
+
const last = decoded.split("/").pop() || "";
|
|
108
|
+
return last.includes(":") ? last.split(":").pop() || "" : last;
|
|
109
|
+
} catch {
|
|
110
|
+
return "";
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
return new Set(names.filter(Boolean));
|
|
114
|
+
} catch {
|
|
115
|
+
return new Set<string>();
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
const saveFileToDownloads = async () => {
|
|
120
|
+
try {
|
|
121
|
+
const filePath = FileSystem.cacheDirectory + displayFileName;
|
|
122
|
+
const download = await fetchToCache(activeFile.fileURL, filePath);
|
|
123
|
+
|
|
124
|
+
if (download.status !== 200) {
|
|
125
|
+
Alert.alert("Error", "Failed to save the file.");
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (Platform.OS === "android") {
|
|
130
|
+
const saf = (FileSystem as any).StorageAccessFramework;
|
|
131
|
+
let dirUri = safDirUriRef.current;
|
|
132
|
+
if (!dirUri) {
|
|
133
|
+
const perm = await saf.requestDirectoryPermissionsAsync();
|
|
134
|
+
if (!perm.granted) {
|
|
135
|
+
Alert.alert(
|
|
136
|
+
"Permission Denied",
|
|
137
|
+
"Storage permission is required to save the file."
|
|
138
|
+
);
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
dirUri = perm.directoryUri;
|
|
142
|
+
safDirUriRef.current = dirUri;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Avoid silently overwriting a different attachment that happens
|
|
146
|
+
// to share the same display name (common now that we prefer the
|
|
147
|
+
// human-picked name over the unique server hash - bug #40) by
|
|
148
|
+
// suffixing " (1)", " (2)", ... on a collision.
|
|
149
|
+
const existingNames = await listSafDirectoryNames(
|
|
150
|
+
saf,
|
|
151
|
+
dirUri as string
|
|
152
|
+
);
|
|
153
|
+
const uniqueName = await getUniqueFileName(
|
|
154
|
+
displayFileName,
|
|
155
|
+
async (candidate) => existingNames.has(candidate)
|
|
156
|
+
);
|
|
157
|
+
const baseName = uniqueName.replace(/\.[^/.]+$/, "");
|
|
158
|
+
const mime = activeFile.mimetype || "application/octet-stream";
|
|
159
|
+
const base64 = await FileSystem.readAsStringAsync(download.uri, {
|
|
160
|
+
encoding: FileSystem.EncodingType.Base64,
|
|
161
|
+
});
|
|
162
|
+
const destUri = await saf.createFileAsync(dirUri, baseName, mime);
|
|
163
|
+
await FileSystem.writeAsStringAsync(destUri, base64, {
|
|
164
|
+
encoding: FileSystem.EncodingType.Base64,
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
showToast({
|
|
168
|
+
id: Date.now().toString(),
|
|
169
|
+
title: t("toast.successTitle"),
|
|
170
|
+
message: t("toast.saveSuccessful"),
|
|
171
|
+
type: "success",
|
|
172
|
+
});
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// iOS (and any non-Android): there is no app-writable "Downloads"
|
|
177
|
+
// folder, and MediaLibrary only accepts images/videos — which is why
|
|
178
|
+
// documents previously hit the success toast without ever being
|
|
179
|
+
// saved. Present the system share sheet so the user can "Save to
|
|
180
|
+
// Files", AirDrop, etc. The sheet itself is the confirmation (the
|
|
181
|
+
// user may cancel), so we don't show a "Save successful" toast here.
|
|
182
|
+
await Share.share({
|
|
183
|
+
url: download.uri,
|
|
184
|
+
title: displayFileName,
|
|
185
|
+
});
|
|
186
|
+
} catch (err) {
|
|
187
|
+
console.error("Error saving file:", err);
|
|
188
|
+
Alert.alert("Error", "Failed to save the file.");
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
const saveClick = async () => {
|
|
193
|
+
if (
|
|
194
|
+
activeFile.mimetype.startsWith("image/") ||
|
|
195
|
+
activeFile.mimetype.startsWith("video/")
|
|
196
|
+
) {
|
|
197
|
+
await saveToGallery();
|
|
198
|
+
} else {
|
|
199
|
+
await saveFileToDownloads();
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
await saveClick();
|
|
204
|
+
},
|
|
205
|
+
[showToast, t]
|
|
206
|
+
);
|
|
207
|
+
};
|
|
@@ -46,13 +46,18 @@ import {
|
|
|
46
46
|
useMessageSearch,
|
|
47
47
|
} from './useMessageSearch';
|
|
48
48
|
import { openSearchHit } from './openHit';
|
|
49
|
+
import { setActiveModal } from '../../../roomStore/chatSettingsSlice';
|
|
50
|
+
import { MODAL_TYPES } from '../../../helpers/constants/MODAL_TYPES';
|
|
49
51
|
|
|
50
52
|
interface MessageSearchModalProps {
|
|
51
53
|
handleCloseModal: () => void;
|
|
54
|
+
/** Search the open chat only: no "All chats" scope to switch to. */
|
|
55
|
+
chatOnly?: boolean;
|
|
52
56
|
}
|
|
53
57
|
|
|
54
58
|
const MessageSearchModalContent: React.FC<MessageSearchModalProps> = ({
|
|
55
59
|
handleCloseModal,
|
|
60
|
+
chatOnly = false,
|
|
56
61
|
}) => {
|
|
57
62
|
const t = useT();
|
|
58
63
|
const dispatch = useDispatch();
|
|
@@ -71,7 +76,7 @@ const MessageSearchModalContent: React.FC<MessageSearchModalProps> = ({
|
|
|
71
76
|
const [query, setQuery] = useState('');
|
|
72
77
|
const roomName = activeRoomJID ? activeRoomJID.split('@')[0] : undefined;
|
|
73
78
|
const [scope, setScope] = useState<SearchScope>(
|
|
74
|
-
activeRoomJID ? 'chat' : 'all'
|
|
79
|
+
chatOnly || activeRoomJID ? 'chat' : 'all'
|
|
75
80
|
);
|
|
76
81
|
|
|
77
82
|
const [filtersOpen, setFiltersOpen] = useState(false);
|
|
@@ -182,34 +187,36 @@ const MessageSearchModalContent: React.FC<MessageSearchModalProps> = ({
|
|
|
182
187
|
/>
|
|
183
188
|
</View>
|
|
184
189
|
|
|
185
|
-
|
|
186
|
-
{(
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
{
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
190
|
+
{!chatOnly && (
|
|
191
|
+
<View style={styles.scopeRow} accessibilityLabel={t('search.messages.scope')}>
|
|
192
|
+
{(['chat', 'all'] as SearchScope[]).map((value) => {
|
|
193
|
+
const active = scope === value;
|
|
194
|
+
const disabled = value === 'chat' && !roomName;
|
|
195
|
+
return (
|
|
196
|
+
<TouchableOpacity
|
|
197
|
+
key={value}
|
|
198
|
+
testID={`message-search-scope-${value}`}
|
|
199
|
+
disabled={disabled}
|
|
200
|
+
onPress={() => setScope(value)}
|
|
201
|
+
accessibilityState={{ selected: active, disabled }}
|
|
202
|
+
style={[
|
|
203
|
+
styles.scopeButton,
|
|
204
|
+
active && { borderColor: primary, backgroundColor: primary + '1F' },
|
|
205
|
+
disabled && styles.hitDisabled,
|
|
206
|
+
]}
|
|
207
|
+
>
|
|
208
|
+
<Text style={[styles.scopeLabel, active && { color: primary }]}>
|
|
209
|
+
{t(
|
|
210
|
+
value === 'chat'
|
|
211
|
+
? 'search.messages.scopeChat'
|
|
212
|
+
: 'search.messages.scopeAll'
|
|
213
|
+
)}
|
|
214
|
+
</Text>
|
|
215
|
+
</TouchableOpacity>
|
|
216
|
+
);
|
|
217
|
+
})}
|
|
218
|
+
</View>
|
|
219
|
+
)}
|
|
213
220
|
|
|
214
221
|
<TouchableOpacity
|
|
215
222
|
testID="message-search-filters-toggle"
|
|
@@ -368,7 +375,11 @@ const MessageSearchModalContent: React.FC<MessageSearchModalProps> = ({
|
|
|
368
375
|
<ModalContainerFullScreen style={styles.screen}>
|
|
369
376
|
<ModalHeaderComponent
|
|
370
377
|
handleCloseModal={handleCloseModal}
|
|
371
|
-
headerTitle={
|
|
378
|
+
headerTitle={
|
|
379
|
+
chatOnly && searchedRoom?.title
|
|
380
|
+
? searchedRoom.title
|
|
381
|
+
: t('search.messages.title')
|
|
382
|
+
}
|
|
372
383
|
/>
|
|
373
384
|
<FlatList
|
|
374
385
|
style={styles.list}
|
|
@@ -484,4 +495,19 @@ const MessageSearchModal: React.FC<MessageSearchModalProps> = (props) => {
|
|
|
484
495
|
return <MessageSearchModalContent {...props} />;
|
|
485
496
|
};
|
|
486
497
|
|
|
498
|
+
/**
|
|
499
|
+
* The chat profile's Search: the same search as the room list's, inside this
|
|
500
|
+
* chat only. Back returns to the profile it was opened from; opening a hit
|
|
501
|
+
* closes both and lands on the message in the chat.
|
|
502
|
+
*/
|
|
503
|
+
export const ChatMessageSearchModal: React.FC<MessageSearchModalProps> = () => {
|
|
504
|
+
const dispatch = useDispatch();
|
|
505
|
+
return (
|
|
506
|
+
<MessageSearchModal
|
|
507
|
+
chatOnly
|
|
508
|
+
handleCloseModal={() => dispatch(setActiveModal(MODAL_TYPES.CHAT_PROFILE))}
|
|
509
|
+
/>
|
|
510
|
+
);
|
|
511
|
+
};
|
|
512
|
+
|
|
487
513
|
export default MessageSearchModal;
|
|
@@ -2,8 +2,7 @@ import { useEffect, useMemo, useRef, useState } from 'react';
|
|
|
2
2
|
import { Platform } from 'react-native';
|
|
3
3
|
import { useDispatch, useSelector } from 'react-redux';
|
|
4
4
|
import { RootState, store } from '../../roomStore';
|
|
5
|
-
import { acceptIncomingCall
|
|
6
|
-
import { sendCallStateSignal } from '../../networking/callTokenStanza';
|
|
5
|
+
import { acceptIncomingCall } from '../../roomStore/callSlice';
|
|
7
6
|
import { declineActiveCallWhenOnline, terminateActiveCall } from './callActions';
|
|
8
7
|
import { setCallMicEnabled, useCallSession } from './callSessionController';
|
|
9
8
|
import { loadLiveKit } from './livekitRuntime';
|
|
@@ -162,8 +161,7 @@ export const useCallKeep = (): void => {
|
|
|
162
161
|
snapshot.direction === 'incoming' &&
|
|
163
162
|
snapshot.phase === 'ringing-incoming'
|
|
164
163
|
) {
|
|
165
|
-
|
|
166
|
-
dispatch(declineIncomingCall());
|
|
164
|
+
declineActiveCallWhenOnline();
|
|
167
165
|
return;
|
|
168
166
|
}
|
|
169
167
|
// Ended from the Dynamic Island, the lock screen or a headset.
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
setActiveModal,
|
|
12
12
|
} from '../../roomStore/chatSettingsSlice';
|
|
13
13
|
import { MODAL_TYPES } from '../../helpers/constants/MODAL_TYPES';
|
|
14
|
+
import { useMediaSource } from '../../context/MediaSourceContext';
|
|
14
15
|
import { ActivityIndicator, Image, TouchableOpacity, View } from 'react-native';
|
|
15
16
|
import {
|
|
16
17
|
defaultMediaDims,
|
|
@@ -72,6 +73,7 @@ const CustomMessageImage: React.FC<CustomMessageImageProps> = ({
|
|
|
72
73
|
const dims: MediaDims = tile ?? measured;
|
|
73
74
|
|
|
74
75
|
const dispatch = useDispatch();
|
|
76
|
+
const source = useMediaSource();
|
|
75
77
|
|
|
76
78
|
useEffect(() => {
|
|
77
79
|
if (!displayURL || tile) {
|
|
@@ -105,7 +107,7 @@ const CustomMessageImage: React.FC<CustomMessageImageProps> = ({
|
|
|
105
107
|
onOpen();
|
|
106
108
|
return;
|
|
107
109
|
}
|
|
108
|
-
dispatch(setActiveFile({ fileName, fileURL, mimetype, originalName }));
|
|
110
|
+
dispatch(setActiveFile({ fileName, fileURL, mimetype, originalName, source }));
|
|
109
111
|
dispatch(setActiveModal(MODAL_TYPES.FILE_PREVIEW));
|
|
110
112
|
};
|
|
111
113
|
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
setActiveModal,
|
|
11
11
|
} from '../../roomStore/chatSettingsSlice';
|
|
12
12
|
import { MODAL_TYPES } from '../../helpers/constants/MODAL_TYPES';
|
|
13
|
+
import { useMediaSource } from '../../context/MediaSourceContext';
|
|
13
14
|
import { PlayIcon } from '../../assets/icons';
|
|
14
15
|
import {
|
|
15
16
|
defaultMediaDims,
|
|
@@ -155,6 +156,7 @@ const CustomMessageVideo: React.FC<CustomMessageVideoProps> = ({
|
|
|
155
156
|
onOpen,
|
|
156
157
|
}) => {
|
|
157
158
|
const dispatch = useDispatch();
|
|
159
|
+
const source = useMediaSource();
|
|
158
160
|
const [posterFailed, setPosterFailed] = useState(false);
|
|
159
161
|
|
|
160
162
|
const handleOpen = () => {
|
|
@@ -162,7 +164,9 @@ const CustomMessageVideo: React.FC<CustomMessageVideoProps> = ({
|
|
|
162
164
|
onOpen();
|
|
163
165
|
return;
|
|
164
166
|
}
|
|
165
|
-
dispatch(
|
|
167
|
+
dispatch(
|
|
168
|
+
setActiveFile({ fileName, fileURL, mimetype, originalName, previewURL, source })
|
|
169
|
+
);
|
|
166
170
|
dispatch(setActiveModal(MODAL_TYPES.FILE_PREVIEW));
|
|
167
171
|
};
|
|
168
172
|
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { createContext, useContext } from 'react';
|
|
2
|
+
import type { ModalFileSource } from '../types/types';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The message a bubble's pictures and videos belong to. MediaMessage provides
|
|
6
|
+
* it, and the image / video / album components that open the viewer read it,
|
|
7
|
+
* so the viewer knows what its Reply and Delete act on without every card in
|
|
8
|
+
* between passing the message along.
|
|
9
|
+
*/
|
|
10
|
+
const MediaSourceContext = createContext<ModalFileSource | undefined>(undefined);
|
|
11
|
+
|
|
12
|
+
export const MediaSourceProvider = MediaSourceContext.Provider;
|
|
13
|
+
|
|
14
|
+
export const useMediaSource = () => useContext(MediaSourceContext);
|
|
@@ -7,7 +7,9 @@ import React from 'react';
|
|
|
7
7
|
import { MODAL_TYPES } from './MODAL_TYPES';
|
|
8
8
|
import ChatProfileModal from '../../components/Modals/ChatProfileModal/ChatProfileModal';
|
|
9
9
|
import FilePreviewModal from '../../components/Modals/FilePreviewModal/FilePreviewModal';
|
|
10
|
-
import MessageSearchModal
|
|
10
|
+
import MessageSearchModal, {
|
|
11
|
+
ChatMessageSearchModal,
|
|
12
|
+
} from '../../components/Modals/MessageSearchModal/MessageSearchModal';
|
|
11
13
|
import PublicChatsModal from '../../components/Modals/PublicChatsModal/PublicChatsModal';
|
|
12
14
|
import NewChatModal from '../../components/Modals/NewChatModal/NewChatModal';
|
|
13
15
|
import BlockedUsersModal from '../../components/Modals/SettingsModals/BlockedUsers/BlockedUsersModal';
|
|
@@ -37,5 +39,6 @@ export const MODAL_COMPONENTS: Record<
|
|
|
37
39
|
[MODAL_TYPES.BLOCKED_USERS]: BlockedUsersModal,
|
|
38
40
|
[MODAL_TYPES.FILE_PREVIEW]: FilePreviewModal,
|
|
39
41
|
[MODAL_TYPES.MESSAGE_SEARCH]: MessageSearchModal,
|
|
42
|
+
[MODAL_TYPES.CHAT_MESSAGE_SEARCH]: ChatMessageSearchModal,
|
|
40
43
|
[MODAL_TYPES.PUBLIC_CHATS]: PublicChatsModal,
|
|
41
44
|
};
|
|
@@ -2,14 +2,15 @@ import type { IConfig } from '../types/types';
|
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* "Discover chats" lives in the room list menu sheet. A host that takes the
|
|
5
|
-
* burger over (`headerMenu` as a function)
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* header
|
|
9
|
-
*
|
|
5
|
+
* burger over (`headerMenu` as a function) would otherwise have no obvious
|
|
6
|
+
* way to reach the directory, so it also gets a button of its own in the
|
|
7
|
+
* room list header. `chatHeaderSettings.disableMenu` does not count: the
|
|
8
|
+
* header avatar still opens the sheet, Discover included, and a second
|
|
9
|
+
* compass next to it was a duplicate. `disableRoomMenu` removes the menu
|
|
10
|
+
* entry point, and with it the button; `disablePublicChatsDirectory`
|
|
11
|
+
* removes the feature altogether.
|
|
10
12
|
*/
|
|
11
13
|
export const showsStandaloneDiscoverButton = (config?: IConfig): boolean =>
|
|
12
14
|
!config?.disablePublicChatsDirectory &&
|
|
13
15
|
!config?.disableRoomMenu &&
|
|
14
|
-
|
|
15
|
-
typeof config?.headerMenu === 'function');
|
|
16
|
+
typeof config?.headerMenu === 'function';
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import type { IMessage, ModalFile } from '../types/types';
|
|
2
|
+
import { getAttachmentName, getMessageAttachments } from './attachments';
|
|
3
|
+
import { getFileKind } from './fileKind';
|
|
4
|
+
import { appendFileToken } from './secureFileUrl';
|
|
5
|
+
import { parseMessageReference } from './parseMessageReference';
|
|
6
|
+
import { composeName } from './displayName';
|
|
7
|
+
import { isOwnMessage } from './isOwnMessage';
|
|
8
|
+
|
|
9
|
+
export interface MediaOptions {
|
|
10
|
+
/** The signed-in user, as isOwnMessage takes it. */
|
|
11
|
+
me: Parameters<typeof isOwnMessage>[1];
|
|
12
|
+
fileToken?: string;
|
|
13
|
+
interactive: boolean;
|
|
14
|
+
repliesOn: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Every picture and video of the room's loaded history, newest first, each
|
|
19
|
+
* carrying the message it came from so the viewer's Reply and Delete act on
|
|
20
|
+
* the right one (same rules as a bubble: Delete on one's own, Reply where
|
|
21
|
+
* replies are on).
|
|
22
|
+
*/
|
|
23
|
+
export const collectRoomMedia = (
|
|
24
|
+
messages: IMessage[] | undefined,
|
|
25
|
+
roomJid: string,
|
|
26
|
+
{ me, fileToken, interactive, repliesOn }: MediaOptions
|
|
27
|
+
): ModalFile[] => {
|
|
28
|
+
const files: ModalFile[] = [];
|
|
29
|
+
for (let i = (messages?.length ?? 0) - 1; i >= 0; i--) {
|
|
30
|
+
const message = messages![i]!;
|
|
31
|
+
if (
|
|
32
|
+
message.isMediafile !== 'true' ||
|
|
33
|
+
message.isDeleted ||
|
|
34
|
+
message.pending ||
|
|
35
|
+
message.clientEncrypted === 'true' ||
|
|
36
|
+
!message.id
|
|
37
|
+
) {
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
const parent = parseMessageReference(message);
|
|
41
|
+
const sentAt = new Date(message.date as any).getTime();
|
|
42
|
+
const source = {
|
|
43
|
+
messageId: message.id,
|
|
44
|
+
roomJid: message.roomJid || roomJid,
|
|
45
|
+
own: interactive && isOwnMessage(message, me),
|
|
46
|
+
replyTo:
|
|
47
|
+
interactive && repliesOn
|
|
48
|
+
? parent
|
|
49
|
+
? { id: parent.id, chatJID: parent.roomJid || message.roomJid || roomJid }
|
|
50
|
+
: { id: message.id, chatJID: message.roomJid || roomJid }
|
|
51
|
+
: undefined,
|
|
52
|
+
senderName:
|
|
53
|
+
composeName(message.user?.firstName, message.user?.lastName) ||
|
|
54
|
+
message.user?.name ||
|
|
55
|
+
undefined,
|
|
56
|
+
timestamp: Number.isFinite(sentAt) ? sentAt : undefined,
|
|
57
|
+
};
|
|
58
|
+
for (const attachment of getMessageAttachments(message)) {
|
|
59
|
+
if (!attachment.location) {continue;}
|
|
60
|
+
const kind = getFileKind({
|
|
61
|
+
mimetype: attachment.mimetype,
|
|
62
|
+
name: getAttachmentName(attachment),
|
|
63
|
+
url: attachment.location,
|
|
64
|
+
duration: attachment.duration,
|
|
65
|
+
});
|
|
66
|
+
if (kind !== 'image' && kind !== 'video') {continue;}
|
|
67
|
+
files.push({
|
|
68
|
+
fileName: getAttachmentName(attachment),
|
|
69
|
+
originalName: attachment.originalName,
|
|
70
|
+
fileURL: appendFileToken(attachment.location, fileToken) || attachment.location,
|
|
71
|
+
mimetype: attachment.mimetype || (kind === 'image' ? 'image/jpeg' : 'video/mp4'),
|
|
72
|
+
duration: attachment.duration,
|
|
73
|
+
previewURL: appendFileToken(attachment.locationPreview, fileToken),
|
|
74
|
+
source,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return files;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const sentAt = (message: IMessage) => new Date(message.date as any).getTime() || 0;
|
|
82
|
+
|
|
83
|
+
/** A thread-only reply: shown in its thread, not in the channel. */
|
|
84
|
+
const threadParentOf = (message: IMessage): string | null =>
|
|
85
|
+
message.mainMessage && message.showInChannel !== 'true'
|
|
86
|
+
? parseMessageReference(message)?.id ?? null
|
|
87
|
+
: null;
|
|
88
|
+
|
|
89
|
+
/** The file's address without its access token, which may differ per copy. */
|
|
90
|
+
const bareUrl = (url: string | undefined) => (url || '').split('?')[0];
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* The media feed the viewer pages through when a picture or video is opened
|
|
94
|
+
* from a bubble: every picture and video of the list that bubble sits in (the
|
|
95
|
+
* channel, or the one thread for a thread-only reply), oldest first so a
|
|
96
|
+
* swipe to the left goes forward in time, the way the chat reads. The opened
|
|
97
|
+
* file comes back with that feed as its siblings; null when the file is not
|
|
98
|
+
* in the loaded history (the caller then keeps what it was opened with).
|
|
99
|
+
*/
|
|
100
|
+
export const withChatSiblings = (
|
|
101
|
+
file: ModalFile,
|
|
102
|
+
messages: IMessage[] | undefined,
|
|
103
|
+
options: MediaOptions
|
|
104
|
+
): ModalFile | null => {
|
|
105
|
+
const source = file.source;
|
|
106
|
+
if (!source || !messages?.length) {return null;}
|
|
107
|
+
const opened = messages.find((m) => String(m.id) === String(source.messageId));
|
|
108
|
+
if (!opened) {return null;}
|
|
109
|
+
|
|
110
|
+
const thread = threadParentOf(opened);
|
|
111
|
+
// collectRoomMedia walks from the end, so newest-first in gives oldest
|
|
112
|
+
// first out, with an album's files still in their own order.
|
|
113
|
+
const scoped = messages
|
|
114
|
+
.filter((m) => threadParentOf(m) === thread)
|
|
115
|
+
.sort((a, b) => sentAt(b) - sentAt(a));
|
|
116
|
+
const feed = collectRoomMedia(scoped, source.roomJid, options);
|
|
117
|
+
|
|
118
|
+
const url = bareUrl(file.fileURL);
|
|
119
|
+
const index = feed.findIndex(
|
|
120
|
+
(f) =>
|
|
121
|
+
f.source?.messageId === source.messageId && bareUrl(f.fileURL) === url
|
|
122
|
+
);
|
|
123
|
+
if (index < 0) {return null;}
|
|
124
|
+
// The bubble's own rules for Reply and Delete stay with the opened file.
|
|
125
|
+
return { ...feed[index]!, source, siblings: feed, index };
|
|
126
|
+
};
|