opendesk-react-native 0.1.2 → 0.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +157 -133
- package/assets/chime.wav +0 -0
- package/lib/api.js +25 -8
- package/lib/client.d.ts +9 -0
- package/lib/client.js +14 -0
- package/lib/errors.d.ts +6 -2
- package/lib/errors.js +6 -2
- package/lib/index.d.ts +2 -0
- package/lib/index.js +3 -1
- package/lib/realtime.d.ts +5 -0
- package/lib/realtime.js +9 -1
- package/lib/sound.d.ts +22 -0
- package/lib/sound.js +33 -0
- package/lib/types.d.ts +2 -0
- package/lib/ui/OpenDeskChat.d.ts +5 -1
- package/lib/ui/OpenDeskChat.js +41 -11
- package/lib/ui/keyboard.d.ts +15 -0
- package/lib/ui/keyboard.js +62 -0
- package/lib/ui/strings.d.ts +2 -0
- package/lib/ui/strings.js +2 -0
- package/package.json +46 -45
- package/src/api.ts +195 -172
- package/src/assets.d.ts +5 -0
- package/src/client.ts +680 -659
- package/src/errors.ts +20 -16
- package/src/index.ts +27 -25
- package/src/realtime.ts +174 -166
- package/src/sound.ts +40 -0
- package/src/types.ts +202 -200
- package/src/ui/OpenDeskChat.tsx +1408 -1318
- package/src/ui/keyboard.ts +68 -0
- package/src/ui/strings.ts +59 -57
package/lib/ui/OpenDeskChat.js
CHANGED
|
@@ -7,6 +7,7 @@ const react_native_1 = require("react-native");
|
|
|
7
7
|
const react_2 = require("../react");
|
|
8
8
|
const BotFace_1 = require("./BotFace");
|
|
9
9
|
const icons_1 = require("./icons");
|
|
10
|
+
const keyboard_1 = require("./keyboard");
|
|
10
11
|
const strings_1 = require("./strings");
|
|
11
12
|
const theme_1 = require("./theme");
|
|
12
13
|
const MAX_UPLOAD_BYTES = 25 * 1024 * 1024;
|
|
@@ -26,8 +27,10 @@ function OpenDeskChat({ initialConversationId, onClose, pickAttachment, accentCo
|
|
|
26
27
|
const strings = (0, react_1.useMemo)(() => ({ ...strings_1.defaultStrings, ...stringOverrides }), [stringOverrides]);
|
|
27
28
|
const styles = (0, react_1.useMemo)(() => createStyles(theme), [theme]);
|
|
28
29
|
const ui = (0, react_1.useMemo)(() => ({ theme, strings, styles, bot: config?.bot ?? null }), [theme, strings, styles, config?.bot]);
|
|
30
|
+
const rootRef = (0, react_1.useRef)(null);
|
|
31
|
+
const keyboard = (0, keyboard_1.useKeyboardInset)(rootRef, keyboardVerticalOffset);
|
|
29
32
|
const inThread = conversationId !== null || composing;
|
|
30
|
-
return ((0, jsx_runtime_1.jsx)(react_native_1.
|
|
33
|
+
return ((0, jsx_runtime_1.jsx)(react_native_1.View, { ref: rootRef, style: [styles.root, { paddingBottom: keyboard.inset }], onLayout: keyboard.onLayout, children: inThread ? ((0, jsx_runtime_1.jsx)(Thread, { ui: ui, conversationId: conversationId, pickAttachment: pickAttachment, onBack: () => {
|
|
31
34
|
setConversationId(null);
|
|
32
35
|
setComposing(false);
|
|
33
36
|
}, onClose: onClose, onStarted: (id) => {
|
|
@@ -67,6 +70,12 @@ function Thread({ ui, conversationId, pickAttachment, onBack, onClose, onStarted
|
|
|
67
70
|
const [focused, setFocused] = (0, react_1.useState)(false);
|
|
68
71
|
const [inputHeight, setInputHeight] = (0, react_1.useState)(30);
|
|
69
72
|
const [error, setError] = (0, react_1.useState)(null);
|
|
73
|
+
const [viewing, setViewing] = (0, react_1.useState)(null);
|
|
74
|
+
const openImage = (0, react_1.useCallback)((attachment) => {
|
|
75
|
+
react_native_1.Keyboard.dismiss();
|
|
76
|
+
setViewing(attachment);
|
|
77
|
+
}, []);
|
|
78
|
+
const closeImage = (0, react_1.useCallback)(() => setViewing(null), []);
|
|
70
79
|
const topics = config?.topics ?? [];
|
|
71
80
|
const topic = topics.find((t) => t.id === topicId) ?? null;
|
|
72
81
|
const needsTopic = conversationId === null && topics.length > 0 && topic === null;
|
|
@@ -160,14 +169,14 @@ function Thread({ ui, conversationId, pickAttachment, onBack, onClose, onStarted
|
|
|
160
169
|
}, [client, config?.attachment_extensions, pickAttachment, strings]);
|
|
161
170
|
const showAttach = Boolean(pickAttachment) && config?.attachments !== false && (config?.attachment_extensions?.length ?? 1) > 0;
|
|
162
171
|
const bot = config?.ai_answers ? config.bot : null;
|
|
163
|
-
return ((0, jsx_runtime_1.jsxs)(react_native_1.View, { style: styles.flex, children: [(0, jsx_runtime_1.jsxs)(react_native_1.View, { style: styles.head, children: [(0, jsx_runtime_1.jsx)(IconButton, { ui: ui, label: strings.back, onPress: onBack, icon: (0, jsx_runtime_1.jsx)(icons_1.ChevronIcon, { color: theme.fg3, direction: "left" }) }), bot ? ((0, jsx_runtime_1.jsx)(BotFace_1.BotFace, { size: 26, look: bot.look, avatarUrl: bot.avatar_url })) : ((0, jsx_runtime_1.jsx)(Glyph, { ui: ui, name: name, url: config?.avatar_url ?? null })), (0, jsx_runtime_1.jsxs)(react_native_1.View, { style: styles.headMain, children: [(0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.title, numberOfLines: 1, children: conversation?.subject || name }), (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.titleSub, numberOfLines: 1, children: subtitleFor(config, strings) })] }), onClose ? (0, jsx_runtime_1.jsx)(IconButton, { ui: ui, label: strings.closeChat, onPress: onClose, icon: (0, jsx_runtime_1.jsx)(icons_1.CloseIcon, { color: theme.fg3 }) }) : null] }), rows.length === 0 ? ((0, jsx_runtime_1.jsx)(react_native_1.ScrollView, { style: styles.flex, contentContainerStyle: styles.emptyScroll, keyboardShouldPersistTaps: "handled", children: needsTopic ? ((0, jsx_runtime_1.jsxs)(react_native_1.View, { style: [styles.card, styles.picker], children: [(0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.pickerTitle, children: strings.topicsTitle }), (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.pickerHint, children: strings.topicsHint }), topics.map((item, index) => ((0, jsx_runtime_1.jsxs)(react_native_1.Pressable, { accessibilityRole: "button", onPress: () => setTopicId(item.id), style: ({ pressed }) => [styles.topic, index > 0 && styles.convBorder, pressed && { backgroundColor: theme.bg2 }], children: [(0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.topicText, children: item.label }), (0, jsx_runtime_1.jsx)(icons_1.ChevronIcon, { color: theme.fg4, size: 14, direction: "right" })] }, item.id)))] })) : ((0, jsx_runtime_1.jsxs)(react_native_1.View, { style: styles.empty, children: [(0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.emptyTitle, children: config?.greeting || strings.greeting }), (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.emptyText, children: config?.intro || strings.emptyHint })] })) })) : ((0, jsx_runtime_1.jsx)(react_native_1.FlatList, { inverted: true, style: styles.flex, data: rows, keyExtractor: (row) => row.key, contentContainerStyle: styles.thread, keyboardShouldPersistTaps: "handled", renderItem: ({ item }) => ((0, jsx_runtime_1.jsx)(ThreadRow, { ui: ui, row: item, showHandoff: item.kind === 'message' && item.message.id === handoffMessageId, onDecide: decide, onRequestHuman: requestHuman })) })), (0, jsx_runtime_1.jsxs)(react_native_1.View, { style: styles.composer, children: [topic && conversationId === null ? ((0, jsx_runtime_1.jsx)(react_native_1.View, { style: styles.topicRow, children: (0, jsx_runtime_1.jsxs)(react_native_1.View, { style: styles.topicChip, children: [(0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.topicChipText, numberOfLines: 1, children: topic.label }), (0, jsx_runtime_1.jsx)(react_native_1.Pressable, { accessibilityRole: "button", accessibilityLabel: strings.changeTopic, onPress: () => setTopicId(null), style: styles.topicChipX, children: (0, jsx_runtime_1.jsx)(icons_1.CloseIcon, { color: theme.fg3, size: 10 }) })] }) })) : null, attachments.length > 0 || uploadingName ? ((0, jsx_runtime_1.jsxs)(react_native_1.View, { style: styles.pendingFiles, children: [attachments.map((attachment) => ((0, jsx_runtime_1.jsxs)(react_native_1.View, { style: [styles.chip, styles.pendingChip], children: [(0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.chipText, numberOfLines: 1, children: attachment.filename }), (0, jsx_runtime_1.jsx)(react_native_1.Pressable, { accessibilityRole: "button", accessibilityLabel: (0, strings_1.format)(strings.removeFile, { name: attachment.filename }), onPress: () => setAttachments((current) => current.filter((a) => a.id !== attachment.id)), hitSlop: 8, children: (0, jsx_runtime_1.jsx)(icons_1.CloseIcon, { color: theme.fg3, size: 10 }) })] }, attachment.id))), uploadingName ? ((0, jsx_runtime_1.jsxs)(react_native_1.View, { style: [styles.chip, styles.pendingChip], children: [(0, jsx_runtime_1.jsx)(react_native_1.ActivityIndicator, { size: "small", color: theme.fg3, style: styles.chipSpinner }), (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.chipText, numberOfLines: 1, children: uploadingName })] })) : null] })) : null, error ? (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.err, children: error }) : null, (0, jsx_runtime_1.jsxs)(react_native_1.View, { style: [styles.cbar, showAttach && styles.cbarLead, focused && styles.cbarFocus, needsTopic && styles.cbarOff], children: [showAttach ? ((0, jsx_runtime_1.jsx)(IconButton, { ui: ui, label: strings.attach, onPress: attach, disabled: uploadingName !== null || needsTopic, icon: (0, jsx_runtime_1.jsx)(icons_1.PaperclipIcon, { color: theme.fg3 }) })) : null, (0, jsx_runtime_1.jsx)(react_native_1.TextInput, { style: [styles.input, { height: inputHeight }], value: draft, onContentSizeChange: (event) => setInputHeight(Math.min(120, Math.max(30, Math.ceil(event.nativeEvent.contentSize.height)))), ...(react_native_1.Platform.OS === 'web' ? { rows: 1 } : {}), editable: !needsTopic, onFocus: () => setFocused(true), onBlur: () => setFocused(false), onChangeText: (text) => {
|
|
172
|
+
return ((0, jsx_runtime_1.jsxs)(react_native_1.View, { style: styles.flex, children: [(0, jsx_runtime_1.jsxs)(react_native_1.View, { style: styles.head, children: [(0, jsx_runtime_1.jsx)(IconButton, { ui: ui, label: strings.back, onPress: onBack, icon: (0, jsx_runtime_1.jsx)(icons_1.ChevronIcon, { color: theme.fg3, direction: "left" }) }), bot ? ((0, jsx_runtime_1.jsx)(BotFace_1.BotFace, { size: 26, look: bot.look, avatarUrl: bot.avatar_url })) : ((0, jsx_runtime_1.jsx)(Glyph, { ui: ui, name: name, url: config?.avatar_url ?? null })), (0, jsx_runtime_1.jsxs)(react_native_1.View, { style: styles.headMain, children: [(0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.title, numberOfLines: 1, children: conversation?.subject || name }), (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.titleSub, numberOfLines: 1, children: subtitleFor(config, strings) })] }), onClose ? (0, jsx_runtime_1.jsx)(IconButton, { ui: ui, label: strings.closeChat, onPress: onClose, icon: (0, jsx_runtime_1.jsx)(icons_1.CloseIcon, { color: theme.fg3 }) }) : null] }), rows.length === 0 ? ((0, jsx_runtime_1.jsx)(react_native_1.ScrollView, { style: styles.flex, contentContainerStyle: styles.emptyScroll, keyboardShouldPersistTaps: "handled", children: needsTopic ? ((0, jsx_runtime_1.jsxs)(react_native_1.View, { style: [styles.card, styles.picker], children: [(0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.pickerTitle, children: strings.topicsTitle }), (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.pickerHint, children: strings.topicsHint }), topics.map((item, index) => ((0, jsx_runtime_1.jsxs)(react_native_1.Pressable, { accessibilityRole: "button", onPress: () => setTopicId(item.id), style: ({ pressed }) => [styles.topic, index > 0 && styles.convBorder, pressed && { backgroundColor: theme.bg2 }], children: [(0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.topicText, children: item.label }), (0, jsx_runtime_1.jsx)(icons_1.ChevronIcon, { color: theme.fg4, size: 14, direction: "right" })] }, item.id)))] })) : ((0, jsx_runtime_1.jsxs)(react_native_1.View, { style: styles.empty, children: [(0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.emptyTitle, children: config?.greeting || strings.greeting }), (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.emptyText, children: config?.intro || strings.emptyHint })] })) })) : ((0, jsx_runtime_1.jsx)(react_native_1.FlatList, { inverted: true, style: styles.flex, data: rows, keyExtractor: (row) => row.key, contentContainerStyle: styles.thread, keyboardShouldPersistTaps: "handled", renderItem: ({ item }) => ((0, jsx_runtime_1.jsx)(ThreadRow, { ui: ui, row: item, showHandoff: item.kind === 'message' && item.message.id === handoffMessageId, onDecide: decide, onRequestHuman: requestHuman, onOpenImage: openImage })) })), (0, jsx_runtime_1.jsxs)(react_native_1.View, { style: styles.composer, children: [topic && conversationId === null ? ((0, jsx_runtime_1.jsx)(react_native_1.View, { style: styles.topicRow, children: (0, jsx_runtime_1.jsxs)(react_native_1.View, { style: styles.topicChip, children: [(0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.topicChipText, numberOfLines: 1, children: topic.label }), (0, jsx_runtime_1.jsx)(react_native_1.Pressable, { accessibilityRole: "button", accessibilityLabel: strings.changeTopic, onPress: () => setTopicId(null), style: styles.topicChipX, children: (0, jsx_runtime_1.jsx)(icons_1.CloseIcon, { color: theme.fg3, size: 10 }) })] }) })) : null, attachments.length > 0 || uploadingName ? ((0, jsx_runtime_1.jsxs)(react_native_1.View, { style: styles.pendingFiles, children: [attachments.map((attachment) => ((0, jsx_runtime_1.jsxs)(react_native_1.View, { style: [styles.chip, styles.pendingChip], children: [(0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.chipText, numberOfLines: 1, children: attachment.filename }), (0, jsx_runtime_1.jsx)(react_native_1.Pressable, { accessibilityRole: "button", accessibilityLabel: (0, strings_1.format)(strings.removeFile, { name: attachment.filename }), onPress: () => setAttachments((current) => current.filter((a) => a.id !== attachment.id)), hitSlop: 8, children: (0, jsx_runtime_1.jsx)(icons_1.CloseIcon, { color: theme.fg3, size: 10 }) })] }, attachment.id))), uploadingName ? ((0, jsx_runtime_1.jsxs)(react_native_1.View, { style: [styles.chip, styles.pendingChip], children: [(0, jsx_runtime_1.jsx)(react_native_1.ActivityIndicator, { size: "small", color: theme.fg3, style: styles.chipSpinner }), (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.chipText, numberOfLines: 1, children: uploadingName })] })) : null] })) : null, error ? (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.err, children: error }) : null, (0, jsx_runtime_1.jsxs)(react_native_1.View, { style: [styles.cbar, showAttach && styles.cbarLead, focused && styles.cbarFocus, needsTopic && styles.cbarOff], children: [showAttach ? ((0, jsx_runtime_1.jsx)(IconButton, { ui: ui, label: strings.attach, onPress: attach, disabled: uploadingName !== null || needsTopic, icon: (0, jsx_runtime_1.jsx)(icons_1.PaperclipIcon, { color: theme.fg3 }) })) : null, (0, jsx_runtime_1.jsx)(react_native_1.TextInput, { style: [styles.input, { height: inputHeight }], value: draft, onContentSizeChange: (event) => setInputHeight(Math.min(120, Math.max(30, Math.ceil(event.nativeEvent.contentSize.height)))), ...(react_native_1.Platform.OS === 'web' ? { rows: 1 } : {}), editable: !needsTopic, onFocus: () => setFocused(true), onBlur: () => setFocused(false), onChangeText: (text) => {
|
|
164
173
|
setDraft(text);
|
|
165
174
|
if (conversationId) {
|
|
166
175
|
client.typing(conversationId);
|
|
167
176
|
}
|
|
168
|
-
}, placeholder: needsTopic ? strings.placeholderTopic : strings.placeholder, placeholderTextColor: theme.fg4, accessibilityLabel: strings.placeholder, multiline: true }), (0, jsx_runtime_1.jsx)(react_native_1.Pressable, { accessibilityRole: "button", accessibilityLabel: strings.send, disabled: !ready, onPress: send, style: [styles.send, ready && styles.sendReady], children: (0, jsx_runtime_1.jsx)(icons_1.ArrowIcon, { color: theme.accentFg, size: 14, angle: -90 }) })] })] })] }));
|
|
177
|
+
}, placeholder: needsTopic ? strings.placeholderTopic : strings.placeholder, placeholderTextColor: theme.fg4, accessibilityLabel: strings.placeholder, multiline: true }), (0, jsx_runtime_1.jsx)(react_native_1.Pressable, { accessibilityRole: "button", accessibilityLabel: strings.send, disabled: !ready, onPress: send, style: [styles.send, ready && styles.sendReady], children: (0, jsx_runtime_1.jsx)(icons_1.ArrowIcon, { color: theme.accentFg, size: 14, angle: -90 }) })] })] }), viewing ? (0, jsx_runtime_1.jsx)(ImageViewer, { ui: ui, attachment: viewing, onClose: closeImage }) : null] }));
|
|
169
178
|
}
|
|
170
|
-
const ThreadRow = (0, react_1.memo)(function ThreadRow({ ui, row, showHandoff, onDecide, onRequestHuman, }) {
|
|
179
|
+
const ThreadRow = (0, react_1.memo)(function ThreadRow({ ui, row, showHandoff, onDecide, onRequestHuman, onOpenImage, }) {
|
|
171
180
|
const { theme, strings, styles } = ui;
|
|
172
181
|
switch (row.kind) {
|
|
173
182
|
case 'day':
|
|
@@ -177,16 +186,16 @@ const ThreadRow = (0, react_1.memo)(function ThreadRow({ ui, row, showHandoff, o
|
|
|
177
186
|
case 'handed':
|
|
178
187
|
return ((0, jsx_runtime_1.jsxs)(react_native_1.View, { style: styles.handed, children: [(0, jsx_runtime_1.jsx)(react_native_1.View, { style: styles.handedIc, children: (0, jsx_runtime_1.jsx)(icons_1.PersonIcon, { color: theme.fg2 }) }), (0, jsx_runtime_1.jsxs)(react_native_1.View, { style: styles.flex, children: [(0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.handedTitle, children: strings.handedOff }), (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.handedText, children: strings.handedOffHint })] })] }));
|
|
179
188
|
case 'reply':
|
|
180
|
-
return (0, jsx_runtime_1.jsx)(Bubble, { ui: ui, reply: row.reply, first: true, last: true, gap: true, showHandoff: false, onDecide: onDecide, onRequestHuman: onRequestHuman });
|
|
189
|
+
return ((0, jsx_runtime_1.jsx)(Bubble, { ui: ui, reply: row.reply, first: true, last: true, gap: true, showHandoff: false, onDecide: onDecide, onRequestHuman: onRequestHuman, onOpenImage: onOpenImage }));
|
|
181
190
|
case 'message':
|
|
182
|
-
return ((0, jsx_runtime_1.jsx)(Bubble, { ui: ui, message: row.message, first: row.first, last: row.last, gap: row.gap, showHandoff: showHandoff, onDecide: onDecide, onRequestHuman: onRequestHuman }));
|
|
191
|
+
return ((0, jsx_runtime_1.jsx)(Bubble, { ui: ui, message: row.message, first: row.first, last: row.last, gap: row.gap, showHandoff: showHandoff, onDecide: onDecide, onRequestHuman: onRequestHuman, onOpenImage: onOpenImage }));
|
|
183
192
|
}
|
|
184
193
|
});
|
|
185
194
|
/**
|
|
186
195
|
* One bubble for a stored message or an AI answer still streaming. Both share one element
|
|
187
196
|
* tree, so when the stored message replaces the stream (same row key) nothing remounts.
|
|
188
197
|
*/
|
|
189
|
-
function Bubble({ ui, message, reply, first, last, gap, showHandoff, onDecide, onRequestHuman, }) {
|
|
198
|
+
function Bubble({ ui, message, reply, first, last, gap, showHandoff, onDecide, onRequestHuman, onOpenImage, }) {
|
|
190
199
|
const { theme, strings, styles, bot: fallbackBot } = ui;
|
|
191
200
|
const mine = message?.direction === 'inbound';
|
|
192
201
|
const ai = reply !== undefined || message?.ai === true;
|
|
@@ -210,7 +219,7 @@ function Bubble({ ui, message, reply, first, last, gap, showHandoff, onDecide, o
|
|
|
210
219
|
mine ? styles.msgMe : ai ? styles.msgAi : styles.msgThem,
|
|
211
220
|
corners,
|
|
212
221
|
local && styles.sendingMsg,
|
|
213
|
-
], children: text ? (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: [styles.msgText, mine && { color: theme.accentFg }], children: text }) : (0, jsx_runtime_1.jsx)(Dots, { color: theme.ai }) })) : null, images.map((attachment) => ((0, jsx_runtime_1.jsx)(react_native_1.Pressable, { onPress: () =>
|
|
222
|
+
], children: text ? (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: [styles.msgText, mine && { color: theme.accentFg }], children: text }) : (0, jsx_runtime_1.jsx)(Dots, { color: theme.ai }) })) : null, images.map((attachment) => ((0, jsx_runtime_1.jsx)(react_native_1.Pressable, { accessibilityRole: "imagebutton", accessibilityLabel: (0, strings_1.format)(strings.viewImage, { name: attachment.filename }), onPress: () => onOpenImage(attachment), style: ({ pressed }) => [styles.fileGap, pressed && styles.pressed], children: (0, jsx_runtime_1.jsx)(react_native_1.Image, { source: { uri: attachment.url }, style: styles.thumb, resizeMode: "cover" }) }, attachment.id))), files.map((attachment) => ((0, jsx_runtime_1.jsxs)(react_native_1.Pressable, { onPress: () => react_native_1.Linking.openURL(attachment.url), style: [styles.chip, styles.fileGap], children: [(0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.chipText, numberOfLines: 1, children: attachment.filename }), (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.chipSize, children: fileSize(attachment.size_bytes) })] }, attachment.id))), message && action ? (0, jsx_runtime_1.jsx)(ActionCard, { ui: ui, message: message, onDecide: onDecide }) : null, message && showHandoff ? ((0, jsx_runtime_1.jsxs)(react_native_1.Pressable, { accessibilityRole: "button", onPress: () => onRequestHuman(message), style: ({ pressed }) => [styles.handoff, pressed && { backgroundColor: theme.bg2 }], children: [(0, jsx_runtime_1.jsx)(icons_1.PersonIcon, { color: theme.fg, size: 14 }), (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.handoffText, children: strings.talkToPerson })] })) : null, last && message ? (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.meta, children: local ? strings.sending : clockTime(message.created_at) }) : null] }));
|
|
214
223
|
}
|
|
215
224
|
function ActionCard({ ui, message, onDecide, }) {
|
|
216
225
|
const { theme, strings, styles } = ui;
|
|
@@ -225,6 +234,21 @@ function ActionCard({ ui, message, onDecide, }) {
|
|
|
225
234
|
}[action.status];
|
|
226
235
|
return ((0, jsx_runtime_1.jsxs)(react_native_1.View, { style: [styles.action, pending && styles.actionPending], children: [(0, jsx_runtime_1.jsxs)(react_native_1.View, { style: styles.actionTop, children: [(0, jsx_runtime_1.jsxs)(react_native_1.View, { style: styles.actionTool, children: [(0, jsx_runtime_1.jsx)(icons_1.SparkIcon, { color: theme.ai, size: 8 }), (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.actionToolText, numberOfLines: 1, children: action.tool.replace(/_/g, ' ') })] }), (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: [styles.actionNeed, action.status === 'failed' && { color: theme.danger }], children: label })] }), (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.actionTitle, children: action.title }), action.summary ? (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.actionSum, children: action.summary }) : null, pending ? ((0, jsx_runtime_1.jsxs)(react_native_1.View, { style: styles.actionBtns, children: [(0, jsx_runtime_1.jsx)(react_native_1.Pressable, { onPress: () => onDecide(message, 'decline'), style: [styles.btn, styles.btnGhost], children: (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.btnGhostText, children: strings.cancel }) }), (0, jsx_runtime_1.jsx)(react_native_1.Pressable, { onPress: () => onDecide(message, 'confirm'), style: [styles.btn, styles.btnSolid], children: (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.btnSolidText, children: strings.confirm }) })] })) : null] }));
|
|
227
236
|
}
|
|
237
|
+
/** An attached image shown full size over the thread, so it opens in the chat instead of a browser. */
|
|
238
|
+
function ImageViewer({ ui, attachment, onClose }) {
|
|
239
|
+
const { strings, styles } = ui;
|
|
240
|
+
(0, react_1.useEffect)(() => {
|
|
241
|
+
if (react_native_1.Platform.OS !== 'android') {
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
const subscription = react_native_1.BackHandler.addEventListener('hardwareBackPress', () => {
|
|
245
|
+
onClose();
|
|
246
|
+
return true;
|
|
247
|
+
});
|
|
248
|
+
return () => subscription.remove();
|
|
249
|
+
}, [onClose]);
|
|
250
|
+
return ((0, jsx_runtime_1.jsxs)(react_native_1.View, { style: styles.viewer, accessibilityViewIsModal: true, children: [(0, jsx_runtime_1.jsxs)(react_native_1.View, { style: styles.viewerHead, children: [(0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.viewerName, numberOfLines: 1, children: attachment.filename }), (0, jsx_runtime_1.jsx)(react_native_1.Pressable, { accessibilityRole: "button", accessibilityLabel: strings.closeImage, onPress: onClose, hitSlop: 6, style: ({ pressed }) => [styles.iconBtn, pressed && styles.viewerPressed], children: (0, jsx_runtime_1.jsx)(icons_1.CloseIcon, { color: "#ffffff" }) })] }), (0, jsx_runtime_1.jsx)(react_native_1.ScrollView, { style: styles.flex, contentContainerStyle: styles.viewerBody, maximumZoomScale: 4, minimumZoomScale: 1, centerContent: true, showsHorizontalScrollIndicator: false, showsVerticalScrollIndicator: false, children: (0, jsx_runtime_1.jsx)(react_native_1.Image, { source: { uri: attachment.url }, style: styles.viewerImage, resizeMode: "contain", accessibilityLabel: attachment.filename }) })] }));
|
|
251
|
+
}
|
|
228
252
|
function IconButton({ ui, label, onPress, icon, disabled, }) {
|
|
229
253
|
return ((0, jsx_runtime_1.jsx)(react_native_1.Pressable, { accessibilityRole: "button", accessibilityLabel: label, onPress: onPress, disabled: disabled, hitSlop: 6, style: ({ pressed }) => [ui.styles.iconBtn, pressed && { backgroundColor: ui.theme.bg2 }, disabled && ui.styles.disabled], children: icon }));
|
|
230
254
|
}
|
|
@@ -377,13 +401,13 @@ function createStyles(t) {
|
|
|
377
401
|
alignItems: 'center',
|
|
378
402
|
gap: 10,
|
|
379
403
|
paddingHorizontal: 12,
|
|
380
|
-
paddingTop:
|
|
381
|
-
paddingBottom:
|
|
404
|
+
paddingTop: 18,
|
|
405
|
+
paddingBottom: 14,
|
|
382
406
|
borderBottomWidth: 1,
|
|
383
407
|
borderBottomColor: t.line,
|
|
384
408
|
backgroundColor: t.bg,
|
|
385
409
|
},
|
|
386
|
-
headHome: { flexDirection: 'column', alignItems: 'stretch', gap: 0, padding: 16, paddingBottom: 20, backgroundColor: t.headBg },
|
|
410
|
+
headHome: { flexDirection: 'column', alignItems: 'stretch', gap: 0, padding: 16, paddingTop: 22, paddingBottom: 20, backgroundColor: t.headBg },
|
|
387
411
|
headRow: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', gap: 8 },
|
|
388
412
|
headMain: { flex: 1, minWidth: 0 },
|
|
389
413
|
ws: { flexDirection: 'row', alignItems: 'center', gap: 9, flexShrink: 1 },
|
|
@@ -489,6 +513,12 @@ function createStyles(t) {
|
|
|
489
513
|
toolText: { fontSize: 12, lineHeight: 18, color: t.fg3 },
|
|
490
514
|
fileGap: { marginTop: 4 },
|
|
491
515
|
thumb: { width: 220, height: 160, borderRadius: r.card, borderWidth: 1, borderColor: t.line, backgroundColor: t.bg2 },
|
|
516
|
+
viewer: { position: 'absolute', top: 0, right: 0, bottom: 0, left: 0, zIndex: 10, elevation: 10, backgroundColor: '#000000' },
|
|
517
|
+
viewerHead: { flexDirection: 'row', alignItems: 'center', gap: 10, paddingHorizontal: 12, paddingTop: 18, paddingBottom: 14 },
|
|
518
|
+
viewerName: { flex: 1, fontSize: 14, fontWeight: '600', color: '#ffffff' },
|
|
519
|
+
viewerPressed: { backgroundColor: 'rgba(255,255,255,0.16)' },
|
|
520
|
+
viewerBody: { flexGrow: 1 },
|
|
521
|
+
viewerImage: { flex: 1, width: '100%' },
|
|
492
522
|
chip: {
|
|
493
523
|
flexDirection: 'row',
|
|
494
524
|
alignItems: 'center',
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type RefObject } from 'react';
|
|
2
|
+
import { type View } from 'react-native';
|
|
3
|
+
/**
|
|
4
|
+
* How far the keyboard covers the view in `ref`, for use as its bottom padding.
|
|
5
|
+
*
|
|
6
|
+
* KeyboardAvoidingView compares the keyboard with a layout relative to its parent, so it misses
|
|
7
|
+
* anything above that parent (a header, a sheet), and on Android it relies on the window
|
|
8
|
+
* shrinking, which edge-to-edge apps no longer do. This measures the view in window coordinates
|
|
9
|
+
* instead and works the same on both platforms. Call `onLayout` from the view's `onLayout` so a
|
|
10
|
+
* window that does shrink is measured again and the padding drops back.
|
|
11
|
+
*/
|
|
12
|
+
export declare function useKeyboardInset(ref: RefObject<View | null>, extraOffset?: number): {
|
|
13
|
+
inset: number;
|
|
14
|
+
onLayout: () => void;
|
|
15
|
+
};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.useKeyboardInset = useKeyboardInset;
|
|
4
|
+
const react_1 = require("react");
|
|
5
|
+
const react_native_1 = require("react-native");
|
|
6
|
+
/**
|
|
7
|
+
* How far the keyboard covers the view in `ref`, for use as its bottom padding.
|
|
8
|
+
*
|
|
9
|
+
* KeyboardAvoidingView compares the keyboard with a layout relative to its parent, so it misses
|
|
10
|
+
* anything above that parent (a header, a sheet), and on Android it relies on the window
|
|
11
|
+
* shrinking, which edge-to-edge apps no longer do. This measures the view in window coordinates
|
|
12
|
+
* instead and works the same on both platforms. Call `onLayout` from the view's `onLayout` so a
|
|
13
|
+
* window that does shrink is measured again and the padding drops back.
|
|
14
|
+
*/
|
|
15
|
+
function useKeyboardInset(ref, extraOffset = 0) {
|
|
16
|
+
const [inset, setInset] = (0, react_1.useState)(0);
|
|
17
|
+
const keyboard = (0, react_1.useRef)(null);
|
|
18
|
+
const measure = (0, react_1.useCallback)(() => {
|
|
19
|
+
const event = keyboard.current;
|
|
20
|
+
const view = ref.current;
|
|
21
|
+
if (!event || !view) {
|
|
22
|
+
setInset(0);
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
view.measureInWindow((_x, y, _width, height) => {
|
|
26
|
+
if (keyboard.current !== event) {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
const next = Math.max(0, Math.round(y + height - event.endCoordinates.screenY + extraOffset));
|
|
30
|
+
setInset((current) => {
|
|
31
|
+
if (current !== next) {
|
|
32
|
+
animateWithKeyboard(event);
|
|
33
|
+
}
|
|
34
|
+
return next;
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
}, [ref, extraOffset]);
|
|
38
|
+
(0, react_1.useEffect)(() => {
|
|
39
|
+
const show = (event) => {
|
|
40
|
+
keyboard.current = event;
|
|
41
|
+
measure();
|
|
42
|
+
};
|
|
43
|
+
const hide = (event) => {
|
|
44
|
+
keyboard.current = null;
|
|
45
|
+
animateWithKeyboard(event);
|
|
46
|
+
setInset(0);
|
|
47
|
+
};
|
|
48
|
+
const subscriptions = react_native_1.Platform.OS === 'ios'
|
|
49
|
+
? [react_native_1.Keyboard.addListener('keyboardWillChangeFrame', show), react_native_1.Keyboard.addListener('keyboardWillHide', hide)]
|
|
50
|
+
: [react_native_1.Keyboard.addListener('keyboardDidShow', show), react_native_1.Keyboard.addListener('keyboardDidHide', hide)];
|
|
51
|
+
return () => subscriptions.forEach((subscription) => subscription.remove());
|
|
52
|
+
}, [measure]);
|
|
53
|
+
return { inset, onLayout: measure };
|
|
54
|
+
}
|
|
55
|
+
/** iOS reports the keyboard's own curve, so the padding can move in step with it. */
|
|
56
|
+
function animateWithKeyboard(event) {
|
|
57
|
+
if (react_native_1.Platform.OS !== 'ios' || !event?.duration) {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
const type = react_native_1.LayoutAnimation.Types[event.easing] ?? react_native_1.LayoutAnimation.Types.keyboard;
|
|
61
|
+
react_native_1.LayoutAnimation.configureNext({ duration: event.duration, update: { duration: event.duration, type } });
|
|
62
|
+
}
|
package/lib/ui/strings.d.ts
CHANGED
package/lib/ui/strings.js
CHANGED
|
@@ -24,6 +24,8 @@ exports.defaultStrings = {
|
|
|
24
24
|
sending: 'Sending…',
|
|
25
25
|
uploading: 'Uploading…',
|
|
26
26
|
removeFile: 'Remove {name}',
|
|
27
|
+
viewImage: 'View {name}',
|
|
28
|
+
closeImage: 'Close image',
|
|
27
29
|
fileTypeRefused: "{name} can't be sent here. Try another file type.",
|
|
28
30
|
fileTooLarge: '{name} is larger than 25 MB.',
|
|
29
31
|
uploadFailed: "Couldn't upload {name}. Try again.",
|
package/package.json
CHANGED
|
@@ -1,45 +1,46 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "opendesk-react-native",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "React Native SDK for openDesk live chat: headless client, realtime, hooks and a drop-in chat screen.",
|
|
5
|
-
"main": "lib/index.js",
|
|
6
|
-
"types": "lib/index.d.ts",
|
|
7
|
-
"files": [
|
|
8
|
-
"lib",
|
|
9
|
-
"src",
|
|
10
|
-
"
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
"
|
|
15
|
-
"
|
|
16
|
-
"
|
|
17
|
-
"
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
"
|
|
22
|
-
"
|
|
23
|
-
"
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
"
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
"react
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
"@types/
|
|
36
|
-
"@
|
|
37
|
-
"react": "^
|
|
38
|
-
"react
|
|
39
|
-
"react-
|
|
40
|
-
"react-native
|
|
41
|
-
"
|
|
42
|
-
"
|
|
43
|
-
"
|
|
44
|
-
|
|
45
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "opendesk-react-native",
|
|
3
|
+
"version": "0.1.6",
|
|
4
|
+
"description": "React Native SDK for openDesk live chat: headless client, realtime, hooks and a drop-in chat screen.",
|
|
5
|
+
"main": "lib/index.js",
|
|
6
|
+
"types": "lib/index.d.ts",
|
|
7
|
+
"files": [
|
|
8
|
+
"lib",
|
|
9
|
+
"src",
|
|
10
|
+
"assets",
|
|
11
|
+
"!src/**/__tests__"
|
|
12
|
+
],
|
|
13
|
+
"scripts": {
|
|
14
|
+
"build": "tsc -p tsconfig.build.json",
|
|
15
|
+
"typecheck": "tsc --noEmit",
|
|
16
|
+
"test": "vitest run",
|
|
17
|
+
"prepare": "npm run build",
|
|
18
|
+
"example": "vite --config example/vite.config.ts"
|
|
19
|
+
},
|
|
20
|
+
"keywords": [
|
|
21
|
+
"opendesk",
|
|
22
|
+
"react-native",
|
|
23
|
+
"live-chat",
|
|
24
|
+
"support"
|
|
25
|
+
],
|
|
26
|
+
"license": "MIT",
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"phoenix": "^1.8.0"
|
|
29
|
+
},
|
|
30
|
+
"peerDependencies": {
|
|
31
|
+
"react": ">=18",
|
|
32
|
+
"react-native": ">=0.72"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"@types/phoenix": "^1.6.6",
|
|
36
|
+
"@types/react": "^19.1.0",
|
|
37
|
+
"@vitejs/plugin-react": "^5.2.0",
|
|
38
|
+
"react": "^19.1.0",
|
|
39
|
+
"react-dom": "^19.3.1",
|
|
40
|
+
"react-native": "^0.81.0",
|
|
41
|
+
"react-native-web": "^0.21.4",
|
|
42
|
+
"typescript": "^5.9.2",
|
|
43
|
+
"vite": "^7.3.7",
|
|
44
|
+
"vitest": "^3.2.4"
|
|
45
|
+
}
|
|
46
|
+
}
|