@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.
Files changed (100) hide show
  1. package/README.md +40 -0
  2. package/lib/commonjs/components/MainComponents/ChatHeader.js +15 -2
  3. package/lib/commonjs/components/MainComponents/ChatHeader.js.map +1 -1
  4. package/lib/commonjs/components/MessageBubble/Message.js +24 -9
  5. package/lib/commonjs/components/MessageBubble/Message.js.map +1 -1
  6. package/lib/commonjs/components/Modals/AttachSheet/AttachSheet.js +11 -7
  7. package/lib/commonjs/components/Modals/AttachSheet/AttachSheet.js.map +1 -1
  8. package/lib/commonjs/components/Modals/ChatProfileModal/ChatProfileModal.js +10 -3
  9. package/lib/commonjs/components/Modals/ChatProfileModal/ChatProfileModal.js.map +1 -1
  10. package/lib/commonjs/components/Modals/FilePreviewModal/FilePreviewModal.js +23 -5
  11. package/lib/commonjs/components/Modals/FilePreviewModal/FilePreviewModal.js.map +1 -1
  12. package/lib/commonjs/components/Modals/ModalHeaderComponent.js +15 -1
  13. package/lib/commonjs/components/Modals/ModalHeaderComponent.js.map +1 -1
  14. package/lib/commonjs/components/Modals/styledModalComponents.js +6 -2
  15. package/lib/commonjs/components/Modals/styledModalComponents.js.map +1 -1
  16. package/lib/commonjs/components/styled/SendInput.js +34 -9
  17. package/lib/commonjs/components/styled/SendInput.js.map +1 -1
  18. package/lib/commonjs/components/styled/StyledComponents.js +6 -2
  19. package/lib/commonjs/components/styled/StyledComponents.js.map +1 -1
  20. package/lib/commonjs/components/styled/StyledInputComponents/StyledInputComponents.js +6 -1
  21. package/lib/commonjs/components/styled/StyledInputComponents/StyledInputComponents.js.map +1 -1
  22. package/lib/commonjs/helpers/headerLayout.js +35 -0
  23. package/lib/commonjs/helpers/headerLayout.js.map +1 -0
  24. package/lib/commonjs/helpers/parseMessageBody.js +12 -5
  25. package/lib/commonjs/helpers/parseMessageBody.js.map +1 -1
  26. package/lib/commonjs/helpers/typography.js +28 -0
  27. package/lib/commonjs/helpers/typography.js.map +1 -0
  28. package/lib/commonjs/hooks/useChatFonts.js +91 -7
  29. package/lib/commonjs/hooks/useChatFonts.js.map +1 -1
  30. package/lib/module/components/MainComponents/ChatHeader.js +15 -2
  31. package/lib/module/components/MainComponents/ChatHeader.js.map +1 -1
  32. package/lib/module/components/MessageBubble/Message.js +24 -9
  33. package/lib/module/components/MessageBubble/Message.js.map +1 -1
  34. package/lib/module/components/Modals/AttachSheet/AttachSheet.js +11 -7
  35. package/lib/module/components/Modals/AttachSheet/AttachSheet.js.map +1 -1
  36. package/lib/module/components/Modals/ChatProfileModal/ChatProfileModal.js +10 -3
  37. package/lib/module/components/Modals/ChatProfileModal/ChatProfileModal.js.map +1 -1
  38. package/lib/module/components/Modals/FilePreviewModal/FilePreviewModal.js +24 -6
  39. package/lib/module/components/Modals/FilePreviewModal/FilePreviewModal.js.map +1 -1
  40. package/lib/module/components/Modals/ModalHeaderComponent.js +15 -1
  41. package/lib/module/components/Modals/ModalHeaderComponent.js.map +1 -1
  42. package/lib/module/components/Modals/styledModalComponents.js +6 -2
  43. package/lib/module/components/Modals/styledModalComponents.js.map +1 -1
  44. package/lib/module/components/styled/SendInput.js +35 -10
  45. package/lib/module/components/styled/SendInput.js.map +1 -1
  46. package/lib/module/components/styled/StyledComponents.js +6 -2
  47. package/lib/module/components/styled/StyledComponents.js.map +1 -1
  48. package/lib/module/components/styled/StyledInputComponents/StyledInputComponents.js +6 -1
  49. package/lib/module/components/styled/StyledInputComponents/StyledInputComponents.js.map +1 -1
  50. package/lib/module/helpers/headerLayout.js +30 -0
  51. package/lib/module/helpers/headerLayout.js.map +1 -0
  52. package/lib/module/helpers/parseMessageBody.js +12 -5
  53. package/lib/module/helpers/parseMessageBody.js.map +1 -1
  54. package/lib/module/helpers/typography.js +23 -0
  55. package/lib/module/helpers/typography.js.map +1 -0
  56. package/lib/module/hooks/useChatFonts.js +92 -8
  57. package/lib/module/hooks/useChatFonts.js.map +1 -1
  58. package/lib/typescript/components/MainComponents/ChatHeader.d.ts.map +1 -1
  59. package/lib/typescript/components/MessageBubble/Message.d.ts.map +1 -1
  60. package/lib/typescript/components/Modals/AttachSheet/AttachSheet.d.ts +0 -3
  61. package/lib/typescript/components/Modals/AttachSheet/AttachSheet.d.ts.map +1 -1
  62. package/lib/typescript/components/Modals/ChatProfileModal/ChatProfileModal.d.ts.map +1 -1
  63. package/lib/typescript/components/Modals/FilePreviewModal/FilePreviewModal.d.ts.map +1 -1
  64. package/lib/typescript/components/Modals/ModalHeaderComponent.d.ts +3 -0
  65. package/lib/typescript/components/Modals/ModalHeaderComponent.d.ts.map +1 -1
  66. package/lib/typescript/components/Modals/styledModalComponents.d.ts +7 -1
  67. package/lib/typescript/components/Modals/styledModalComponents.d.ts.map +1 -1
  68. package/lib/typescript/components/styled/SendInput.d.ts.map +1 -1
  69. package/lib/typescript/components/styled/StyledComponents.d.ts +7 -1
  70. package/lib/typescript/components/styled/StyledComponents.d.ts.map +1 -1
  71. package/lib/typescript/components/styled/StyledInputComponents/StyledInputComponents.d.ts +6 -2
  72. package/lib/typescript/components/styled/StyledInputComponents/StyledInputComponents.d.ts.map +1 -1
  73. package/lib/typescript/helpers/headerLayout.d.ts +26 -0
  74. package/lib/typescript/helpers/headerLayout.d.ts.map +1 -0
  75. package/lib/typescript/helpers/parseMessageBody.d.ts +2 -1
  76. package/lib/typescript/helpers/parseMessageBody.d.ts.map +1 -1
  77. package/lib/typescript/helpers/typography.d.ts +14 -0
  78. package/lib/typescript/helpers/typography.d.ts.map +1 -0
  79. package/lib/typescript/hooks/useChatFonts.d.ts.map +1 -1
  80. package/lib/typescript/main.d.ts +1 -1
  81. package/lib/typescript/main.d.ts.map +1 -1
  82. package/lib/typescript/types/types.d.ts +82 -47
  83. package/lib/typescript/types/types.d.ts.map +1 -1
  84. package/package.json +1 -1
  85. package/src/components/MainComponents/ChatHeader.tsx +15 -3
  86. package/src/components/MessageBubble/Message.tsx +27 -11
  87. package/src/components/Modals/AttachSheet/AttachSheet.tsx +8 -32
  88. package/src/components/Modals/ChatProfileModal/ChatProfileModal.tsx +16 -7
  89. package/src/components/Modals/FilePreviewModal/FilePreviewModal.tsx +22 -5
  90. package/src/components/Modals/ModalHeaderComponent.tsx +20 -3
  91. package/src/components/Modals/styledModalComponents.tsx +6 -3
  92. package/src/components/styled/SendInput.tsx +31 -8
  93. package/src/components/styled/StyledComponents.ts +6 -3
  94. package/src/components/styled/StyledInputComponents/StyledInputComponents.tsx +4 -1
  95. package/src/helpers/headerLayout.ts +30 -0
  96. package/src/helpers/parseMessageBody.tsx +13 -4
  97. package/src/helpers/typography.ts +19 -0
  98. package/src/hooks/useChatFonts.ts +69 -11
  99. package/src/main.ts +1 -0
  100. 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 { getElementFont } from '../../../helpers/getElementFont';
16
- import { IConfig } from '../../../types/types';
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, getElementFont(config, 'attachSheetTitle')]}>
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
- style={[
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 style={getElementFont(config, 'chatProfileTitle')}>
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>Description</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>Chat type</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
- getElementFont(config, 'chatProfileMemberName'),
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
- showToast({
210
- id: Date.now().toString(),
211
- title: 'Success',
212
- message: 'Save successful',
213
- type: 'success',
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 style={{ paddingTop: insets.top + 8 }}>
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: 24px;
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?.inputLayout?.minHeight ?? 40,
525
- maxHeight: config?.inputLayout?.maxHeight ?? 120,
549
+ minHeight: config?.typography?.input?.minHeight ?? 40,
550
+ maxHeight: config?.typography?.input?.maxHeight ?? 120,
526
551
  flex: 1,
527
552
  },
528
- getElementFont(config, 'input'),
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: sendSize,
617
- height: sendSize,
618
- borderRadius: sendSize / 2,
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: 16px;
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: 16px;
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
- export const parseMessageBody = (text: string): JSX.Element => {
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 render time
80
- * by the patched components below, so updating it (then triggering a re-render)
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
- activeFamily &&
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
  }
package/src/main.ts CHANGED
@@ -47,4 +47,5 @@ export type {
47
47
  MessageProps,
48
48
  TypographyConfig,
49
49
  RNFontSource,
50
+ ChatTextStyle,
50
51
  } from './types/types';