@ethora/chat-component-rn 26.5.15 → 26.5.17

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 (56) hide show
  1. package/lib/commonjs/components/MainComponents/ReduxWrapper.js +4 -0
  2. package/lib/commonjs/components/MainComponents/ReduxWrapper.js.map +1 -1
  3. package/lib/commonjs/components/Modals/FilePreviewModal/FilePreviewModal.js +32 -16
  4. package/lib/commonjs/components/Modals/FilePreviewModal/FilePreviewModal.js.map +1 -1
  5. package/lib/commonjs/context/xmppProvider.js +4 -0
  6. package/lib/commonjs/context/xmppProvider.js.map +1 -1
  7. package/lib/commonjs/helpers/historyPreloadScheduler.js +118 -113
  8. package/lib/commonjs/helpers/historyPreloadScheduler.js.map +1 -1
  9. package/lib/commonjs/hooks/useChatFonts.js +137 -0
  10. package/lib/commonjs/hooks/useChatFonts.js.map +1 -0
  11. package/lib/commonjs/hooks/useUnreadMessagesCounter.js +4 -2
  12. package/lib/commonjs/hooks/useUnreadMessagesCounter.js.map +1 -1
  13. package/lib/commonjs/main.js +7 -0
  14. package/lib/commonjs/main.js.map +1 -1
  15. package/lib/commonjs/roomStore/roomsSlice.js +9 -1
  16. package/lib/commonjs/roomStore/roomsSlice.js.map +1 -1
  17. package/lib/module/components/MainComponents/ReduxWrapper.js +4 -0
  18. package/lib/module/components/MainComponents/ReduxWrapper.js.map +1 -1
  19. package/lib/module/components/Modals/FilePreviewModal/FilePreviewModal.js +33 -17
  20. package/lib/module/components/Modals/FilePreviewModal/FilePreviewModal.js.map +1 -1
  21. package/lib/module/context/xmppProvider.js +5 -1
  22. package/lib/module/context/xmppProvider.js.map +1 -1
  23. package/lib/module/helpers/historyPreloadScheduler.js +119 -114
  24. package/lib/module/helpers/historyPreloadScheduler.js.map +1 -1
  25. package/lib/module/hooks/useChatFonts.js +132 -0
  26. package/lib/module/hooks/useChatFonts.js.map +1 -0
  27. package/lib/module/hooks/useUnreadMessagesCounter.js +4 -2
  28. package/lib/module/hooks/useUnreadMessagesCounter.js.map +1 -1
  29. package/lib/module/main.js +2 -0
  30. package/lib/module/main.js.map +1 -1
  31. package/lib/module/roomStore/roomsSlice.js +7 -0
  32. package/lib/module/roomStore/roomsSlice.js.map +1 -1
  33. package/lib/typescript/components/MainComponents/ReduxWrapper.d.ts.map +1 -1
  34. package/lib/typescript/components/Modals/FilePreviewModal/FilePreviewModal.d.ts.map +1 -1
  35. package/lib/typescript/context/xmppProvider.d.ts.map +1 -1
  36. package/lib/typescript/helpers/historyPreloadScheduler.d.ts.map +1 -1
  37. package/lib/typescript/hooks/useChatFonts.d.ts +23 -0
  38. package/lib/typescript/hooks/useChatFonts.d.ts.map +1 -0
  39. package/lib/typescript/hooks/useUnreadMessagesCounter.d.ts +1 -0
  40. package/lib/typescript/hooks/useUnreadMessagesCounter.d.ts.map +1 -1
  41. package/lib/typescript/main.d.ts +2 -1
  42. package/lib/typescript/main.d.ts.map +1 -1
  43. package/lib/typescript/roomStore/roomsSlice.d.ts +3 -1
  44. package/lib/typescript/roomStore/roomsSlice.d.ts.map +1 -1
  45. package/lib/typescript/types/types.d.ts +39 -0
  46. package/lib/typescript/types/types.d.ts.map +1 -1
  47. package/package.json +5 -1
  48. package/src/components/MainComponents/ReduxWrapper.tsx +4 -0
  49. package/src/components/Modals/FilePreviewModal/FilePreviewModal.tsx +39 -17
  50. package/src/context/xmppProvider.tsx +5 -0
  51. package/src/helpers/historyPreloadScheduler.ts +7 -1
  52. package/src/hooks/useChatFonts.ts +153 -0
  53. package/src/hooks/useUnreadMessagesCounter.ts +5 -2
  54. package/src/main.ts +4 -0
  55. package/src/roomStore/roomsSlice.ts +11 -0
  56. package/src/types/types.ts +41 -0
@@ -1,7 +1,7 @@
1
1
  import { AppState } from 'react-native';
2
2
  import XmppClient from '../networking/xmppClient';
3
3
  import { store } from '../roomStore';
4
- import { applyRoomsPreloadBatch } from '../roomStore/roomsSlice';
4
+ import { applyRoomsPreloadBatch, setUnreadSyncing } from '../roomStore/roomsSlice';
5
5
  import { IMessage, IRoom } from '../types/types';
6
6
 
7
7
  interface HistoryPreloadSchedulerOptions {
@@ -112,6 +112,9 @@ export const runHistoryPreloadScheduler = async (
112
112
 
113
113
  if (signal?.aborted) {return;}
114
114
 
115
+ store.dispatch(setUnreadSyncing(true));
116
+ try {
117
+
115
118
  const state = store.getState();
116
119
  const rooms = (state.rooms.rooms || {}) as Record<string, IRoom>;
117
120
  const defaultSet = new Set(defaultRoomJids);
@@ -276,6 +279,9 @@ export const runHistoryPreloadScheduler = async (
276
279
  // Yield to JS event loop (no requestIdleCallback in RN).
277
280
  await new Promise((r) => setTimeout(r, 0));
278
281
  }
282
+ } finally {
283
+ store.dispatch(setUnreadSyncing(false));
284
+ }
279
285
  };
280
286
 
281
287
  export default runHistoryPreloadScheduler;
@@ -0,0 +1,153 @@
1
+ import { useEffect, useState } from 'react';
2
+ import { Text, TextInput } from 'react-native';
3
+ import type { TypographyConfig } from '../types/types';
4
+
5
+ /**
6
+ * Loads and applies the host-provided font configuration for the chat UI.
7
+ *
8
+ * React Native has no web-style runtime `@font-face`, so a custom font must be
9
+ * registered with `expo-font` before it can be used. This hook:
10
+ * 1. loads every entry in `typography.fonts` via `expo-font` (remote URL or
11
+ * bundled `require(...)`), then
12
+ * 2. sets the resolved `fontFamily` as the default for all `<Text>` and
13
+ * `<TextInput>` rendered by the chat — mirroring how the web SDK sets a
14
+ * CSS variable. Individual components can still override per-style.
15
+ *
16
+ * `expo-font` is an optional peer dependency: if the host hasn't installed it,
17
+ * the hook degrades gracefully (font simply isn't loaded) instead of throwing.
18
+ *
19
+ * No-op when `typography` is undefined — the system font is kept, so existing
20
+ * integrations are unaffected.
21
+ *
22
+ * @returns `true` once fonts are ready (or when there's nothing to load), so
23
+ * callers can gate first paint to avoid a font "flash" if desired.
24
+ */
25
+ export function useChatFonts(typography?: TypographyConfig): boolean {
26
+ const [ready, setReady] = useState(!typography?.fonts?.length);
27
+
28
+ applyDefaultFont(typography);
29
+
30
+ useEffect(() => {
31
+ let cancelled = false;
32
+
33
+ async function load() {
34
+ // Apply the default family immediately so already-available fonts
35
+ // (system, or host-loaded) take effect without waiting on a download.
36
+ applyDefaultFont(typography);
37
+
38
+ if (!typography?.fonts?.length) {
39
+ setReady(true);
40
+ return;
41
+ }
42
+
43
+ try {
44
+ // Optional dependency — resolve lazily so apps that don't use custom
45
+ // fonts don't need expo-font installed.
46
+ const ExpoFont = await import('expo-font').catch(() => null);
47
+ if (!ExpoFont) {
48
+ if (!cancelled) setReady(true);
49
+ return;
50
+ }
51
+ const map: Record<string, string | number> = {};
52
+ for (const f of typography.fonts) map[f.family] = f.source as never;
53
+ await ExpoFont.loadAsync(map);
54
+ if (!cancelled) {
55
+ applyDefaultFont(typography);
56
+ setReady(true);
57
+ }
58
+ } catch {
59
+ // Loading failed (e.g. bad URL) — keep the fallback font, don't block UI.
60
+ if (!cancelled) setReady(true);
61
+ }
62
+ }
63
+
64
+ load();
65
+ return () => {
66
+ cancelled = true;
67
+ };
68
+ // eslint-disable-next-line react-hooks/exhaustive-deps
69
+ }, [
70
+ typography?.fontFamily,
71
+ JSON.stringify(typography?.fonts ?? []),
72
+ JSON.stringify(typography?.weightFamilies ?? {}),
73
+ ]);
74
+
75
+ return ready;
76
+ }
77
+
78
+ /**
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.
82
+ */
83
+ let activeFamily: string | undefined;
84
+
85
+
86
+ function wrapFactory(orig: any): any {
87
+ if (typeof orig !== 'function') return orig;
88
+ if (orig.__ethoraFontWrapped) return orig;
89
+ 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] };
96
+ }
97
+ return orig.call(this, type, props, ...rest);
98
+ };
99
+ wrapped.__ethoraFontWrapped = true;
100
+ return wrapped;
101
+ }
102
+
103
+ let factoriesPatched = false;
104
+
105
+ function ensurePatched(): void {
106
+ if (factoriesPatched) return;
107
+
108
+ let any = false;
109
+
110
+ const patchMod = (mod: any): void => {
111
+ if (!mod) return;
112
+ for (const fn of ['jsx', 'jsxs', 'jsxDEV']) {
113
+ if (typeof mod[fn] === 'function' && !mod[fn].__ethoraFontWrapped) {
114
+ mod[fn] = wrapFactory(mod[fn]);
115
+ any = true;
116
+ }
117
+ }
118
+ };
119
+
120
+ try {
121
+ patchMod(require('react/jsx-runtime'));
122
+ } catch {
123
+ /* ignore */
124
+ }
125
+ try {
126
+ patchMod(require('react/jsx-dev-runtime'));
127
+ } catch {
128
+ /* ignore */
129
+ }
130
+
131
+ try {
132
+ const React = require('react');
133
+ if (
134
+ typeof React.createElement === 'function' &&
135
+ !React.createElement.__ethoraFontWrapped
136
+ ) {
137
+ React.createElement = wrapFactory(React.createElement);
138
+ any = true;
139
+ }
140
+ } catch {
141
+ /* ignore */
142
+ }
143
+
144
+ if (any) factoriesPatched = true;
145
+ }
146
+
147
+ /**
148
+ * Set (or clear) the chat-wide default font family.
149
+ */
150
+ function applyDefaultFont(typography?: TypographyConfig): void {
151
+ activeFamily = typography?.fontFamily;
152
+ if (activeFamily) ensurePatched();
153
+ }
@@ -10,6 +10,7 @@ interface UnreadMessagesStats {
10
10
  hasUnread: boolean;
11
11
  totalCount: number;
12
12
  unreadByRoom: UnreadMessagesMap;
13
+ isLoading: boolean;
13
14
  }
14
15
 
15
16
  export const useUnreadMessagesCounter = (): UnreadMessagesStats => {
@@ -26,11 +27,12 @@ export const useUnreadMessagesCounter = (): UnreadMessagesStats => {
26
27
  // so non-rooms dispatches still don't trigger a re-render.)
27
28
  const subscribe = (callback: () => void) => store.subscribe(callback);
28
29
 
29
- const rooms = useSyncExternalStore(
30
+ const roomsState = useSyncExternalStore(
30
31
  subscribe,
31
- () => store.getState().rooms.rooms
32
+ () => store.getState().rooms
32
33
  );
33
34
 
35
+ const rooms = roomsState.rooms;
34
36
  const unreadByRoom: UnreadMessagesMap = {};
35
37
  let totalCount = 0;
36
38
 
@@ -46,6 +48,7 @@ export const useUnreadMessagesCounter = (): UnreadMessagesStats => {
46
48
  hasUnread: totalCount > 0,
47
49
  totalCount,
48
50
  unreadByRoom,
51
+ isLoading: !!roomsState.isUnreadSyncing || !!roomsState.isLoading,
49
52
  };
50
53
  };
51
54
 
package/src/main.ts CHANGED
@@ -32,6 +32,8 @@ export { useChatRoomFocus } from './hooks/useChatRoomFocus';
32
32
  export { logoutService } from './hooks/useLogout';
33
33
  export { useQRCodeChat, handleQRChatId } from './hooks/useQRCodeChatHandler';
34
34
  export { resendMessage } from './utils/resendMessage';
35
+ // Font loader — exposed so hosts can also load the font outside <Chat> if needed.
36
+ export { useChatFonts } from './hooks/useChatFonts';
35
37
 
36
38
  // Public types — needed so consumers can type their `config` prop and
37
39
  // any event handlers they pass in.
@@ -43,4 +45,6 @@ export type {
43
45
  User,
44
46
  ConfigUser,
45
47
  MessageProps,
48
+ TypographyConfig,
49
+ RNFontSource,
46
50
  } from './types/types';
@@ -96,6 +96,7 @@ export interface RoomMessagesState {
96
96
  editAction?: EditAction;
97
97
  isLoading: boolean;
98
98
  loadingText?: string;
99
+ isUnreadSyncing?: boolean;
99
100
  usersSet?: Record<string, any>;
100
101
  pendingNotificationJid?: string | null;
101
102
  // Server-side read markers fetched from the XMPP private store
@@ -111,6 +112,7 @@ const initialState: RoomMessagesState = {
111
112
  activeRoomJID: null,
112
113
  visibleRoomJID: null,
113
114
  isLoading: false,
115
+ isUnreadSyncing: false,
114
116
  editAction: {
115
117
  isEdit: false,
116
118
  roomJid: '',
@@ -362,6 +364,7 @@ const reducers = {
362
364
  state.rooms = {};
363
365
  state.visibleRoomJID = null;
364
366
  state.privateStoreMarkers = {};
367
+ state.isUnreadSyncing = false;
365
368
  },
366
369
  setComposing(
367
370
  state: WritableDraft<RoomMessagesState>,
@@ -386,6 +389,12 @@ const reducers = {
386
389
  state.isLoading = loading;
387
390
  state.loadingText = loadingText;
388
391
  },
392
+ setUnreadSyncing: (
393
+ state: WritableDraft<RoomMessagesState>,
394
+ action: PayloadAction<boolean>
395
+ ) => {
396
+ state.isUnreadSyncing = action.payload;
397
+ },
389
398
  setLastViewedTimestamp: (
390
399
  state: WritableDraft<RoomMessagesState>,
391
400
  action: PayloadAction<{ chatJID: string; timestamp: number }>
@@ -538,6 +547,7 @@ const reducers = {
538
547
  state.activeRoomJID = null;
539
548
  state.visibleRoomJID = null;
540
549
  state.isLoading = false;
550
+ state.isUnreadSyncing = false;
541
551
  state.privateStoreMarkers = {};
542
552
  },
543
553
  setActiveMessage: (
@@ -729,6 +739,7 @@ export const {
729
739
  editRoomMessage,
730
740
  setComposing,
731
741
  setIsLoading,
742
+ setUnreadSyncing,
732
743
  setLastViewedTimestamp,
733
744
  applyPrivateStoreMarkers,
734
745
  setRoomNoMessages,
@@ -223,6 +223,45 @@ export interface PartialRoomWithMandatoryKeys {
223
223
  pinned?: boolean;
224
224
  }
225
225
 
226
+ /**
227
+ * A custom font to register at runtime via `expo-font`. On React Native a
228
+ * font must be loaded before it can be referenced by name — there is no
229
+ * web-style runtime `@font-face`. Each weight is typically its own file and
230
+ * its own family name (RN does not synthesise weights reliably).
231
+ *
232
+ * `source` accepts what `Font.loadAsync` accepts: a remote URL string (e.g. a
233
+ * hosted e-Ukraine `.ttf`), or a bundled asset via `require('./e-Ukraine.ttf')`.
234
+ */
235
+ export interface RNFontSource {
236
+ /** Family name used in text styles, e.g. "e-Ukraine" or "e-Ukraine-Medium". */
237
+ family: string;
238
+ /** Remote URL string or `require(...)` module for the font file. */
239
+ source: string | number;
240
+ }
241
+
242
+ /**
243
+ * Font configuration for the chat UI. When omitted, the component keeps the
244
+ * platform's default system font, so existing integrations are unaffected.
245
+ *
246
+ * Provide `fonts` for the SDK to load via expo-font, then set `fontFamily` to
247
+ * the family the chat should render in. If the host already loaded the font
248
+ * itself (e.g. via `@expo-google-fonts/*` or its own `useFonts`), pass only
249
+ * `fontFamily` and leave `fonts` empty.
250
+ */
251
+ export interface TypographyConfig {
252
+ /** Family applied across the chat UI. Must be loaded (via `fonts` or by the host). */
253
+ fontFamily?: string;
254
+ /** Optional per-weight families, used where the chat renders bolder text. */
255
+ weightFamilies?: {
256
+ regular?: string;
257
+ medium?: string;
258
+ semibold?: string;
259
+ bold?: string;
260
+ };
261
+ /** Fonts for the SDK to load with expo-font before applying `fontFamily`. */
262
+ fonts?: RNFontSource[];
263
+ }
264
+
226
265
  export interface IConfig {
227
266
  // ----- identity / network -----
228
267
  appId?: string;
@@ -232,6 +271,8 @@ export interface IConfig {
232
271
 
233
272
  // ----- theming -----
234
273
  colors?: { primary: string; secondary: string };
274
+ /** Configurable font family / weights for the chat UI. See TypographyConfig. */
275
+ typography?: TypographyConfig;
235
276
  messageColor?: {
236
277
  backgroundMessage: string;
237
278
  backgroundMessageUser: string;