@bigbinary/neetoui-rn 3.4.0 → 3.5.1
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/typescript/src/components/ChatInput/AttachmentsView.d.ts +2 -1
- package/lib/typescript/src/components/ChatInput/AttachmentsView.d.ts.map +1 -1
- package/lib/typescript/src/components/ChatInput/ChatInput.d.ts +76 -0
- package/lib/typescript/src/components/ChatInput/ChatInput.d.ts.map +1 -1
- package/lib/typescript/src/components/ChatInput/MentionSuggestionList.d.ts +9 -0
- package/lib/typescript/src/components/ChatInput/MentionSuggestionList.d.ts.map +1 -0
- package/lib/typescript/src/components/ChatInput/MentionsInput.d.ts +3 -1
- package/lib/typescript/src/components/ChatInput/MentionsInput.d.ts.map +1 -1
- package/lib/typescript/src/components/ChatInput/SegmentedOptions.d.ts +10 -0
- package/lib/typescript/src/components/ChatInput/SegmentedOptions.d.ts.map +1 -0
- package/lib/typescript/src/components/ChatInput/constants.d.ts +3 -0
- package/lib/typescript/src/components/ChatInput/constants.d.ts.map +1 -1
- package/lib/typescript/src/components/ChatInput/index.d.ts +1 -1
- package/lib/typescript/src/components/ChatInput/index.d.ts.map +1 -1
- package/lib/typescript/src/components/ChatInput/mentions.d.ts +2 -0
- package/lib/typescript/src/components/ChatInput/mentions.d.ts.map +1 -1
- package/lib/typescript/src/components/RichTextEditor/LazyRichTextEditor.d.ts +3 -0
- package/lib/typescript/src/components/RichTextEditor/LazyRichTextEditor.d.ts.map +1 -0
- package/lib/typescript/src/components/RichTextEditor/MentionSuggestionBar.d.ts +8 -0
- package/lib/typescript/src/components/RichTextEditor/MentionSuggestionBar.d.ts.map +1 -0
- package/lib/typescript/src/components/RichTextEditor/RichTextEditor.d.ts +39 -0
- package/lib/typescript/src/components/RichTextEditor/RichTextEditor.d.ts.map +1 -0
- package/lib/typescript/src/components/RichTextEditor/RichTextToolbar.d.ts +8 -0
- package/lib/typescript/src/components/RichTextEditor/RichTextToolbar.d.ts.map +1 -0
- package/lib/typescript/src/components/RichTextEditor/bridges/CodeBlockBridge.d.ts +13 -0
- package/lib/typescript/src/components/RichTextEditor/bridges/CodeBlockBridge.d.ts.map +1 -0
- package/lib/typescript/src/components/RichTextEditor/bridges/ComposerBridge.d.ts +27 -0
- package/lib/typescript/src/components/RichTextEditor/bridges/ComposerBridge.d.ts.map +1 -0
- package/lib/typescript/src/components/RichTextEditor/bridges/FigureBridge.d.ts +3 -0
- package/lib/typescript/src/components/RichTextEditor/bridges/FigureBridge.d.ts.map +1 -0
- package/lib/typescript/src/components/RichTextEditor/bridges/MentionBridge.d.ts +35 -0
- package/lib/typescript/src/components/RichTextEditor/bridges/MentionBridge.d.ts.map +1 -0
- package/lib/typescript/src/components/RichTextEditor/bridges/VariableBridge.d.ts +3 -0
- package/lib/typescript/src/components/RichTextEditor/bridges/VariableBridge.d.ts.map +1 -0
- package/lib/typescript/src/components/RichTextEditor/bridges/index.d.ts +8 -0
- package/lib/typescript/src/components/RichTextEditor/bridges/index.d.ts.map +1 -0
- package/lib/typescript/src/components/RichTextEditor/bridges/nodes/Figure.d.ts +3 -0
- package/lib/typescript/src/components/RichTextEditor/bridges/nodes/Figure.d.ts.map +1 -0
- package/lib/typescript/src/components/RichTextEditor/bridges/nodes/Mention.d.ts +3 -0
- package/lib/typescript/src/components/RichTextEditor/bridges/nodes/Mention.d.ts.map +1 -0
- package/lib/typescript/src/components/RichTextEditor/bridges/nodes/Variable.d.ts +3 -0
- package/lib/typescript/src/components/RichTextEditor/bridges/nodes/Variable.d.ts.map +1 -0
- package/lib/typescript/src/components/RichTextEditor/bridges/types.d.ts +7 -0
- package/lib/typescript/src/components/RichTextEditor/bridges/types.d.ts.map +1 -0
- package/lib/typescript/src/components/RichTextEditor/editorCss.d.ts +21 -0
- package/lib/typescript/src/components/RichTextEditor/editorCss.d.ts.map +1 -0
- package/lib/typescript/src/components/RichTextEditor/icons/index.d.ts +9 -0
- package/lib/typescript/src/components/RichTextEditor/icons/index.d.ts.map +1 -0
- package/lib/typescript/src/components/RichTextEditor/index.d.ts +3 -0
- package/lib/typescript/src/components/RichTextEditor/index.d.ts.map +1 -0
- package/lib/typescript/src/components/RichTextEditor/injectableText.d.ts +2 -0
- package/lib/typescript/src/components/RichTextEditor/injectableText.d.ts.map +1 -0
- package/lib/typescript/src/components/RichTextEditor/web/build/editorHtml.d.ts +2 -0
- package/lib/typescript/src/components/RichTextEditor/web/build/editorHtml.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +2 -1
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/package.json +30 -7
- package/src/components/ChatInput/AttachmentsView.tsx +6 -0
- package/src/components/ChatInput/ChatInput.tsx +557 -91
- package/src/components/ChatInput/MentionSuggestionList.tsx +71 -0
- package/src/components/ChatInput/MentionsInput.tsx +34 -46
- package/src/components/ChatInput/SegmentedOptions.tsx +84 -0
- package/src/components/ChatInput/constants.ts +10 -0
- package/src/components/ChatInput/index.ts +3 -0
- package/src/components/ChatInput/mentions.ts +22 -0
- package/src/components/RichTextEditor/LazyRichTextEditor.tsx +19 -0
- package/src/components/RichTextEditor/MentionSuggestionBar.tsx +79 -0
- package/src/components/RichTextEditor/RichTextEditor.tsx +213 -0
- package/src/components/RichTextEditor/RichTextToolbar.tsx +452 -0
- package/src/components/RichTextEditor/bridges/CodeBlockBridge.ts +29 -0
- package/src/components/RichTextEditor/bridges/ComposerBridge.ts +107 -0
- package/src/components/RichTextEditor/bridges/FigureBridge.ts +7 -0
- package/src/components/RichTextEditor/bridges/MentionBridge.ts +79 -0
- package/src/components/RichTextEditor/bridges/VariableBridge.ts +7 -0
- package/src/components/RichTextEditor/bridges/index.ts +66 -0
- package/src/components/RichTextEditor/bridges/nodes/Figure.ts +127 -0
- package/src/components/RichTextEditor/bridges/nodes/Mention.ts +48 -0
- package/src/components/RichTextEditor/bridges/nodes/Variable.ts +68 -0
- package/src/components/RichTextEditor/bridges/types.ts +24 -0
- package/src/components/RichTextEditor/editorCss.ts +69 -0
- package/src/components/RichTextEditor/icons/index.tsx +41 -0
- package/src/components/RichTextEditor/index.ts +2 -0
- package/src/components/RichTextEditor/injectableText.ts +11 -0
- package/src/components/RichTextEditor/web/build/editorHtml.ts +3 -0
- package/src/index.ts +8 -0
|
@@ -6,35 +6,50 @@ import {
|
|
|
6
6
|
useState,
|
|
7
7
|
type ReactNode,
|
|
8
8
|
} from "react";
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
Keyboard,
|
|
11
|
+
type TextInput,
|
|
12
|
+
View,
|
|
13
|
+
type TextInputProps,
|
|
14
|
+
} from "react-native";
|
|
10
15
|
import Attachment from "@bigbinary/neeto-icons-rn/icons/Attachment";
|
|
11
16
|
import BookOpen from "@bigbinary/neeto-icons-rn/icons/BookOpen";
|
|
12
17
|
import CannedResponses from "@bigbinary/neeto-icons-rn/icons/CannedResponses";
|
|
13
|
-
import ChatBubble from "@bigbinary/neeto-icons-rn/icons/ChatBubble";
|
|
14
18
|
import Down from "@bigbinary/neeto-icons-rn/icons/Down";
|
|
15
19
|
import Expand from "@bigbinary/neeto-icons-rn/icons/Expand";
|
|
16
|
-
import Forward from "@bigbinary/neeto-icons-rn/icons/Forward";
|
|
17
20
|
import Minimize from "@bigbinary/neeto-icons-rn/icons/Minimize";
|
|
18
|
-
import Notes from "@bigbinary/neeto-icons-rn/icons/Notes";
|
|
19
21
|
import SendPlane from "@bigbinary/neeto-icons-rn/icons/SendPlane";
|
|
22
|
+
import { Gesture, GestureDetector } from "react-native-gesture-handler";
|
|
23
|
+
import Animated, {
|
|
24
|
+
runOnJS,
|
|
25
|
+
useAnimatedStyle,
|
|
26
|
+
useSharedValue,
|
|
27
|
+
} from "react-native-reanimated";
|
|
20
28
|
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
|
21
29
|
|
|
22
30
|
import { moderateScale, scaleFor } from "../../utils/scale";
|
|
23
31
|
import { BottomSheet } from "../BottomSheet";
|
|
32
|
+
import { RichTextEditor, type RichTextEditorRef } from "../RichTextEditor";
|
|
33
|
+
import { TextType } from "../RichTextEditor/icons";
|
|
24
34
|
import { Skeleton } from "../Skeleton";
|
|
35
|
+
import { Toast } from "../Toast";
|
|
25
36
|
import { Touchable } from "../Touchable";
|
|
26
37
|
import { Typography } from "../Typography";
|
|
27
38
|
import { AttachmentsView } from "./AttachmentsView";
|
|
28
39
|
import { IconButton } from "./IconButton";
|
|
29
40
|
import { MentionsInput } from "./MentionsInput";
|
|
41
|
+
import { SegmentedOptions } from "./SegmentedOptions";
|
|
30
42
|
import {
|
|
31
43
|
ACTION_LABELS,
|
|
44
|
+
REPLY_ACTION_LABEL,
|
|
45
|
+
NOTE_HINT,
|
|
32
46
|
OPTION_TYPES,
|
|
33
47
|
PLACEHOLDERS,
|
|
34
48
|
type ChatInputOption,
|
|
35
49
|
} from "./constants";
|
|
36
50
|
import {
|
|
37
51
|
convertMentionsToHTMLAndPlainText,
|
|
52
|
+
htmlToPlainText,
|
|
38
53
|
type MentionSuggestion,
|
|
39
54
|
} from "./mentions";
|
|
40
55
|
|
|
@@ -47,8 +62,77 @@ export type ChatInputPayload = {
|
|
|
47
62
|
status?: string;
|
|
48
63
|
};
|
|
49
64
|
|
|
65
|
+
export type ChatInputLayout = "compact" | "expanded";
|
|
66
|
+
|
|
67
|
+
// tentap's async messages never reject, so a WebView that stops answering
|
|
68
|
+
// would otherwise leave Send silently stuck.
|
|
69
|
+
const RICH_CONTENT_TIMEOUT = 5000;
|
|
70
|
+
|
|
71
|
+
const withTimeout = <T,>(promise: Promise<T>) =>
|
|
72
|
+
new Promise<T>((resolve, reject) => {
|
|
73
|
+
const timer = setTimeout(
|
|
74
|
+
() => reject(new Error("Editor did not respond")),
|
|
75
|
+
RICH_CONTENT_TIMEOUT
|
|
76
|
+
);
|
|
77
|
+
promise.then(
|
|
78
|
+
value => {
|
|
79
|
+
clearTimeout(timer);
|
|
80
|
+
resolve(value);
|
|
81
|
+
},
|
|
82
|
+
error => {
|
|
83
|
+
clearTimeout(timer);
|
|
84
|
+
reject(error);
|
|
85
|
+
}
|
|
86
|
+
);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// Each report is a WebView round trip; typing should not queue one per key.
|
|
90
|
+
const RICH_TEXT_REPORT_DELAY = 150;
|
|
91
|
+
|
|
92
|
+
export type ChatInputResizable = {
|
|
93
|
+
initialHeight: number;
|
|
94
|
+
minHeight: number;
|
|
95
|
+
maxHeight: number;
|
|
96
|
+
/** Called with the settled height after each drag, e.g. to persist it. */
|
|
97
|
+
onResizeEnd?: (height: number) => void;
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
// Notes get their own document: a draft reply and its signature must not
|
|
101
|
+
// follow the agent into an internal note, or the other way round.
|
|
102
|
+
type RichEditorKey = "EMAIL" | "NOTE";
|
|
103
|
+
|
|
104
|
+
const RICH_EDITOR_OPTIONS: Record<RichEditorKey, ChatInputOption[]> = {
|
|
105
|
+
EMAIL: [OPTION_TYPES.REPLY, OPTION_TYPES.FORWARD],
|
|
106
|
+
NOTE: [OPTION_TYPES.NOTE],
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const RICH_EDITOR_DEFAULT_OPTION: Record<RichEditorKey, ChatInputOption> = {
|
|
110
|
+
EMAIL: OPTION_TYPES.REPLY,
|
|
111
|
+
NOTE: OPTION_TYPES.NOTE,
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const editorKeyFor = (option: ChatInputOption): RichEditorKey =>
|
|
115
|
+
option === OPTION_TYPES.NOTE ? "NOTE" : "EMAIL";
|
|
116
|
+
|
|
117
|
+
const clampHeight = (height: number, min: number, max: number) => {
|
|
118
|
+
"worklet";
|
|
119
|
+
|
|
120
|
+
return Math.min(Math.max(height, min), max);
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
export type ChatInputFocusTarget = "start" | "end" | "emptyParagraph";
|
|
124
|
+
|
|
50
125
|
export type ChatInputRef = {
|
|
51
126
|
clearEmailFields: () => void;
|
|
127
|
+
/** Rich: replaces the document. Plain: replaces the text with the HTML's text via onChangeText. */
|
|
128
|
+
setHTML: (html: string) => void;
|
|
129
|
+
/**
|
|
130
|
+
* Rich: inserts at the cursor of the option's editor (default: the
|
|
131
|
+
* selected one). Plain: appends the HTML's text via onChangeText.
|
|
132
|
+
*/
|
|
133
|
+
insertHTML: (html: string, option?: ChatInputOption) => void;
|
|
134
|
+
focus: (where?: ChatInputFocusTarget) => void;
|
|
135
|
+
getHTML: () => Promise<string>;
|
|
52
136
|
};
|
|
53
137
|
|
|
54
138
|
export type ChatInputProps = Omit<
|
|
@@ -79,6 +163,35 @@ export type ChatInputProps = Omit<
|
|
|
79
163
|
suggestions?: MentionSuggestion[];
|
|
80
164
|
disabled?: boolean;
|
|
81
165
|
isLoading?: boolean;
|
|
166
|
+
/** "expanded" (tablets) moves the mode toggles into a header row and keeps attachments open. */
|
|
167
|
+
layout?: ChatInputLayout;
|
|
168
|
+
/** Minimum visible lines of text. */
|
|
169
|
+
minLines?: number;
|
|
170
|
+
/** Rendered between the input and the attachments. */
|
|
171
|
+
Signature?: ReactNode;
|
|
172
|
+
/** Rendered at the end of the toolbar's left group. */
|
|
173
|
+
ToolbarExtras?: ReactNode;
|
|
174
|
+
/** Header hint shown in expanded layout while a note is selected. */
|
|
175
|
+
noteHint?: string;
|
|
176
|
+
/**
|
|
177
|
+
* "rich" swaps the text input for RichTextEditor; it then ignores value and
|
|
178
|
+
* suggestions, reports the editor's plain text through onChangeText
|
|
179
|
+
* (debounced), and content is reset through the ref.
|
|
180
|
+
*/
|
|
181
|
+
editor?: "plain" | "rich";
|
|
182
|
+
/** Adds a drag handle on top that resizes the composer within the bounds. */
|
|
183
|
+
resizable?: ChatInputResizable;
|
|
184
|
+
/** Rich only: options that keep the plain mention input. */
|
|
185
|
+
plainEditorFor?: ChatInputOption[];
|
|
186
|
+
/**
|
|
187
|
+
* Rich only: receives the plain text of the option's editor; defaults to
|
|
188
|
+
* onChangeText. Notes and replies keep separate documents.
|
|
189
|
+
*/
|
|
190
|
+
onRichTextChange?: (text: string, option: ChatInputOption) => void;
|
|
191
|
+
/** Rich only: the signature at the end of initialHTML; first focus lands above it. */
|
|
192
|
+
signatureHTML?: string;
|
|
193
|
+
/** Rich only: the document the reply editor starts with; notes start empty. */
|
|
194
|
+
initialHTML?: string;
|
|
82
195
|
};
|
|
83
196
|
|
|
84
197
|
/**
|
|
@@ -114,12 +227,71 @@ export const ChatInput = forwardRef<ChatInputRef, ChatInputProps>(
|
|
|
114
227
|
suggestions = [],
|
|
115
228
|
disabled = false,
|
|
116
229
|
isLoading = false,
|
|
230
|
+
layout = "compact",
|
|
231
|
+
minLines,
|
|
232
|
+
Signature,
|
|
233
|
+
ToolbarExtras,
|
|
234
|
+
noteHint = NOTE_HINT,
|
|
235
|
+
editor = "plain",
|
|
236
|
+
plainEditorFor = [],
|
|
237
|
+
resizable,
|
|
238
|
+
onRichTextChange,
|
|
239
|
+
initialHTML,
|
|
240
|
+
signatureHTML,
|
|
117
241
|
...inputProps
|
|
118
242
|
},
|
|
119
243
|
ref
|
|
120
244
|
) => {
|
|
121
245
|
const { theme } = useUnistyles();
|
|
122
246
|
const inputRef = useRef<TextInput>(null);
|
|
247
|
+
const richRefs = useRef<Record<RichEditorKey, RichTextEditorRef | null>>({
|
|
248
|
+
EMAIL: null,
|
|
249
|
+
NOTE: null,
|
|
250
|
+
});
|
|
251
|
+
const isSendingRich = useRef(false);
|
|
252
|
+
|
|
253
|
+
const minHeight = resizable?.minHeight ?? 0;
|
|
254
|
+
const maxHeight = resizable?.maxHeight ?? 0;
|
|
255
|
+
const height = useSharedValue(resizable?.initialHeight ?? 0);
|
|
256
|
+
const dragStartHeight = useSharedValue(0);
|
|
257
|
+
const reportHeight = (settled: number) => resizable?.onResizeEnd?.(settled);
|
|
258
|
+
|
|
259
|
+
const resizeGesture = Gesture.Pan()
|
|
260
|
+
.withTestId("composer-resize")
|
|
261
|
+
.hitSlop({ top: 8, bottom: 8 })
|
|
262
|
+
.onStart(() => {
|
|
263
|
+
dragStartHeight.value = clampHeight(height.value, minHeight, maxHeight);
|
|
264
|
+
})
|
|
265
|
+
.onUpdate(({ translationY }) => {
|
|
266
|
+
height.value = clampHeight(
|
|
267
|
+
dragStartHeight.value - translationY,
|
|
268
|
+
minHeight,
|
|
269
|
+
maxHeight
|
|
270
|
+
);
|
|
271
|
+
})
|
|
272
|
+
.onEnd(() => {
|
|
273
|
+
runOnJS(reportHeight)(height.value);
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
// The bound is re-read here so a shrinking maxHeight (keyboard up)
|
|
277
|
+
// clamps a stored height without a drag.
|
|
278
|
+
const resizedStyle = useAnimatedStyle(() => ({
|
|
279
|
+
height: clampHeight(height.value, minHeight, maxHeight),
|
|
280
|
+
}));
|
|
281
|
+
const [isFormattingVisible, setIsFormattingVisible] = useState(true);
|
|
282
|
+
const richTextTimers = useRef<
|
|
283
|
+
Partial<Record<RichEditorKey, ReturnType<typeof setTimeout>>>
|
|
284
|
+
>({});
|
|
285
|
+
|
|
286
|
+
useEffect(
|
|
287
|
+
() => () =>
|
|
288
|
+
Object.values(richTextTimers.current).forEach(timer =>
|
|
289
|
+
clearTimeout(timer)
|
|
290
|
+
),
|
|
291
|
+
[]
|
|
292
|
+
);
|
|
293
|
+
|
|
294
|
+
const hasRichEditor = editor === "rich";
|
|
123
295
|
|
|
124
296
|
const [selectedOption, setSelectedOption] = useState<ChatInputOption>(
|
|
125
297
|
initialSelectedOption
|
|
@@ -135,6 +307,48 @@ export const ChatInput = forwardRef<ChatInputRef, ChatInputProps>(
|
|
|
135
307
|
const isReplySelected = selectedOption === OPTION_TYPES.REPLY;
|
|
136
308
|
const isNoteSelected = selectedOption === OPTION_TYPES.NOTE;
|
|
137
309
|
const isForwardSelected = selectedOption === OPTION_TYPES.FORWARD;
|
|
310
|
+
const isExpanded = layout === "expanded";
|
|
311
|
+
const isRich = hasRichEditor && !plainEditorFor.includes(selectedOption);
|
|
312
|
+
const activeKey = editorKeyFor(selectedOption);
|
|
313
|
+
const activeEditor = () => richRefs.current[activeKey];
|
|
314
|
+
|
|
315
|
+
// The editor a mode needs mounts on first use and then stays, so a
|
|
316
|
+
// draft survives switching back and forth.
|
|
317
|
+
const [editorSeeds, setEditorSeeds] = useState<
|
|
318
|
+
Partial<Record<RichEditorKey, string>>
|
|
319
|
+
>(() =>
|
|
320
|
+
hasRichEditor ? { [editorKeyFor(initialSelectedOption)]: "" } : {}
|
|
321
|
+
);
|
|
322
|
+
const [lateEditors, setLateEditors] = useState<RichEditorKey[]>([]);
|
|
323
|
+
|
|
324
|
+
const mountEditor = (key: RichEditorKey, html = "") => {
|
|
325
|
+
setEditorSeeds(seeds => ({ ...seeds, [key]: (seeds[key] ?? "") + html }));
|
|
326
|
+
setLateEditors(keys => (keys.includes(key) ? keys : [...keys, key]));
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
useEffect(() => {
|
|
330
|
+
if (isRich && editorSeeds[activeKey] === undefined) {
|
|
331
|
+
mountEditor(activeKey);
|
|
332
|
+
}
|
|
333
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
334
|
+
}, [isRich, activeKey]);
|
|
335
|
+
|
|
336
|
+
const optionInEditor = (key: RichEditorKey) =>
|
|
337
|
+
RICH_EDITOR_OPTIONS[key].includes(selectedOption)
|
|
338
|
+
? selectedOption
|
|
339
|
+
: RICH_EDITOR_DEFAULT_OPTION[key];
|
|
340
|
+
|
|
341
|
+
const handleRichChange = (key: RichEditorKey) => {
|
|
342
|
+
clearTimeout(richTextTimers.current[key]);
|
|
343
|
+
const option = optionInEditor(key);
|
|
344
|
+
richTextTimers.current[key] = setTimeout(async () => {
|
|
345
|
+
const text = await richRefs.current[key]?.getText();
|
|
346
|
+
if (text === undefined) return;
|
|
347
|
+
|
|
348
|
+
if (onRichTextChange) onRichTextChange(text, option);
|
|
349
|
+
else onChangeText(text);
|
|
350
|
+
}, RICH_TEXT_REPORT_DELAY);
|
|
351
|
+
};
|
|
138
352
|
|
|
139
353
|
useImperativeHandle(
|
|
140
354
|
ref,
|
|
@@ -144,10 +358,44 @@ export const ChatInput = forwardRef<ChatInputRef, ChatInputProps>(
|
|
|
144
358
|
setToEmailsForForward([]);
|
|
145
359
|
setCcEmails([]);
|
|
146
360
|
setBccEmails([]);
|
|
147
|
-
|
|
361
|
+
if (isRich) activeEditor()?.blur();
|
|
362
|
+
else inputRef.current?.blur();
|
|
363
|
+
},
|
|
364
|
+
setHTML: html =>
|
|
365
|
+
isRich
|
|
366
|
+
? activeEditor()?.setHTML(html)
|
|
367
|
+
: onChangeText(htmlToPlainText(html)),
|
|
368
|
+
insertHTML: (html, option = selectedOption) => {
|
|
369
|
+
if (!hasRichEditor || plainEditorFor.includes(option)) {
|
|
370
|
+
onChangeText(`${value}${htmlToPlainText(html)}`);
|
|
371
|
+
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
const key = editorKeyFor(option);
|
|
376
|
+
const target = richRefs.current[key];
|
|
377
|
+
if (target) target.insertHTML(html);
|
|
378
|
+
else mountEditor(key, html);
|
|
148
379
|
},
|
|
380
|
+
focus: where =>
|
|
381
|
+
isRich ? activeEditor()?.focus(where) : inputRef.current?.focus(),
|
|
382
|
+
getHTML: async () =>
|
|
383
|
+
isRich
|
|
384
|
+
? ((await activeEditor()?.getHTML()) ?? "")
|
|
385
|
+
: convertMentionsToHTMLAndPlainText({ suggestions, value }).html,
|
|
149
386
|
}),
|
|
150
|
-
|
|
387
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
388
|
+
[
|
|
389
|
+
initialToEmails,
|
|
390
|
+
hasRichEditor,
|
|
391
|
+
isRich,
|
|
392
|
+
activeKey,
|
|
393
|
+
selectedOption,
|
|
394
|
+
plainEditorFor,
|
|
395
|
+
onChangeText,
|
|
396
|
+
suggestions,
|
|
397
|
+
value,
|
|
398
|
+
]
|
|
151
399
|
);
|
|
152
400
|
|
|
153
401
|
useEffect(() => {
|
|
@@ -167,15 +415,22 @@ export const ChatInput = forwardRef<ChatInputRef, ChatInputProps>(
|
|
|
167
415
|
|
|
168
416
|
const selectOption = (option: ChatInputOption) => {
|
|
169
417
|
setIsAttachmentsVisible(false);
|
|
170
|
-
|
|
418
|
+
const target =
|
|
419
|
+
hasRichEditor && !plainEditorFor.includes(option)
|
|
420
|
+
? richRefs.current[editorKeyFor(option)]
|
|
421
|
+
: inputRef.current;
|
|
422
|
+
target?.focus();
|
|
171
423
|
setSelectedOption(option);
|
|
172
424
|
};
|
|
173
425
|
|
|
174
|
-
const buildPayload = (
|
|
426
|
+
const buildPayload = (
|
|
427
|
+
content: Pick<ChatInputPayload, "html" | "plainText">,
|
|
428
|
+
extra?: Partial<ChatInputPayload>
|
|
429
|
+
) => ({
|
|
175
430
|
toEmails: isForwardSelected ? toEmailsForForward : toEmails,
|
|
176
431
|
ccEmails,
|
|
177
432
|
bccEmails,
|
|
178
|
-
...
|
|
433
|
+
...content,
|
|
179
434
|
...extra,
|
|
180
435
|
});
|
|
181
436
|
|
|
@@ -188,42 +443,182 @@ export const ChatInput = forwardRef<ChatInputRef, ChatInputProps>(
|
|
|
188
443
|
FORWARD: onForward ?? (() => {}),
|
|
189
444
|
};
|
|
190
445
|
|
|
191
|
-
const handleAction = (extra?: Partial<ChatInputPayload>) => {
|
|
192
|
-
actionHandlers[selectedOption]
|
|
446
|
+
const handleAction = async (extra?: Partial<ChatInputPayload>) => {
|
|
447
|
+
const handler = actionHandlers[selectedOption];
|
|
193
448
|
setIsAttachmentsVisible(false);
|
|
449
|
+
|
|
450
|
+
const editorRef = activeEditor();
|
|
451
|
+
if (!isRich || !editorRef) {
|
|
452
|
+
handler(
|
|
453
|
+
buildPayload(
|
|
454
|
+
convertMentionsToHTMLAndPlainText({ suggestions, value }),
|
|
455
|
+
extra
|
|
456
|
+
)
|
|
457
|
+
);
|
|
458
|
+
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
if (isSendingRich.current) return;
|
|
463
|
+
|
|
464
|
+
isSendingRich.current = true;
|
|
465
|
+
let content: [string, string];
|
|
466
|
+
try {
|
|
467
|
+
content = await withTimeout(
|
|
468
|
+
Promise.all([editorRef.getHTML(), editorRef.getText()])
|
|
469
|
+
);
|
|
470
|
+
} catch {
|
|
471
|
+
Toast.show({
|
|
472
|
+
type: "error",
|
|
473
|
+
text1: "Couldn't read the message. Try sending again.",
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
return;
|
|
477
|
+
} finally {
|
|
478
|
+
isSendingRich.current = false;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
const [html, plainText] = content;
|
|
482
|
+
handler(buildPayload({ html, plainText }, extra));
|
|
483
|
+
};
|
|
484
|
+
|
|
485
|
+
// Keyboard.dismiss() only reaches native inputs; the rich editor's
|
|
486
|
+
// keyboard belongs to its WebView and closes on blur.
|
|
487
|
+
const dismissKeyboard = () => {
|
|
488
|
+
if (isRich) activeEditor()?.blur();
|
|
489
|
+
Keyboard.dismiss();
|
|
490
|
+
};
|
|
491
|
+
|
|
492
|
+
const withKeyboardDismissed = (action?: () => void) => () => {
|
|
493
|
+
dismissKeyboard();
|
|
494
|
+
action?.();
|
|
194
495
|
};
|
|
195
496
|
|
|
196
497
|
const handleAttachmentPress = () => {
|
|
498
|
+
dismissKeyboard();
|
|
197
499
|
// v2: when collapsed attachments already exist, first tap only
|
|
198
500
|
// expands them instead of re-opening the upload flow.
|
|
199
|
-
if (isAttachmentsVisible || attachmentsCount === 0)
|
|
501
|
+
if (isExpanded || isAttachmentsVisible || attachmentsCount === 0) {
|
|
502
|
+
onAttachment();
|
|
503
|
+
}
|
|
200
504
|
setIsAttachmentsVisible(true);
|
|
201
505
|
};
|
|
202
506
|
|
|
203
|
-
const shouldShowExpandToggle =
|
|
204
|
-
|
|
507
|
+
const shouldShowExpandToggle = !isExpanded && attachmentsCount > 0;
|
|
508
|
+
|
|
509
|
+
const recipients = toEmails.filter(Boolean).join(", ");
|
|
510
|
+
let headerHint: string | undefined;
|
|
511
|
+
if (isNoteSelected) headerHint = noteHint;
|
|
512
|
+
else if (isReplySelected && recipients) headerHint = `To: ${recipients}`;
|
|
205
513
|
|
|
206
514
|
const isActionDisabled =
|
|
207
515
|
disabled || (isForwardSelected && toEmailsForForward.length === 0);
|
|
208
516
|
|
|
517
|
+
// The formatting bar draws its own bottom line; a second one here would
|
|
518
|
+
// double it when nothing sits between them.
|
|
519
|
+
const isFooterBelowToolbar =
|
|
520
|
+
isRich && isFormattingVisible && attachmentsCount === 0;
|
|
521
|
+
|
|
522
|
+
const modeOptions: ChatInputOption[] = onForward
|
|
523
|
+
? [OPTION_TYPES.REPLY, OPTION_TYPES.NOTE, OPTION_TYPES.FORWARD]
|
|
524
|
+
: [OPTION_TYPES.REPLY, OPTION_TYPES.NOTE];
|
|
525
|
+
|
|
526
|
+
const actionLabel =
|
|
527
|
+
isExpanded && isReplySelected
|
|
528
|
+
? REPLY_ACTION_LABEL
|
|
529
|
+
: ACTION_LABELS[selectedOption];
|
|
530
|
+
|
|
209
531
|
return (
|
|
210
|
-
<View
|
|
532
|
+
<Animated.View
|
|
533
|
+
style={[styles.root, resizable && [styles.fixedHeight, resizedStyle]]}
|
|
534
|
+
>
|
|
535
|
+
{resizable && (
|
|
536
|
+
<GestureDetector gesture={resizeGesture}>
|
|
537
|
+
<View
|
|
538
|
+
accessibilityLabel="Resize composer"
|
|
539
|
+
accessibilityRole="adjustable"
|
|
540
|
+
style={styles.resizeHandleArea}
|
|
541
|
+
>
|
|
542
|
+
<View style={styles.resizeHandle} />
|
|
543
|
+
</View>
|
|
544
|
+
</GestureDetector>
|
|
545
|
+
)}
|
|
211
546
|
{isLoading && (
|
|
212
547
|
<Skeleton animated height={moderateScale(2)} variant="line" />
|
|
213
548
|
)}
|
|
214
|
-
<View
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
549
|
+
<View
|
|
550
|
+
style={[
|
|
551
|
+
styles.card,
|
|
552
|
+
resizable && styles.resizableCard,
|
|
553
|
+
isNoteSelected && styles.noteSurface,
|
|
554
|
+
]}
|
|
555
|
+
>
|
|
556
|
+
{headerHint && (
|
|
557
|
+
<View style={styles.headerBand}>
|
|
558
|
+
<Typography numberOfLines={1} size="s" style={styles.headerHint}>
|
|
559
|
+
{headerHint}
|
|
560
|
+
</Typography>
|
|
561
|
+
</View>
|
|
562
|
+
)}
|
|
563
|
+
<View
|
|
564
|
+
style={[
|
|
565
|
+
styles.inputRow,
|
|
566
|
+
resizable && styles.fill,
|
|
567
|
+
!isRich && styles.plainInputRow,
|
|
568
|
+
]}
|
|
569
|
+
>
|
|
570
|
+
{(Object.keys(RICH_EDITOR_OPTIONS) as RichEditorKey[]).map(key => {
|
|
571
|
+
const seed = editorSeeds[key];
|
|
572
|
+
if (seed === undefined) return null;
|
|
573
|
+
|
|
574
|
+
const isNote = key === "NOTE";
|
|
575
|
+
const options = RICH_EDITOR_OPTIONS[key];
|
|
576
|
+
const isShown = isRich && key === activeKey;
|
|
577
|
+
|
|
578
|
+
return (
|
|
579
|
+
<RichTextEditor
|
|
580
|
+
autoFocus={
|
|
581
|
+
lateEditors.includes(key) || !!inputProps.autoFocus
|
|
582
|
+
}
|
|
583
|
+
backgroundColor={
|
|
584
|
+
isNote ? theme.colors.background.oldLace : undefined
|
|
585
|
+
}
|
|
586
|
+
initialHTML={isNote ? seed : `${initialHTML ?? ""}${seed}`}
|
|
587
|
+
key={key}
|
|
588
|
+
placeholder={PLACEHOLDERS[optionInEditor(key)]}
|
|
589
|
+
ref={instance => {
|
|
590
|
+
richRefs.current[key] = instance;
|
|
591
|
+
}}
|
|
592
|
+
style={[
|
|
593
|
+
styles.richEditor,
|
|
594
|
+
resizable && styles.resizableRichEditor,
|
|
595
|
+
!isShown && styles.hidden,
|
|
596
|
+
]}
|
|
597
|
+
mentions={
|
|
598
|
+
options.some(option => showSuggestionsFor.includes(option))
|
|
599
|
+
? suggestions
|
|
600
|
+
: undefined
|
|
601
|
+
}
|
|
602
|
+
toolbar={isFormattingVisible ? "fixed" : "none"}
|
|
603
|
+
trailingHTML={isNote ? undefined : signatureHTML}
|
|
604
|
+
onChange={() => handleRichChange(key)}
|
|
605
|
+
/>
|
|
606
|
+
);
|
|
607
|
+
})}
|
|
608
|
+
{!isRich && (
|
|
609
|
+
<MentionsInput
|
|
610
|
+
inputRef={inputRef}
|
|
611
|
+
minLines={minLines}
|
|
612
|
+
placeholder={PLACEHOLDERS[selectedOption]}
|
|
613
|
+
suggestions={suggestions}
|
|
614
|
+
value={value}
|
|
615
|
+
shouldShowSuggestions={showSuggestionsFor.includes(
|
|
616
|
+
selectedOption
|
|
617
|
+
)}
|
|
618
|
+
onChange={onChangeText}
|
|
619
|
+
{...inputProps}
|
|
620
|
+
/>
|
|
621
|
+
)}
|
|
227
622
|
{shouldShowExpandToggle && (
|
|
228
623
|
<IconButton
|
|
229
624
|
accessibilityLabel={
|
|
@@ -235,52 +630,54 @@ export const ChatInput = forwardRef<ChatInputRef, ChatInputProps>(
|
|
|
235
630
|
/>
|
|
236
631
|
)}
|
|
237
632
|
</View>
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
{
|
|
633
|
+
{Signature}
|
|
634
|
+
<View style={styles.attachments}>
|
|
635
|
+
<AttachmentsView
|
|
636
|
+
Attachments={Attachments}
|
|
637
|
+
attachmentsCount={attachmentsCount}
|
|
638
|
+
isAlwaysExpanded={isExpanded}
|
|
639
|
+
isAttachmentsVisible={isAttachmentsVisible}
|
|
640
|
+
isNoteOptionSelected={isNoteSelected}
|
|
641
|
+
onExpand={() => setIsAttachmentsVisible(true)}
|
|
642
|
+
/>
|
|
643
|
+
</View>
|
|
644
|
+
<View
|
|
645
|
+
style={[
|
|
646
|
+
styles.footer,
|
|
647
|
+
!isExpanded && styles.compactFooter,
|
|
648
|
+
isFooterBelowToolbar && styles.footerBelowToolbar,
|
|
649
|
+
]}
|
|
650
|
+
>
|
|
651
|
+
<SegmentedOptions
|
|
652
|
+
isCompact={!isExpanded}
|
|
653
|
+
options={modeOptions}
|
|
654
|
+
selected={selectedOption}
|
|
655
|
+
onSelect={selectOption}
|
|
656
|
+
/>
|
|
657
|
+
<View style={styles.footerActions}>
|
|
658
|
+
{isRich && (
|
|
260
659
|
<IconButton
|
|
261
|
-
accessibilityLabel=
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
660
|
+
accessibilityLabel={
|
|
661
|
+
isFormattingVisible ? "Hide formatting" : "Show formatting"
|
|
662
|
+
}
|
|
663
|
+
Icon={TextType}
|
|
664
|
+
isActive={isFormattingVisible}
|
|
665
|
+
onPress={() => setIsFormattingVisible(visible => !visible)}
|
|
265
666
|
/>
|
|
266
667
|
)}
|
|
267
|
-
<View style={styles.toolbarDivider} />
|
|
268
|
-
{/* isActive is the selected-option tint for the three mode
|
|
269
|
-
toggles above. These two are actions, not modes, so they
|
|
270
|
-
stay at full strength like the attachment button. */}
|
|
271
668
|
{onCannedResponse &&
|
|
272
669
|
showCannedResponsesFor.includes(selectedOption) && (
|
|
273
670
|
<IconButton
|
|
274
671
|
accessibilityLabel="Canned responses"
|
|
275
672
|
Icon={CannedResponses}
|
|
276
|
-
onPress={onCannedResponse}
|
|
673
|
+
onPress={withKeyboardDismissed(onCannedResponse)}
|
|
277
674
|
/>
|
|
278
675
|
)}
|
|
279
676
|
{onArticles && showArticlesFor.includes(selectedOption) && (
|
|
280
677
|
<IconButton
|
|
281
678
|
accessibilityLabel="Articles"
|
|
282
679
|
Icon={BookOpen}
|
|
283
|
-
onPress={onArticles}
|
|
680
|
+
onPress={withKeyboardDismissed(onArticles)}
|
|
284
681
|
/>
|
|
285
682
|
)}
|
|
286
683
|
<IconButton
|
|
@@ -289,11 +686,7 @@ export const ChatInput = forwardRef<ChatInputRef, ChatInputProps>(
|
|
|
289
686
|
Icon={Attachment}
|
|
290
687
|
onPress={handleAttachmentPress}
|
|
291
688
|
/>
|
|
292
|
-
|
|
293
|
-
<View style={styles.toolbarGroup}>
|
|
294
|
-
{/* Both actions share one filled pill, split by a divider, as
|
|
295
|
-
on web. The send affordance is the plane alone on a reply;
|
|
296
|
-
note and forward keep their labels. */}
|
|
689
|
+
{ToolbarExtras}
|
|
297
690
|
<View
|
|
298
691
|
style={[
|
|
299
692
|
styles.sendGroup,
|
|
@@ -304,15 +697,17 @@ export const ChatInput = forwardRef<ChatInputRef, ChatInputProps>(
|
|
|
304
697
|
accessibilityLabel="Send"
|
|
305
698
|
accessibilityRole="button"
|
|
306
699
|
disabled={isActionDisabled}
|
|
307
|
-
style={
|
|
308
|
-
|
|
700
|
+
style={[
|
|
701
|
+
styles.sendAction,
|
|
702
|
+
!isExpanded && styles.compactSendAction,
|
|
703
|
+
]}
|
|
704
|
+
onPress={() => void handleAction()}
|
|
309
705
|
>
|
|
310
|
-
{
|
|
706
|
+
{actionLabel ? (
|
|
311
707
|
<Typography size="m" style={styles.sendLabel}>
|
|
312
|
-
{
|
|
708
|
+
{actionLabel}
|
|
313
709
|
</Typography>
|
|
314
|
-
)
|
|
315
|
-
{isReplySelected && (
|
|
710
|
+
) : (
|
|
316
711
|
<SendPlane
|
|
317
712
|
color={theme.colors.text.inverse}
|
|
318
713
|
size={moderateScale(20)}
|
|
@@ -327,7 +722,9 @@ export const ChatInput = forwardRef<ChatInputRef, ChatInputProps>(
|
|
|
327
722
|
accessibilityRole="button"
|
|
328
723
|
disabled={disabled}
|
|
329
724
|
style={styles.sendMenu}
|
|
330
|
-
onPress={() =>
|
|
725
|
+
onPress={withKeyboardDismissed(() =>
|
|
726
|
+
setIsReplyMenuVisible(true)
|
|
727
|
+
)}
|
|
331
728
|
>
|
|
332
729
|
<Down
|
|
333
730
|
color={theme.colors.text.inverse}
|
|
@@ -342,13 +739,13 @@ export const ChatInput = forwardRef<ChatInputRef, ChatInputProps>(
|
|
|
342
739
|
data={["Send and set as closed"]}
|
|
343
740
|
hide={() => setIsReplyMenuVisible(false)}
|
|
344
741
|
isVisible={isReplyMenuVisible}
|
|
345
|
-
onItemPress={() => handleAction({ status: "closed" })}
|
|
742
|
+
onItemPress={() => void handleAction({ status: "closed" })}
|
|
346
743
|
/>
|
|
347
744
|
)}
|
|
348
745
|
</View>
|
|
349
746
|
</View>
|
|
350
747
|
</View>
|
|
351
|
-
</View>
|
|
748
|
+
</Animated.View>
|
|
352
749
|
);
|
|
353
750
|
}
|
|
354
751
|
);
|
|
@@ -361,51 +758,121 @@ const styles = StyleSheet.create((theme, rt) => {
|
|
|
361
758
|
return {
|
|
362
759
|
// A caller that caps this composer's height (desk clamps it to a share
|
|
363
760
|
// of the screen) would otherwise have Yoga keep every child at its
|
|
364
|
-
// natural height and let overflow clip the last one — the
|
|
761
|
+
// natural height and let overflow clip the last one — the footer.
|
|
365
762
|
// Declaring what may shrink makes the text area absorb the squeeze
|
|
366
763
|
// instead, so the actions stay reachable however tall the attachments
|
|
367
764
|
// and typed content get.
|
|
368
765
|
root: {
|
|
369
766
|
flexShrink: 1,
|
|
370
767
|
},
|
|
371
|
-
|
|
768
|
+
// A dragged height is the agent's choice; the conversation gives way.
|
|
769
|
+
fixedHeight: {
|
|
770
|
+
flexShrink: 0,
|
|
771
|
+
},
|
|
772
|
+
fill: {
|
|
773
|
+
flex: 1,
|
|
774
|
+
},
|
|
775
|
+
// The drag sets the height, so the editor gives way instead of pushing
|
|
776
|
+
// the footer out; the card clips while the pill above stays visible.
|
|
777
|
+
resizableCard: {
|
|
778
|
+
flex: 1,
|
|
779
|
+
overflow: "hidden",
|
|
780
|
+
},
|
|
781
|
+
resizableRichEditor: {
|
|
782
|
+
minHeight: 0,
|
|
783
|
+
},
|
|
784
|
+
// desk-web's handle: a pill on the line above the composer. It overlays
|
|
785
|
+
// the top edge instead of taking a row, so the To band starts flush; the
|
|
786
|
+
// pill's upper half shows only where the parent does not clip.
|
|
787
|
+
resizeHandleArea: {
|
|
788
|
+
alignItems: "center",
|
|
789
|
+
height: moderateScale(16),
|
|
790
|
+
left: 0,
|
|
791
|
+
position: "absolute",
|
|
792
|
+
right: 0,
|
|
793
|
+
top: 0,
|
|
794
|
+
zIndex: 1,
|
|
795
|
+
},
|
|
796
|
+
resizeHandle: {
|
|
797
|
+
backgroundColor: theme.colors.border.secondary,
|
|
798
|
+
borderRadius: moderateScale(3),
|
|
799
|
+
height: moderateScale(6),
|
|
800
|
+
marginTop: -moderateScale(3),
|
|
801
|
+
width: moderateScale(48),
|
|
802
|
+
},
|
|
803
|
+
card: {
|
|
804
|
+
backgroundColor: theme.colors.background.primary,
|
|
372
805
|
flexShrink: 1,
|
|
373
|
-
paddingBottom: moderateScale(5),
|
|
374
|
-
paddingHorizontal: moderateScale(16),
|
|
375
806
|
},
|
|
376
807
|
noteSurface: {
|
|
377
808
|
backgroundColor: theme.colors.background.oldLace,
|
|
378
809
|
},
|
|
810
|
+
headerBand: {
|
|
811
|
+
backgroundColor: theme.colors.background.secondary,
|
|
812
|
+
borderBottomColor: theme.colors.border.primary,
|
|
813
|
+
borderBottomWidth: 1,
|
|
814
|
+
paddingHorizontal: moderateScale(14),
|
|
815
|
+
paddingVertical: moderateScale(10),
|
|
816
|
+
},
|
|
817
|
+
headerHint: {
|
|
818
|
+
color: theme.colors.text.secondary,
|
|
819
|
+
},
|
|
379
820
|
inputRow: {
|
|
380
821
|
flexDirection: "row",
|
|
381
822
|
flexShrink: 1,
|
|
382
823
|
justifyContent: "space-between",
|
|
383
824
|
},
|
|
825
|
+
plainInputRow: {
|
|
826
|
+
paddingHorizontal: moderateScale(14),
|
|
827
|
+
paddingTop: moderateScale(8),
|
|
828
|
+
},
|
|
829
|
+
// Kept mounted while hidden so the reply draft survives a mode switch.
|
|
830
|
+
hidden: {
|
|
831
|
+
display: "none",
|
|
832
|
+
},
|
|
833
|
+
richEditor: {
|
|
834
|
+
flex: 1,
|
|
835
|
+
minHeight: moderateScale(160),
|
|
836
|
+
},
|
|
384
837
|
expandButton: {
|
|
385
838
|
alignSelf: "center",
|
|
386
839
|
},
|
|
387
|
-
|
|
840
|
+
attachments: {
|
|
841
|
+
paddingHorizontal: moderateScale(14),
|
|
842
|
+
},
|
|
843
|
+
footer: {
|
|
388
844
|
alignItems: "center",
|
|
845
|
+
borderTopColor: theme.colors.border.primary,
|
|
846
|
+
borderTopWidth: 1,
|
|
389
847
|
flexDirection: "row",
|
|
390
848
|
flexShrink: 0,
|
|
849
|
+
gap: moderateScale(8),
|
|
391
850
|
justifyContent: "space-between",
|
|
392
|
-
|
|
851
|
+
paddingHorizontal: moderateScale(10),
|
|
852
|
+
paddingVertical: moderateScale(8),
|
|
393
853
|
},
|
|
394
|
-
|
|
854
|
+
footerBelowToolbar: {
|
|
855
|
+
borderTopWidth: 0,
|
|
856
|
+
},
|
|
857
|
+
compactFooter: {
|
|
858
|
+
gap: moderateScale(4),
|
|
859
|
+
paddingHorizontal: moderateScale(6),
|
|
860
|
+
},
|
|
861
|
+
compactSendAction: {
|
|
862
|
+
paddingHorizontal: moderateScale(10),
|
|
863
|
+
},
|
|
864
|
+
footerActions: {
|
|
395
865
|
alignItems: "center",
|
|
396
866
|
flexDirection: "row",
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
alignSelf: "stretch",
|
|
400
|
-
backgroundColor: theme.colors.palette.grey400,
|
|
401
|
-
marginHorizontal: moderateScale(5),
|
|
402
|
-
width: moderateScale(2),
|
|
867
|
+
flexShrink: 1,
|
|
868
|
+
gap: moderateScale(2),
|
|
403
869
|
},
|
|
404
870
|
sendGroup: {
|
|
405
871
|
alignItems: "center",
|
|
406
872
|
backgroundColor: theme.colors.primary,
|
|
407
873
|
borderRadius: moderateScale(8),
|
|
408
874
|
flexDirection: "row",
|
|
875
|
+
marginLeft: moderateScale(4),
|
|
409
876
|
// Clips the divider to the rounded corners.
|
|
410
877
|
overflow: "hidden",
|
|
411
878
|
},
|
|
@@ -415,10 +882,9 @@ const styles = StyleSheet.create((theme, rt) => {
|
|
|
415
882
|
sendAction: {
|
|
416
883
|
alignItems: "center",
|
|
417
884
|
flexDirection: "row",
|
|
418
|
-
gap: moderateScale(6),
|
|
419
885
|
justifyContent: "center",
|
|
420
|
-
minHeight: moderateScale(
|
|
421
|
-
paddingHorizontal: moderateScale(
|
|
886
|
+
minHeight: moderateScale(36),
|
|
887
|
+
paddingHorizontal: moderateScale(14),
|
|
422
888
|
},
|
|
423
889
|
sendGroupDivider: {
|
|
424
890
|
alignSelf: "stretch",
|
|
@@ -429,7 +895,7 @@ const styles = StyleSheet.create((theme, rt) => {
|
|
|
429
895
|
sendMenu: {
|
|
430
896
|
alignItems: "center",
|
|
431
897
|
justifyContent: "center",
|
|
432
|
-
minHeight: moderateScale(
|
|
898
|
+
minHeight: moderateScale(36),
|
|
433
899
|
paddingHorizontal: moderateScale(8),
|
|
434
900
|
},
|
|
435
901
|
sendLabel: {
|