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