Skip to content

Commit 7b428ad

Browse files
committed
feat: image gallery redesign
1 parent efc9512 commit 7b428ad

36 files changed

+825
-1248
lines changed

package/expo-package/src/optionalDependencies/Video.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ if (videoPackage) {
8686
}
8787
// expo-av
8888
else if (ExpoAVVideoComponent) {
89-
Video = ({ onPlaybackStatusUpdate, paused, resizeMode, style, uri, videoRef }) => {
89+
Video = ({ onPlaybackStatusUpdate, paused, resizeMode, style, uri, videoRef, rate }) => {
9090
// This is done so that the audio of the video is not muted when the phone is in silent mode for iOS.
9191
useEffect(() => {
9292
const initializeSound = async () => {
@@ -107,6 +107,7 @@ else if (ExpoAVVideoComponent) {
107107
uri,
108108
}}
109109
style={[style]}
110+
playbackRate={rate}
110111
/>
111112
);
112113
};

package/native-package/src/optionalDependencies/Video.tsx

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,19 @@ import React from 'react';
22
import AudioVideoPlayer from './AudioVideo';
33

44
export const Video = AudioVideoPlayer
5-
? ({ onBuffer, onEnd, onLoad, onProgress, paused, repeat, resizeMode, style, uri, videoRef }) => (
5+
? ({
6+
onBuffer,
7+
onEnd,
8+
onLoad,
9+
onProgress,
10+
paused,
11+
repeat,
12+
resizeMode,
13+
style,
14+
uri,
15+
videoRef,
16+
rate,
17+
}) => (
618
<AudioVideoPlayer
719
ignoreSilentSwitch={'ignore'}
820
onBuffer={onBuffer}
@@ -20,6 +32,7 @@ export const Video = AudioVideoPlayer
2032
uri,
2133
}}
2234
style={style}
35+
rate={rate}
2336
/>
2437
)
2538
: null;

package/src/components/Attachment/Audio/PlayPauseButton.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@ import React, { useMemo } from 'react';
22
import { Pressable, PressableProps, StyleProp, StyleSheet, ViewStyle } from 'react-native';
33

44
import { useTheme } from '../../../contexts';
5-
import { NewPause } from '../../../icons/NewPause';
6-
import { NewPlay } from '../../../icons/NewPlay';
5+
import { Pause } from '../../../icons/Pause';
6+
import { Play } from '../../../icons/Play';
77
import { primitives } from '../../../theme';
88
import { buttonSizes } from '../../ui/Button/constants';
99

@@ -45,9 +45,9 @@ export const PlayPauseButton = ({
4545
{...rest}
4646
>
4747
{isPlaying ? (
48-
<NewPause fill={semantics.textSecondary} height={20} width={20} strokeWidth={1.5} />
48+
<Pause fill={semantics.textSecondary} height={20} width={20} strokeWidth={1.5} />
4949
) : (
50-
<NewPlay fill={semantics.textSecondary} height={20} width={20} strokeWidth={1.5} />
50+
<Play fill={semantics.textSecondary} height={20} width={20} strokeWidth={1.5} />
5151
)}
5252
</Pressable>
5353
);

package/src/components/ImageGallery/ImageGallery.tsx

Lines changed: 59 additions & 136 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useCallback, useEffect, useRef, useState } from 'react';
1+
import React, { useCallback, useEffect, useState } from 'react';
22
import { Image, ImageStyle, StyleSheet, ViewStyle } from 'react-native';
33
import { Gesture, GestureDetector } from 'react-native-gesture-handler';
44

@@ -11,24 +11,8 @@ import Animated, {
1111
withTiming,
1212
} from 'react-native-reanimated';
1313

14-
import BottomSheet from '@gorhom/bottom-sheet';
15-
1614
import { AnimatedGalleryImage } from './components/AnimatedGalleryImage';
1715
import { AnimatedGalleryVideo } from './components/AnimatedGalleryVideo';
18-
import {
19-
ImageGalleryFooter,
20-
ImageGalleryFooterCustomComponentProps,
21-
} from './components/ImageGalleryFooter';
22-
import {
23-
ImageGalleryHeader,
24-
ImageGalleryHeaderCustomComponentProps,
25-
} from './components/ImageGalleryHeader';
26-
import { ImageGalleryOverlay } from './components/ImageGalleryOverlay';
27-
import { ImageGalleryGridImageComponents, ImageGrid } from './components/ImageGrid';
28-
import {
29-
ImageGalleryGridHandleCustomComponentProps,
30-
ImageGridHandle,
31-
} from './components/ImageGridHandle';
3216

3317
import { useImageGalleryGestures } from './hooks/useImageGalleryGestures';
3418

@@ -43,9 +27,21 @@ import {
4327
import { useTheme } from '../../contexts/themeContext/ThemeContext';
4428
import { useStateStore } from '../../hooks';
4529
import { useViewport } from '../../hooks/useViewport';
30+
import { IconProps } from '../../icons/utils/base';
4631
import { ImageGalleryState } from '../../state-store/image-gallery-state-store';
4732
import { FileTypes } from '../../types/types';
4833
import { dismissKeyboard } from '../KeyboardCompatibleView/KeyboardControllerAvoidingView';
34+
import { BottomSheetModal } from '../UIComponents';
35+
36+
export type ImageGalleryActionHandler = () => Promise<void> | void;
37+
38+
export type ImageGalleryActionItem = {
39+
action: ImageGalleryActionHandler;
40+
Icon: React.ComponentType<IconProps>;
41+
id: 'showInChat' | 'save' | 'reply' | 'delete' | string;
42+
label: string;
43+
type: 'destructive' | 'standard';
44+
};
4945

5046
const MARGIN = 32;
5147

@@ -60,68 +56,31 @@ export enum IsSwiping {
6056
FALSE,
6157
}
6258

63-
export type ImageGalleryCustomComponents = {
64-
/**
65-
* Override props for following UI components, which are part of [image gallery](https://github.com/GetStream/stream-chat-react-native/wiki/Cookbook-v3.0#gallery-components).
66-
*
67-
* - [ImageGalleryFooter](#ImageGalleryFooter)
68-
*
69-
* - [ImageGrid](#ImageGrid)
70-
*
71-
* - [ImageGridHandle](#ImageGridHandle)
72-
*
73-
* - [ImageGalleryHeader](#ImageGalleryHeader)
74-
*
75-
* e.g.,
76-
*
77-
* ```js
78-
* {
79-
* footer: {
80-
* ShareIcon: CustomShareIconComponent
81-
* },
82-
* grid: {
83-
* avatarComponent: CustomAvatarComponent
84-
* },
85-
* gridHandle: {
86-
* centerComponent: CustomCenterComponent
87-
* },
88-
* header: {
89-
* CloseIcon: CustomCloseButtonComponent
90-
* },
91-
* }
92-
* ```
93-
* @overrideType object
94-
*/
95-
imageGalleryCustomComponents?: {
96-
footer?: ImageGalleryFooterCustomComponentProps;
97-
grid?: ImageGalleryGridImageComponents;
98-
gridHandle?: ImageGalleryGridHandleCustomComponentProps;
99-
header?: ImageGalleryHeaderCustomComponentProps;
100-
};
101-
};
102-
10359
const imageGallerySelector = (state: ImageGalleryState) => ({
10460
assets: state.assets,
10561
currentIndex: state.currentIndex,
10662
});
10763

10864
type ImageGalleryWithContextProps = Pick<
10965
ImageGalleryProviderProps,
110-
| 'imageGalleryCustomComponents'
111-
| 'imageGalleryGridSnapPoints'
112-
| 'imageGalleryGridHandleHeight'
11366
| 'numberOfImageGalleryGridColumns'
67+
| 'ImageGalleryHeader'
68+
| 'ImageGalleryFooter'
69+
| 'ImageGalleryVideoControls'
70+
| 'ImageGalleryGrid'
11471
> &
11572
Pick<OverlayContextValue, 'overlayOpacity'>;
11673

11774
export const ImageGalleryWithContext = (props: ImageGalleryWithContextProps) => {
11875
const {
119-
imageGalleryGridHandleHeight,
120-
imageGalleryGridSnapPoints,
121-
imageGalleryCustomComponents,
12276
numberOfImageGalleryGridColumns,
12377
overlayOpacity,
78+
ImageGalleryHeader,
79+
ImageGalleryFooter,
80+
ImageGalleryVideoControls,
81+
ImageGalleryGrid,
12482
} = props;
83+
const [isGridViewVisible, setIsGridViewVisible] = useState(false);
12584
const {
12685
theme: {
12786
colors: { white_snow },
@@ -143,22 +102,6 @@ export const ImageGalleryWithContext = (props: ImageGalleryWithContextProps) =>
143102

144103
const halfScreenHeight = fullWindowHeight / 2;
145104
const quarterScreenHeight = fullWindowHeight / 4;
146-
const snapPoints = React.useMemo(
147-
() => [(fullWindowHeight * 3) / 4, fullWindowHeight - (imageGalleryGridHandleHeight ?? 0)],
148-
// eslint-disable-next-line react-hooks/exhaustive-deps
149-
[],
150-
);
151-
152-
/**
153-
* BottomSheetModal ref
154-
*/
155-
const bottomSheetModalRef = useRef<BottomSheet>(null);
156-
157-
/**
158-
* BottomSheetModal state
159-
*/
160-
const [currentBottomSheetIndex, setCurrentBottomSheetIndex] = useState(0);
161-
const animatedBottomSheetIndex = useSharedValue(0);
162105

163106
/**
164107
* Fade animation for screen, it is always rendered with pointerEvents
@@ -322,30 +265,16 @@ export const ImageGalleryWithContext = (props: ImageGalleryWithContextProps) =>
322265
* Functions toclose BottomSheetModal with image grid
323266
*/
324267
const closeGridView = () => {
325-
if (bottomSheetModalRef.current?.close) {
326-
bottomSheetModalRef.current.close();
327-
}
268+
setIsGridViewVisible(false);
328269
};
329270

330271
/**
331272
* Function to open BottomSheetModal with image grid
332273
*/
333274
const openGridView = () => {
334-
if (bottomSheetModalRef.current?.snapToIndex) {
335-
bottomSheetModalRef.current.snapToIndex(0);
336-
}
275+
setIsGridViewVisible(true);
337276
};
338277

339-
const MemoizedImageGridHandle = useCallback(
340-
() => (
341-
<ImageGridHandle
342-
closeGridView={closeGridView}
343-
{...imageGalleryCustomComponents?.gridHandle}
344-
/>
345-
),
346-
[imageGalleryCustomComponents?.gridHandle],
347-
);
348-
349278
return (
350279
<Animated.View
351280
accessibilityLabel='Image Gallery'
@@ -403,42 +332,34 @@ export const ImageGalleryWithContext = (props: ImageGalleryWithContextProps) =>
403332
</Animated.View>
404333
</Animated.View>
405334
</GestureDetector>
406-
<ImageGalleryHeader
407-
opacity={headerFooterOpacity}
408-
visible={headerFooterVisible}
409-
{...imageGalleryCustomComponents?.header}
410-
/>
411-
412-
<ImageGalleryFooter
413-
accessibilityLabel={'Image Gallery Footer'}
414-
opacity={headerFooterOpacity}
415-
openGridView={openGridView}
416-
visible={headerFooterVisible}
417-
{...imageGalleryCustomComponents?.footer}
418-
/>
419-
420-
<ImageGalleryOverlay
421-
animatedBottomSheetIndex={animatedBottomSheetIndex}
422-
closeGridView={closeGridView}
423-
currentBottomSheetIndex={currentBottomSheetIndex}
424-
/>
425-
<BottomSheet
426-
animatedIndex={animatedBottomSheetIndex}
427-
enablePanDownToClose={true}
428-
handleComponent={MemoizedImageGridHandle}
429-
// @ts-ignore
430-
handleHeight={imageGalleryGridHandleHeight}
431-
index={-1}
432-
onChange={(index: number) => setCurrentBottomSheetIndex(index)}
433-
ref={bottomSheetModalRef}
434-
snapPoints={imageGalleryGridSnapPoints || snapPoints}
435-
>
436-
<ImageGrid
437-
closeGridView={closeGridView}
438-
numberOfImageGalleryGridColumns={numberOfImageGalleryGridColumns}
439-
{...imageGalleryCustomComponents?.grid}
335+
{ImageGalleryHeader ? (
336+
<ImageGalleryHeader opacity={headerFooterOpacity} visible={headerFooterVisible} />
337+
) : null}
338+
339+
{ImageGalleryFooter ? (
340+
<ImageGalleryFooter
341+
accessibilityLabel={'Image Gallery Footer'}
342+
opacity={headerFooterOpacity}
343+
openGridView={openGridView}
344+
visible={headerFooterVisible}
345+
ImageGalleryVideoControls={ImageGalleryVideoControls}
440346
/>
441-
</BottomSheet>
347+
) : null}
348+
349+
<BottomSheetModal
350+
height={350}
351+
onClose={() => {
352+
setIsGridViewVisible(false);
353+
}}
354+
visible={isGridViewVisible}
355+
>
356+
{ImageGalleryGrid ? (
357+
<ImageGalleryGrid
358+
closeGridView={closeGridView}
359+
numberOfImageGalleryGridColumns={numberOfImageGalleryGridColumns}
360+
/>
361+
) : null}
362+
</BottomSheetModal>
442363
</Animated.View>
443364
);
444365
};
@@ -447,19 +368,21 @@ export type ImageGalleryProps = Partial<ImageGalleryWithContextProps>;
447368

448369
export const ImageGallery = (props: ImageGalleryProps) => {
449370
const {
450-
imageGalleryCustomComponents,
451-
imageGalleryGridHandleHeight,
452-
imageGalleryGridSnapPoints,
453371
numberOfImageGalleryGridColumns,
372+
ImageGalleryHeader,
373+
ImageGalleryFooter,
374+
ImageGalleryVideoControls,
375+
ImageGalleryGrid,
454376
} = useImageGalleryContext();
455377
const { overlayOpacity } = useOverlayContext();
456378
return (
457379
<ImageGalleryWithContext
458-
imageGalleryCustomComponents={imageGalleryCustomComponents}
459-
imageGalleryGridHandleHeight={imageGalleryGridHandleHeight}
460-
imageGalleryGridSnapPoints={imageGalleryGridSnapPoints}
461380
numberOfImageGalleryGridColumns={numberOfImageGalleryGridColumns}
462381
overlayOpacity={overlayOpacity}
382+
ImageGalleryHeader={ImageGalleryHeader}
383+
ImageGalleryFooter={ImageGalleryFooter}
384+
ImageGalleryVideoControls={ImageGalleryVideoControls}
385+
ImageGalleryGrid={ImageGalleryGrid}
463386
{...props}
464387
/>
465388
);

package/src/components/ImageGallery/__tests__/ImageGallery.test.tsx

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ import duration from 'dayjs/plugin/duration';
88

99
import { LocalMessage } from 'stream-chat';
1010

11+
import { ImageGalleryFooter as ImageGalleryFooterDefault } from '../../../components/ImageGallery/components/ImageGalleryFooter';
12+
import { ImageGalleryHeader as ImageGalleryHeaderDefault } from '../../../components/ImageGallery/components/ImageGalleryHeader';
13+
import { ImageGalleryVideoControl as ImageGalleryVideoControlDefault } from '../../../components/ImageGallery/components/ImageGalleryVideoControl';
14+
import { ImageGalleryGrid as ImageGalleryGridDefault } from '../../../components/ImageGallery/components/ImageGrid';
1115
import {
1216
ImageGalleryContext,
1317
ImageGalleryContextValue,
@@ -58,7 +62,15 @@ const ImageGalleryComponent = (props: ImageGalleryProps & { message: LocalMessag
5862
return (
5963
<OverlayProvider value={{ overlayOpacity: { value: 1 } as SharedValue<number> }}>
6064
<ImageGalleryContext.Provider
61-
value={{ imageGalleryStateStore } as unknown as ImageGalleryContextValue}
65+
value={
66+
{
67+
imageGalleryStateStore,
68+
ImageGalleryHeader: ImageGalleryHeaderDefault,
69+
ImageGalleryFooter: ImageGalleryFooterDefault,
70+
ImageGalleryVideoControls: ImageGalleryVideoControlDefault,
71+
ImageGalleryGrid: ImageGalleryGridDefault,
72+
} as unknown as ImageGalleryContextValue
73+
}
6274
>
6375
<ImageGallery {...props} />
6476
</ImageGalleryContext.Provider>

0 commit comments

Comments
 (0)