Skip to content

Commit 2d8e9ad

Browse files
authored
feat!: introduce image gallery state store (#3330)
This pull request refactors the image gallery to simplify state management and remove legacy props and logic related to image gallery behavior. The main improvement is the introduction of a centralized `imageGalleryStateStore`, which replaces the previous pattern of passing multiple state setters and props. Additionally, the video is now managed by a pool and a player class same as audio. This is for easy state management and ease of usage. Additionally, the dependency on `@gorhom/bottom-sheet` is updated, and related code is streamlined to use the new approach. **Refactoring and State Management Improvements:** * Replaced legacy props (`setMessages`, `setSelectedMessage`, `legacyImageViewerSwipeBehaviour`) in `Gallery`, `Giphy`, and related components with a single `imageGalleryStateStore` prop, centralizing image gallery state handling. [[1]](diffhunk://#diff-e7d0e211073121199347eedf392123d964951dbd5ebbda17d1f86c921ec39782L39-R39) [[2]](diffhunk://#diff-e7d0e211073121199347eedf392123d964951dbd5ebbda17d1f86c921ec39782R51-L58) [[3]](diffhunk://#diff-e7d0e211073121199347eedf392123d964951dbd5ebbda17d1f86c921ec39782L68-L80) [[4]](diffhunk://#diff-e7d0e211073121199347eedf392123d964951dbd5ebbda17d1f86c921ec39782R73-L101) [[5]](diffhunk://#diff-e7d0e211073121199347eedf392123d964951dbd5ebbda17d1f86c921ec39782R189-L213) [[6]](diffhunk://#diff-e7d0e211073121199347eedf392123d964951dbd5ebbda17d1f86c921ec39782L222-L224) [[7]](diffhunk://#diff-e7d0e211073121199347eedf392123d964951dbd5ebbda17d1f86c921ec39782L255-L274) [[8]](diffhunk://#diff-e7d0e211073121199347eedf392123d964951dbd5ebbda17d1f86c921ec39782L283-L285) [[9]](diffhunk://#diff-e7d0e211073121199347eedf392123d964951dbd5ebbda17d1f86c921ec39782L307-R288) [[10]](diffhunk://#diff-e7d0e211073121199347eedf392123d964951dbd5ebbda17d1f86c921ec39782L588-R564) [[11]](diffhunk://#diff-e7d0e211073121199347eedf392123d964951dbd5ebbda17d1f86c921ec39782L612) [[12]](diffhunk://#diff-e7d0e211073121199347eedf392123d964951dbd5ebbda17d1f86c921ec39782L634) [[13]](diffhunk://#diff-e7d0e211073121199347eedf392123d964951dbd5ebbda17d1f86c921ec39782R620-L665) [[14]](diffhunk://#diff-9175eb9fcc09f05267618eca638f8502019ed92e3751d38c2771d7083903ab5bL135-R135) [[15]](diffhunk://#diff-9175eb9fcc09f05267618eca638f8502019ed92e3751d38c2771d7083903ab5bR164) [[16]](diffhunk://#diff-9175eb9fcc09f05267618eca638f8502019ed92e3751d38c2771d7083903ab5bL175-L177) [[17]](diffhunk://#diff-9175eb9fcc09f05267618eca638f8502019ed92e3751d38c2771d7083903ab5bL212-R211) [[18]](diffhunk://#diff-9175eb9fcc09f05267618eca638f8502019ed92e3751d38c2771d7083903ab5bL455-R453) [[19]](diffhunk://#diff-9175eb9fcc09f05267618eca638f8502019ed92e3751d38c2771d7083903ab5bR471) [[20]](diffhunk://#diff-9175eb9fcc09f05267618eca638f8502019ed92e3751d38c2771d7083903ab5bL481-L483) * Updated the logic for opening the image viewer and Giphy attachments to use `imageGalleryStateStore.openImageGallery`, removing conditional logic for legacy behavior. [[1]](diffhunk://#diff-e7d0e211073121199347eedf392123d964951dbd5ebbda17d1f86c921ec39782L307-R288) [[2]](diffhunk://#diff-9175eb9fcc09f05267618eca638f8502019ed92e3751d38c2771d7083903ab5bL212-R211) * Removed all references to `legacyImageViewerSwipeBehaviour` from the codebase, including context, props, and hooks. [[1]](diffhunk://#diff-f7139f4cdb523365cfc277d72b827a3432325b9c6460cf14628f9df67d0e4d85L346) [[2]](diffhunk://#diff-f7139f4cdb523365cfc277d72b827a3432325b9c6460cf14628f9df67d0e4d85L664) [[3]](diffhunk://#diff-f7139f4cdb523365cfc277d72b827a3432325b9c6460cf14628f9df67d0e4d85L1945) [[4]](diffhunk://#diff-d3e4f4cdcef10807a38eab86f6ead6bbfe01980e7326f3cadfeb8186760d7af1L55) [[5]](diffhunk://#diff-d3e4f4cdcef10807a38eab86f6ead6bbfe01980e7326f3cadfeb8186760d7af1L173) **Dependency and Import Updates:** * Upgraded `@gorhom/bottom-sheet` dependency from version 5.1.8 to 5.2.8. * Updated `ImageGallery.tsx` to use the new `imageGalleryStateStore` and removed unused imports and legacy bottom sheet modal code. [[1]](diffhunk://#diff-b6da04367e722c0873941022e180cec3e33f89445cb2427ecaa78c2040037e5aL1-R16) [[2]](diffhunk://#diff-b6da04367e722c0873941022e180cec3e33f89445cb2427ecaa78c2040037e5aL39-L52) [[3]](diffhunk://#diff-b6da04367e722c0873941022e180cec3e33f89445cb2427ecaa78c2040037e5aR99-R128) These changes modernize and simplify the gallery and attachment components, making state management more robust and maintainable.
1 parent 66c4d00 commit 2d8e9ad

41 files changed

Lines changed: 3318 additions & 1602 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

examples/SampleApp/src/screens/ChannelImagesScreen.tsx

Lines changed: 16 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -13,18 +13,18 @@ import Dayjs from 'dayjs';
1313
import { SafeAreaView } from 'react-native-safe-area-context';
1414
import {
1515
DateHeader,
16-
Photo,
1716
useImageGalleryContext,
1817
useOverlayContext,
1918
useTheme,
19+
ImageGalleryState,
20+
useStateStore,
2021
} from 'stream-chat-react-native';
2122

2223
import { ScreenHeader } from '../components/ScreenHeader';
2324
import { usePaginatedAttachments } from '../hooks/usePaginatedAttachments';
2425
import { Picture } from '../icons/Picture';
2526

2627
import type { RouteProp } from '@react-navigation/native';
27-
import type { Attachment } from 'stream-chat';
2828

2929
import type { StackNavigatorParamList } from '../types';
3030

@@ -61,16 +61,17 @@ export type ChannelImagesScreenProps = {
6161
route: ChannelImagesScreenRouteProp;
6262
};
6363

64+
const selector = (state: ImageGalleryState) => ({
65+
assets: state.assets,
66+
});
67+
6468
export const ChannelImagesScreen: React.FC<ChannelImagesScreenProps> = ({
6569
route: {
6670
params: { channel },
6771
},
6872
}) => {
69-
const {
70-
messages: images,
71-
setMessages: setImages,
72-
setSelectedMessage: setImage,
73-
} = useImageGalleryContext();
73+
const { imageGalleryStateStore } = useImageGalleryContext();
74+
const { assets } = useStateStore(imageGalleryStateStore.state, selector);
7475
const { setOverlay } = useOverlayContext();
7576
const { loading, loadMore, messages } = usePaginatedAttachments(channel, 'image');
7677
const {
@@ -79,8 +80,6 @@ export const ChannelImagesScreen: React.FC<ChannelImagesScreenProps> = ({
7980
},
8081
} = useTheme();
8182

82-
const channelImages = useRef(images);
83-
8483
const [stickyHeaderDate, setStickyHeaderDate] = useState(
8584
Dayjs(messages?.[0]?.created_at).format('MMM YYYY'),
8685
);
@@ -106,30 +105,6 @@ export const ChannelImagesScreen: React.FC<ChannelImagesScreenProps> = ({
106105
}
107106
});
108107

109-
/**
110-
* Photos array created from all currently available
111-
* photo attachments
112-
*/
113-
const photos = messages.reduce((acc: Photo[], cur) => {
114-
const attachmentImages =
115-
(cur.attachments as Attachment[])?.filter(
116-
(attachment) =>
117-
attachment.type === 'image' &&
118-
!attachment.title_link &&
119-
!attachment.og_scrape_url &&
120-
(attachment.image_url || attachment.thumb_url),
121-
) || [];
122-
123-
const attachmentPhotos = attachmentImages.map((attachmentImage) => ({
124-
created_at: cur.created_at,
125-
id: `photoId-${cur.id}-${attachmentImage.image_url || attachmentImage.thumb_url}`,
126-
messageId: cur.id,
127-
uri: attachmentImage.image_url || (attachmentImage.thumb_url as string),
128-
}));
129-
130-
return [...acc, ...attachmentPhotos];
131-
}, []);
132-
133108
const messagesWithImages = messages
134109
.map((message) => ({ ...message, groupStyles: [], readBy: false }))
135110
.filter((message) => {
@@ -145,32 +120,19 @@ export const ChannelImagesScreen: React.FC<ChannelImagesScreenProps> = ({
145120
return false;
146121
});
147122

148-
/**
149-
* This is for the useEffect to run again in the case that a message
150-
* gets edited with more or the same number of images
151-
*/
152-
const imageString = messagesWithImages
153-
.map((message) =>
154-
(message.attachments as Attachment[])
155-
.map((attachment) => attachment.image_url || attachment.thumb_url || '')
156-
.join(),
157-
)
158-
.join();
159-
160123
useEffect(() => {
161-
setImages(messagesWithImages);
162-
const channelImagesCurrent = channelImages.current;
163-
return () => setImages(channelImagesCurrent);
124+
imageGalleryStateStore.openImageGallery({ messages: messagesWithImages });
125+
return () => imageGalleryStateStore.clear();
164126
// eslint-disable-next-line react-hooks/exhaustive-deps
165-
}, [imageString, setImages]);
127+
}, [imageGalleryStateStore, messagesWithImages.length]);
166128

167129
return (
168130
<SafeAreaView style={[styles.flex, { backgroundColor: white }]}>
169131
<ScreenHeader inSafeArea titleText='Photos and Videos' />
170132
<View style={styles.flex}>
171133
<FlatList
172134
contentContainerStyle={styles.contentContainer}
173-
data={photos}
135+
data={assets}
174136
keyExtractor={(item, index) => `${item.id}-${index}`}
175137
ListEmptyComponent={EmptyListComponent}
176138
numColumns={3}
@@ -180,9 +142,9 @@ export const ChannelImagesScreen: React.FC<ChannelImagesScreenProps> = ({
180142
renderItem={({ item }) => (
181143
<TouchableOpacity
182144
onPress={() => {
183-
setImage({
184-
messageId: item.messageId,
185-
url: item.uri,
145+
imageGalleryStateStore.openImageGallery({
146+
messages: messagesWithImages,
147+
selectedAttachmentUrl: item.uri,
186148
});
187149
setOverlay('gallery');
188150
}}
@@ -202,7 +164,7 @@ export const ChannelImagesScreen: React.FC<ChannelImagesScreenProps> = ({
202164
viewAreaCoveragePercentThreshold: 50,
203165
}}
204166
/>
205-
{photos && photos.length ? (
167+
{assets.length > 0 ? (
206168
<View style={styles.stickyHeader}>
207169
<DateHeader dateString={stickyHeaderDate} />
208170
</View>

package/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@
6868
]
6969
},
7070
"dependencies": {
71-
"@gorhom/bottom-sheet": "^5.1.8",
71+
"@gorhom/bottom-sheet": "^5.2.8",
7272
"@ungap/structured-clone": "^1.3.0",
7373
"dayjs": "1.11.13",
7474
"emoji-regex": "^10.4.0",

package/src/components/Attachment/Gallery.tsx

Lines changed: 15 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -36,10 +36,7 @@ import { isVideoPlayerAvailable } from '../../native';
3636
import { FileTypes } from '../../types/types';
3737
import { getUrlWithoutParams } from '../../utils/utils';
3838

39-
export type GalleryPropsWithContext = Pick<
40-
ImageGalleryContextValue,
41-
'setSelectedMessage' | 'setMessages'
42-
> &
39+
export type GalleryPropsWithContext = Pick<ImageGalleryContextValue, 'imageGalleryStateStore'> &
4340
Pick<
4441
MessageContextValue,
4542
| 'alignment'
@@ -51,11 +48,11 @@ export type GalleryPropsWithContext = Pick<
5148
| 'onPressIn'
5249
| 'preventPress'
5350
| 'threadList'
51+
| 'message'
5452
> &
5553
Pick<
5654
MessagesContextValue,
5755
| 'additionalPressableProps'
58-
| 'legacyImageViewerSwipeBehaviour'
5956
| 'VideoThumbnail'
6057
| 'ImageLoadingIndicator'
6158
| 'ImageLoadingFailedIndicator'
@@ -65,19 +62,6 @@ export type GalleryPropsWithContext = Pick<
6562
Pick<OverlayContextValue, 'setOverlay'> & {
6663
channelId: string | undefined;
6764
hasThreadReplies?: boolean;
68-
/**
69-
* `message` prop has been introduced here as part of `legacyImageViewerSwipeBehaviour` prop.
70-
* https://github.com/GetStream/stream-chat-react-native/commit/d5eac6193047916f140efe8e396a671675c9a63f
71-
* messageId and messageText may seem redundant now, but to avoid breaking change as part
72-
* of minor release, we are keeping those props.
73-
*
74-
* Also `message` type should ideally be imported from MessageContextValue and not be explicitely mentioned
75-
* here, but due to some circular dependencies within the SDK, it causes "excessive deep nesting" issue with
76-
* typescript within Channel component. We should take it as a mini-project and resolve all these circular imports.
77-
*
78-
* TODO: Fix circular dependencies of imports
79-
*/
80-
message?: LocalMessage;
8165
};
8266

8367
const GalleryWithContext = (props: GalleryPropsWithContext) => {
@@ -86,19 +70,17 @@ const GalleryWithContext = (props: GalleryPropsWithContext) => {
8670
alignment,
8771
groupStyles,
8872
hasThreadReplies,
73+
imageGalleryStateStore,
8974
ImageLoadingFailedIndicator,
9075
ImageLoadingIndicator,
9176
ImageReloadIndicator,
9277
images,
93-
legacyImageViewerSwipeBehaviour,
9478
message,
9579
onLongPress,
9680
onPress,
9781
onPressIn,
9882
preventPress,
99-
setMessages,
10083
setOverlay,
101-
setSelectedMessage,
10284
threadList,
10385
videos,
10486
VideoThumbnail,
@@ -204,13 +186,13 @@ const GalleryWithContext = (props: GalleryPropsWithContext) => {
204186
additionalPressableProps={additionalPressableProps}
205187
borderRadius={borderRadius}
206188
colIndex={colIndex}
189+
imageGalleryStateStore={imageGalleryStateStore}
207190
ImageLoadingFailedIndicator={ImageLoadingFailedIndicator}
208191
ImageLoadingIndicator={ImageLoadingIndicator}
209192
ImageReloadIndicator={ImageReloadIndicator}
210193
imagesAndVideos={imagesAndVideos}
211194
invertedDirections={invertedDirections || false}
212195
key={rowIndex}
213-
legacyImageViewerSwipeBehaviour={legacyImageViewerSwipeBehaviour}
214196
message={message}
215197
numOfColumns={numOfColumns}
216198
numOfRows={numOfRows}
@@ -219,9 +201,7 @@ const GalleryWithContext = (props: GalleryPropsWithContext) => {
219201
onPressIn={onPressIn}
220202
preventPress={preventPress}
221203
rowIndex={rowIndex}
222-
setMessages={setMessages}
223204
setOverlay={setOverlay}
224-
setSelectedMessage={setSelectedMessage}
225205
thumbnail={thumbnail}
226206
VideoThumbnail={VideoThumbnail}
227207
/>
@@ -252,26 +232,25 @@ type GalleryThumbnailProps = {
252232
} & Pick<
253233
MessagesContextValue,
254234
| 'additionalPressableProps'
255-
| 'legacyImageViewerSwipeBehaviour'
256235
| 'VideoThumbnail'
257236
| 'ImageLoadingIndicator'
258237
| 'ImageLoadingFailedIndicator'
259238
| 'ImageReloadIndicator'
260239
> &
261-
Pick<ImageGalleryContextValue, 'setSelectedMessage' | 'setMessages'> &
240+
Pick<ImageGalleryContextValue, 'imageGalleryStateStore'> &
262241
Pick<MessageContextValue, 'onLongPress' | 'onPress' | 'onPressIn' | 'preventPress'> &
263242
Pick<OverlayContextValue, 'setOverlay'>;
264243

265244
const GalleryThumbnail = ({
266245
additionalPressableProps,
267246
borderRadius,
268247
colIndex,
248+
imageGalleryStateStore,
269249
ImageLoadingFailedIndicator,
270250
ImageLoadingIndicator,
271251
ImageReloadIndicator,
272252
imagesAndVideos,
273253
invertedDirections,
274-
legacyImageViewerSwipeBehaviour,
275254
message,
276255
numOfColumns,
277256
numOfRows,
@@ -280,9 +259,7 @@ const GalleryThumbnail = ({
280259
onPressIn,
281260
preventPress,
282261
rowIndex,
283-
setMessages,
284262
setOverlay,
285-
setSelectedMessage,
286263
thumbnail,
287264
VideoThumbnail,
288265
}: GalleryThumbnailProps) => {
@@ -304,17 +281,14 @@ const GalleryThumbnail = ({
304281
const { t } = useTranslationContext();
305282

306283
const openImageViewer = () => {
307-
if (!legacyImageViewerSwipeBehaviour && message) {
308-
// Added if-else to keep the logic readable, instead of DRY.
309-
// if - legacyImageViewerSwipeBehaviour is disabled
310-
// else - legacyImageViewerSwipeBehaviour is enabled
311-
setMessages([message]);
312-
setSelectedMessage({ messageId: message.id, url: thumbnail.url });
313-
setOverlay('gallery');
314-
} else if (legacyImageViewerSwipeBehaviour) {
315-
setSelectedMessage({ messageId: message?.id, url: thumbnail.url });
316-
setOverlay('gallery');
284+
if (!message) {
285+
return;
317286
}
287+
imageGalleryStateStore.openImageGallery({
288+
messages: [message],
289+
selectedAttachmentUrl: thumbnail.url,
290+
});
291+
setOverlay('gallery');
318292
};
319293

320294
const defaultOnPress = () => {
@@ -585,13 +559,12 @@ export const Gallery = (props: GalleryProps) => {
585559
onPressIn: propOnPressIn,
586560
preventPress: propPreventPress,
587561
setOverlay: propSetOverlay,
588-
setSelectedMessage: propSetSelectedMessage,
589562
threadList: propThreadList,
590563
videos: propVideos,
591564
VideoThumbnail: PropVideoThumbnail,
592565
} = props;
593566

594-
const { setMessages, setSelectedMessage: contextSetSelectedMessage } = useImageGalleryContext();
567+
const { imageGalleryStateStore } = useImageGalleryContext();
595568
const {
596569
alignment: contextAlignment,
597570
groupStyles: contextGroupStyles,
@@ -609,7 +582,6 @@ export const Gallery = (props: GalleryProps) => {
609582
ImageLoadingFailedIndicator: ContextImageLoadingFailedIndicator,
610583
ImageLoadingIndicator: ContextImageLoadingIndicator,
611584
ImageReloadIndicator: ContextImageReloadIndicator,
612-
legacyImageViewerSwipeBehaviour,
613585
myMessageTheme: contextMyMessageTheme,
614586
VideoThumbnail: ContextVideoThumnbnail,
615587
} = useMessagesContext();
@@ -631,7 +603,6 @@ export const Gallery = (props: GalleryProps) => {
631603
const onPress = propOnPress || contextOnPress;
632604
const preventPress =
633605
typeof propPreventPress === 'boolean' ? propPreventPress : contextPreventPress;
634-
const setSelectedMessage = propSetSelectedMessage || contextSetSelectedMessage;
635606
const setOverlay = propSetOverlay || contextSetOverlay;
636607
const threadList = propThreadList || contextThreadList;
637608
const VideoThumbnail = PropVideoThumbnail || ContextVideoThumnbnail;
@@ -649,20 +620,18 @@ export const Gallery = (props: GalleryProps) => {
649620
channelId: message?.cid,
650621
groupStyles,
651622
hasThreadReplies: hasThreadReplies || !!message?.reply_count,
623+
imageGalleryStateStore,
652624
ImageLoadingFailedIndicator,
653625
ImageLoadingIndicator,
654626
ImageReloadIndicator,
655627
images,
656-
legacyImageViewerSwipeBehaviour,
657628
message,
658629
myMessageTheme,
659630
onLongPress,
660631
onPress,
661632
onPressIn,
662633
preventPress,
663-
setMessages,
664634
setOverlay,
665-
setSelectedMessage,
666635
threadList,
667636
videos,
668637
VideoThumbnail,

0 commit comments

Comments
 (0)