Skip to content

Commit e87cc22

Browse files
authored
perf: image gallery rendering improvements (#3108)
1 parent efd080a commit e87cc22

File tree

5 files changed

+97
-94
lines changed

5 files changed

+97
-94
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 { Channel, OfflineDBState } from 'stream-chat';
@@ -11,8 +11,7 @@ import { useMutedUsers } from './hooks/useMutedUsers';
1111
import { ChannelsStateProvider } from '../../contexts/channelsStateContext/ChannelsStateContext';
1212
import { ChatContextValue, ChatProvider } from '../../contexts/chatContext/ChatContext';
1313
import { useDebugContext } from '../../contexts/debugContext/DebugContext';
14-
import { useOverlayContext } from '../../contexts/overlayContext/OverlayContext';
15-
import { DeepPartial, ThemeProvider } from '../../contexts/themeContext/ThemeContext';
14+
import { DeepPartial, ThemeProvider, useTheme } from '../../contexts/themeContext/ThemeContext';
1615
import type { Theme } from '../../contexts/themeContext/utils/theme';
1716
import {
1817
DEFAULT_USER_LANGUAGE,
@@ -156,6 +155,11 @@ const ChatWithContext = (props: PropsWithChildren<ChatProps>) => {
156155
// Setup translators
157156
const translators = useStreami18n(i18nInstance);
158157

158+
const translationContextValue = useMemo(
159+
() => ({ ...translators, userLanguage: client.user?.language || DEFAULT_USER_LANGUAGE }),
160+
[client.user?.language, translators],
161+
);
162+
159163
/**
160164
* Setup connection event listeners
161165
*/
@@ -262,9 +266,7 @@ const ChatWithContext = (props: PropsWithChildren<ChatProps>) => {
262266

263267
return (
264268
<ChatProvider value={chatContext}>
265-
<TranslationProvider
266-
value={{ ...translators, userLanguage: client.user?.language || DEFAULT_USER_LANGUAGE }}
267-
>
269+
<TranslationProvider value={translationContextValue}>
268270
<ThemeProvider style={style}>
269271
<ChannelsStateProvider>{children}</ChannelsStateProvider>
270272
</ThemeProvider>
@@ -286,7 +288,7 @@ const ChatWithContext = (props: PropsWithChildren<ChatProps>) => {
286288
* - setActiveChannel - function to set the currently active channel
287289
*/
288290
export const Chat = (props: PropsWithChildren<ChatProps>) => {
289-
const { style } = useOverlayContext();
291+
const { theme } = useTheme();
290292

291-
return <ChatWithContext {...{ style }} {...props} />;
293+
return <ChatWithContext style={theme as DeepPartial<Theme>} {...props} />;
292294
};

package/src/components/ImageGallery/ImageGallery.tsx

Lines changed: 86 additions & 75 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';
@@ -125,7 +125,6 @@ export const ImageGallery = (props: Props) => {
125125
numberOfImageGalleryGridColumns,
126126
overlayOpacity,
127127
} = props;
128-
const [imageGalleryAttachments, setImageGalleryAttachments] = useState<Photo[]>([]);
129128
const { resizableCDNHosts } = useChatConfigContext();
130129
const {
131130
theme: {
@@ -188,13 +187,6 @@ export const ImageGallery = (props: Props) => {
188187
*/
189188
const [currentImageHeight, setCurrentImageHeight] = useState<number>(fullWindowHeight);
190189

191-
/**
192-
* JS and UI index values, the JS follows the UI but is needed
193-
* for rendering the virtualized image list
194-
*/
195-
const [selectedIndex, setSelectedIndex] = useState(0);
196-
const index = useSharedValue(0);
197-
198190
/**
199191
* Header visible value for animating in out
200192
*/
@@ -214,57 +206,86 @@ export const ImageGallery = (props: Props) => {
214206
* photo attachments
215207
*/
216208

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

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

269290
/**
270291
* Photos length needs to be kept as a const here so if the length
@@ -274,17 +295,6 @@ export const ImageGallery = (props: Props) => {
274295
*/
275296
const photoLength = photos.length;
276297

277-
useEffect(() => {
278-
setImageGalleryAttachments(photos);
279-
// eslint-disable-next-line react-hooks/exhaustive-deps
280-
}, []);
281-
282-
/**
283-
* The URL for the images may differ because of dimensions passed as
284-
* part of the query.
285-
*/
286-
const stripQueryFromUrl = (url: string) => url.split('?')[0];
287-
288298
/**
289299
* Set selected photo when changed via pressing in the message list
290300
*/
@@ -306,8 +316,7 @@ export const ImageGallery = (props: Props) => {
306316
);
307317

308318
runOnUI(updatePosition)(newIndex);
309-
// eslint-disable-next-line react-hooks/exhaustive-deps
310-
}, [selectedMessage, photoLength]);
319+
}, [selectedMessage, photos, index, translationX, fullWindowWidth]);
311320

312321
/**
313322
* Image heights are not provided and therefore need to be calculated.
@@ -318,22 +327,24 @@ export const ImageGallery = (props: Props) => {
318327
const uriForCurrentImage = imageGalleryAttachments[selectedIndex]?.uri;
319328

320329
useEffect(() => {
321-
setCurrentImageHeight(fullWindowHeight);
330+
let currentImageHeight = fullWindowHeight;
322331
const photo = imageGalleryAttachments[index.value];
323332
const height = photo?.original_height;
324333
const width = photo?.original_width;
325334

326335
if (height && width) {
327336
const imageHeight = Math.floor(height * (fullWindowWidth / width));
328-
setCurrentImageHeight(imageHeight > fullWindowHeight ? fullWindowHeight : imageHeight);
337+
currentImageHeight = imageHeight > fullWindowHeight ? fullWindowHeight : imageHeight;
329338
} else if (photo?.uri) {
330339
if (photo.type === FileTypes.Image) {
331340
Image.getSize(photo.uri, (width, height) => {
332341
const imageHeight = Math.floor(height * (fullWindowWidth / width));
333-
setCurrentImageHeight(imageHeight > fullWindowHeight ? fullWindowHeight : imageHeight);
342+
currentImageHeight = imageHeight > fullWindowHeight ? fullWindowHeight : imageHeight;
334343
});
335344
}
336345
}
346+
347+
setCurrentImageHeight(currentImageHeight);
337348
// eslint-disable-next-line react-hooks/exhaustive-deps
338349
}, [uriForCurrentImage]);
339350

package/src/components/MessageList/MessageList.tsx

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -38,10 +38,6 @@ import {
3838
MessagesContextValue,
3939
useMessagesContext,
4040
} from '../../contexts/messagesContext/MessagesContext';
41-
import {
42-
OverlayContextValue,
43-
useOverlayContext,
44-
} from '../../contexts/overlayContext/OverlayContext';
4541
import {
4642
PaginatedMessageListContextValue,
4743
usePaginatedMessageListContext,
@@ -149,7 +145,6 @@ type MessageListPropsWithContext = Pick<
149145
Pick<ChatContextValue, 'client'> &
150146
Pick<ImageGalleryContextValue, 'setMessages'> &
151147
Pick<PaginatedMessageListContextValue, 'loadMore' | 'loadMoreRecent'> &
152-
Pick<OverlayContextValue, 'overlay'> &
153148
Pick<
154149
MessagesContextValue,
155150
| 'DateHeader'
@@ -278,7 +273,6 @@ const MessageListWithContext = (props: MessageListPropsWithContext) => {
278273
noGroupByUser,
279274
onListScroll,
280275
onThreadSelect,
281-
overlay,
282276
reloadChannel,
283277
ScrollToBottomButton,
284278
selectedPicker,
@@ -1253,7 +1247,6 @@ const MessageListWithContext = (props: MessageListPropsWithContext) => {
12531247
onViewableItemsChanged={stableOnViewableItemsChanged}
12541248
ref={refCallback}
12551249
renderItem={renderItem}
1256-
scrollEnabled={overlay === 'none'}
12571250
showsVerticalScrollIndicator={!shouldApplyAndroidWorkaround}
12581251
style={flatListStyle}
12591252
testID='message-flat-list'
@@ -1330,7 +1323,6 @@ export const MessageList = (props: MessageListProps) => {
13301323
UnreadMessagesNotification,
13311324
} = useMessagesContext();
13321325
const { loadMore, loadMoreRecent } = usePaginatedMessageListContext();
1333-
const { overlay } = useOverlayContext();
13341326
const { loadMoreRecentThread, loadMoreThread, thread, threadInstance } = useThreadContext();
13351327

13361328
return (
@@ -1365,7 +1357,6 @@ export const MessageList = (props: MessageListProps) => {
13651357
MessageSystem,
13661358
myMessageTheme,
13671359
NetworkDownIndicator,
1368-
overlay,
13691360
reloadChannel,
13701361
ScrollToBottomButton,
13711362
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
@@ -158,7 +158,6 @@ exports[`Thread should match thread snapshot 1`] = `
158158
onViewableItemsChanged={[Function]}
159159
removeClippedSubviews={false}
160160
renderItem={[Function]}
161-
scrollEnabled={false}
162161
scrollEventThrottle={0.0001}
163162
showsVerticalScrollIndicator={true}
164163
stickyHeaderIndices={[]}

package/src/contexts/overlayContext/OverlayProvider.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -204,8 +204,8 @@ export const OverlayProvider = (props: PropsWithChildren<OverlayProviderProps>)
204204
<OverlayContext.Provider value={overlayContext}>
205205
<AttachmentPickerProvider value={attachmentPickerContext}>
206206
<ImageGalleryProvider>
207-
{children}
208207
<ThemeProvider style={overlayContext.style}>
208+
{children}
209209
{overlay === 'gallery' && (
210210
<ImageGallery
211211
autoPlayVideo={autoPlayVideo}

0 commit comments

Comments
 (0)