@ethora/chat-component-rn 26.5.19 → 26.5.21
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 +40 -0
- package/lib/commonjs/components/MainComponents/ChatHeader.js +15 -2
- package/lib/commonjs/components/MainComponents/ChatHeader.js.map +1 -1
- package/lib/commonjs/components/MessageBubble/Message.js +24 -9
- package/lib/commonjs/components/MessageBubble/Message.js.map +1 -1
- package/lib/commonjs/components/Modals/AttachSheet/AttachSheet.js +11 -7
- package/lib/commonjs/components/Modals/AttachSheet/AttachSheet.js.map +1 -1
- package/lib/commonjs/components/Modals/ChatProfileModal/ChatProfileModal.js +10 -3
- package/lib/commonjs/components/Modals/ChatProfileModal/ChatProfileModal.js.map +1 -1
- package/lib/commonjs/components/Modals/FilePreviewModal/FilePreviewModal.js +23 -5
- package/lib/commonjs/components/Modals/FilePreviewModal/FilePreviewModal.js.map +1 -1
- package/lib/commonjs/components/Modals/ModalHeaderComponent.js +15 -1
- package/lib/commonjs/components/Modals/ModalHeaderComponent.js.map +1 -1
- package/lib/commonjs/components/Modals/styledModalComponents.js +6 -2
- package/lib/commonjs/components/Modals/styledModalComponents.js.map +1 -1
- package/lib/commonjs/components/styled/SendInput.js +34 -9
- package/lib/commonjs/components/styled/SendInput.js.map +1 -1
- package/lib/commonjs/components/styled/StyledComponents.js +6 -2
- package/lib/commonjs/components/styled/StyledComponents.js.map +1 -1
- package/lib/commonjs/components/styled/StyledInputComponents/StyledInputComponents.js +6 -1
- package/lib/commonjs/components/styled/StyledInputComponents/StyledInputComponents.js.map +1 -1
- package/lib/commonjs/helpers/headerLayout.js +35 -0
- package/lib/commonjs/helpers/headerLayout.js.map +1 -0
- package/lib/commonjs/helpers/parseMessageBody.js +12 -5
- package/lib/commonjs/helpers/parseMessageBody.js.map +1 -1
- package/lib/commonjs/helpers/typography.js +28 -0
- package/lib/commonjs/helpers/typography.js.map +1 -0
- package/lib/commonjs/hooks/useChatFonts.js +91 -7
- package/lib/commonjs/hooks/useChatFonts.js.map +1 -1
- package/lib/module/components/MainComponents/ChatHeader.js +15 -2
- package/lib/module/components/MainComponents/ChatHeader.js.map +1 -1
- package/lib/module/components/MessageBubble/Message.js +24 -9
- package/lib/module/components/MessageBubble/Message.js.map +1 -1
- package/lib/module/components/Modals/AttachSheet/AttachSheet.js +11 -7
- package/lib/module/components/Modals/AttachSheet/AttachSheet.js.map +1 -1
- package/lib/module/components/Modals/ChatProfileModal/ChatProfileModal.js +10 -3
- package/lib/module/components/Modals/ChatProfileModal/ChatProfileModal.js.map +1 -1
- package/lib/module/components/Modals/FilePreviewModal/FilePreviewModal.js +24 -6
- package/lib/module/components/Modals/FilePreviewModal/FilePreviewModal.js.map +1 -1
- package/lib/module/components/Modals/ModalHeaderComponent.js +15 -1
- package/lib/module/components/Modals/ModalHeaderComponent.js.map +1 -1
- package/lib/module/components/Modals/styledModalComponents.js +6 -2
- package/lib/module/components/Modals/styledModalComponents.js.map +1 -1
- package/lib/module/components/styled/SendInput.js +35 -10
- package/lib/module/components/styled/SendInput.js.map +1 -1
- package/lib/module/components/styled/StyledComponents.js +6 -2
- package/lib/module/components/styled/StyledComponents.js.map +1 -1
- package/lib/module/components/styled/StyledInputComponents/StyledInputComponents.js +6 -1
- package/lib/module/components/styled/StyledInputComponents/StyledInputComponents.js.map +1 -1
- package/lib/module/helpers/headerLayout.js +30 -0
- package/lib/module/helpers/headerLayout.js.map +1 -0
- package/lib/module/helpers/parseMessageBody.js +12 -5
- package/lib/module/helpers/parseMessageBody.js.map +1 -1
- package/lib/module/helpers/typography.js +23 -0
- package/lib/module/helpers/typography.js.map +1 -0
- package/lib/module/hooks/useChatFonts.js +92 -8
- package/lib/module/hooks/useChatFonts.js.map +1 -1
- package/lib/typescript/components/MainComponents/ChatHeader.d.ts.map +1 -1
- package/lib/typescript/components/MessageBubble/Message.d.ts.map +1 -1
- package/lib/typescript/components/Modals/AttachSheet/AttachSheet.d.ts +0 -3
- package/lib/typescript/components/Modals/AttachSheet/AttachSheet.d.ts.map +1 -1
- package/lib/typescript/components/Modals/ChatProfileModal/ChatProfileModal.d.ts.map +1 -1
- package/lib/typescript/components/Modals/FilePreviewModal/FilePreviewModal.d.ts.map +1 -1
- package/lib/typescript/components/Modals/ModalHeaderComponent.d.ts +3 -0
- package/lib/typescript/components/Modals/ModalHeaderComponent.d.ts.map +1 -1
- package/lib/typescript/components/Modals/styledModalComponents.d.ts +7 -1
- package/lib/typescript/components/Modals/styledModalComponents.d.ts.map +1 -1
- package/lib/typescript/components/styled/SendInput.d.ts.map +1 -1
- package/lib/typescript/components/styled/StyledComponents.d.ts +7 -1
- package/lib/typescript/components/styled/StyledComponents.d.ts.map +1 -1
- package/lib/typescript/components/styled/StyledInputComponents/StyledInputComponents.d.ts +6 -2
- package/lib/typescript/components/styled/StyledInputComponents/StyledInputComponents.d.ts.map +1 -1
- package/lib/typescript/helpers/headerLayout.d.ts +26 -0
- package/lib/typescript/helpers/headerLayout.d.ts.map +1 -0
- package/lib/typescript/helpers/parseMessageBody.d.ts +2 -1
- package/lib/typescript/helpers/parseMessageBody.d.ts.map +1 -1
- package/lib/typescript/helpers/typography.d.ts +14 -0
- package/lib/typescript/helpers/typography.d.ts.map +1 -0
- package/lib/typescript/hooks/useChatFonts.d.ts.map +1 -1
- package/lib/typescript/main.d.ts +1 -1
- package/lib/typescript/main.d.ts.map +1 -1
- package/lib/typescript/types/types.d.ts +82 -47
- package/lib/typescript/types/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/MainComponents/ChatHeader.tsx +15 -3
- package/src/components/MessageBubble/Message.tsx +27 -11
- package/src/components/Modals/AttachSheet/AttachSheet.tsx +8 -32
- package/src/components/Modals/ChatProfileModal/ChatProfileModal.tsx +16 -7
- package/src/components/Modals/FilePreviewModal/FilePreviewModal.tsx +22 -5
- package/src/components/Modals/ModalHeaderComponent.tsx +20 -3
- package/src/components/Modals/styledModalComponents.tsx +6 -3
- package/src/components/styled/SendInput.tsx +31 -8
- package/src/components/styled/StyledComponents.ts +6 -3
- package/src/components/styled/StyledInputComponents/StyledInputComponents.tsx +4 -1
- package/src/helpers/headerLayout.ts +30 -0
- package/src/helpers/parseMessageBody.tsx +13 -4
- package/src/helpers/typography.ts +19 -0
- package/src/hooks/useChatFonts.ts +69 -11
- package/src/main.ts +1 -0
- package/src/types/types.ts +90 -48
|
@@ -12,8 +12,8 @@ import {
|
|
|
12
12
|
View,
|
|
13
13
|
} from 'react-native';
|
|
14
14
|
import { CameraIcon, DocumentIcon, MediaIcon } from '../../../assets/icons';
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
15
|
+
import { useChatSettingState } from '../../../hooks/useChatSettingState';
|
|
16
|
+
import { chatTextStyle } from '../../../helpers/typography';
|
|
17
17
|
|
|
18
18
|
interface AttachSheetProps {
|
|
19
19
|
visible: boolean;
|
|
@@ -22,8 +22,6 @@ interface AttachSheetProps {
|
|
|
22
22
|
onGallery: () => void;
|
|
23
23
|
onDocument: () => void;
|
|
24
24
|
primaryColor?: string;
|
|
25
|
-
/** For `typography.elements.attachSheet*` font overrides. */
|
|
26
|
-
config?: IConfig;
|
|
27
25
|
}
|
|
28
26
|
|
|
29
27
|
// Starting translateY for the sheet. Larger than any realistic sheet
|
|
@@ -38,8 +36,9 @@ const AttachSheet: React.FC<AttachSheetProps> = ({
|
|
|
38
36
|
onGallery,
|
|
39
37
|
onDocument,
|
|
40
38
|
primaryColor = '#0052CD',
|
|
41
|
-
config,
|
|
42
39
|
}) => {
|
|
40
|
+
const { config } = useChatSettingState();
|
|
41
|
+
const ts = config?.typography?.attachSheet;
|
|
43
42
|
// Pending handler captured on row tap — runs once the EXIT animation
|
|
44
43
|
// finishes so iOS doesn't try to present an image picker over a
|
|
45
44
|
// still-dismissing modal (was the original reason for the
|
|
@@ -184,9 +183,7 @@ const AttachSheet: React.FC<AttachSheetProps> = ({
|
|
|
184
183
|
>
|
|
185
184
|
<TouchableOpacity activeOpacity={1}>
|
|
186
185
|
<View style={styles.grabber} />
|
|
187
|
-
<Text style={[styles.title,
|
|
188
|
-
Attach
|
|
189
|
-
</Text>
|
|
186
|
+
<Text style={[styles.title, chatTextStyle(ts?.title)]}>Attach</Text>
|
|
190
187
|
{rows.map(({ label, hint, Icon, handler }, idx) => (
|
|
191
188
|
<TouchableOpacity
|
|
192
189
|
key={label}
|
|
@@ -206,22 +203,8 @@ const AttachSheet: React.FC<AttachSheetProps> = ({
|
|
|
206
203
|
<Icon color={primaryColor} width={20} height={20} />
|
|
207
204
|
</View>
|
|
208
205
|
<View style={styles.rowText}>
|
|
209
|
-
<Text
|
|
210
|
-
|
|
211
|
-
styles.rowLabel,
|
|
212
|
-
getElementFont(config, 'attachSheetRowLabel'),
|
|
213
|
-
]}
|
|
214
|
-
>
|
|
215
|
-
{label}
|
|
216
|
-
</Text>
|
|
217
|
-
<Text
|
|
218
|
-
style={[
|
|
219
|
-
styles.rowHint,
|
|
220
|
-
getElementFont(config, 'attachSheetRowHint'),
|
|
221
|
-
]}
|
|
222
|
-
>
|
|
223
|
-
{hint}
|
|
224
|
-
</Text>
|
|
206
|
+
<Text style={[styles.rowLabel, chatTextStyle(ts?.rowLabel)]}>{label}</Text>
|
|
207
|
+
<Text style={[styles.rowHint, chatTextStyle(ts?.rowHint)]}>{hint}</Text>
|
|
225
208
|
</View>
|
|
226
209
|
</TouchableOpacity>
|
|
227
210
|
))}
|
|
@@ -230,14 +213,7 @@ const AttachSheet: React.FC<AttachSheetProps> = ({
|
|
|
230
213
|
style={styles.cancelRow}
|
|
231
214
|
onPress={onClose}
|
|
232
215
|
>
|
|
233
|
-
<Text
|
|
234
|
-
style={[
|
|
235
|
-
styles.cancelLabel,
|
|
236
|
-
getElementFont(config, 'attachSheetCancel'),
|
|
237
|
-
]}
|
|
238
|
-
>
|
|
239
|
-
Cancel
|
|
240
|
-
</Text>
|
|
216
|
+
<Text style={[styles.cancelLabel, chatTextStyle(ts?.cancelButton)]}>Cancel</Text>
|
|
241
217
|
</TouchableOpacity>
|
|
242
218
|
</TouchableOpacity>
|
|
243
219
|
</Animated.View>
|
|
@@ -25,6 +25,8 @@ import Button from '../../styled/Button';
|
|
|
25
25
|
import { DeleteIcon, MoreIcon, QrIcon } from '../../../assets/icons';
|
|
26
26
|
import Switch from '../../MainComponents/Switch';
|
|
27
27
|
import { useChatSettingState } from '../../../hooks/useChatSettingState';
|
|
28
|
+
import { chatTextStyle } from '../../../helpers/typography';
|
|
29
|
+
import { getElementFont } from '../../../helpers/getElementFont';
|
|
28
30
|
import { deleteRoomMember } from '../../../networking/api-requests/rooms.api';
|
|
29
31
|
import { RoomMember } from '../../../types/models/room.model';
|
|
30
32
|
import { setActiveModal, setSelectedUser } from '../../../roomStore/chatSettingsSlice';
|
|
@@ -33,7 +35,6 @@ import DropdownMenu, { MenuOption } from '../../DropdownMenu/DropdownMenu';
|
|
|
33
35
|
import SelectUsersModal from '../SelectUsersModal/SelectUsersModal';
|
|
34
36
|
import { useToast } from '../../../context/ToastContext';
|
|
35
37
|
import DeleteChatModal from './DeleteChatModal';
|
|
36
|
-
import { getElementFont } from '../../../helpers/getElementFont';
|
|
37
38
|
|
|
38
39
|
|
|
39
40
|
interface ChatProfileModalProps {
|
|
@@ -268,6 +269,7 @@ const ChatProfileModal: React.FC<ChatProfileModalProps> = ({
|
|
|
268
269
|
<ModalHeaderComponent
|
|
269
270
|
handleCloseModal={handleCloseModal}
|
|
270
271
|
headerTitle={'Chat Profile'}
|
|
272
|
+
titleStyle={chatTextStyle(config?.typography?.profile?.screenTitle)}
|
|
271
273
|
rightMenu={
|
|
272
274
|
<>
|
|
273
275
|
{activeRoom?.type === 'public' && (
|
|
@@ -314,10 +316,13 @@ const ChatProfileModal: React.FC<ChatProfileModalProps> = ({
|
|
|
314
316
|
size={128}
|
|
315
317
|
/>
|
|
316
318
|
<UserInfo>
|
|
317
|
-
<UserName
|
|
319
|
+
<UserName
|
|
320
|
+
fontSize={config?.typography?.profile?.title?.fontSize}
|
|
321
|
+
fontWeight={config?.typography?.profile?.title?.fontWeight as any}
|
|
322
|
+
>
|
|
318
323
|
{activeRoom.title || activeRoom.name}
|
|
319
324
|
</UserName>
|
|
320
|
-
<UserStatus>
|
|
325
|
+
<UserStatus style={getElementFont(config, 'profileStatus')}>
|
|
321
326
|
{activeRoom.usersCnt}{' '}
|
|
322
327
|
{activeRoom.usersCnt > 1 ? 'members' : 'member'}
|
|
323
328
|
</UserStatus>
|
|
@@ -330,13 +335,17 @@ const ChatProfileModal: React.FC<ChatProfileModalProps> = ({
|
|
|
330
335
|
)}
|
|
331
336
|
{!config?.disableChatInfo?.disableDescription && (
|
|
332
337
|
<BorderedContainer>
|
|
333
|
-
<LabelData>
|
|
338
|
+
<LabelData style={getElementFont(config, 'profileSectionLabel')}>
|
|
339
|
+
Description
|
|
340
|
+
</LabelData>
|
|
334
341
|
<Label>{activeRoom?.description || '—'}</Label>
|
|
335
342
|
</BorderedContainer>
|
|
336
343
|
)}
|
|
337
344
|
{!config?.disableChatInfo?.disableType && (
|
|
338
345
|
<BorderedContainer>
|
|
339
|
-
<LabelData
|
|
346
|
+
<LabelData style={getElementFont(config, 'profileSectionLabel')}>
|
|
347
|
+
Chat type
|
|
348
|
+
</LabelData>
|
|
340
349
|
<Label>{activeRoom.type || '—'}</Label>
|
|
341
350
|
</BorderedContainer>
|
|
342
351
|
)}
|
|
@@ -419,13 +428,13 @@ const ChatProfileModal: React.FC<ChatProfileModalProps> = ({
|
|
|
419
428
|
<Label
|
|
420
429
|
style={[
|
|
421
430
|
{ fontSize: 16, fontWeight: 600 },
|
|
422
|
-
|
|
431
|
+
chatTextStyle(config?.typography?.profile?.memberName),
|
|
423
432
|
]}
|
|
424
433
|
>
|
|
425
434
|
{user.firstName} {user.lastName}
|
|
426
435
|
</Label>
|
|
427
436
|
{user.last_active && (
|
|
428
|
-
<LabelData>
|
|
437
|
+
<LabelData style={getElementFont(config, 'profileSectionLabel')}>
|
|
429
438
|
{new Date(user.last_active * 1000).toLocaleString()}
|
|
430
439
|
</LabelData>
|
|
431
440
|
)}
|
|
@@ -6,6 +6,8 @@ import {
|
|
|
6
6
|
} from '../styledModalComponents';
|
|
7
7
|
import { SaveIcon } from '../../../assets/icons';
|
|
8
8
|
import ModalHeaderComponent from '../ModalHeaderComponent';
|
|
9
|
+
import { useChatSettingState } from '../../../hooks/useChatSettingState';
|
|
10
|
+
import { chatTextStyle } from '../../../helpers/typography';
|
|
9
11
|
import { useDispatch, useSelector } from 'react-redux';
|
|
10
12
|
import Button from '../../styled/Button';
|
|
11
13
|
import { RootState } from '../../../roomStore';
|
|
@@ -18,6 +20,7 @@ import {
|
|
|
18
20
|
Platform,
|
|
19
21
|
TouchableOpacity,
|
|
20
22
|
StyleSheet,
|
|
23
|
+
Share,
|
|
21
24
|
} from 'react-native';
|
|
22
25
|
import { useVideoPlayer, VideoView } from 'expo-video';
|
|
23
26
|
import { PlayIcon } from '../../../assets/icons';
|
|
@@ -109,6 +112,7 @@ const FilePreviewModal: React.FC<FilePreviewModalProps> = ({
|
|
|
109
112
|
}) => {
|
|
110
113
|
const dispatch = useDispatch();
|
|
111
114
|
const { showToast } = useToast();
|
|
115
|
+
const { config } = useChatSettingState();
|
|
112
116
|
|
|
113
117
|
// Cache the SAF directory the user granted so saving several documents
|
|
114
118
|
// in a row doesn't re-prompt for a folder every time (Android only).
|
|
@@ -204,13 +208,25 @@ const FilePreviewModal: React.FC<FilePreviewModalProps> = ({
|
|
|
204
208
|
await FileSystem.writeAsStringAsync(destUri, base64, {
|
|
205
209
|
encoding: FileSystem.EncodingType.Base64,
|
|
206
210
|
});
|
|
211
|
+
|
|
212
|
+
showToast({
|
|
213
|
+
id: Date.now().toString(),
|
|
214
|
+
title: 'Success',
|
|
215
|
+
message: 'Save successful',
|
|
216
|
+
type: 'success',
|
|
217
|
+
});
|
|
218
|
+
return;
|
|
207
219
|
}
|
|
208
220
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
221
|
+
// iOS (and any non-Android): there is no app-writable "Downloads"
|
|
222
|
+
// folder, and MediaLibrary only accepts images/videos — which is why
|
|
223
|
+
// documents previously hit the success toast without ever being
|
|
224
|
+
// saved. Present the system share sheet so the user can "Save to
|
|
225
|
+
// Files", AirDrop, etc. The sheet itself is the confirmation (the
|
|
226
|
+
// user may cancel), so we don't show a "Save successful" toast here.
|
|
227
|
+
await Share.share({
|
|
228
|
+
url: download.uri,
|
|
229
|
+
title: fileName,
|
|
214
230
|
});
|
|
215
231
|
} catch (err) {
|
|
216
232
|
console.error('Error saving file:', err);
|
|
@@ -377,6 +393,7 @@ const FilePreviewModal: React.FC<FilePreviewModalProps> = ({
|
|
|
377
393
|
<ModalHeaderComponent
|
|
378
394
|
handleCloseModal={closeModal}
|
|
379
395
|
headerTitle={'File preview'}
|
|
396
|
+
titleStyle={chatTextStyle(config?.typography?.profile?.screenTitle)}
|
|
380
397
|
rightMenu={
|
|
381
398
|
<>
|
|
382
399
|
<Button onPress={saveClick}>
|
|
@@ -6,12 +6,16 @@ import {
|
|
|
6
6
|
} from './styledModalComponents';
|
|
7
7
|
import { BackIcon, MoreIcon, QrIcon } from '../../assets/icons';
|
|
8
8
|
import Button from '../styled/Button';
|
|
9
|
-
import { Text } from 'react-native';
|
|
9
|
+
import { Text, StyleProp, TextStyle } from 'react-native';
|
|
10
10
|
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
11
|
+
import { resolveHeaderHeight } from '../../helpers/headerLayout';
|
|
12
|
+
import { useChatSettingState } from '../../hooks/useChatSettingState';
|
|
11
13
|
|
|
12
14
|
interface ModalHeaderComponentProps {
|
|
13
15
|
handleCloseModal?: any;
|
|
14
16
|
headerTitle?: string;
|
|
17
|
+
/** Optional size/weight override for the header title text. */
|
|
18
|
+
titleStyle?: StyleProp<TextStyle>;
|
|
15
19
|
rightMenu?: React.ReactNode;
|
|
16
20
|
leftMenu?: React.ReactNode;
|
|
17
21
|
}
|
|
@@ -19,6 +23,7 @@ interface ModalHeaderComponentProps {
|
|
|
19
23
|
const ModalHeaderComponent: React.FC<ModalHeaderComponentProps> = ({
|
|
20
24
|
handleCloseModal,
|
|
21
25
|
headerTitle,
|
|
26
|
+
titleStyle,
|
|
22
27
|
rightMenu,
|
|
23
28
|
leftMenu,
|
|
24
29
|
}) => {
|
|
@@ -28,15 +33,27 @@ const ModalHeaderComponent: React.FC<ModalHeaderComponentProps> = ({
|
|
|
28
33
|
// mounted by ReduxWrapper, so this reads the live per-device inset
|
|
29
34
|
// (notch / Dynamic Island / nothing).
|
|
30
35
|
const insets = useSafeAreaInsets();
|
|
36
|
+
const { config } = useChatSettingState();
|
|
37
|
+
// Same band height as the in-chat header, sitting BELOW the status-bar
|
|
38
|
+
// inset. Total = inset + band; paddingTop carries the inset and the band
|
|
39
|
+
// is the content area (centered via HeaderContainer's align-items:center),
|
|
40
|
+
// so both headers match in visible height regardless of device notch.
|
|
41
|
+
const band = resolveHeaderHeight(config?.headerLayout?.height);
|
|
31
42
|
return (
|
|
32
|
-
<HeaderContainer
|
|
43
|
+
<HeaderContainer
|
|
44
|
+
style={{
|
|
45
|
+
paddingTop: insets.top,
|
|
46
|
+
paddingBottom: 0,
|
|
47
|
+
height: insets.top + band,
|
|
48
|
+
}}
|
|
49
|
+
>
|
|
33
50
|
<HeaderLeft>
|
|
34
51
|
{leftMenu ? (
|
|
35
52
|
leftMenu
|
|
36
53
|
) : (
|
|
37
54
|
<>
|
|
38
55
|
<Button EndIcon={<BackIcon />} onPress={handleCloseModal} />
|
|
39
|
-
<Text>{headerTitle ?? 'Go back'}</Text>
|
|
56
|
+
<Text style={titleStyle}>{headerTitle ?? 'Go back'}</Text>
|
|
40
57
|
</>
|
|
41
58
|
)}
|
|
42
59
|
</HeaderLeft>
|
|
@@ -128,10 +128,13 @@ export const UserInfo = styled.View`
|
|
|
128
128
|
align-items: center;
|
|
129
129
|
`;
|
|
130
130
|
|
|
131
|
-
export const UserName = styled.Text
|
|
131
|
+
export const UserName = styled.Text<{
|
|
132
|
+
fontSize?: number;
|
|
133
|
+
fontWeight?: string;
|
|
134
|
+
}>`
|
|
132
135
|
color: #141414;
|
|
133
|
-
font-size:
|
|
134
|
-
font-weight: 400;
|
|
136
|
+
font-size: ${({ fontSize }) => fontSize ?? 24}px;
|
|
137
|
+
font-weight: ${({ fontWeight }) => fontWeight ?? 400};
|
|
135
138
|
`;
|
|
136
139
|
|
|
137
140
|
export const UserStatus = styled.Text`
|
|
@@ -121,6 +121,13 @@ const SendInput: React.FC<SendInputProps> = ({
|
|
|
121
121
|
// (versus duplicating the send with stale closure state).
|
|
122
122
|
const sendingRef = useRef(false);
|
|
123
123
|
|
|
124
|
+
// Re-entry guard for the document picker. expo-document-picker keeps a
|
|
125
|
+
// single native `pickingContext`; invoking getDocumentAsync again while a
|
|
126
|
+
// pick is still in flight throws "Different document picking in progress"
|
|
127
|
+
// (iOS, bug #4). A double-fire happens on a fast double-tap of the
|
|
128
|
+
// "Document" row or a dev-mode double-run of the sheet's close callback.
|
|
129
|
+
const pickingDocRef = useRef(false);
|
|
130
|
+
|
|
124
131
|
const handleFileSelect = (files: MediaFile[]) => {
|
|
125
132
|
// Enforce the upload size cap before anything reaches the network.
|
|
126
133
|
// The backend rejects oversized bodies with HTTP 413; catching it
|
|
@@ -238,7 +245,21 @@ const SendInput: React.FC<SendInputProps> = ({
|
|
|
238
245
|
};
|
|
239
246
|
|
|
240
247
|
const handleFileSelection = async () => {
|
|
248
|
+
// Drop overlapping invocations — a second getDocumentAsync while the
|
|
249
|
+
// first is still presented throws PickingInProgressException on iOS.
|
|
250
|
+
if (pickingDocRef.current) {return;}
|
|
251
|
+
pickingDocRef.current = true;
|
|
241
252
|
try {
|
|
253
|
+
// Let the attach-sheet's modal finish its NATIVE dismissal before we
|
|
254
|
+
// present the picker. The sheet fires this handler right after its
|
|
255
|
+
// slide-out animation, but the underlying RN Modal view-controller
|
|
256
|
+
// dismissal is async — presenting the document picker over a still-
|
|
257
|
+
// dismissing modal makes iOS tear it down without a cancel callback,
|
|
258
|
+
// which strands expo-document-picker's pickingContext and makes every
|
|
259
|
+
// subsequent open throw "Different document picking in progress".
|
|
260
|
+
if (Platform.OS === 'ios') {
|
|
261
|
+
await new Promise((resolve) => setTimeout(resolve, 350));
|
|
262
|
+
}
|
|
242
263
|
const result = await DocumentPicker.getDocumentAsync({
|
|
243
264
|
type: '*/*',
|
|
244
265
|
multiple: false,
|
|
@@ -256,6 +277,8 @@ const SendInput: React.FC<SendInputProps> = ({
|
|
|
256
277
|
]);
|
|
257
278
|
} catch (err) {
|
|
258
279
|
console.error('DocumentPicker error:', err);
|
|
280
|
+
} finally {
|
|
281
|
+
pickingDocRef.current = false;
|
|
259
282
|
}
|
|
260
283
|
};
|
|
261
284
|
|
|
@@ -496,6 +519,8 @@ const SendInput: React.FC<SendInputProps> = ({
|
|
|
496
519
|
accessibilityLabel="chat-message-input"
|
|
497
520
|
isFocused={isFocused}
|
|
498
521
|
color={config?.colors?.primary}
|
|
522
|
+
fontSize={config?.typography?.input?.fontSize}
|
|
523
|
+
fontWeight={config?.typography?.input?.fontWeight as any}
|
|
499
524
|
placeholder="Type message"
|
|
500
525
|
placeholderTextColor="#999"
|
|
501
526
|
value={message}
|
|
@@ -521,11 +546,11 @@ const SendInput: React.FC<SendInputProps> = ({
|
|
|
521
546
|
// the content-sized input.
|
|
522
547
|
style={[
|
|
523
548
|
{
|
|
524
|
-
minHeight: config?.
|
|
525
|
-
maxHeight: config?.
|
|
549
|
+
minHeight: config?.typography?.input?.minHeight ?? 40,
|
|
550
|
+
maxHeight: config?.typography?.input?.maxHeight ?? 120,
|
|
526
551
|
flex: 1,
|
|
527
552
|
},
|
|
528
|
-
getElementFont(config, '
|
|
553
|
+
getElementFont(config, 'inputText'),
|
|
529
554
|
]}
|
|
530
555
|
/>
|
|
531
556
|
</>
|
|
@@ -598,7 +623,6 @@ const SendInput: React.FC<SendInputProps> = ({
|
|
|
598
623
|
: handleSendClick;
|
|
599
624
|
const disabled = !isRecording && !showMic && !hasContent;
|
|
600
625
|
const filled = isRecording || hasContent || showMic;
|
|
601
|
-
const sendSize = config?.inputLayout?.sendButtonSize ?? 40;
|
|
602
626
|
return (
|
|
603
627
|
<Button
|
|
604
628
|
testID={showMic ? 'chat-record-button' : 'chat-send-button'}
|
|
@@ -613,9 +637,9 @@ const SendInput: React.FC<SendInputProps> = ({
|
|
|
613
637
|
)
|
|
614
638
|
}
|
|
615
639
|
style={{
|
|
616
|
-
width:
|
|
617
|
-
height:
|
|
618
|
-
borderRadius:
|
|
640
|
+
width: config?.typography?.input?.sendButtonSize ?? 40,
|
|
641
|
+
height: config?.typography?.input?.sendButtonSize ?? 40,
|
|
642
|
+
borderRadius: 50,
|
|
619
643
|
alignItems: 'center',
|
|
620
644
|
justifyContent: 'center',
|
|
621
645
|
marginLeft: 0,
|
|
@@ -636,7 +660,6 @@ const SendInput: React.FC<SendInputProps> = ({
|
|
|
636
660
|
onGallery={handleGallerySelection}
|
|
637
661
|
onDocument={handleFileSelection}
|
|
638
662
|
primaryColor={getIconColor(config)}
|
|
639
|
-
config={config}
|
|
640
663
|
/>
|
|
641
664
|
</InputContainer>
|
|
642
665
|
);
|
|
@@ -75,10 +75,13 @@ export const ChatContainerHeaderInfo = styled.View`
|
|
|
75
75
|
gap: 2px;
|
|
76
76
|
`;
|
|
77
77
|
|
|
78
|
-
export const ChatContainerHeaderLabel = styled.Text
|
|
78
|
+
export const ChatContainerHeaderLabel = styled.Text<{
|
|
79
|
+
fontSize?: number;
|
|
80
|
+
fontWeight?: string;
|
|
81
|
+
}>`
|
|
79
82
|
color: #141414;
|
|
80
|
-
font-weight: 600;
|
|
81
|
-
font-size:
|
|
83
|
+
font-weight: ${({ fontWeight }) => fontWeight ?? 600};
|
|
84
|
+
font-size: ${({ fontSize }) => fontSize ?? 16}px;
|
|
82
85
|
text-align: left;
|
|
83
86
|
overflow: hidden;
|
|
84
87
|
max-width: 100%;
|
|
@@ -45,6 +45,8 @@ export const MessageInputContainer = styled.View`
|
|
|
45
45
|
export const MessageInput = styled.TextInput<{
|
|
46
46
|
isFocused?: boolean;
|
|
47
47
|
color?: string;
|
|
48
|
+
fontSize?: number;
|
|
49
|
+
fontWeight?: string;
|
|
48
50
|
}>`
|
|
49
51
|
padding: 8px 14px;
|
|
50
52
|
border-radius: 20px;
|
|
@@ -52,7 +54,8 @@ export const MessageInput = styled.TextInput<{
|
|
|
52
54
|
border-color: ${(props) =>
|
|
53
55
|
props.isFocused ? props.color || '#0052CD' : 'transparent'};
|
|
54
56
|
color: #141414;
|
|
55
|
-
font-size:
|
|
57
|
+
font-size: ${({ fontSize }) => fontSize ?? 16}px;
|
|
58
|
+
${({ fontWeight }) => (fontWeight ? `font-weight: ${fontWeight};` : '')}
|
|
56
59
|
background-color: #f5f7f9;
|
|
57
60
|
flex: 1;
|
|
58
61
|
/* No explicit line-height: on iOS a line-height larger than the
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/** @format */
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Default (and minimum) height in px of the header content band — the bar
|
|
5
|
+
* itself, NOT counting the status-bar safe-area inset that sits above it.
|
|
6
|
+
*
|
|
7
|
+
* This single value is the "common denominator" shared by BOTH headers:
|
|
8
|
+
* • the in-chat header (ChatHeader / ChatContainerHeader), and
|
|
9
|
+
* • the full-screen modal header (ModalHeaderComponent / HeaderContainer)
|
|
10
|
+
* so they render at the same visible height below the status bar.
|
|
11
|
+
*
|
|
12
|
+
* It's also the floor: 64px comfortably fits the 40px avatar / back button
|
|
13
|
+
* plus breathing room. A smaller band clips that content, so configured
|
|
14
|
+
* values at or below this are ignored (see `resolveHeaderHeight`).
|
|
15
|
+
*/
|
|
16
|
+
export const DEFAULT_HEADER_HEIGHT = 64;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Resolve the effective header band height from an optional config override.
|
|
20
|
+
*
|
|
21
|
+
* The override can only make the header TALLER, never shorter than the
|
|
22
|
+
* default — a value <= DEFAULT_HEADER_HEIGHT (or non-numeric) is ignored and
|
|
23
|
+
* the default is used, so a too-small number (e.g. 10) can't collapse the
|
|
24
|
+
* bar and break its layout. Only a value strictly greater than the default
|
|
25
|
+
* takes effect.
|
|
26
|
+
*/
|
|
27
|
+
export const resolveHeaderHeight = (height?: number): number =>
|
|
28
|
+
typeof height === 'number' && height > DEFAULT_HEADER_HEIGHT
|
|
29
|
+
? height
|
|
30
|
+
: DEFAULT_HEADER_HEIGHT;
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
ScrollView,
|
|
9
9
|
StyleProp,
|
|
10
10
|
ViewStyle,
|
|
11
|
+
TextStyle,
|
|
11
12
|
} from 'react-native';
|
|
12
13
|
|
|
13
14
|
let elementKeyCounter = 0;
|
|
@@ -292,7 +293,15 @@ const renderMarkdownTableRN = (
|
|
|
292
293
|
);
|
|
293
294
|
};
|
|
294
295
|
|
|
295
|
-
|
|
296
|
+
// `baseStyle` is applied to the normal body-text leaves (paragraphs, list
|
|
297
|
+
// items, quotes) so a host can configure message font size / weight. Headings
|
|
298
|
+
// and code blocks keep their own semantic sizes. Markdown wraps text in
|
|
299
|
+
// <View>s, which break RN's Text-style inheritance — so the size has to be set
|
|
300
|
+
// on the leaf <Text>s here rather than on the bubble wrapper.
|
|
301
|
+
export const parseMessageBody = (
|
|
302
|
+
text: string,
|
|
303
|
+
baseStyle?: TextStyle
|
|
304
|
+
): JSX.Element => {
|
|
296
305
|
if (typeof text !== 'string') {
|
|
297
306
|
return <View />;
|
|
298
307
|
}
|
|
@@ -339,14 +348,14 @@ export const parseMessageBody = (text: string): JSX.Element => {
|
|
|
339
348
|
<View style={[styles.checkboxBox, item.checked && { backgroundColor: '#e8f3ff', borderColor: '#0a66c2' }]}>
|
|
340
349
|
{item.checked ? <Text style={styles.checkboxTick}>✓</Text> : null}
|
|
341
350
|
</View>
|
|
342
|
-
<Text style={{ flex: 1 }}>{parseInline(item.content)}</Text>
|
|
351
|
+
<Text style={[{ flex: 1 }, baseStyle]}>{parseInline(item.content)}</Text>
|
|
343
352
|
</View>
|
|
344
353
|
);
|
|
345
354
|
} else {
|
|
346
355
|
contentEl = (
|
|
347
356
|
<View key={`ulitem-${elementKeyCounter++}`} style={styles.listItemRow}>
|
|
348
357
|
<Text style={styles.listMarker}>{'\u2022'}</Text>
|
|
349
|
-
<Text style={{ flex: 1 }}>{parseInline(item.content)}</Text>
|
|
358
|
+
<Text style={[{ flex: 1 }, baseStyle]}>{parseInline(item.content)}</Text>
|
|
350
359
|
</View>
|
|
351
360
|
);
|
|
352
361
|
}
|
|
@@ -493,7 +502,7 @@ export const parseMessageBody = (text: string): JSX.Element => {
|
|
|
493
502
|
flushList();
|
|
494
503
|
elements.push(
|
|
495
504
|
<View key={`p-${elementKeyCounter++}`} style={styles.paragraph}>
|
|
496
|
-
<Text>{parseInline(trimmedLine)}</Text>
|
|
505
|
+
<Text style={baseStyle}>{parseInline(trimmedLine)}</Text>
|
|
497
506
|
</View>
|
|
498
507
|
);
|
|
499
508
|
} else {
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { TextStyle } from 'react-native';
|
|
2
|
+
import type { ChatTextStyle } from '../types/types';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Turn a `ChatTextStyle` config (fontSize / fontWeight) into a partial RN
|
|
6
|
+
* `TextStyle` that can be spread on top of a base style. Only sets fields the
|
|
7
|
+
* consumer actually provided, so it never clobbers a component default with
|
|
8
|
+
* `undefined`. Pair with a base style: `style={[styles.title, chatTextStyle(cfg)]}`.
|
|
9
|
+
*
|
|
10
|
+
* Note: with a custom font, `fontWeight` only renders visibly when
|
|
11
|
+
* `typography.weightFamilies` is configured — the global font patch
|
|
12
|
+
* (useChatFonts) maps the weight to the matching family variant.
|
|
13
|
+
*/
|
|
14
|
+
export const chatTextStyle = (cfg?: ChatTextStyle): TextStyle => {
|
|
15
|
+
const s: TextStyle = {};
|
|
16
|
+
if (cfg?.fontSize != null) {s.fontSize = cfg.fontSize;}
|
|
17
|
+
if (cfg?.fontWeight != null) {s.fontWeight = cfg.fontWeight;}
|
|
18
|
+
return s;
|
|
19
|
+
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useEffect, useState } from 'react';
|
|
2
|
-
import { Text, TextInput } from 'react-native';
|
|
2
|
+
import { StyleSheet, Text, TextInput } from 'react-native';
|
|
3
3
|
import type { TypographyConfig } from '../types/types';
|
|
4
4
|
|
|
5
5
|
/**
|
|
@@ -25,6 +25,8 @@ import type { TypographyConfig } from '../types/types';
|
|
|
25
25
|
export function useChatFonts(typography?: TypographyConfig): boolean {
|
|
26
26
|
const [ready, setReady] = useState(!typography?.fonts?.length);
|
|
27
27
|
|
|
28
|
+
// Apply synchronously on render (not just in the effect) so the factory
|
|
29
|
+
// patch sees the active family/weights before the first child paints.
|
|
28
30
|
applyDefaultFont(typography);
|
|
29
31
|
|
|
30
32
|
useEffect(() => {
|
|
@@ -76,23 +78,78 @@ export function useChatFonts(typography?: TypographyConfig): boolean {
|
|
|
76
78
|
}
|
|
77
79
|
|
|
78
80
|
/**
|
|
79
|
-
* The family currently applied as the chat-wide default. Read at
|
|
80
|
-
* by the patched
|
|
81
|
-
* restyles the whole tree.
|
|
81
|
+
* The family currently applied as the chat-wide default. Read at element-
|
|
82
|
+
* creation time by the patched JSX factories below, so updating it (then
|
|
83
|
+
* triggering a re-render) restyles the whole tree.
|
|
82
84
|
*/
|
|
83
85
|
let activeFamily: string | undefined;
|
|
84
86
|
|
|
87
|
+
/**
|
|
88
|
+
* Per-weight families (regular/medium/semibold/bold). When set, the factory
|
|
89
|
+
* patch maps a Text's effective `fontWeight` to the matching family so custom
|
|
90
|
+
* fonts render intermediate weights correctly — RN can't synthesise 500/600
|
|
91
|
+
* from a single font file (only fake-bold ~700), so each weight needs its own
|
|
92
|
+
* loaded family. This is why a bare `fontWeight: '500'` looked like it did
|
|
93
|
+
* nothing before: there was no medium family to switch to.
|
|
94
|
+
*/
|
|
95
|
+
let activeWeightFamilies: TypographyConfig['weightFamilies'] | undefined;
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Resolve a desired weight to a configured family variant. Returns undefined
|
|
99
|
+
* when there's no variant for that tier (so we leave the default family /
|
|
100
|
+
* weight untouched rather than guessing).
|
|
101
|
+
*/
|
|
102
|
+
function familyForWeight(weight: unknown): string | undefined {
|
|
103
|
+
const wf = activeWeightFamilies;
|
|
104
|
+
if (!wf || weight == null) {return undefined;}
|
|
105
|
+
let w: number;
|
|
106
|
+
if (typeof weight === 'number') {
|
|
107
|
+
w = weight;
|
|
108
|
+
} else if (weight === 'bold') {
|
|
109
|
+
w = 700;
|
|
110
|
+
} else if (weight === 'normal') {
|
|
111
|
+
w = 400;
|
|
112
|
+
} else {
|
|
113
|
+
const n = parseInt(String(weight), 10);
|
|
114
|
+
w = Number.isFinite(n) ? n : 400;
|
|
115
|
+
}
|
|
116
|
+
if (w >= 700) {return wf.bold;}
|
|
117
|
+
if (w >= 600) {return wf.semibold;}
|
|
118
|
+
if (w >= 500) {return wf.medium;}
|
|
119
|
+
return wf.regular;
|
|
120
|
+
}
|
|
85
121
|
|
|
122
|
+
/**
|
|
123
|
+
* Build the style array for a Text/TextInput element: the default family as the
|
|
124
|
+
* lowest-priority layer (explicit per-component styles still win), and — when
|
|
125
|
+
* per-weight families are configured — the variant family that matches the
|
|
126
|
+
* element's effective `fontWeight` as a top layer (with the numeric weight
|
|
127
|
+
* cleared so the OS doesn't also try to synthesise on top). An explicit
|
|
128
|
+
* per-element `fontFamily` override is respected.
|
|
129
|
+
*/
|
|
130
|
+
function styleForElement(style: any): any {
|
|
131
|
+
const base = [{ fontFamily: activeFamily }, style];
|
|
132
|
+
if (!activeWeightFamilies) {return base;}
|
|
133
|
+
const flat = (StyleSheet.flatten(style) || {}) as any;
|
|
134
|
+
if (flat.fontFamily && flat.fontFamily !== activeFamily) {return base;}
|
|
135
|
+
const fam = familyForWeight(flat.fontWeight);
|
|
136
|
+
if (!fam) {return base;}
|
|
137
|
+
return [{ fontFamily: activeFamily }, style, { fontFamily: fam, fontWeight: 'normal' }];
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Wrap a JSX factory (`jsx`/`jsxs`/`jsxDEV`/`createElement`) so every Text /
|
|
142
|
+
* TextInput it creates gets the chat-wide font injected. We patch the factory
|
|
143
|
+
* rather than `Text.render` because render-wrapping a `forwardRef` component is
|
|
144
|
+
* fragile across React 19 / RN 0.81; intercepting element creation is version-
|
|
145
|
+
* independent and catches styled-components' inner Text elements too.
|
|
146
|
+
*/
|
|
86
147
|
function wrapFactory(orig: any): any {
|
|
87
148
|
if (typeof orig !== 'function') return orig;
|
|
88
149
|
if (orig.__ethoraFontWrapped) return orig;
|
|
89
150
|
const wrapped = function (this: any, type: any, props: any, ...rest: any[]) {
|
|
90
|
-
if (
|
|
91
|
-
|
|
92
|
-
props &&
|
|
93
|
-
(type === Text || type === TextInput)
|
|
94
|
-
) {
|
|
95
|
-
props = { ...props, style: [{ fontFamily: activeFamily }, props.style] };
|
|
151
|
+
if (activeFamily && props && (type === Text || type === TextInput)) {
|
|
152
|
+
props = { ...props, style: styleForElement(props.style) };
|
|
96
153
|
}
|
|
97
154
|
return orig.call(this, type, props, ...rest);
|
|
98
155
|
};
|
|
@@ -145,9 +202,10 @@ function ensurePatched(): void {
|
|
|
145
202
|
}
|
|
146
203
|
|
|
147
204
|
/**
|
|
148
|
-
* Set (or clear) the chat-wide default font family.
|
|
205
|
+
* Set (or clear) the chat-wide default font family + per-weight families.
|
|
149
206
|
*/
|
|
150
207
|
function applyDefaultFont(typography?: TypographyConfig): void {
|
|
151
208
|
activeFamily = typography?.fontFamily;
|
|
209
|
+
activeWeightFamilies = typography?.weightFamilies;
|
|
152
210
|
if (activeFamily) ensurePatched();
|
|
153
211
|
}
|