Skip to content

Commit 8d4965e

Browse files
committed
perf: image gallery rendering improvements (#3108)
1 parent 0e14acc commit 8d4965e

File tree

5 files changed

+97
-96
lines changed

5 files changed

+97
-96
lines changed

package/src/components/Chat/Chat.tsx

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { PropsWithChildren, useEffect, useState } from 'react';
1+
import React, { PropsWithChildren, useEffect, useMemo, useState } from 'react';
22
import { Image, Platform } from 'react-native';
33

44
import type { Channel, StreamChat } from 'stream-chat';
@@ -13,8 +13,7 @@ import { useSyncDatabase } from './hooks/useSyncDatabase';
1313
import { ChannelsStateProvider } from '../../contexts/channelsStateContext/ChannelsStateContext';
1414
import { ChatContextValue, ChatProvider } from '../../contexts/chatContext/ChatContext';
1515
import { useDebugContext } from '../../contexts/debugContext/DebugContext';
16-
import { useOverlayContext } from '../../contexts/overlayContext/OverlayContext';
17-
import { DeepPartial, ThemeProvider } from '../../contexts/themeContext/ThemeContext';
16+
import { DeepPartial, ThemeProvider, useTheme } from '../../contexts/themeContext/ThemeContext';
1817
import type { Theme } from '../../contexts/themeContext/utils/theme';
1918
import {
2019
DEFAULT_USER_LANGUAGE,
@@ -158,6 +157,11 @@ const ChatWithContext = <
158157
// Setup translators
159158
const translators = useStreami18n(i18nInstance);
160159

160+
const translationContextValue = useMemo(
161+
() => ({ ...translators, userLanguage: client.user?.language || DEFAULT_USER_LANGUAGE }),
162+
[client.user?.language, translators],
163+
);
164+
161165
/**
162166
* Setup connection event listeners
163167
*/
@@ -295,9 +299,7 @@ const ChatWithContext = <
295299

296300
return (
297301
<ChatProvider<StreamChatGenerics> value={chatContext}>
298-
<TranslationProvider
299-
value={{ ...translators, userLanguage: client.user?.language || DEFAULT_USER_LANGUAGE }}
300-
>
302+
<TranslationProvider value={translationContextValue}>
301303
<ThemeProvider style={style}>
302304
<ChannelsStateProvider<StreamChatGenerics>>{children}</ChannelsStateProvider>
303305
</ThemeProvider>
@@ -332,7 +334,7 @@ export const Chat = <
332334
>(
333335
props: PropsWithChildren<ChatProps<StreamChatGenerics>>,
334336
) => {
335-
const { style } = useOverlayContext();
337+
const { theme } = useTheme();
336338

337-
return <ChatWithContext {...{ style }} {...props} />;
339+
return <ChatWithContext style={theme as DeepPartial<Theme>} {...props} />;
338340
};

package/src/components/ImageGallery/ImageGallery.tsx

Lines changed: 86 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { RefObject, useCallback, useEffect, useRef, useState } from 'react';
1+
import React, { RefObject, useCallback, useEffect, useMemo, useRef, useState } from 'react';
22
import { Image, ImageStyle, Keyboard, StyleSheet, ViewStyle } from 'react-native';
33

44
import { Gesture, GestureDetector } from 'react-native-gesture-handler';
@@ -132,9 +132,6 @@ export const ImageGallery = <
132132
numberOfImageGalleryGridColumns,
133133
overlayOpacity,
134134
} = props;
135-
const [imageGalleryAttachments, setImageGalleryAttachments] = useState<
136-
Photo<StreamChatGenerics>[]
137-
>([]);
138135
const { resizableCDNHosts } = useChatConfigContext();
139136
const {
140137
theme: {
@@ -198,13 +195,6 @@ export const ImageGallery = <
198195
*/
199196
const [currentImageHeight, setCurrentImageHeight] = useState<number>(fullWindowHeight);
200197

201-
/**
202-
* JS and UI index values, the JS follows the UI but is needed
203-
* for rendering the virtualized image list
204-
*/
205-
const [selectedIndex, setSelectedIndex] = useState(0);
206-
const index = useSharedValue(0);
207-
208198
/**
209199
* Header visible value for animating in out
210200
*/
@@ -224,57 +214,86 @@ export const ImageGallery = <
224214
* photo attachments
225215
*/
226216

227-
const photos = messages.reduce((acc: Photo<StreamChatGenerics>[], cur) => {
228-
const attachmentImages =
229-
cur.attachments
230-
?.filter(
231-
(attachment) =>
232-
(attachment.type === FileTypes.Giphy &&
233-
(attachment.giphy?.[giphyVersion]?.url ||
234-
attachment.thumb_url ||
235-
attachment.image_url)) ||
236-
(attachment.type === FileTypes.Image &&
237-
!attachment.title_link &&
238-
!attachment.og_scrape_url &&
239-
getUrlOfImageAttachment(attachment)) ||
240-
(isVideoPlayerAvailable() && attachment.type === FileTypes.Video),
241-
)
242-
.reverse() || [];
243-
244-
const attachmentPhotos = attachmentImages.map((a) => {
245-
const imageUrl = getUrlOfImageAttachment(a) as string;
246-
const giphyURL = a.giphy?.[giphyVersion]?.url || a.thumb_url || a.image_url;
247-
const isInitiallyPaused = !autoPlayVideo;
248-
249-
return {
250-
channelId: cur.cid,
251-
created_at: cur.created_at,
252-
duration: 0,
253-
id: `photoId-${cur.id}-${imageUrl}`,
254-
messageId: cur.id,
255-
mime_type: a.type === 'giphy' ? getGiphyMimeType(giphyURL ?? '') : a.mime_type,
256-
original_height: a.original_height,
257-
original_width: a.original_width,
258-
paused: isInitiallyPaused,
259-
progress: 0,
260-
thumb_url: a.thumb_url,
261-
type: a.type,
262-
uri:
263-
a.type === 'giphy'
264-
? giphyURL
265-
: getResizedImageUrl({
266-
height: fullWindowHeight,
267-
resizableCDNHosts,
268-
url: imageUrl,
269-
width: fullWindowWidth,
270-
}),
271-
user: cur.user,
272-
user_id: cur.user_id,
273-
};
274-
});
217+
const photos = useMemo(
218+
() =>
219+
messages.reduce((acc: Photo<StreamChatGenerics>[], cur) => {
220+
const attachmentImages =
221+
cur.attachments
222+
?.filter(
223+
(attachment) =>
224+
(attachment.type === FileTypes.Giphy &&
225+
(attachment.giphy?.[giphyVersion]?.url ||
226+
attachment.thumb_url ||
227+
attachment.image_url)) ||
228+
(attachment.type === FileTypes.Image &&
229+
!attachment.title_link &&
230+
!attachment.og_scrape_url &&
231+
getUrlOfImageAttachment(attachment)) ||
232+
(isVideoPlayerAvailable() && attachment.type === FileTypes.Video),
233+
)
234+
.reverse() || [];
235+
236+
const attachmentPhotos = attachmentImages.map((a) => {
237+
const imageUrl = getUrlOfImageAttachment(a) as string;
238+
const giphyURL = a.giphy?.[giphyVersion]?.url || a.thumb_url || a.image_url;
239+
const isInitiallyPaused = !autoPlayVideo;
240+
241+
return {
242+
channelId: cur.cid,
243+
created_at: cur.created_at,
244+
duration: 0,
245+
id: `photoId-${cur.id}-${imageUrl}`,
246+
messageId: cur.id,
247+
mime_type: a.type === 'giphy' ? getGiphyMimeType(giphyURL ?? '') : a.mime_type,
248+
original_height: a.original_height,
249+
original_width: a.original_width,
250+
paused: isInitiallyPaused,
251+
progress: 0,
252+
thumb_url: a.thumb_url,
253+
type: a.type,
254+
uri:
255+
a.type === 'giphy'
256+
? giphyURL
257+
: getResizedImageUrl({
258+
height: fullWindowHeight,
259+
resizableCDNHosts,
260+
url: imageUrl,
261+
width: fullWindowWidth,
262+
}),
263+
user: cur.user,
264+
user_id: cur.user_id,
265+
};
266+
});
275267

276-
return [...attachmentPhotos, ...acc] as Photo<StreamChatGenerics>[];
277-
}, []);
268+
return [...attachmentPhotos, ...acc] as Photo<StreamChatGenerics>[];
269+
}, []),
270+
[autoPlayVideo, fullWindowHeight, fullWindowWidth, giphyVersion, messages, resizableCDNHosts],
271+
);
272+
273+
/**
274+
* The URL for the images may differ because of dimensions passed as
275+
* part of the query.
276+
*/
277+
const stripQueryFromUrl = (url: string) => url.split('?')[0];
278+
279+
const photoSelectedIndex = useMemo(() => {
280+
const idx = photos.findIndex(
281+
(photo) =>
282+
photo.messageId === selectedMessage?.messageId &&
283+
stripQueryFromUrl(photo.uri) === stripQueryFromUrl(selectedMessage?.url || ''),
284+
);
285+
286+
return idx === -1 ? 0 : idx;
287+
}, [photos, selectedMessage]);
288+
289+
/**
290+
* JS and UI index values, the JS follows the UI but is needed
291+
* for rendering the virtualized image list
292+
*/
293+
const [selectedIndex, setSelectedIndex] = useState(photoSelectedIndex);
294+
const index = useSharedValue(photoSelectedIndex);
295+
296+
const [imageGalleryAttachments, setImageGalleryAttachments] = useState<Photo<StreamChatGenerics>[]>(photos);
278297

279298
/**
280299
* Photos length needs to be kept as a const here so if the length
@@ -284,17 +303,6 @@ export const ImageGallery = <
284303
*/
285304
const photoLength = photos.length;
286305

287-
useEffect(() => {
288-
setImageGalleryAttachments(photos);
289-
// eslint-disable-next-line react-hooks/exhaustive-deps
290-
}, []);
291-
292-
/**
293-
* The URL for the images may differ because of dimensions passed as
294-
* part of the query.
295-
*/
296-
const stripQueryFromUrl = (url: string) => url.split('?')[0];
297-
298306
/**
299307
* Set selected photo when changed via pressing in the message list
300308
*/
@@ -316,8 +324,7 @@ export const ImageGallery = <
316324
);
317325

318326
runOnUI(updatePosition)(newIndex);
319-
// eslint-disable-next-line react-hooks/exhaustive-deps
320-
}, [selectedMessage, photoLength]);
327+
}, [selectedMessage, photos, index, translationX, fullWindowWidth]);
321328

322329
/**
323330
* Image heights are not provided and therefore need to be calculated.
@@ -328,22 +335,24 @@ export const ImageGallery = <
328335
const uriForCurrentImage = imageGalleryAttachments[selectedIndex]?.uri;
329336

330337
useEffect(() => {
331-
setCurrentImageHeight(fullWindowHeight);
338+
let currentImageHeight = fullWindowHeight;
332339
const photo = imageGalleryAttachments[index.value];
333340
const height = photo?.original_height;
334341
const width = photo?.original_width;
335342

336343
if (height && width) {
337344
const imageHeight = Math.floor(height * (fullWindowWidth / width));
338-
setCurrentImageHeight(imageHeight > fullWindowHeight ? fullWindowHeight : imageHeight);
345+
currentImageHeight = imageHeight > fullWindowHeight ? fullWindowHeight : imageHeight;
339346
} else if (photo?.uri) {
340347
if (photo.type === FileTypes.Image) {
341348
Image.getSize(photo.uri, (width, height) => {
342349
const imageHeight = Math.floor(height * (fullWindowWidth / width));
343-
setCurrentImageHeight(imageHeight > fullWindowHeight ? fullWindowHeight : imageHeight);
350+
currentImageHeight = imageHeight > fullWindowHeight ? fullWindowHeight : imageHeight;
344351
});
345352
}
346353
}
354+
355+
setCurrentImageHeight(currentImageHeight);
347356
// eslint-disable-next-line react-hooks/exhaustive-deps
348357
}, [uriForCurrentImage]);
349358

package/src/components/MessageList/MessageList.tsx

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -42,10 +42,6 @@ import {
4242
MessagesContextValue,
4343
useMessagesContext,
4444
} from '../../contexts/messagesContext/MessagesContext';
45-
import {
46-
OverlayContextValue,
47-
useOverlayContext,
48-
} from '../../contexts/overlayContext/OverlayContext';
4945
import {
5046
PaginatedMessageListContextValue,
5147
usePaginatedMessageListContext,
@@ -166,7 +162,6 @@ type MessageListPropsWithContext<
166162
Pick<ChatContextValue<StreamChatGenerics>, 'client'> &
167163
Pick<ImageGalleryContextValue<StreamChatGenerics>, 'setMessages'> &
168164
Pick<PaginatedMessageListContextValue<StreamChatGenerics>, 'loadMore' | 'loadMoreRecent'> &
169-
Pick<OverlayContextValue, 'overlay'> &
170165
Pick<
171166
MessagesContextValue<StreamChatGenerics>,
172167
| 'DateHeader'
@@ -299,7 +294,6 @@ const MessageListWithContext = <
299294
noGroupByUser,
300295
onListScroll,
301296
onThreadSelect,
302-
overlay,
303297
reloadChannel,
304298
ScrollToBottomButton,
305299
selectedPicker,
@@ -1276,7 +1270,6 @@ const MessageListWithContext = <
12761270
onViewableItemsChanged={stableOnViwableItemsChanged}
12771271
ref={refCallback}
12781272
renderItem={renderItem}
1279-
scrollEnabled={overlay === 'none'}
12801273
showsVerticalScrollIndicator={!shouldApplyAndroidWorkaround}
12811274
style={flatListStyle}
12821275
testID='message-flat-list'
@@ -1359,7 +1352,6 @@ export const MessageList = <
13591352
UnreadMessagesNotification,
13601353
} = useMessagesContext<StreamChatGenerics>();
13611354
const { loadMore, loadMoreRecent } = usePaginatedMessageListContext<StreamChatGenerics>();
1362-
const { overlay } = useOverlayContext();
13631355
const { loadMoreRecentThread, loadMoreThread, thread, threadInstance } =
13641356
useThreadContext<StreamChatGenerics>();
13651357

@@ -1395,7 +1387,6 @@ export const MessageList = <
13951387
MessageSystem,
13961388
myMessageTheme,
13971389
NetworkDownIndicator,
1398-
overlay,
13991390
reloadChannel,
14001391
ScrollToBottomButton,
14011392
scrollToFirstUnreadThreshold,

package/src/components/Thread/__tests__/__snapshots__/Thread.test.js.snap

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,6 @@ exports[`Thread should match thread snapshot 1`] = `
170170
onViewableItemsChanged={[Function]}
171171
removeClippedSubviews={false}
172172
renderItem={[Function]}
173-
scrollEnabled={false}
174173
scrollEventThrottle={0.0001}
175174
showsVerticalScrollIndicator={true}
176175
stickyHeaderIndices={[]}

package/src/contexts/overlayContext/OverlayProvider.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -208,8 +208,8 @@ export const OverlayProvider = <
208208
<OverlayContext.Provider value={overlayContext}>
209209
<AttachmentPickerProvider value={attachmentPickerContext}>
210210
<ImageGalleryProvider>
211-
{children}
212211
<ThemeProvider style={overlayContext.style}>
212+
{children}
213213
{overlay === 'gallery' && (
214214
<ImageGallery<StreamChatGenerics>
215215
autoPlayVideo={autoPlayVideo}

0 commit comments

Comments
 (0)