Skip to content

Commit e787038

Browse files
perf: message list and input perf fixes (#3051)
* perf: sendMessage memoization from top level ctx * fix: properly memoize onChange * perf: propagate channel from memoized ctx * perf: exit early when setting typing state * perf: critical str performance issue messagelist scrolling * fix: update snapshot * perf: remove ref clearing * chore: remove console.logs * fix: ignore own typing events * perf: memoize the suggestions context value * fix: autocomplete input re-render on received message * perf: input buttons on text render issue * fix: useeffect re-render when text change in useMessageDetailsForState --------- Co-authored-by: Khushal Agarwal <[email protected]>
1 parent ba72973 commit e787038

File tree

13 files changed

+534
-971
lines changed

13 files changed

+534
-971
lines changed

package/src/components/AutoCompleteInput/AutoCompleteInput.tsx

Lines changed: 17 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,6 @@ import { I18nManager, StyleSheet, TextInput, TextInputProps } from 'react-native
33

44
import throttle from 'lodash/throttle';
55

6-
import {
7-
ChannelContextValue,
8-
useChannelContext,
9-
} from '../../contexts/channelContext/ChannelContext';
106
import {
117
MessageInputContextValue,
128
useMessageInputContext,
@@ -53,22 +49,22 @@ const isCommand = (text: string) => text[0] === '/' && text.split(' ').length <=
5349

5450
type AutoCompleteInputPropsWithContext<
5551
StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics,
56-
> = Pick<ChannelContextValue<StreamChatGenerics>, 'giphyEnabled'> &
57-
Pick<
58-
MessageInputContextValue<StreamChatGenerics>,
59-
| 'additionalTextInputProps'
60-
| 'autoCompleteSuggestionsLimit'
61-
| 'giphyActive'
62-
| 'maxMessageLength'
63-
| 'mentionAllAppUsersEnabled'
64-
| 'mentionAllAppUsersQuery'
65-
| 'numberOfLines'
66-
| 'onChange'
67-
| 'setGiphyActive'
68-
| 'setInputBoxRef'
69-
| 'text'
70-
| 'triggerSettings'
71-
> &
52+
> = Pick<
53+
MessageInputContextValue<StreamChatGenerics>,
54+
| 'additionalTextInputProps'
55+
| 'autoCompleteSuggestionsLimit'
56+
| 'giphyActive'
57+
| 'giphyEnabled'
58+
| 'maxMessageLength'
59+
| 'mentionAllAppUsersEnabled'
60+
| 'mentionAllAppUsersQuery'
61+
| 'numberOfLines'
62+
| 'onChange'
63+
| 'setGiphyActive'
64+
| 'setInputBoxRef'
65+
| 'text'
66+
| 'triggerSettings'
67+
> &
7268
Pick<
7369
SuggestionsContextValue<StreamChatGenerics>,
7470
'closeSuggestions' | 'openSuggestions' | 'updateSuggestions'
@@ -484,8 +480,8 @@ export const AutoCompleteInput = <
484480
>(
485481
props: AutoCompleteInputProps<StreamChatGenerics>,
486482
) => {
487-
const { giphyEnabled } = useChannelContext<StreamChatGenerics>();
488483
const {
484+
giphyEnabled,
489485
additionalTextInputProps,
490486
autoCompleteSuggestionsLimit,
491487
giphyActive,

package/src/components/Channel/Channel.tsx

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -781,9 +781,11 @@ const ChannelWithContext = <
781781
}
782782

783783
// If the event is typing.start or typing.stop, set the typing state
784-
const isTypingEvent = event.type === 'typing.start' || event.type === 'typing.stop';
785-
if (isTypingEvent) {
786-
setTyping(channel);
784+
if (event.type === 'typing.start' || event.type === 'typing.stop') {
785+
if (event.user?.id !== client.userID) {
786+
setTyping(channel);
787+
}
788+
return;
787789
} else {
788790
if (thread?.id) {
789791
const updatedThreadMessages =
@@ -1758,6 +1760,11 @@ const ChannelWithContext = <
17581760
const sendMessageRef =
17591761
useRef<InputMessageInputContextValue<StreamChatGenerics>['sendMessage']>(sendMessage);
17601762
sendMessageRef.current = sendMessage;
1763+
const sendMessageStable = useCallback<
1764+
InputMessageInputContextValue<StreamChatGenerics>['sendMessage']
1765+
>((...args) => {
1766+
return sendMessageRef.current(...args);
1767+
}, []);
17611768

17621769
const inputMessageInputContext = useCreateInputMessageInputContext<StreamChatGenerics>({
17631770
additionalTextInputProps,
@@ -1811,7 +1818,7 @@ const ChannelWithContext = <
18111818
quotedMessage,
18121819
SendButton,
18131820
sendImageAsync,
1814-
sendMessage: (...args) => sendMessageRef.current(...args),
1821+
sendMessage: sendMessageStable,
18151822
SendMessageDisallowedIndicator,
18161823
setInputRef,
18171824
setQuotedMessageState,
@@ -1944,11 +1951,13 @@ const ChannelWithContext = <
19441951
VideoThumbnail,
19451952
});
19461953

1947-
const suggestionsContext = {
1948-
AutoCompleteSuggestionHeader,
1949-
AutoCompleteSuggestionItem,
1950-
AutoCompleteSuggestionList,
1951-
};
1954+
const suggestionsContext = useMemo(() => {
1955+
return {
1956+
AutoCompleteSuggestionHeader,
1957+
AutoCompleteSuggestionItem,
1958+
AutoCompleteSuggestionList,
1959+
};
1960+
}, [AutoCompleteSuggestionHeader, AutoCompleteSuggestionItem, AutoCompleteSuggestionList]);
19521961

19531962
const threadContext = useCreateThreadContext({
19541963
allowThreadMessagesInChannel,

package/src/components/Message/MessageSimple/MessageSimple.tsx

Lines changed: 112 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useMemo, useState } from 'react';
1+
import React, { useCallback, useMemo, useState } from 'react';
22
import { Dimensions, LayoutChangeEvent, StyleSheet, View } from 'react-native';
33

44
import { Gesture, GestureDetector } from 'react-native-gesture-handler';
@@ -12,6 +12,8 @@ import Animated, {
1212
withSpring,
1313
} from 'react-native-reanimated';
1414

15+
const AnimatedWrapper = Animated.createAnimatedComponent(View);
16+
1517
import {
1618
MessageContextValue,
1719
useMessageContext,
@@ -205,77 +207,104 @@ const MessageSimpleWithContext = <
205207

206208
const translateX = useSharedValue(0);
207209
const touchStart = useSharedValue<{ x: number; y: number } | null>(null);
210+
const isSwiping = useSharedValue<boolean>(false);
211+
const [isBeingSwiped, setIsBeingSwiped] = useState<boolean>(false);
208212

209-
const onSwipeToReply = () => {
213+
const onSwipeToReply = useCallback(() => {
210214
clearQuotedMessageState();
211215
setQuotedMessageState(message);
212-
};
216+
}, [clearQuotedMessageState, message, setQuotedMessageState]);
213217

214218
const THRESHOLD = 25;
215219

216220
const triggerHaptic = NativeHandlers.triggerHaptic;
217221

218-
const swipeGesture = Gesture.Pan()
219-
.hitSlop(messageSwipeToReplyHitSlop)
220-
.onBegin((event) => {
221-
touchStart.value = { x: event.x, y: event.y };
222-
})
223-
.onTouchesMove((event, state) => {
224-
if (!touchStart.value || !event.changedTouches.length) {
225-
state.fail();
226-
return;
227-
}
228-
229-
const xDiff = Math.abs(event.changedTouches[0].x - touchStart.value.x);
230-
const yDiff = Math.abs(event.changedTouches[0].y - touchStart.value.y);
231-
const isHorizontalPanning = xDiff > yDiff;
232-
233-
if (isHorizontalPanning) {
234-
state.activate();
235-
} else {
236-
state.fail();
237-
}
238-
})
239-
.onStart(() => {
240-
translateX.value = 0;
241-
})
242-
.onChange(({ translationX }) => {
243-
if (translationX > 0) {
244-
translateX.value = translationX;
245-
}
246-
})
247-
.onEnd(() => {
248-
if (translateX.value >= THRESHOLD) {
249-
runOnJS(onSwipeToReply)();
250-
if (triggerHaptic) {
251-
runOnJS(triggerHaptic)('impactMedium');
252-
}
253-
}
254-
translateX.value = withSpring(0, {
255-
dampingRatio: 1,
256-
duration: 500,
257-
overshootClamping: true,
258-
stiffness: 1,
259-
});
260-
});
261-
262-
const messageBubbleAnimatedStyle = useAnimatedStyle(() => ({
263-
transform: [{ translateX: translateX.value }],
264-
}));
265-
266-
const swipeContentAnimatedStyle = useAnimatedStyle(() => ({
267-
opacity: interpolate(translateX.value, [0, THRESHOLD], [0, 1]),
268-
transform: [
269-
{
270-
translateX: interpolate(
271-
translateX.value,
272-
[0, THRESHOLD],
273-
[-THRESHOLD, 0],
274-
Extrapolation.CLAMP,
275-
),
276-
},
277-
],
278-
}));
222+
const swipeGesture = useMemo(
223+
() =>
224+
Gesture.Pan()
225+
.hitSlop(messageSwipeToReplyHitSlop)
226+
.onBegin((event) => {
227+
touchStart.value = { x: event.x, y: event.y };
228+
})
229+
.onTouchesMove((event, state) => {
230+
if (!touchStart.value || !event.changedTouches.length) {
231+
state.fail();
232+
return;
233+
}
234+
235+
const xDiff = Math.abs(event.changedTouches[0].x - touchStart.value.x);
236+
const yDiff = Math.abs(event.changedTouches[0].y - touchStart.value.y);
237+
const isHorizontalPanning = xDiff > yDiff;
238+
239+
if (isHorizontalPanning) {
240+
state.activate();
241+
isSwiping.value = true;
242+
runOnJS(setIsBeingSwiped)(true);
243+
} else {
244+
state.fail();
245+
}
246+
})
247+
.onStart(() => {
248+
translateX.value = 0;
249+
})
250+
.onChange(({ translationX }) => {
251+
if (translationX > 0) {
252+
translateX.value = translationX;
253+
}
254+
})
255+
.onEnd(() => {
256+
if (translateX.value >= THRESHOLD) {
257+
runOnJS(onSwipeToReply)();
258+
if (triggerHaptic) {
259+
runOnJS(triggerHaptic)('impactMedium');
260+
}
261+
}
262+
translateX.value = withSpring(
263+
0,
264+
{
265+
dampingRatio: 1,
266+
duration: 500,
267+
overshootClamping: true,
268+
stiffness: 1,
269+
},
270+
() => {
271+
isSwiping.value = false;
272+
runOnJS(setIsBeingSwiped)(false);
273+
},
274+
);
275+
}),
276+
[isSwiping, messageSwipeToReplyHitSlop, onSwipeToReply, touchStart, translateX, triggerHaptic],
277+
);
278+
279+
const messageBubbleAnimatedStyle = useAnimatedStyle(
280+
() =>
281+
isSwiping.value
282+
? {
283+
transform: [{ translateX: translateX.value }],
284+
}
285+
: {},
286+
[],
287+
);
288+
289+
const swipeContentAnimatedStyle = useAnimatedStyle(
290+
() =>
291+
isSwiping.value
292+
? {
293+
opacity: interpolate(translateX.value, [0, THRESHOLD], [0, 1]),
294+
transform: [
295+
{
296+
translateX: interpolate(
297+
translateX.value,
298+
[0, THRESHOLD],
299+
[-THRESHOLD, 0],
300+
Extrapolation.CLAMP,
301+
),
302+
},
303+
],
304+
}
305+
: {},
306+
[],
307+
);
279308

280309
const renderMessageBubble = useMemo(
281310
() => (
@@ -309,18 +338,31 @@ const MessageSimpleWithContext = <
309338
() => (
310339
<GestureDetector gesture={swipeGesture}>
311340
<View hitSlop={messageSwipeToReplyHitSlop} style={[styles.contentWrapper, contentWrapper]}>
312-
<Animated.View
313-
style={[styles.swipeContentContainer, swipeContentAnimatedStyle, swipeContentContainer]}
314-
>
315-
{MessageSwipeContent ? <MessageSwipeContent /> : null}
316-
</Animated.View>
317-
<Animated.View style={messageBubbleAnimatedStyle}>{renderMessageBubble}</Animated.View>
341+
{isBeingSwiped ? (
342+
<>
343+
<AnimatedWrapper
344+
style={[
345+
styles.swipeContentContainer,
346+
swipeContentAnimatedStyle,
347+
swipeContentContainer,
348+
]}
349+
>
350+
{MessageSwipeContent ? <MessageSwipeContent /> : null}
351+
</AnimatedWrapper>
352+
<AnimatedWrapper pointerEvents='box-none' style={messageBubbleAnimatedStyle}>
353+
{renderMessageBubble}
354+
</AnimatedWrapper>
355+
</>
356+
) : (
357+
renderMessageBubble
358+
)}
318359
</View>
319360
</GestureDetector>
320361
),
321362
[
322363
MessageSwipeContent,
323364
contentWrapper,
365+
isBeingSwiped,
324366
messageBubbleAnimatedStyle,
325367
messageSwipeToReplyHitSlop,
326368
renderMessageBubble,

0 commit comments

Comments
 (0)