stream-chat-react-native-core 9.3.1-beta.2 → 9.3.1-beta.4
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/AutoCompleteInput/AutoCompleteInput.js +38 -9
- package/lib/commonjs/components/AutoCompleteInput/AutoCompleteInput.js.map +1 -1
- package/lib/commonjs/components/Message/MessageItemView/MessageBubble.js +2 -3
- package/lib/commonjs/components/Message/MessageItemView/MessageBubble.js.map +1 -1
- package/lib/commonjs/components/Message/MessageItemView/MessageContent.js +18 -29
- package/lib/commonjs/components/Message/MessageItemView/MessageContent.js.map +1 -1
- package/lib/commonjs/components/Message/MessageItemView/MessageWrapper.js +2 -3
- package/lib/commonjs/components/Message/MessageItemView/MessageWrapper.js.map +1 -1
- package/lib/commonjs/components/Message/MessageItemView/utils/renderText.js +24 -0
- package/lib/commonjs/components/Message/MessageItemView/utils/renderText.js.map +1 -1
- package/lib/commonjs/components/Message/hooks/useMessageDeliveryData.js +10 -5
- package/lib/commonjs/components/Message/hooks/useMessageDeliveryData.js.map +1 -1
- package/lib/commonjs/components/Message/hooks/useMessageReadData.js +10 -5
- package/lib/commonjs/components/Message/hooks/useMessageReadData.js.map +1 -1
- package/lib/commonjs/version.json +1 -1
- package/lib/module/components/AutoCompleteInput/AutoCompleteInput.js +38 -9
- package/lib/module/components/AutoCompleteInput/AutoCompleteInput.js.map +1 -1
- package/lib/module/components/Message/MessageItemView/MessageBubble.js +2 -3
- package/lib/module/components/Message/MessageItemView/MessageBubble.js.map +1 -1
- package/lib/module/components/Message/MessageItemView/MessageContent.js +18 -29
- package/lib/module/components/Message/MessageItemView/MessageContent.js.map +1 -1
- package/lib/module/components/Message/MessageItemView/MessageWrapper.js +2 -3
- package/lib/module/components/Message/MessageItemView/MessageWrapper.js.map +1 -1
- package/lib/module/components/Message/MessageItemView/utils/renderText.js +24 -0
- package/lib/module/components/Message/MessageItemView/utils/renderText.js.map +1 -1
- package/lib/module/components/Message/hooks/useMessageDeliveryData.js +10 -5
- package/lib/module/components/Message/hooks/useMessageDeliveryData.js.map +1 -1
- package/lib/module/components/Message/hooks/useMessageReadData.js +10 -5
- package/lib/module/components/Message/hooks/useMessageReadData.js.map +1 -1
- package/lib/module/version.json +1 -1
- package/lib/typescript/components/AutoCompleteInput/AutoCompleteInput.d.ts.map +1 -1
- package/lib/typescript/components/Message/MessageItemView/MessageBubble.d.ts +1 -1
- package/lib/typescript/components/Message/MessageItemView/MessageBubble.d.ts.map +1 -1
- package/lib/typescript/components/Message/MessageItemView/MessageContent.d.ts.map +1 -1
- package/lib/typescript/components/Message/MessageItemView/MessageWrapper.d.ts +1 -1
- package/lib/typescript/components/Message/MessageItemView/MessageWrapper.d.ts.map +1 -1
- package/lib/typescript/components/Message/MessageItemView/utils/renderText.d.ts.map +1 -1
- package/lib/typescript/components/Message/hooks/useMessageDeliveryData.d.ts.map +1 -1
- package/lib/typescript/components/Message/hooks/useMessageReadData.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/AutoCompleteInput/AutoCompleteInput.tsx +51 -11
- package/src/components/AutoCompleteInput/__tests__/AutoCompleteInput.test.tsx +118 -0
- package/src/components/Message/MessageItemView/MessageBubble.tsx +3 -1
- package/src/components/Message/MessageItemView/MessageContent.tsx +32 -36
- package/src/components/Message/MessageItemView/MessageWrapper.tsx +1 -1
- package/src/components/Message/MessageItemView/utils/renderText.tsx +31 -0
- package/src/components/Message/hooks/useMessageDeliveryData.ts +11 -6
- package/src/components/Message/hooks/useMessageReadData.ts +11 -6
- package/src/components/Thread/__tests__/__snapshots__/Thread.test.tsx.snap +4 -4
- package/src/version.json +1 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
|
1
|
+
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
2
2
|
import {
|
|
3
3
|
I18nManager,
|
|
4
4
|
Platform,
|
|
@@ -109,6 +109,42 @@ const AutoCompleteInputWithContext = (props: AutoCompleteInputPropsWithContext)
|
|
|
109
109
|
const { command, text } = useStateStore(textComposer.state, textComposerStateSelector);
|
|
110
110
|
const { enabled } = useStateStore(messageComposer.configState, configStateSelector);
|
|
111
111
|
|
|
112
|
+
// RN's onChangeText doesn't carry cursor info, and iOS / Android fire
|
|
113
|
+
// onChangeText vs onSelectionChange in different orders. Rather than derive
|
|
114
|
+
// the caret from a text-length delta (fragile — gets clobbered by re-renders
|
|
115
|
+
// and varies across platforms), we hold the latest values reported by native
|
|
116
|
+
// and call into the LLC once both have settled.
|
|
117
|
+
const latestTextRef = useRef('');
|
|
118
|
+
const latestSelectionRef = useRef<{ end: number; start: number }>({
|
|
119
|
+
end: 0,
|
|
120
|
+
start: 0,
|
|
121
|
+
});
|
|
122
|
+
const flushHandleRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
123
|
+
|
|
124
|
+
const flushChange = useCallback(() => {
|
|
125
|
+
flushHandleRef.current = null;
|
|
126
|
+
textComposer.handleChange({
|
|
127
|
+
selection: latestSelectionRef.current,
|
|
128
|
+
text: latestTextRef.current,
|
|
129
|
+
});
|
|
130
|
+
}, [textComposer]);
|
|
131
|
+
|
|
132
|
+
// Defer to the next task so onChangeText and onSelectionChange both land
|
|
133
|
+
// before we forward to the LLC, regardless of platform ordering.
|
|
134
|
+
const scheduleChange = useCallback(() => {
|
|
135
|
+
if (flushHandleRef.current !== null) return;
|
|
136
|
+
flushHandleRef.current = setTimeout(flushChange, 0);
|
|
137
|
+
}, [flushChange]);
|
|
138
|
+
|
|
139
|
+
useEffect(() => {
|
|
140
|
+
return () => {
|
|
141
|
+
if (flushHandleRef.current !== null) {
|
|
142
|
+
clearTimeout(flushHandleRef.current);
|
|
143
|
+
flushHandleRef.current = null;
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
}, []);
|
|
147
|
+
|
|
112
148
|
const maxMessageLength = useMemo(() => {
|
|
113
149
|
return channel.getConfig()?.max_message_length;
|
|
114
150
|
}, [channel]);
|
|
@@ -119,6 +155,14 @@ const AutoCompleteInputWithContext = (props: AutoCompleteInputPropsWithContext)
|
|
|
119
155
|
|
|
120
156
|
useEffect(() => {
|
|
121
157
|
setLocalText(text);
|
|
158
|
+
// Only resync the refs when the text change came from outside (clear after
|
|
159
|
+
// send, draft restore, programmatic setText). For changes we triggered
|
|
160
|
+
// ourselves, latestTextRef is already up to date and overwriting the
|
|
161
|
+
// selection would clobber what onSelectionChange just told us.
|
|
162
|
+
if (text !== latestTextRef.current) {
|
|
163
|
+
latestTextRef.current = text;
|
|
164
|
+
latestSelectionRef.current = { end: text.length, start: text.length };
|
|
165
|
+
}
|
|
122
166
|
}, [text]);
|
|
123
167
|
|
|
124
168
|
const clearState = useCallback(() => {
|
|
@@ -148,24 +192,20 @@ const AutoCompleteInputWithContext = (props: AutoCompleteInputPropsWithContext)
|
|
|
148
192
|
const handleSelectionChange = useCallback(
|
|
149
193
|
(e: TextInputSelectionChangeEvent) => {
|
|
150
194
|
const { selection } = e.nativeEvent;
|
|
195
|
+
latestSelectionRef.current = selection;
|
|
151
196
|
textComposer.setSelection(selection);
|
|
197
|
+
scheduleChange();
|
|
152
198
|
},
|
|
153
|
-
[textComposer],
|
|
199
|
+
[scheduleChange, textComposer],
|
|
154
200
|
);
|
|
155
201
|
|
|
156
202
|
const onChangeTextHandler = useCallback(
|
|
157
203
|
(newText: string) => {
|
|
158
204
|
setLocalText(newText);
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
selection: {
|
|
162
|
-
end: newText.length,
|
|
163
|
-
start: newText.length,
|
|
164
|
-
},
|
|
165
|
-
text: newText,
|
|
166
|
-
});
|
|
205
|
+
latestTextRef.current = newText;
|
|
206
|
+
scheduleChange();
|
|
167
207
|
},
|
|
168
|
-
[
|
|
208
|
+
[scheduleChange],
|
|
169
209
|
);
|
|
170
210
|
|
|
171
211
|
const {
|
|
@@ -112,8 +112,13 @@ describe('AutoCompleteInput', () => {
|
|
|
112
112
|
|
|
113
113
|
const input = queryByTestId('auto-complete-text-input')!;
|
|
114
114
|
|
|
115
|
+
// RN fires both events for every keystroke; we forward to the LLC once
|
|
116
|
+
// both have settled, so the test mirrors that.
|
|
115
117
|
act(() => {
|
|
116
118
|
fireEvent.changeText(input, 'hello');
|
|
119
|
+
fireEvent(input, 'selectionChange', {
|
|
120
|
+
nativeEvent: { selection: { end: 5, start: 5 } },
|
|
121
|
+
});
|
|
117
122
|
});
|
|
118
123
|
|
|
119
124
|
await waitFor(() => {
|
|
@@ -170,6 +175,119 @@ describe('AutoCompleteInput', () => {
|
|
|
170
175
|
});
|
|
171
176
|
});
|
|
172
177
|
|
|
178
|
+
it('forwards the real caret position to handleChange when typing in the middle of multi-line text', async () => {
|
|
179
|
+
// Regression: when the user inserts a character somewhere other than the
|
|
180
|
+
// end of the text (e.g. typing "@" between paragraphs), the original
|
|
181
|
+
// implementation passed selection.end = newText.length, which caused the
|
|
182
|
+
// LLC mention-trigger regex to miss "@" on multi-line input because it
|
|
183
|
+
// only tolerates one whitespace between "@" and end-of-string.
|
|
184
|
+
const { textComposer } = channel.messageComposer;
|
|
185
|
+
const spyHandleChange = jest.spyOn(textComposer, 'handleChange');
|
|
186
|
+
|
|
187
|
+
renderComponent({ channelProps: { channel }, client, props: {} });
|
|
188
|
+
|
|
189
|
+
const input = screen.getByTestId('auto-complete-text-input');
|
|
190
|
+
|
|
191
|
+
// Seed the input with some multi-line text and place the caret between
|
|
192
|
+
// the two leading newlines (position 6 — right after "asdf\n\n", before
|
|
193
|
+
// the trailing "\n\n dsfa").
|
|
194
|
+
const seeded = 'asdf\n\n\n\n dsfa';
|
|
195
|
+
const caret = 6;
|
|
196
|
+
act(() => {
|
|
197
|
+
fireEvent.changeText(input, seeded);
|
|
198
|
+
fireEvent(input, 'selectionChange', {
|
|
199
|
+
nativeEvent: { selection: { end: caret, start: caret } },
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
await waitFor(() => {
|
|
204
|
+
expect(spyHandleChange).toHaveBeenCalledWith({
|
|
205
|
+
selection: { end: caret, start: caret },
|
|
206
|
+
text: seeded,
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
spyHandleChange.mockClear();
|
|
211
|
+
|
|
212
|
+
// User types "@" at the caret. Both events fire in a single keystroke.
|
|
213
|
+
const inserted = 'asdf\n\n@\n\n dsfa';
|
|
214
|
+
const newCaret = caret + 1;
|
|
215
|
+
act(() => {
|
|
216
|
+
fireEvent.changeText(input, inserted);
|
|
217
|
+
fireEvent(input, 'selectionChange', {
|
|
218
|
+
nativeEvent: { selection: { end: newCaret, start: newCaret } },
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
await waitFor(() => {
|
|
223
|
+
// Must land right after the inserted "@", not at end-of-string —
|
|
224
|
+
// otherwise the LLC mention regex misses "@" because of the trailing
|
|
225
|
+
// "\n\n dsfa".
|
|
226
|
+
expect(spyHandleChange).toHaveBeenCalledWith({
|
|
227
|
+
selection: { end: newCaret, start: newCaret },
|
|
228
|
+
text: inserted,
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it('forwards the real caret to handleChange when the user deletes "@" and retypes it', async () => {
|
|
234
|
+
// Regression: deleting "@" and retyping it on the same single line caused
|
|
235
|
+
// the picker to stay hidden on iOS — and on Android even with newlines.
|
|
236
|
+
// The bug was that we derived the caret from a text-length delta plus a
|
|
237
|
+
// stale ref; the coalesced flush now uses whatever native actually
|
|
238
|
+
// reported via onSelectionChange.
|
|
239
|
+
const { textComposer } = channel.messageComposer;
|
|
240
|
+
const spyHandleChange = jest.spyOn(textComposer, 'handleChange');
|
|
241
|
+
|
|
242
|
+
renderComponent({ channelProps: { channel }, client, props: {} });
|
|
243
|
+
|
|
244
|
+
const input = screen.getByTestId('auto-complete-text-input');
|
|
245
|
+
|
|
246
|
+
// 1. Seed "asdf @ dsfa" with the caret right after "@".
|
|
247
|
+
act(() => {
|
|
248
|
+
fireEvent.changeText(input, 'asdf @ dsfa');
|
|
249
|
+
fireEvent(input, 'selectionChange', {
|
|
250
|
+
nativeEvent: { selection: { end: 6, start: 6 } },
|
|
251
|
+
});
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
await waitFor(() => {
|
|
255
|
+
expect(spyHandleChange).toHaveBeenCalledWith({
|
|
256
|
+
selection: { end: 6, start: 6 },
|
|
257
|
+
text: 'asdf @ dsfa',
|
|
258
|
+
});
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
// 2. Delete the "@" — text shrinks by one, caret moves to position 5.
|
|
262
|
+
act(() => {
|
|
263
|
+
fireEvent.changeText(input, 'asdf dsfa');
|
|
264
|
+
fireEvent(input, 'selectionChange', {
|
|
265
|
+
nativeEvent: { selection: { end: 5, start: 5 } },
|
|
266
|
+
});
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
spyHandleChange.mockClear();
|
|
270
|
+
|
|
271
|
+
// 3. Retype "@" at position 5.
|
|
272
|
+
act(() => {
|
|
273
|
+
fireEvent.changeText(input, 'asdf @ dsfa');
|
|
274
|
+
fireEvent(input, 'selectionChange', {
|
|
275
|
+
nativeEvent: { selection: { end: 6, start: 6 } },
|
|
276
|
+
});
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
await waitFor(() => {
|
|
280
|
+
// The cursor must be reported at 6 (right after the new "@"), not
|
|
281
|
+
// somewhere stale. With a wrong caret, the LLC slice would include
|
|
282
|
+
// " dsfa" after the "@" and the query would be " dsfa" instead of "",
|
|
283
|
+
// which returns zero users → picker stays hidden.
|
|
284
|
+
expect(spyHandleChange).toHaveBeenCalledWith({
|
|
285
|
+
selection: { end: 6, start: 6 },
|
|
286
|
+
text: 'asdf @ dsfa',
|
|
287
|
+
});
|
|
288
|
+
});
|
|
289
|
+
});
|
|
290
|
+
|
|
173
291
|
it('should call the textComposer setSelection when the onSelectionChange is triggered', async () => {
|
|
174
292
|
const { textComposer } = channel.messageComposer;
|
|
175
293
|
|
|
@@ -27,7 +27,9 @@ type SwipableMessageWrapperProps = Pick<
|
|
|
27
27
|
onSwipe: () => void;
|
|
28
28
|
};
|
|
29
29
|
|
|
30
|
-
export const SwipableMessageWrapper = React.memo((
|
|
30
|
+
export const SwipableMessageWrapper = React.memo(function SwipableMessageWrapper(
|
|
31
|
+
props: SwipableMessageWrapperProps,
|
|
32
|
+
) {
|
|
31
33
|
const { children, messageSwipeToReplyHitSlop, onSwipe } = props;
|
|
32
34
|
const { MessageSwipeContent } = useComponentsContext();
|
|
33
35
|
const isRTL = I18nManager.isRTL;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React, { useMemo } from 'react';
|
|
2
|
-
import {
|
|
2
|
+
import { ColorValue, Pressable, StyleSheet, View, ViewStyle } from 'react-native';
|
|
3
3
|
|
|
4
4
|
import { MessageTextContainer } from './MessageTextContainer';
|
|
5
5
|
|
|
@@ -169,47 +169,46 @@ const MessageContentWithContext = (props: MessageContentPropsWithContext) => {
|
|
|
169
169
|
[message, isMessageAIGenerated],
|
|
170
170
|
);
|
|
171
171
|
|
|
172
|
-
|
|
172
|
+
// Merged background-color + border-radius object passed directly into the
|
|
173
|
+
// bubble's style array (no spread at the call site). Theme-defined radii
|
|
174
|
+
// override the group-position-computed defaults; theme-undefined radii are
|
|
175
|
+
// omitted so they don't override the computed defaults.
|
|
176
|
+
const bubbleColorAndRadius = useMemo<ViewStyle>(() => {
|
|
173
177
|
// enum('top', 'middle', 'bottom', 'single')
|
|
174
178
|
const groupPosition = groupStyles?.[0];
|
|
175
|
-
|
|
176
179
|
const isBottomOrSingle = groupPosition === 'single' || groupPosition === 'bottom';
|
|
177
|
-
let borderBottomLeftRadius = components.messageBubbleRadiusGroupBottom;
|
|
178
|
-
let borderBottomRightRadius = components.messageBubbleRadiusGroupBottom;
|
|
179
180
|
|
|
181
|
+
let computedBottomLeftRadius = components.messageBubbleRadiusGroupBottom;
|
|
182
|
+
let computedBottomRightRadius = components.messageBubbleRadiusGroupBottom;
|
|
180
183
|
if (isBottomOrSingle) {
|
|
181
|
-
// add relevant sharp corner
|
|
184
|
+
// add relevant sharp corner (the "tail")
|
|
182
185
|
if (isMyMessage) {
|
|
183
|
-
|
|
186
|
+
computedBottomRightRadius = components.messageBubbleRadiusTail;
|
|
184
187
|
} else {
|
|
185
|
-
|
|
188
|
+
computedBottomLeftRadius = components.messageBubbleRadiusTail;
|
|
186
189
|
}
|
|
187
190
|
}
|
|
188
191
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
};
|
|
194
|
-
|
|
195
|
-
const getBorderRadiusFromTheme = () => {
|
|
196
|
-
const bordersFromTheme: Record<string, string | AnimatableNumericValue | undefined> = {
|
|
197
|
-
borderBottomLeftRadius,
|
|
198
|
-
borderBottomRightRadius,
|
|
199
|
-
borderRadius,
|
|
200
|
-
borderTopLeftRadius,
|
|
201
|
-
borderTopRightRadius,
|
|
192
|
+
const style: ViewStyle = {
|
|
193
|
+
backgroundColor,
|
|
194
|
+
borderBottomLeftRadius: borderBottomLeftRadius ?? computedBottomLeftRadius,
|
|
195
|
+
borderBottomRightRadius: borderBottomRightRadius ?? computedBottomRightRadius,
|
|
202
196
|
};
|
|
197
|
+
if (borderRadius !== undefined) style.borderRadius = borderRadius;
|
|
198
|
+
if (borderTopLeftRadius !== undefined) style.borderTopLeftRadius = borderTopLeftRadius;
|
|
199
|
+
if (borderTopRightRadius !== undefined) style.borderTopRightRadius = borderTopRightRadius;
|
|
203
200
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
201
|
+
return style;
|
|
202
|
+
}, [
|
|
203
|
+
backgroundColor,
|
|
204
|
+
borderBottomLeftRadius,
|
|
205
|
+
borderBottomRightRadius,
|
|
206
|
+
borderRadius,
|
|
207
|
+
borderTopLeftRadius,
|
|
208
|
+
borderTopRightRadius,
|
|
209
|
+
groupStyles,
|
|
210
|
+
isMyMessage,
|
|
211
|
+
]);
|
|
213
212
|
|
|
214
213
|
const { setNativeScrollability } = useMessageListItemContext();
|
|
215
214
|
const hasContentSideViews = !!(MessageContentLeadingView || MessageContentTrailingView);
|
|
@@ -357,12 +356,8 @@ const MessageContentWithContext = (props: MessageContentPropsWithContext) => {
|
|
|
357
356
|
<View
|
|
358
357
|
style={[
|
|
359
358
|
styles.containerInner,
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
...getBorderRadius(),
|
|
363
|
-
...getBorderRadiusFromTheme(),
|
|
364
|
-
},
|
|
365
|
-
noBorder ? { borderWidth: 0 } : {},
|
|
359
|
+
bubbleColorAndRadius,
|
|
360
|
+
noBorder ? styles.noBorder : null,
|
|
366
361
|
containerInner,
|
|
367
362
|
messageGroupedSingleOrBottom
|
|
368
363
|
? isVeryLastMessage && enableMessageGroupingByUser
|
|
@@ -684,6 +679,7 @@ const styles = StyleSheet.create({
|
|
|
684
679
|
alignSelf: 'center',
|
|
685
680
|
},
|
|
686
681
|
galleryContainer: {},
|
|
682
|
+
noBorder: { borderWidth: 0 },
|
|
687
683
|
rightAlignContent: {
|
|
688
684
|
justifyContent: 'flex-end',
|
|
689
685
|
},
|
|
@@ -30,7 +30,7 @@ export type MessageWrapperProps = {
|
|
|
30
30
|
nextMessage?: LocalMessage;
|
|
31
31
|
};
|
|
32
32
|
|
|
33
|
-
export const MessageWrapper = React.memo((props: MessageWrapperProps)
|
|
33
|
+
export const MessageWrapper = React.memo(function MessageWrapper(props: MessageWrapperProps) {
|
|
34
34
|
const { message, previousMessage, nextMessage } = props;
|
|
35
35
|
const { client } = useChatContext();
|
|
36
36
|
const {
|
|
@@ -101,6 +101,37 @@ const defaultMarkdownStyles: MarkdownStyle = {
|
|
|
101
101
|
fontSize: primitives.typographyFontSizeMd,
|
|
102
102
|
lineHeight: primitives.typographyLineHeightNormal,
|
|
103
103
|
},
|
|
104
|
+
// Heading sizes are derived from the body font size (`typographyFontSizeMd`) so they
|
|
105
|
+
// scale with the integrator's typography settings. lineHeight = fontSize × 1.25 to
|
|
106
|
+
// give headings room to breathe. Both fields are required here: without lineHeight,
|
|
107
|
+
// the inherited `lineHeight: typographyLineHeightNormal` (20) from `styles.text` (set
|
|
108
|
+
// in renderText below) leaks into the heading's inner Text via the markdown library's
|
|
109
|
+
// text rule (`{...styles.text, ...state.style}`) and squishes larger heading fontSizes
|
|
110
|
+
// into a 20px line box.
|
|
111
|
+
heading1: {
|
|
112
|
+
fontSize: primitives.typographyFontSizeMd * 2,
|
|
113
|
+
lineHeight: primitives.typographyFontSizeMd * 2 * 1.25,
|
|
114
|
+
},
|
|
115
|
+
heading2: {
|
|
116
|
+
fontSize: primitives.typographyFontSizeMd * 1.5,
|
|
117
|
+
lineHeight: primitives.typographyFontSizeMd * 1.5 * 1.25,
|
|
118
|
+
},
|
|
119
|
+
heading3: {
|
|
120
|
+
fontSize: primitives.typographyFontSizeMd * 1.25,
|
|
121
|
+
lineHeight: primitives.typographyFontSizeMd * 1.25 * 1.25,
|
|
122
|
+
},
|
|
123
|
+
heading4: {
|
|
124
|
+
fontSize: primitives.typographyFontSizeMd,
|
|
125
|
+
lineHeight: primitives.typographyFontSizeMd * 1.25,
|
|
126
|
+
},
|
|
127
|
+
heading5: {
|
|
128
|
+
fontSize: primitives.typographyFontSizeMd * 0.875,
|
|
129
|
+
lineHeight: primitives.typographyFontSizeMd * 0.875 * 1.25,
|
|
130
|
+
},
|
|
131
|
+
heading6: {
|
|
132
|
+
fontSize: primitives.typographyFontSizeMd * 0.75,
|
|
133
|
+
lineHeight: primitives.typographyFontSizeMd * 0.75 * 1.25,
|
|
134
|
+
},
|
|
104
135
|
inlineCode: {
|
|
105
136
|
padding: primitives.spacingXxs,
|
|
106
137
|
paddingHorizontal: primitives.spacingXxs,
|
|
@@ -1,14 +1,18 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { useEffect, useRef, useState } from 'react';
|
|
2
2
|
|
|
3
3
|
import { Event, LocalMessage, UserResponse } from 'stream-chat';
|
|
4
4
|
|
|
5
5
|
import { useChannelContext } from '../../../contexts/channelContext/ChannelContext';
|
|
6
6
|
import { useChatContext } from '../../../contexts/chatContext/ChatContext';
|
|
7
|
+
import { useStableCallback } from '../../../hooks';
|
|
7
8
|
|
|
8
9
|
export const useMessageDeliveredData = ({ message }: { message?: LocalMessage }) => {
|
|
9
10
|
const { channel } = useChannelContext();
|
|
10
11
|
const { client } = useChatContext();
|
|
11
|
-
|
|
12
|
+
|
|
13
|
+
const messageIdRef = useRef<string>(message?.id);
|
|
14
|
+
|
|
15
|
+
const calculate = useStableCallback(() => {
|
|
12
16
|
if (!message?.created_at) {
|
|
13
17
|
return [];
|
|
14
18
|
}
|
|
@@ -17,13 +21,14 @@ export const useMessageDeliveredData = ({ message }: { message?: LocalMessage })
|
|
|
17
21
|
timestampMs: new Date(message.created_at).getTime(),
|
|
18
22
|
};
|
|
19
23
|
return channel.messageReceiptsTracker.deliveredForMessage(messageRef);
|
|
20
|
-
}
|
|
24
|
+
});
|
|
21
25
|
|
|
22
|
-
const [deliveredTo, setDeliveredTo] = useState<UserResponse[]>(
|
|
26
|
+
const [deliveredTo, setDeliveredTo] = useState<UserResponse[]>(() => calculate());
|
|
23
27
|
|
|
24
|
-
|
|
28
|
+
if (!!messageIdRef.current && !!message?.id && messageIdRef.current !== message.id) {
|
|
25
29
|
setDeliveredTo(calculate());
|
|
26
|
-
|
|
30
|
+
messageIdRef.current = message.id;
|
|
31
|
+
}
|
|
27
32
|
|
|
28
33
|
useEffect(() => {
|
|
29
34
|
const { unsubscribe } = channel.on('message.delivered', (event: Event) => {
|
|
@@ -1,14 +1,18 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { useEffect, useRef, useState } from 'react';
|
|
2
2
|
|
|
3
3
|
import { Event, LocalMessage, UserResponse } from 'stream-chat';
|
|
4
4
|
|
|
5
5
|
import { useChannelContext } from '../../../contexts/channelContext/ChannelContext';
|
|
6
6
|
import { useChatContext } from '../../../contexts/chatContext/ChatContext';
|
|
7
|
+
import { useStableCallback } from '../../../hooks';
|
|
7
8
|
|
|
8
9
|
export const useMessageReadData = ({ message }: { message?: LocalMessage }) => {
|
|
9
10
|
const { channel } = useChannelContext();
|
|
10
11
|
const { client } = useChatContext();
|
|
11
|
-
|
|
12
|
+
|
|
13
|
+
const messageIdRef = useRef<string>(message?.id);
|
|
14
|
+
|
|
15
|
+
const calculate = useStableCallback(() => {
|
|
12
16
|
if (!message?.created_at) {
|
|
13
17
|
return [];
|
|
14
18
|
}
|
|
@@ -18,13 +22,14 @@ export const useMessageReadData = ({ message }: { message?: LocalMessage }) => {
|
|
|
18
22
|
};
|
|
19
23
|
|
|
20
24
|
return channel.messageReceiptsTracker.readersForMessage(messageRef);
|
|
21
|
-
}
|
|
25
|
+
});
|
|
22
26
|
|
|
23
|
-
const [readBy, setReadBy] = useState<UserResponse[]>(
|
|
27
|
+
const [readBy, setReadBy] = useState<UserResponse[]>(() => calculate());
|
|
24
28
|
|
|
25
|
-
|
|
29
|
+
if (!!messageIdRef.current && !!message?.id && messageIdRef.current !== message.id) {
|
|
26
30
|
setReadBy(calculate());
|
|
27
|
-
|
|
31
|
+
messageIdRef.current = message.id;
|
|
32
|
+
}
|
|
28
33
|
|
|
29
34
|
useEffect(() => {
|
|
30
35
|
const { unsubscribe } = channel.on('message.read', (event: Event) => {
|
|
@@ -511,7 +511,7 @@ exports[`Thread should match thread snapshot 1`] = `
|
|
|
511
511
|
"borderBottomLeftRadius": 0,
|
|
512
512
|
"borderBottomRightRadius": 20,
|
|
513
513
|
},
|
|
514
|
-
|
|
514
|
+
null,
|
|
515
515
|
{},
|
|
516
516
|
{},
|
|
517
517
|
]
|
|
@@ -843,7 +843,7 @@ exports[`Thread should match thread snapshot 1`] = `
|
|
|
843
843
|
"borderBottomLeftRadius": 0,
|
|
844
844
|
"borderBottomRightRadius": 20,
|
|
845
845
|
},
|
|
846
|
-
|
|
846
|
+
null,
|
|
847
847
|
{},
|
|
848
848
|
{},
|
|
849
849
|
]
|
|
@@ -1208,7 +1208,7 @@ exports[`Thread should match thread snapshot 1`] = `
|
|
|
1208
1208
|
"borderBottomLeftRadius": 0,
|
|
1209
1209
|
"borderBottomRightRadius": 20,
|
|
1210
1210
|
},
|
|
1211
|
-
|
|
1211
|
+
null,
|
|
1212
1212
|
{},
|
|
1213
1213
|
{},
|
|
1214
1214
|
]
|
|
@@ -1534,7 +1534,7 @@ exports[`Thread should match thread snapshot 1`] = `
|
|
|
1534
1534
|
"borderBottomLeftRadius": 0,
|
|
1535
1535
|
"borderBottomRightRadius": 20,
|
|
1536
1536
|
},
|
|
1537
|
-
|
|
1537
|
+
null,
|
|
1538
1538
|
{},
|
|
1539
1539
|
{},
|
|
1540
1540
|
]
|
package/src/version.json
CHANGED