@adminide-stack/yantra-mobile 12.0.70-alpha.4 → 12.0.70
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/lib/config/env-config.js +1 -1
- package/lib/config/env-config.js.map +1 -1
- package/lib/features/messaging/ChatAttachments.js +383 -0
- package/lib/features/messaging/ChatAttachments.js.map +1 -0
- package/lib/features/messaging/ChatComponents.js +22 -6
- package/lib/features/messaging/ChatComponents.js.map +1 -1
- package/lib/features/messaging/ContactsScreen.js +15 -16
- package/lib/features/messaging/ContactsScreen.js.map +1 -1
- package/lib/features/messaging/NativeChatExperience.js +5 -1
- package/lib/features/messaging/NativeChatExperience.js.map +1 -1
- package/lib/features/messaging/ThreadScreen.js +88 -13
- package/lib/features/messaging/ThreadScreen.js.map +1 -1
- package/lib/features/messaging/chatModel.js +93 -7
- package/lib/features/messaging/chatModel.js.map +1 -1
- package/lib/features/messaging/chatOperations.js +27 -6
- package/lib/features/messaging/chatOperations.js.map +1 -1
- package/lib/features/messaging/useChatData.js +125 -6
- package/lib/features/messaging/useChatData.js.map +1 -1
- package/package.json +4 -4
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {jsxs,jsx}from'react/jsx-runtime';import*as React from'react';import {StyleSheet,Platform,KeyboardAvoidingView,View,Pressable,Text,ActivityIndicator,FlatList,TextInput}from'react-native';import {SafeAreaView}from'react-native-safe-area-context';import {MaterialCommunityIcons}from'@expo/vector-icons';import {Avatar,ConnectionBanner,InlineBanner,EmptyState,MessageBubble,DaySeparator}from'./ChatComponents.js';import {dayLabel}from'./chatModel.js';import {MessageActionsOverlay}from'./MessageActions.js';function ThreadScreen({
|
|
1
|
+
import {jsxs,jsx}from'react/jsx-runtime';import*as React from'react';import {StyleSheet,Platform,KeyboardAvoidingView,View,Pressable,Text,ActivityIndicator,FlatList,TextInput}from'react-native';import {SafeAreaView}from'react-native-safe-area-context';import {MaterialCommunityIcons}from'@expo/vector-icons';import {Avatar,ConnectionBanner,InlineBanner,EmptyState,MessageBubble,DaySeparator}from'./ChatComponents.js';import {dayLabel}from'./chatModel.js';import {MessageActionsOverlay}from'./MessageActions.js';import {StagedAttachments,RecordingBar,AttachSheet,ImageViewer}from'./ChatAttachments.js';function ThreadScreen({
|
|
2
2
|
title,
|
|
3
3
|
avatarUrl,
|
|
4
4
|
messages,
|
|
@@ -15,10 +15,20 @@ import {jsxs,jsx}from'react/jsx-runtime';import*as React from'react';import {Sty
|
|
|
15
15
|
onDismissError,
|
|
16
16
|
onReact,
|
|
17
17
|
onCopyText,
|
|
18
|
-
onShareText
|
|
18
|
+
onShareText,
|
|
19
|
+
media,
|
|
20
|
+
audio,
|
|
21
|
+
onSendAttachments
|
|
19
22
|
}) {
|
|
20
23
|
var _a;
|
|
21
24
|
const [draft, setDraft] = React.useState("");
|
|
25
|
+
const [staged, setStaged] = React.useState([]);
|
|
26
|
+
const [attachOpen, setAttachOpen] = React.useState(false);
|
|
27
|
+
const [recordingSince, setRecordingSince] = React.useState(null);
|
|
28
|
+
const [viewerUrl, setViewerUrl] = React.useState(null);
|
|
29
|
+
const [composerError, setComposerError] = React.useState(null);
|
|
30
|
+
const canAttach = Boolean(media && onSendAttachments);
|
|
31
|
+
const canRecord = Boolean(audio && onSendAttachments);
|
|
22
32
|
const [selected, setSelected] = React.useState(null);
|
|
23
33
|
const selectedMessage = React.useMemo(() => {
|
|
24
34
|
var _a2;
|
|
@@ -33,7 +43,7 @@ import {jsxs,jsx}from'react/jsx-runtime';import*as React from'react';import {Sty
|
|
|
33
43
|
}, [onShareText]);
|
|
34
44
|
const actions = React.useMemo(() => {
|
|
35
45
|
var _a2;
|
|
36
|
-
const body = (_a2 = selectedMessage == null ? void 0 : selectedMessage.body) != null ? _a2 : "";
|
|
46
|
+
const body = (_a2 = selectedMessage == null ? void 0 : selectedMessage.body.trim()) != null ? _a2 : "";
|
|
37
47
|
const list = [];
|
|
38
48
|
if (onCopyText && body) {
|
|
39
49
|
list.push({
|
|
@@ -62,14 +72,77 @@ import {jsxs,jsx}from'react/jsx-runtime';import*as React from'react';import {Sty
|
|
|
62
72
|
}, [selectedMessage, onCopyText, onShareText, flushPendingShare]);
|
|
63
73
|
const now = React.useMemo(() => Date.now(), []);
|
|
64
74
|
const rendered = React.useMemo(() => messages.slice().reverse(), [messages]);
|
|
65
|
-
const canSend = draft.trim().length > 0;
|
|
75
|
+
const canSend = draft.trim().length > 0 || staged.length > 0;
|
|
66
76
|
const submit = React.useCallback(() => {
|
|
67
77
|
const text = draft.trim();
|
|
78
|
+
if (staged.length && onSendAttachments) {
|
|
79
|
+
setDraft("");
|
|
80
|
+
setStaged([]);
|
|
81
|
+
haptics == null ? void 0 : haptics.impact("light");
|
|
82
|
+
onSendAttachments(staged, text, "image");
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
68
85
|
if (!text) return;
|
|
69
86
|
setDraft("");
|
|
70
87
|
haptics == null ? void 0 : haptics.impact("light");
|
|
71
88
|
onSend(text);
|
|
72
|
-
}, [draft, haptics, onSend]);
|
|
89
|
+
}, [draft, staged, haptics, onSend, onSendAttachments]);
|
|
90
|
+
const pendingPick = React.useRef(null);
|
|
91
|
+
const runPendingPick = React.useCallback(() => {
|
|
92
|
+
const source = pendingPick.current;
|
|
93
|
+
pendingPick.current = null;
|
|
94
|
+
if (!source || !media) return;
|
|
95
|
+
const run = source === "library" ? media.pickFromLibrary({
|
|
96
|
+
limit: Math.max(1, 10 - staged.length)
|
|
97
|
+
}) : media.captureFromCamera();
|
|
98
|
+
run.then((files) => {
|
|
99
|
+
if (files.length) setStaged((prev) => [...prev, ...files].slice(0, 10));
|
|
100
|
+
}).catch((e) => setComposerError(e instanceof Error ? e.message : "Could not open photos."));
|
|
101
|
+
}, [media, staged.length]);
|
|
102
|
+
const pick = React.useCallback((source) => {
|
|
103
|
+
if (!media) return;
|
|
104
|
+
setComposerError(null);
|
|
105
|
+
pendingPick.current = source;
|
|
106
|
+
setAttachOpen(false);
|
|
107
|
+
if (Platform.OS !== "ios") setTimeout(runPendingPick, 400);
|
|
108
|
+
}, [media, runPendingPick]);
|
|
109
|
+
const startRecording = React.useCallback(async () => {
|
|
110
|
+
if (!audio) return;
|
|
111
|
+
setComposerError(null);
|
|
112
|
+
try {
|
|
113
|
+
const ok = await audio.startRecording();
|
|
114
|
+
if (!ok) {
|
|
115
|
+
setComposerError("Microphone access is off. Allow it in Settings to send voice messages.");
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
haptics == null ? void 0 : haptics.impact("medium");
|
|
119
|
+
setRecordingSince(Date.now());
|
|
120
|
+
} catch (e) {
|
|
121
|
+
setComposerError(e instanceof Error ? e.message : "Could not start recording.");
|
|
122
|
+
}
|
|
123
|
+
}, [audio, haptics]);
|
|
124
|
+
const finishRecording = React.useCallback(async (send) => {
|
|
125
|
+
if (!audio) return;
|
|
126
|
+
setRecordingSince(null);
|
|
127
|
+
try {
|
|
128
|
+
if (!send) {
|
|
129
|
+
await audio.cancelRecording();
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
const file = await audio.stopRecording();
|
|
133
|
+
if (file && onSendAttachments) {
|
|
134
|
+
haptics == null ? void 0 : haptics.impact("light");
|
|
135
|
+
onSendAttachments([file], "", "audio");
|
|
136
|
+
}
|
|
137
|
+
} catch (e) {
|
|
138
|
+
setComposerError(e instanceof Error ? e.message : "Could not send the recording.");
|
|
139
|
+
}
|
|
140
|
+
}, [audio, haptics, onSendAttachments]);
|
|
141
|
+
const recordingRef = React.useRef(recordingSince);
|
|
142
|
+
recordingRef.current = recordingSince;
|
|
143
|
+
React.useEffect(() => () => {
|
|
144
|
+
if (recordingRef.current) void (audio == null ? void 0 : audio.cancelRecording());
|
|
145
|
+
}, [audio]);
|
|
73
146
|
return /* @__PURE__ */ jsxs(KeyboardAvoidingView, { style: styles.fill, behavior: Platform.OS === "ios" ? "padding" : void 0, keyboardVerticalOffset: 0, children: [
|
|
74
147
|
/* @__PURE__ */ jsx(SafeAreaView, { edges: ["top"], style: {
|
|
75
148
|
backgroundColor: theme.headerBg
|
|
@@ -103,6 +176,7 @@ import {jsxs,jsx}from'react/jsx-runtime';import*as React from'react';import {Sty
|
|
|
103
176
|
] }) }),
|
|
104
177
|
/* @__PURE__ */ jsx(ConnectionBanner, { state: connectionState, theme }),
|
|
105
178
|
/* @__PURE__ */ jsx(InlineBanner, { message: error, theme, onDismiss: onDismissError }),
|
|
179
|
+
/* @__PURE__ */ jsx(InlineBanner, { message: composerError, theme, onDismiss: () => setComposerError(null) }),
|
|
106
180
|
loading && messages.length === 0 ? /* @__PURE__ */ jsx(View, { style: [styles.center, {
|
|
107
181
|
backgroundColor: theme.threadBg,
|
|
108
182
|
flex: 1
|
|
@@ -122,7 +196,7 @@ import {jsxs,jsx}from'react/jsx-runtime';import*as React from'react';import {Sty
|
|
|
122
196
|
const label = dayLabel(item.createdAt, now);
|
|
123
197
|
const olderLabel = older ? dayLabel(older.createdAt, now) : null;
|
|
124
198
|
return /* @__PURE__ */ jsxs(View, { children: [
|
|
125
|
-
/* @__PURE__ */ jsx(MessageBubble, { message: item, theme, showAuthor, groupWithPrevious, groupWithNext, onRetry: () => onRetry(item.id), onLongPress: (
|
|
199
|
+
/* @__PURE__ */ jsx(MessageBubble, { message: item, theme, fileBaseUrl: media == null ? void 0 : media.fileBaseUrl, audio, onOpenImage: setViewerUrl, showAuthor, groupWithPrevious, groupWithNext, onRetry: () => onRetry(item.id), onLongPress: (
|
|
126
200
|
// Only confirmed messages can be reacted to or acted on.
|
|
127
201
|
!item.isDeleted && (!item.sendState || item.sendState === "sent") ? (frame) => {
|
|
128
202
|
haptics == null ? void 0 : haptics.impact("medium");
|
|
@@ -135,14 +209,15 @@ import {jsxs,jsx}from'react/jsx-runtime';import*as React from'react';import {Sty
|
|
|
135
209
|
olderLabel !== label ? /* @__PURE__ */ jsx(DaySeparator, { label, theme }) : null
|
|
136
210
|
] });
|
|
137
211
|
}, onEndReached: onLoadMore, onEndReachedThreshold: 0.4, ListFooterComponent: loadingMore ? /* @__PURE__ */ jsx(ActivityIndicator, { style: styles.moreSpinner, size: "small" }) : null, contentContainerStyle: rendered.length === 0 ? styles.listContentEmpty : void 0, ListEmptyComponent: /* @__PURE__ */ jsx(View, { style: styles.emptyInverted, children: /* @__PURE__ */ jsx(EmptyState, { icon: "message-text-outline", title: "No messages yet", detail: "Messages you send stay in this chat.", theme }) }), keyboardDismissMode: "interactive", keyboardShouldPersistTaps: "handled", testID: "chat-thread" }),
|
|
138
|
-
/* @__PURE__ */
|
|
212
|
+
/* @__PURE__ */ jsx(StagedAttachments, { files: staged, theme, onRemove: (index) => setStaged((prev) => prev.filter((_, i) => i !== index)) }),
|
|
213
|
+
recordingSince ? /* @__PURE__ */ jsx(RecordingBar, { theme, startedAt: recordingSince, onCancel: () => void finishRecording(false), onSend: () => void finishRecording(true) }) : /* @__PURE__ */ jsxs(View, { style: {
|
|
139
214
|
flexDirection: "row",
|
|
140
215
|
alignItems: "flex-end",
|
|
141
216
|
paddingHorizontal: 8,
|
|
142
217
|
paddingVertical: 8,
|
|
143
218
|
backgroundColor: theme.composerBar
|
|
144
219
|
}, children: [
|
|
145
|
-
/* @__PURE__ */ jsx(Pressable, { hitSlop: 6, accessibilityRole: "button", accessibilityLabel: "Add
|
|
220
|
+
canAttach ? /* @__PURE__ */ jsx(Pressable, { onPress: () => setAttachOpen(true), hitSlop: 6, accessibilityRole: "button", accessibilityLabel: "Add photos", style: styles.composerIcon, testID: "chat-composer-attach", children: /* @__PURE__ */ jsx(MaterialCommunityIcons, { name: "plus-circle-outline", size: 28, color: theme.textMuted }) }) : null,
|
|
146
221
|
/* @__PURE__ */ jsx(View, { style: {
|
|
147
222
|
flex: 1,
|
|
148
223
|
flexDirection: "row",
|
|
@@ -161,12 +236,12 @@ import {jsxs,jsx}from'react/jsx-runtime';import*as React from'react';import {Sty
|
|
|
161
236
|
paddingVertical: Platform.OS === "ios" ? 8 : 6,
|
|
162
237
|
maxHeight: 120
|
|
163
238
|
}, multiline: true, blurOnSubmit: false, testID: "chat-composer-input" }) }),
|
|
164
|
-
/* @__PURE__ */ jsx(Pressable, { onPress: canSend ? submit : void 0, accessibilityRole: "button", accessibilityLabel:
|
|
165
|
-
backgroundColor: theme.accent
|
|
166
|
-
} : {
|
|
167
|
-
backgroundColor: "transparent"
|
|
168
|
-
}], testID: "chat-composer-send", children: /* @__PURE__ */ jsx(MaterialCommunityIcons, { name: canSend ? "arrow-up" : "microphone-outline", size: canSend ? 22 : 26, color: canSend ? theme.onAccent : theme.textMuted }) })
|
|
239
|
+
canSend || !canRecord ? /* @__PURE__ */ jsx(Pressable, { onPress: canSend ? submit : void 0, disabled: !canSend, accessibilityRole: "button", accessibilityLabel: "Send message", style: [styles.composerSend, {
|
|
240
|
+
backgroundColor: canSend ? theme.accent : theme.border
|
|
241
|
+
}], testID: "chat-composer-send", children: /* @__PURE__ */ jsx(MaterialCommunityIcons, { name: "arrow-up", size: 22, color: theme.onAccent }) }) : /* @__PURE__ */ jsx(Pressable, { onPress: () => void startRecording(), hitSlop: 6, accessibilityRole: "button", accessibilityLabel: "Record voice message", style: styles.composerSend, testID: "chat-composer-mic", children: /* @__PURE__ */ jsx(MaterialCommunityIcons, { name: "microphone-outline", size: 26, color: theme.textMuted }) })
|
|
169
242
|
] }),
|
|
243
|
+
/* @__PURE__ */ jsx(AttachSheet, { visible: attachOpen, theme, onClose: () => setAttachOpen(false), onPhotos: () => pick("library"), onCamera: () => pick("camera"), onDismissed: Platform.OS === "ios" ? runPendingPick : void 0 }),
|
|
244
|
+
/* @__PURE__ */ jsx(ImageViewer, { url: viewerUrl, onClose: () => setViewerUrl(null) }),
|
|
170
245
|
/* @__PURE__ */ jsx(MessageActionsOverlay, { message: selectedMessage, frame: (_a = selected == null ? void 0 : selected.frame) != null ? _a : null, theme, actions, onReact: (emoji) => {
|
|
171
246
|
if (selectedMessage && onReact) {
|
|
172
247
|
haptics == null ? void 0 : haptics.impact("selection");
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ThreadScreen.js","sources":["../../../src/features/messaging/ThreadScreen.tsx"],"sourcesContent":["/**\n * One conversation: transcript plus composer.\n *\n * The list is INVERTED. That is the standard trick for a chat transcript - it\n * pins to the bottom for free, keeps position when older pages prepend, and\n * makes \"scrolled to newest\" simply `offset === 0`. The data is therefore\n * reversed for rendering while `chatModel` keeps it oldest-first everywhere\n * else, which is the order every merge and comparison reasons about.\n */\nimport * as React from 'react';\nimport {\n ActivityIndicator,\n FlatList,\n KeyboardAvoidingView,\n Platform,\n Pressable,\n StyleSheet,\n Text,\n TextInput,\n View,\n} from 'react-native';\nimport { SafeAreaView } from 'react-native-safe-area-context';\nimport { MaterialCommunityIcons } from '@expo/vector-icons';\nimport {\n Avatar,\n ConnectionBanner,\n DaySeparator,\n EmptyState,\n InlineBanner,\n MessageBubble,\n type ChatTheme,\n} from './ChatComponents';\nimport { dayLabel, type ChatMessage } from './chatModel';\nimport { MessageActionsOverlay, type BubbleFrame, type MessageAction } from './MessageActions';\nimport type { ChatConnectionState, ChatHapticsSeam } from './chatHost';\n\nexport function ThreadScreen({\n title,\n avatarUrl,\n messages,\n loading,\n loadingMore,\n error,\n theme,\n connectionState,\n haptics,\n onBack,\n onSend,\n onLoadMore,\n onRetry,\n onDismissError,\n onReact,\n onCopyText,\n onShareText,\n}: {\n title: string;\n avatarUrl?: string | null;\n messages: ChatMessage[];\n loading: boolean;\n loadingMore: boolean;\n error: string | null;\n theme: ChatTheme;\n connectionState: ChatConnectionState;\n haptics?: ChatHapticsSeam;\n onBack: () => void;\n onSend: (body: string) => void;\n onLoadMore: () => void;\n onRetry: (id: string) => void;\n onDismissError: () => void;\n /** Pick or remove the viewer's Tapback; absent disables long-press reactions. */\n onReact?: (messageId: string, emoji: string) => void;\n /** Present only when the host provides a clipboard. */\n onCopyText?: (text: string) => void;\n onShareText?: (text: string) => void;\n}) {\n const [draft, setDraft] = React.useState('');\n const [selected, setSelected] = React.useState<{ id: string; frame: BubbleFrame } | null>(null);\n const selectedMessage = React.useMemo(\n () => (selected ? (messages.find((m) => m.id === selected.id) ?? null) : null),\n [selected, messages],\n );\n const closeActions = React.useCallback(() => setSelected(null), []);\n // Share must open AFTER the overlay's modal is gone: iOS refuses to present the\n // share sheet from a view controller that is still dismissing, and fails silently.\n const pendingShare = React.useRef<string | null>(null);\n const flushPendingShare = React.useCallback(() => {\n const text = pendingShare.current;\n pendingShare.current = null;\n if (text) onShareText?.(text);\n }, [onShareText]);\n\n const actions = React.useMemo<MessageAction[]>(() => {\n const body = selectedMessage?.body ?? '';\n const list: MessageAction[] = [];\n if (onCopyText && body) {\n list.push({\n key: 'copy',\n label: 'Copy',\n icon: 'content-copy',\n onPress: () => {\n onCopyText(body);\n setSelected(null);\n },\n });\n }\n if (onShareText && body) {\n list.push({\n key: 'share',\n label: 'Share',\n icon: 'export-variant',\n onPress: () => {\n pendingShare.current = body;\n setSelected(null);\n // Android has no Modal onDismiss; its modal is gone after the fade.\n if (Platform.OS !== 'ios') setTimeout(flushPendingShare, 300);\n },\n });\n }\n return list;\n }, [selectedMessage, onCopyText, onShareText, flushPendingShare]);\n const now = React.useMemo(() => Date.now(), []);\n const rendered = React.useMemo(() => messages.slice().reverse(), [messages]);\n const canSend = draft.trim().length > 0;\n\n const submit = React.useCallback(() => {\n const text = draft.trim();\n if (!text) return;\n setDraft('');\n haptics?.impact('light');\n onSend(text);\n }, [draft, haptics, onSend]);\n\n return (\n <KeyboardAvoidingView\n style={styles.fill}\n behavior={Platform.OS === 'ios' ? 'padding' : undefined}\n keyboardVerticalOffset={0}\n >\n <SafeAreaView edges={['top']} style={{ backgroundColor: theme.headerBg }}>\n <View\n style={{\n flexDirection: 'row',\n alignItems: 'center',\n paddingHorizontal: 4,\n paddingVertical: 6,\n borderBottomWidth: StyleSheet.hairlineWidth,\n borderBottomColor: theme.border,\n backgroundColor: theme.headerBg,\n }}\n >\n <Pressable onPress={onBack} hitSlop={10} accessibilityRole=\"button\" accessibilityLabel=\"Back\">\n <MaterialCommunityIcons name=\"chevron-left\" size={32} color={theme.accent} />\n </Pressable>\n <Pressable\n onPress={onBack}\n accessibilityRole=\"button\"\n style={{\n flexDirection: 'row',\n alignItems: 'center',\n flex: 1,\n marginLeft: 4,\n minWidth: 0,\n }}\n >\n <Avatar name={title} uri={avatarUrl} theme={theme} size={36} />\n <Text\n numberOfLines={1}\n style={{\n flex: 1,\n fontSize: 17,\n fontWeight: '600',\n color: theme.text,\n marginLeft: 10,\n }}\n >\n {title}\n </Text>\n </Pressable>\n <Pressable hitSlop={8} accessibilityRole=\"button\" accessibilityLabel=\"More options\">\n <MaterialCommunityIcons name=\"dots-vertical\" size={22} color={theme.textMuted} />\n </Pressable>\n </View>\n </SafeAreaView>\n\n <ConnectionBanner state={connectionState} theme={theme} />\n <InlineBanner message={error} theme={theme} onDismiss={onDismissError} />\n\n {loading && messages.length === 0 ? (\n <View style={[styles.center, { backgroundColor: theme.threadBg, flex: 1 }]}>\n <ActivityIndicator color={theme.accent} />\n </View>\n ) : (\n <FlatList\n inverted\n data={rendered}\n style={{ flex: 1, backgroundColor: theme.threadBg }}\n keyExtractor={(m) => m.id}\n renderItem={({ item, index }) => {\n const older = rendered[index + 1];\n const newer = index > 0 ? rendered[index - 1] : null;\n const sameAuthor = (a: ChatMessage | null | undefined, b: ChatMessage) =>\n Boolean(a && !a.isDeleted && !b.isDeleted && a.authorId && a.authorId === b.authorId);\n const showAuthor = !older || older.authorId !== item.authorId;\n const groupWithPrevious = sameAuthor(older, item);\n const groupWithNext = sameAuthor(newer, item);\n const label = dayLabel(item.createdAt, now);\n const olderLabel = older ? dayLabel(older.createdAt, now) : null;\n return (\n <View>\n <MessageBubble\n message={item}\n theme={theme}\n showAuthor={showAuthor}\n groupWithPrevious={groupWithPrevious}\n groupWithNext={groupWithNext}\n onRetry={() => onRetry(item.id)}\n onLongPress={\n // Only confirmed messages can be reacted to or acted on.\n !item.isDeleted && (!item.sendState || item.sendState === 'sent')\n ? (frame) => {\n haptics?.impact('medium');\n setSelected({ id: item.id, frame });\n }\n : undefined\n }\n />\n {olderLabel !== label ? <DaySeparator label={label} theme={theme} /> : null}\n </View>\n );\n }}\n onEndReached={onLoadMore}\n onEndReachedThreshold={0.4}\n ListFooterComponent={\n loadingMore ? <ActivityIndicator style={styles.moreSpinner} size=\"small\" /> : null\n }\n contentContainerStyle={rendered.length === 0 ? styles.listContentEmpty : undefined}\n ListEmptyComponent={\n <View style={styles.emptyInverted}>\n <EmptyState\n icon=\"message-text-outline\"\n title=\"No messages yet\"\n detail=\"Messages you send stay in this chat.\"\n theme={theme}\n />\n </View>\n }\n keyboardDismissMode=\"interactive\"\n keyboardShouldPersistTaps=\"handled\"\n testID=\"chat-thread\"\n />\n )}\n\n <View\n style={{\n flexDirection: 'row',\n alignItems: 'flex-end',\n paddingHorizontal: 8,\n paddingVertical: 8,\n backgroundColor: theme.composerBar,\n }}\n >\n <Pressable\n hitSlop={6}\n accessibilityRole=\"button\"\n accessibilityLabel=\"Add attachment\"\n style={styles.composerIcon}\n >\n <MaterialCommunityIcons name=\"plus-circle-outline\" size={28} color={theme.textMuted} />\n </Pressable>\n <View\n style={{\n flex: 1,\n flexDirection: 'row',\n alignItems: 'flex-end',\n backgroundColor: theme.bg,\n borderRadius: 20,\n borderWidth: StyleSheet.hairlineWidth,\n borderColor: theme.border,\n paddingHorizontal: 12,\n minHeight: 38,\n marginHorizontal: 6,\n }}\n >\n <TextInput\n value={draft}\n onChangeText={setDraft}\n placeholder=\"Message\"\n placeholderTextColor={theme.textMuted}\n style={{\n flex: 1,\n color: theme.text,\n fontSize: 17,\n paddingVertical: Platform.OS === 'ios' ? 8 : 6,\n maxHeight: 120,\n }}\n multiline\n blurOnSubmit={false}\n testID=\"chat-composer-input\"\n />\n </View>\n <Pressable\n onPress={canSend ? submit : undefined}\n accessibilityRole=\"button\"\n accessibilityLabel={canSend ? 'Send message' : 'Voice message'}\n style={[\n styles.composerSend,\n canSend ? { backgroundColor: theme.accent } : { backgroundColor: 'transparent' },\n ]}\n testID=\"chat-composer-send\"\n >\n <MaterialCommunityIcons\n name={canSend ? 'arrow-up' : 'microphone-outline'}\n size={canSend ? 22 : 26}\n color={canSend ? theme.onAccent : theme.textMuted}\n />\n </Pressable>\n </View>\n\n <MessageActionsOverlay\n message={selectedMessage}\n frame={selected?.frame ?? null}\n theme={theme}\n actions={actions}\n onReact={(emoji) => {\n if (selectedMessage && onReact) {\n haptics?.impact('selection');\n onReact(selectedMessage.id, emoji);\n }\n setSelected(null);\n }}\n onClose={closeActions}\n onDismissed={Platform.OS === 'ios' ? flushPendingShare : undefined}\n />\n </KeyboardAvoidingView>\n );\n}\n\nconst styles = StyleSheet.create({\n fill: { flex: 1 },\n center: { flex: 1, alignItems: 'center', justifyContent: 'center' },\n listContentEmpty: { flexGrow: 1, justifyContent: 'center' },\n moreSpinner: { paddingVertical: 12 },\n emptyInverted: { transform: [{ scaleY: -1 }], paddingVertical: 24, alignItems: 'center' },\n composerIcon: {\n width: 36,\n height: 36,\n alignItems: 'center',\n justifyContent: 'center',\n },\n composerSend: {\n width: 32,\n height: 32,\n borderRadius: 16,\n alignItems: 'center',\n justifyContent: 'center',\n },\n});\n"],"names":["_a"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"ThreadScreen.js","sources":["../../../src/features/messaging/ThreadScreen.tsx"],"sourcesContent":["/**\n * One conversation: transcript plus composer.\n *\n * The list is INVERTED. That is the standard trick for a chat transcript - it\n * pins to the bottom for free, keeps position when older pages prepend, and\n * makes \"scrolled to newest\" simply `offset === 0`. The data is therefore\n * reversed for rendering while `chatModel` keeps it oldest-first everywhere\n * else, which is the order every merge and comparison reasons about.\n */\nimport * as React from 'react';\nimport {\n ActivityIndicator,\n FlatList,\n KeyboardAvoidingView,\n Platform,\n Pressable,\n StyleSheet,\n Text,\n TextInput,\n View,\n} from 'react-native';\nimport { SafeAreaView } from 'react-native-safe-area-context';\nimport { MaterialCommunityIcons } from '@expo/vector-icons';\nimport {\n Avatar,\n ConnectionBanner,\n DaySeparator,\n EmptyState,\n InlineBanner,\n MessageBubble,\n type ChatTheme,\n} from './ChatComponents';\nimport { dayLabel, type ChatMessage } from './chatModel';\nimport { MessageActionsOverlay, type BubbleFrame, type MessageAction } from './MessageActions';\nimport type { ChatAudioSeam, ChatConnectionState, ChatHapticsSeam, ChatMediaSeam, ChatPickedImage } from './chatHost';\nimport type { ChatAttachmentKind } from './chatModel';\nimport { AttachSheet, ImageViewer, RecordingBar, StagedAttachments } from './ChatAttachments';\n\nexport function ThreadScreen({\n title,\n avatarUrl,\n messages,\n loading,\n loadingMore,\n error,\n theme,\n connectionState,\n haptics,\n onBack,\n onSend,\n onLoadMore,\n onRetry,\n onDismissError,\n onReact,\n onCopyText,\n onShareText,\n media,\n audio,\n onSendAttachments,\n}: {\n title: string;\n avatarUrl?: string | null;\n messages: ChatMessage[];\n loading: boolean;\n loadingMore: boolean;\n error: string | null;\n theme: ChatTheme;\n connectionState: ChatConnectionState;\n haptics?: ChatHapticsSeam;\n onBack: () => void;\n onSend: (body: string) => void;\n onLoadMore: () => void;\n onRetry: (id: string) => void;\n onDismissError: () => void;\n /** Pick or remove the viewer's Tapback; absent disables long-press reactions. */\n onReact?: (messageId: string, emoji: string) => void;\n /** Present only when the host provides a clipboard. */\n onCopyText?: (text: string) => void;\n onShareText?: (text: string) => void;\n /** Photos / camera; without it (or `onSendAttachments`) the \"+\" button is hidden. */\n media?: ChatMediaSeam;\n /** Voice notes; without it the mic button is hidden. */\n audio?: ChatAudioSeam;\n onSendAttachments?: (files: ChatPickedImage[], caption: string, kind: ChatAttachmentKind) => void;\n}) {\n const [draft, setDraft] = React.useState('');\n const [staged, setStaged] = React.useState<ChatPickedImage[]>([]);\n const [attachOpen, setAttachOpen] = React.useState(false);\n const [recordingSince, setRecordingSince] = React.useState<number | null>(null);\n const [viewerUrl, setViewerUrl] = React.useState<string | null>(null);\n const [composerError, setComposerError] = React.useState<string | null>(null);\n const canAttach = Boolean(media && onSendAttachments);\n const canRecord = Boolean(audio && onSendAttachments);\n const [selected, setSelected] = React.useState<{ id: string; frame: BubbleFrame } | null>(null);\n const selectedMessage = React.useMemo(\n () => (selected ? (messages.find((m) => m.id === selected.id) ?? null) : null),\n [selected, messages],\n );\n const closeActions = React.useCallback(() => setSelected(null), []);\n // Share must open AFTER the overlay's modal is gone: iOS refuses to present the\n // share sheet from a view controller that is still dismissing, and fails silently.\n const pendingShare = React.useRef<string | null>(null);\n const flushPendingShare = React.useCallback(() => {\n const text = pendingShare.current;\n pendingShare.current = null;\n if (text) onShareText?.(text);\n }, [onShareText]);\n\n const actions = React.useMemo<MessageAction[]>(() => {\n const body = selectedMessage?.body.trim() ?? '';\n const list: MessageAction[] = [];\n if (onCopyText && body) {\n list.push({\n key: 'copy',\n label: 'Copy',\n icon: 'content-copy',\n onPress: () => {\n onCopyText(body);\n setSelected(null);\n },\n });\n }\n if (onShareText && body) {\n list.push({\n key: 'share',\n label: 'Share',\n icon: 'export-variant',\n onPress: () => {\n pendingShare.current = body;\n setSelected(null);\n // Android has no Modal onDismiss; its modal is gone after the fade.\n if (Platform.OS !== 'ios') setTimeout(flushPendingShare, 300);\n },\n });\n }\n return list;\n }, [selectedMessage, onCopyText, onShareText, flushPendingShare]);\n const now = React.useMemo(() => Date.now(), []);\n const rendered = React.useMemo(() => messages.slice().reverse(), [messages]);\n const canSend = draft.trim().length > 0 || staged.length > 0;\n\n const submit = React.useCallback(() => {\n const text = draft.trim();\n if (staged.length && onSendAttachments) {\n setDraft('');\n setStaged([]);\n haptics?.impact('light');\n onSendAttachments(staged, text, 'image');\n return;\n }\n if (!text) return;\n setDraft('');\n haptics?.impact('light');\n onSend(text);\n }, [draft, staged, haptics, onSend, onSendAttachments]);\n\n // The menu must be fully closed before the picker opens: iOS silently refuses to present over\n // a modal that is still animating away (same as Share). iOS waits for the sheet's onDismiss;\n // Android has no onDismiss, so it waits out the slide.\n const pendingPick = React.useRef<'library' | 'camera' | null>(null);\n const runPendingPick = React.useCallback(() => {\n const source = pendingPick.current;\n pendingPick.current = null;\n if (!source || !media) return;\n const run =\n source === 'library'\n ? media.pickFromLibrary({ limit: Math.max(1, 10 - staged.length) })\n : media.captureFromCamera();\n run.then((files) => {\n if (files.length) setStaged((prev) => [...prev, ...files].slice(0, 10));\n }).catch((e: unknown) => setComposerError(e instanceof Error ? e.message : 'Could not open photos.'));\n }, [media, staged.length]);\n const pick = React.useCallback(\n (source: 'library' | 'camera') => {\n if (!media) return;\n setComposerError(null);\n pendingPick.current = source;\n setAttachOpen(false);\n if (Platform.OS !== 'ios') setTimeout(runPendingPick, 400);\n },\n [media, runPendingPick],\n );\n\n const startRecording = React.useCallback(async () => {\n if (!audio) return;\n setComposerError(null);\n try {\n const ok = await audio.startRecording();\n if (!ok) {\n setComposerError('Microphone access is off. Allow it in Settings to send voice messages.');\n return;\n }\n haptics?.impact('medium');\n setRecordingSince(Date.now());\n } catch (e) {\n setComposerError(e instanceof Error ? e.message : 'Could not start recording.');\n }\n }, [audio, haptics]);\n\n const finishRecording = React.useCallback(\n async (send: boolean) => {\n if (!audio) return;\n setRecordingSince(null);\n try {\n if (!send) {\n await audio.cancelRecording();\n return;\n }\n const file = await audio.stopRecording();\n if (file && onSendAttachments) {\n haptics?.impact('light');\n onSendAttachments([file], '', 'audio');\n }\n } catch (e) {\n setComposerError(e instanceof Error ? e.message : 'Could not send the recording.');\n }\n },\n [audio, haptics, onSendAttachments],\n );\n\n // Leaving the thread mid-recording throws the recording away.\n const recordingRef = React.useRef(recordingSince);\n recordingRef.current = recordingSince;\n React.useEffect(\n () => () => {\n if (recordingRef.current) void audio?.cancelRecording();\n },\n [audio],\n );\n\n return (\n <KeyboardAvoidingView\n style={styles.fill}\n behavior={Platform.OS === 'ios' ? 'padding' : undefined}\n keyboardVerticalOffset={0}\n >\n <SafeAreaView edges={['top']} style={{ backgroundColor: theme.headerBg }}>\n <View\n style={{\n flexDirection: 'row',\n alignItems: 'center',\n paddingHorizontal: 4,\n paddingVertical: 6,\n borderBottomWidth: StyleSheet.hairlineWidth,\n borderBottomColor: theme.border,\n backgroundColor: theme.headerBg,\n }}\n >\n <Pressable onPress={onBack} hitSlop={10} accessibilityRole=\"button\" accessibilityLabel=\"Back\">\n <MaterialCommunityIcons name=\"chevron-left\" size={32} color={theme.accent} />\n </Pressable>\n <Pressable\n onPress={onBack}\n accessibilityRole=\"button\"\n style={{\n flexDirection: 'row',\n alignItems: 'center',\n flex: 1,\n marginLeft: 4,\n minWidth: 0,\n }}\n >\n <Avatar name={title} uri={avatarUrl} theme={theme} size={36} />\n <Text\n numberOfLines={1}\n style={{\n flex: 1,\n fontSize: 17,\n fontWeight: '600',\n color: theme.text,\n marginLeft: 10,\n }}\n >\n {title}\n </Text>\n </Pressable>\n <Pressable hitSlop={8} accessibilityRole=\"button\" accessibilityLabel=\"More options\">\n <MaterialCommunityIcons name=\"dots-vertical\" size={22} color={theme.textMuted} />\n </Pressable>\n </View>\n </SafeAreaView>\n\n <ConnectionBanner state={connectionState} theme={theme} />\n <InlineBanner message={error} theme={theme} onDismiss={onDismissError} />\n <InlineBanner message={composerError} theme={theme} onDismiss={() => setComposerError(null)} />\n\n {loading && messages.length === 0 ? (\n <View style={[styles.center, { backgroundColor: theme.threadBg, flex: 1 }]}>\n <ActivityIndicator color={theme.accent} />\n </View>\n ) : (\n <FlatList\n inverted\n data={rendered}\n style={{ flex: 1, backgroundColor: theme.threadBg }}\n keyExtractor={(m) => m.id}\n renderItem={({ item, index }) => {\n const older = rendered[index + 1];\n const newer = index > 0 ? rendered[index - 1] : null;\n const sameAuthor = (a: ChatMessage | null | undefined, b: ChatMessage) =>\n Boolean(a && !a.isDeleted && !b.isDeleted && a.authorId && a.authorId === b.authorId);\n const showAuthor = !older || older.authorId !== item.authorId;\n const groupWithPrevious = sameAuthor(older, item);\n const groupWithNext = sameAuthor(newer, item);\n const label = dayLabel(item.createdAt, now);\n const olderLabel = older ? dayLabel(older.createdAt, now) : null;\n return (\n <View>\n <MessageBubble\n message={item}\n theme={theme}\n fileBaseUrl={media?.fileBaseUrl}\n audio={audio}\n onOpenImage={setViewerUrl}\n showAuthor={showAuthor}\n groupWithPrevious={groupWithPrevious}\n groupWithNext={groupWithNext}\n onRetry={() => onRetry(item.id)}\n onLongPress={\n // Only confirmed messages can be reacted to or acted on.\n !item.isDeleted && (!item.sendState || item.sendState === 'sent')\n ? (frame) => {\n haptics?.impact('medium');\n setSelected({ id: item.id, frame });\n }\n : undefined\n }\n />\n {olderLabel !== label ? <DaySeparator label={label} theme={theme} /> : null}\n </View>\n );\n }}\n onEndReached={onLoadMore}\n onEndReachedThreshold={0.4}\n ListFooterComponent={\n loadingMore ? <ActivityIndicator style={styles.moreSpinner} size=\"small\" /> : null\n }\n contentContainerStyle={rendered.length === 0 ? styles.listContentEmpty : undefined}\n ListEmptyComponent={\n <View style={styles.emptyInverted}>\n <EmptyState\n icon=\"message-text-outline\"\n title=\"No messages yet\"\n detail=\"Messages you send stay in this chat.\"\n theme={theme}\n />\n </View>\n }\n keyboardDismissMode=\"interactive\"\n keyboardShouldPersistTaps=\"handled\"\n testID=\"chat-thread\"\n />\n )}\n\n <StagedAttachments\n files={staged}\n theme={theme}\n onRemove={(index) => setStaged((prev) => prev.filter((_, i) => i !== index))}\n />\n {recordingSince ? (\n <RecordingBar\n theme={theme}\n startedAt={recordingSince}\n onCancel={() => void finishRecording(false)}\n onSend={() => void finishRecording(true)}\n />\n ) : (\n <View\n style={{\n flexDirection: 'row',\n alignItems: 'flex-end',\n paddingHorizontal: 8,\n paddingVertical: 8,\n backgroundColor: theme.composerBar,\n }}\n >\n {canAttach ? (\n <Pressable\n onPress={() => setAttachOpen(true)}\n hitSlop={6}\n accessibilityRole=\"button\"\n accessibilityLabel=\"Add photos\"\n style={styles.composerIcon}\n testID=\"chat-composer-attach\"\n >\n <MaterialCommunityIcons name=\"plus-circle-outline\" size={28} color={theme.textMuted} />\n </Pressable>\n ) : null}\n <View\n style={{\n flex: 1,\n flexDirection: 'row',\n alignItems: 'flex-end',\n backgroundColor: theme.bg,\n borderRadius: 20,\n borderWidth: StyleSheet.hairlineWidth,\n borderColor: theme.border,\n paddingHorizontal: 12,\n minHeight: 38,\n marginHorizontal: 6,\n }}\n >\n <TextInput\n value={draft}\n onChangeText={setDraft}\n placeholder=\"Message\"\n placeholderTextColor={theme.textMuted}\n style={{\n flex: 1,\n color: theme.text,\n fontSize: 17,\n paddingVertical: Platform.OS === 'ios' ? 8 : 6,\n maxHeight: 120,\n }}\n multiline\n blurOnSubmit={false}\n testID=\"chat-composer-input\"\n />\n </View>\n {canSend || !canRecord ? (\n <Pressable\n onPress={canSend ? submit : undefined}\n disabled={!canSend}\n accessibilityRole=\"button\"\n accessibilityLabel=\"Send message\"\n style={[styles.composerSend, { backgroundColor: canSend ? theme.accent : theme.border }]}\n testID=\"chat-composer-send\"\n >\n <MaterialCommunityIcons name=\"arrow-up\" size={22} color={theme.onAccent} />\n </Pressable>\n ) : (\n <Pressable\n onPress={() => void startRecording()}\n hitSlop={6}\n accessibilityRole=\"button\"\n accessibilityLabel=\"Record voice message\"\n style={styles.composerSend}\n testID=\"chat-composer-mic\"\n >\n <MaterialCommunityIcons name=\"microphone-outline\" size={26} color={theme.textMuted} />\n </Pressable>\n )}\n </View>\n )}\n\n <AttachSheet\n visible={attachOpen}\n theme={theme}\n onClose={() => setAttachOpen(false)}\n onPhotos={() => pick('library')}\n onCamera={() => pick('camera')}\n onDismissed={Platform.OS === 'ios' ? runPendingPick : undefined}\n />\n <ImageViewer url={viewerUrl} onClose={() => setViewerUrl(null)} />\n\n <MessageActionsOverlay\n message={selectedMessage}\n frame={selected?.frame ?? null}\n theme={theme}\n actions={actions}\n onReact={(emoji) => {\n if (selectedMessage && onReact) {\n haptics?.impact('selection');\n onReact(selectedMessage.id, emoji);\n }\n setSelected(null);\n }}\n onClose={closeActions}\n onDismissed={Platform.OS === 'ios' ? flushPendingShare : undefined}\n />\n </KeyboardAvoidingView>\n );\n}\n\nconst styles = StyleSheet.create({\n fill: { flex: 1 },\n center: { flex: 1, alignItems: 'center', justifyContent: 'center' },\n listContentEmpty: { flexGrow: 1, justifyContent: 'center' },\n moreSpinner: { paddingVertical: 12 },\n emptyInverted: { transform: [{ scaleY: -1 }], paddingVertical: 24, alignItems: 'center' },\n composerIcon: {\n width: 36,\n height: 36,\n alignItems: 'center',\n justifyContent: 'center',\n },\n composerSend: {\n width: 32,\n height: 32,\n borderRadius: 16,\n alignItems: 'center',\n justifyContent: 'center',\n },\n});\n"],"names":["_a"],"mappings":"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"}
|
|
@@ -45,22 +45,97 @@ function toEpoch(value) {
|
|
|
45
45
|
return Number.isNaN(parsed) ? 0 : parsed;
|
|
46
46
|
}
|
|
47
47
|
function toChatMessage(post, viewerId) {
|
|
48
|
-
var _a, _b, _c
|
|
48
|
+
var _a, _b, _c;
|
|
49
49
|
const author = pickAuthor(post);
|
|
50
|
+
const attachments = toChatAttachments(post);
|
|
50
51
|
return {
|
|
51
52
|
id: String(post.id),
|
|
52
|
-
body: (
|
|
53
|
+
body: bodyOf(post, attachments.length > 0),
|
|
53
54
|
createdAt: toEpoch(post.createdAt),
|
|
54
55
|
authorId: (author == null ? void 0 : author.id) ? String(author.id) : null,
|
|
55
56
|
authorName: displayNameOf(author),
|
|
56
|
-
authorAvatar: (
|
|
57
|
+
authorAvatar: (_c = (_b = author == null ? void 0 : author.picture) != null ? _b : (_a = author == null ? void 0 : author.profile) == null ? void 0 : _a.picture) != null ? _c : null,
|
|
57
58
|
isMine: !!(author == null ? void 0 : author.id) && String(author.id) === viewerId,
|
|
58
59
|
isDeleted: !!post.deletedAt,
|
|
59
60
|
isEdited: !!post.editedAt,
|
|
60
|
-
attachments
|
|
61
|
+
attachments,
|
|
61
62
|
reactions: toChatReactions(post.reactionSummary, viewerId)
|
|
62
63
|
};
|
|
63
64
|
}
|
|
65
|
+
function kindOfMime(mimeType) {
|
|
66
|
+
if (mimeType.startsWith("image/")) return "image";
|
|
67
|
+
if (mimeType.startsWith("audio/")) return "audio";
|
|
68
|
+
return "file";
|
|
69
|
+
}
|
|
70
|
+
function fileKeyFromUrl(url) {
|
|
71
|
+
const value = (url != null ? url : "").trim();
|
|
72
|
+
if (!value) return null;
|
|
73
|
+
const resized = value.match(/\/fit-in\/\d+x\d+\/(.+)$/);
|
|
74
|
+
if (resized) return decodeURIComponent(resized[1].split("?")[0]);
|
|
75
|
+
if (!/^[a-z]+:\/\//i.test(value)) return value.split("?")[0];
|
|
76
|
+
if (/^(file|content|ph|assets-library):/i.test(value)) return null;
|
|
77
|
+
try {
|
|
78
|
+
return decodeURIComponent(new URL(value).pathname.replace(/^\/+/, "")) || null;
|
|
79
|
+
} catch (e) {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
function attachmentUrl(attachment, fileBaseUrl) {
|
|
84
|
+
if (!attachment.key || !fileBaseUrl) return attachment.url;
|
|
85
|
+
return `${fileBaseUrl.replace(/\/+$/, "")}/${attachment.key}`;
|
|
86
|
+
}
|
|
87
|
+
function propsOf(post) {
|
|
88
|
+
const raw = post == null ? void 0 : post.props;
|
|
89
|
+
if (raw && typeof raw === "object") return raw;
|
|
90
|
+
if (typeof raw === "string") {
|
|
91
|
+
try {
|
|
92
|
+
const parsed = JSON.parse(raw);
|
|
93
|
+
return parsed && typeof parsed === "object" ? parsed : {};
|
|
94
|
+
} catch (e) {
|
|
95
|
+
return {};
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return {};
|
|
99
|
+
}
|
|
100
|
+
function toChatAttachments(post) {
|
|
101
|
+
var _a, _b;
|
|
102
|
+
const props = propsOf(post);
|
|
103
|
+
const durationMs = typeof props.voiceDurationMs === "number" ? props.voiceDurationMs : null;
|
|
104
|
+
return ((_b = (_a = post.files) == null ? void 0 : _a.data) != null ? _b : []).filter((f) => Boolean(f == null ? void 0 : f.url)).map((f, i) => {
|
|
105
|
+
var _a2, _b2, _c, _d, _e;
|
|
106
|
+
const mimeType = (_a2 = f.mimeType) != null ? _a2 : "application/octet-stream";
|
|
107
|
+
const kind = kindOfMime(mimeType);
|
|
108
|
+
return {
|
|
109
|
+
id: String((_b2 = f.id) != null ? _b2 : `${post.id}-${i}`),
|
|
110
|
+
kind,
|
|
111
|
+
name: (_c = f.name) != null ? _c : "file",
|
|
112
|
+
mimeType,
|
|
113
|
+
url: String(f.url),
|
|
114
|
+
key: fileKeyFromUrl(f.url),
|
|
115
|
+
width: (_d = f.width) != null ? _d : null,
|
|
116
|
+
height: (_e = f.height) != null ? _e : null,
|
|
117
|
+
durationMs: kind === "audio" ? durationMs : null
|
|
118
|
+
};
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
const ATTACHMENT_LABELS = {
|
|
122
|
+
image: "\u{1F4F7} Photo",
|
|
123
|
+
audio: "\u{1F3A4} Voice message",
|
|
124
|
+
file: "\u{1F4CE} Attachment"
|
|
125
|
+
};
|
|
126
|
+
function attachmentPreview(post) {
|
|
127
|
+
const kind = propsOf(post).attachmentKind;
|
|
128
|
+
return typeof kind === "string" && kind in ATTACHMENT_LABELS ? ATTACHMENT_LABELS[kind] : "";
|
|
129
|
+
}
|
|
130
|
+
const LABEL_TEXTS = new Set(Object.values(ATTACHMENT_LABELS));
|
|
131
|
+
function bodyOf(post, hasAttachments) {
|
|
132
|
+
var _a;
|
|
133
|
+
const body = (_a = post.message) != null ? _a : "";
|
|
134
|
+
const trimmed = body.trim();
|
|
135
|
+
const label = attachmentPreview(post);
|
|
136
|
+
if (label && trimmed === label) return "";
|
|
137
|
+
return hasAttachments && LABEL_TEXTS.has(trimmed) ? "" : body;
|
|
138
|
+
}
|
|
64
139
|
function toChatReactions(summary, viewerId) {
|
|
65
140
|
return (summary != null ? summary : []).filter((r) => r && r.reaction && r.count > 0).map((r) => {
|
|
66
141
|
var _a;
|
|
@@ -113,6 +188,17 @@ function mergeMessages(existing, incoming) {
|
|
|
113
188
|
}
|
|
114
189
|
function reconcileOptimistic(messages, confirmed, windowMs = 6e4) {
|
|
115
190
|
if (!confirmed.isMine) return mergeMessages(messages, [confirmed]);
|
|
191
|
+
const sameIdIdx = messages.findIndex((m) => m.id === confirmed.id && m.sendState === "sending");
|
|
192
|
+
if (sameIdIdx !== -1) {
|
|
193
|
+
const next2 = messages.slice();
|
|
194
|
+
const pending = messages[sameIdIdx];
|
|
195
|
+
next2[sameIdIdx] = __spreadProps(__spreadValues({}, confirmed), {
|
|
196
|
+
// Keep the local previews until the server copy lists its files.
|
|
197
|
+
attachments: confirmed.attachments.length ? confirmed.attachments : pending.attachments,
|
|
198
|
+
sendState: "sent"
|
|
199
|
+
});
|
|
200
|
+
return next2;
|
|
201
|
+
}
|
|
116
202
|
const pendingIdx = messages.findIndex((m) => m.sendState === "sending" && m.isMine && m.body === confirmed.body && // Same author, not merely both "mine" - guards the same collision
|
|
117
203
|
// when authorId is populated on both sides.
|
|
118
204
|
(!m.authorId || !confirmed.authorId || m.authorId === confirmed.authorId) && Math.abs(m.createdAt - confirmed.createdAt) <= windowMs);
|
|
@@ -172,7 +258,7 @@ function toConversation(channel, viewerId) {
|
|
|
172
258
|
const storedLabel = !looksLikeMachineTitle(channel.displayName) ? channel.displayName.trim() : !looksLikeMachineTitle(channel.title) ? channel.title.trim() : "";
|
|
173
259
|
const title = isDirect ? displayNameOf(peer, storedLabel || lastAuthorName || "Direct message") : storedLabel || groupFallback;
|
|
174
260
|
const topic = looksLikeMachineTitle(channel.topic) ? "" : ((_c = channel.topic) != null ? _c : "").trim();
|
|
175
|
-
const preview = ((_e = (_d = channel.lastMessage) == null ? void 0 : _d.message) == null ? void 0 : _e.trim()) || topic;
|
|
261
|
+
const preview = ((_e = (_d = channel.lastMessage) == null ? void 0 : _d.message) == null ? void 0 : _e.trim()) || attachmentPreview(channel.lastMessage) || topic;
|
|
176
262
|
return {
|
|
177
263
|
id: String(channel.id),
|
|
178
264
|
title,
|
|
@@ -219,7 +305,7 @@ function mergeConversations(existing, incoming) {
|
|
|
219
305
|
}
|
|
220
306
|
function patchConversationAfterPost(conv, post, viewerId) {
|
|
221
307
|
var _a;
|
|
222
|
-
const preview = (_a = post.message) == null ? void 0 : _a.trim();
|
|
308
|
+
const preview = ((_a = post.message) == null ? void 0 : _a.trim()) || attachmentPreview(post);
|
|
223
309
|
const nextSubtitle = preview == null || preview === "" ? conv.subtitle : preview === conv.title ? "" : preview;
|
|
224
310
|
const activity = toEpoch(post.createdAt) || Date.now();
|
|
225
311
|
const author = pickAuthor(post);
|
|
@@ -275,4 +361,4 @@ function initialsOf(name) {
|
|
|
275
361
|
if (!parts.length) return "?";
|
|
276
362
|
if (parts.length === 1) return parts[0].slice(0, 2).toUpperCase();
|
|
277
363
|
return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase();
|
|
278
|
-
}export{applyReactionSummary,conversationListKey,dayLabel,dedupeConversationList,displayNameOf,initialsOf,mergeConversations,mergeMessages,patchConversationAfterPost,peerOf,pickAuthor,reconcileOptimistic,relativeLabel,sortConversations,timeLabel,toChatMessage,toChatReactions,toConversation,toEpoch,unreadOf,withViewerReaction};//# sourceMappingURL=chatModel.js.map
|
|
364
|
+
}export{ATTACHMENT_LABELS,applyReactionSummary,attachmentPreview,attachmentUrl,conversationListKey,dayLabel,dedupeConversationList,displayNameOf,fileKeyFromUrl,initialsOf,mergeConversations,mergeMessages,patchConversationAfterPost,peerOf,pickAuthor,reconcileOptimistic,relativeLabel,sortConversations,timeLabel,toChatAttachments,toChatMessage,toChatReactions,toConversation,toEpoch,unreadOf,withViewerReaction};//# sourceMappingURL=chatModel.js.map
|