@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.
- package/lib/commonjs/components/MainComponents/ReduxWrapper.js +4 -0
- package/lib/commonjs/components/MainComponents/ReduxWrapper.js.map +1 -1
- package/lib/commonjs/components/Modals/FilePreviewModal/FilePreviewModal.js +32 -16
- package/lib/commonjs/components/Modals/FilePreviewModal/FilePreviewModal.js.map +1 -1
- package/lib/commonjs/context/xmppProvider.js +4 -0
- package/lib/commonjs/context/xmppProvider.js.map +1 -1
- package/lib/commonjs/helpers/historyPreloadScheduler.js +118 -113
- package/lib/commonjs/helpers/historyPreloadScheduler.js.map +1 -1
- package/lib/commonjs/hooks/useChatFonts.js +137 -0
- package/lib/commonjs/hooks/useChatFonts.js.map +1 -0
- package/lib/commonjs/hooks/useUnreadMessagesCounter.js +4 -2
- package/lib/commonjs/hooks/useUnreadMessagesCounter.js.map +1 -1
- package/lib/commonjs/main.js +7 -0
- package/lib/commonjs/main.js.map +1 -1
- package/lib/commonjs/roomStore/roomsSlice.js +9 -1
- package/lib/commonjs/roomStore/roomsSlice.js.map +1 -1
- package/lib/module/components/MainComponents/ReduxWrapper.js +4 -0
- package/lib/module/components/MainComponents/ReduxWrapper.js.map +1 -1
- package/lib/module/components/Modals/FilePreviewModal/FilePreviewModal.js +33 -17
- package/lib/module/components/Modals/FilePreviewModal/FilePreviewModal.js.map +1 -1
- package/lib/module/context/xmppProvider.js +5 -1
- package/lib/module/context/xmppProvider.js.map +1 -1
- package/lib/module/helpers/historyPreloadScheduler.js +119 -114
- package/lib/module/helpers/historyPreloadScheduler.js.map +1 -1
- package/lib/module/hooks/useChatFonts.js +132 -0
- package/lib/module/hooks/useChatFonts.js.map +1 -0
- package/lib/module/hooks/useUnreadMessagesCounter.js +4 -2
- package/lib/module/hooks/useUnreadMessagesCounter.js.map +1 -1
- package/lib/module/main.js +2 -0
- package/lib/module/main.js.map +1 -1
- package/lib/module/roomStore/roomsSlice.js +7 -0
- package/lib/module/roomStore/roomsSlice.js.map +1 -1
- package/lib/typescript/components/MainComponents/ReduxWrapper.d.ts.map +1 -1
- package/lib/typescript/components/Modals/FilePreviewModal/FilePreviewModal.d.ts.map +1 -1
- package/lib/typescript/context/xmppProvider.d.ts.map +1 -1
- package/lib/typescript/helpers/historyPreloadScheduler.d.ts.map +1 -1
- package/lib/typescript/hooks/useChatFonts.d.ts +23 -0
- package/lib/typescript/hooks/useChatFonts.d.ts.map +1 -0
- package/lib/typescript/hooks/useUnreadMessagesCounter.d.ts +1 -0
- package/lib/typescript/hooks/useUnreadMessagesCounter.d.ts.map +1 -1
- package/lib/typescript/main.d.ts +2 -1
- package/lib/typescript/main.d.ts.map +1 -1
- package/lib/typescript/roomStore/roomsSlice.d.ts +3 -1
- package/lib/typescript/roomStore/roomsSlice.d.ts.map +1 -1
- package/lib/typescript/types/types.d.ts +39 -0
- package/lib/typescript/types/types.d.ts.map +1 -1
- package/package.json +5 -1
- package/src/components/MainComponents/ReduxWrapper.tsx +4 -0
- package/src/components/Modals/FilePreviewModal/FilePreviewModal.tsx +39 -17
- package/src/context/xmppProvider.tsx +5 -0
- package/src/helpers/historyPreloadScheduler.ts +7 -1
- package/src/hooks/useChatFonts.ts +153 -0
- package/src/hooks/useUnreadMessagesCounter.ts +5 -2
- package/src/main.ts +4 -0
- package/src/roomStore/roomsSlice.ts +11 -0
- 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
|
|
30
|
+
const roomsState = useSyncExternalStore(
|
|
30
31
|
subscribe,
|
|
31
|
-
() => store.getState().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,
|
package/src/types/types.ts
CHANGED
|
@@ -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;
|