@amityco/react-native-social-uikit 4.1.0 → 4.1.1-21d3e899.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/commonjs/core/assets/icons/xml.js +43 -1
- package/lib/commonjs/core/assets/icons/xml.js.map +1 -1
- package/lib/commonjs/social/components/LoadingImage/index.js +4 -3
- package/lib/commonjs/social/components/LoadingImage/index.js.map +1 -1
- package/lib/commonjs/social/components/LoadingImage/styles.js +4 -0
- package/lib/commonjs/social/components/LoadingImage/styles.js.map +1 -1
- package/lib/commonjs/social/components/LoadingVideo/index.js +12 -6
- package/lib/commonjs/social/components/LoadingVideo/index.js.map +1 -1
- package/lib/commonjs/social/components/LoadingVideo/styles.js +4 -0
- package/lib/commonjs/social/components/LoadingVideo/styles.js.map +1 -1
- package/lib/commonjs/social/components/MediaSelector/MediaSelector.js +164 -0
- package/lib/commonjs/social/components/MediaSelector/MediaSelector.js.map +1 -0
- package/lib/commonjs/social/components/MediaSelector/index.js +13 -0
- package/lib/commonjs/social/components/MediaSelector/index.js.map +1 -0
- package/lib/commonjs/social/components/MediaSelector/styles.js +130 -0
- package/lib/commonjs/social/components/MediaSelector/styles.js.map +1 -0
- package/lib/commonjs/social/components/MediaViewer/MediaViewer.js +134 -0
- package/lib/commonjs/social/components/MediaViewer/MediaViewer.js.map +1 -0
- package/lib/commonjs/social/components/MediaViewer/index.js +13 -0
- package/lib/commonjs/social/components/MediaViewer/index.js.map +1 -0
- package/lib/commonjs/social/components/MediaViewer/styles.js +60 -0
- package/lib/commonjs/social/components/MediaViewer/styles.js.map +1 -0
- package/lib/commonjs/social/components/PostContent/index.js +75 -120
- package/lib/commonjs/social/components/PostContent/index.js.map +1 -1
- package/lib/commonjs/social/components/SelectedMediaComponent/SelectedMediaComponent.js +147 -0
- package/lib/commonjs/social/components/SelectedMediaComponent/SelectedMediaComponent.js.map +1 -0
- package/lib/commonjs/social/components/SelectedMediaComponent/index.js +13 -0
- package/lib/commonjs/social/components/SelectedMediaComponent/index.js.map +1 -0
- package/lib/commonjs/social/components/SelectedMediaComponent/styles.js +43 -0
- package/lib/commonjs/social/components/SelectedMediaComponent/styles.js.map +1 -0
- package/lib/commonjs/social/constants/index.js +11 -0
- package/lib/commonjs/social/constants/index.js.map +1 -0
- package/lib/commonjs/social/elements/PostMediaElement/PostMediaElement.js +244 -0
- package/lib/commonjs/social/elements/PostMediaElement/PostMediaElement.js.map +1 -0
- package/lib/commonjs/social/elements/PostMediaElement/index.js +13 -0
- package/lib/commonjs/social/elements/PostMediaElement/index.js.map +1 -0
- package/lib/commonjs/social/elements/PostMediaElement/styles.js +117 -0
- package/lib/commonjs/social/elements/PostMediaElement/styles.js.map +1 -0
- package/lib/commonjs/social/features/post/Composer/PostComposer.js +57 -91
- package/lib/commonjs/social/features/post/Composer/PostComposer.js.map +1 -1
- package/lib/commonjs/social/features/post/components/DetailedMediaAttachment/DetailedMediaAttachment.js +17 -7
- package/lib/commonjs/social/features/post/components/DetailedMediaAttachment/DetailedMediaAttachment.js.map +1 -1
- package/lib/commonjs/social/features/post/components/MediaAttachment/MediaAttachment.js +17 -7
- package/lib/commonjs/social/features/post/components/MediaAttachment/MediaAttachment.js.map +1 -1
- package/lib/commonjs/social/utils/getFrameRatio.js +52 -0
- package/lib/commonjs/social/utils/getFrameRatio.js.map +1 -0
- package/lib/commonjs/social/utils/getIndicatorConfig.js +59 -0
- package/lib/commonjs/social/utils/getIndicatorConfig.js.map +1 -0
- package/lib/module/core/assets/icons/xml.js +38 -0
- package/lib/module/core/assets/icons/xml.js.map +1 -1
- package/lib/module/social/components/LoadingImage/index.js +4 -3
- package/lib/module/social/components/LoadingImage/index.js.map +1 -1
- package/lib/module/social/components/LoadingImage/styles.js +4 -0
- package/lib/module/social/components/LoadingImage/styles.js.map +1 -1
- package/lib/module/social/components/LoadingVideo/index.js +13 -7
- package/lib/module/social/components/LoadingVideo/index.js.map +1 -1
- package/lib/module/social/components/LoadingVideo/styles.js +4 -0
- package/lib/module/social/components/LoadingVideo/styles.js.map +1 -1
- package/lib/module/social/components/MediaSelector/MediaSelector.js +158 -0
- package/lib/module/social/components/MediaSelector/MediaSelector.js.map +1 -0
- package/lib/module/social/components/MediaSelector/index.js +2 -0
- package/lib/module/social/components/MediaSelector/index.js.map +1 -0
- package/lib/module/social/components/MediaSelector/styles.js +123 -0
- package/lib/module/social/components/MediaSelector/styles.js.map +1 -0
- package/lib/module/social/components/MediaViewer/MediaViewer.js +127 -0
- package/lib/module/social/components/MediaViewer/MediaViewer.js.map +1 -0
- package/lib/module/social/components/MediaViewer/index.js +2 -0
- package/lib/module/social/components/MediaViewer/index.js.map +1 -0
- package/lib/module/social/components/MediaViewer/styles.js +53 -0
- package/lib/module/social/components/MediaViewer/styles.js.map +1 -0
- package/lib/module/social/components/PostContent/index.js +77 -122
- package/lib/module/social/components/PostContent/index.js.map +1 -1
- package/lib/module/social/components/SelectedMediaComponent/SelectedMediaComponent.js +141 -0
- package/lib/module/social/components/SelectedMediaComponent/SelectedMediaComponent.js.map +1 -0
- package/lib/module/social/components/SelectedMediaComponent/index.js +2 -0
- package/lib/module/social/components/SelectedMediaComponent/index.js.map +1 -0
- package/lib/module/social/components/SelectedMediaComponent/styles.js +36 -0
- package/lib/module/social/components/SelectedMediaComponent/styles.js.map +1 -0
- package/lib/module/social/constants/index.js +5 -0
- package/lib/module/social/constants/index.js.map +1 -0
- package/lib/module/social/elements/PostMediaElement/PostMediaElement.js +237 -0
- package/lib/module/social/elements/PostMediaElement/PostMediaElement.js.map +1 -0
- package/lib/module/social/elements/PostMediaElement/index.js +2 -0
- package/lib/module/social/elements/PostMediaElement/index.js.map +1 -0
- package/lib/module/social/elements/PostMediaElement/styles.js +110 -0
- package/lib/module/social/elements/PostMediaElement/styles.js.map +1 -0
- package/lib/module/social/features/post/Composer/PostComposer.js +59 -93
- package/lib/module/social/features/post/Composer/PostComposer.js.map +1 -1
- package/lib/module/social/features/post/components/DetailedMediaAttachment/DetailedMediaAttachment.js +17 -7
- package/lib/module/social/features/post/components/DetailedMediaAttachment/DetailedMediaAttachment.js.map +1 -1
- package/lib/module/social/features/post/components/MediaAttachment/MediaAttachment.js +17 -7
- package/lib/module/social/features/post/components/MediaAttachment/MediaAttachment.js.map +1 -1
- package/lib/module/social/utils/getFrameRatio.js +44 -0
- package/lib/module/social/utils/getFrameRatio.js.map +1 -0
- package/lib/module/social/utils/getIndicatorConfig.js +53 -0
- package/lib/module/social/utils/getIndicatorConfig.js.map +1 -0
- package/lib/typescript/core/assets/icons/xml.d.ts +5 -0
- package/lib/typescript/core/assets/icons/xml.d.ts.map +1 -1
- package/lib/typescript/social/components/LoadingImage/index.d.ts +2 -1
- package/lib/typescript/social/components/LoadingImage/index.d.ts.map +1 -1
- package/lib/typescript/social/components/LoadingImage/styles.d.ts +4 -0
- package/lib/typescript/social/components/LoadingImage/styles.d.ts.map +1 -1
- package/lib/typescript/social/components/LoadingVideo/index.d.ts +2 -1
- package/lib/typescript/social/components/LoadingVideo/index.d.ts.map +1 -1
- package/lib/typescript/social/components/LoadingVideo/styles.d.ts +4 -0
- package/lib/typescript/social/components/LoadingVideo/styles.d.ts.map +1 -1
- package/lib/typescript/social/components/MediaSelector/MediaSelector.d.ts +14 -0
- package/lib/typescript/social/components/MediaSelector/MediaSelector.d.ts.map +1 -0
- package/lib/typescript/social/components/MediaSelector/index.d.ts +2 -0
- package/lib/typescript/social/components/MediaSelector/index.d.ts.map +1 -0
- package/lib/typescript/social/components/MediaSelector/styles.d.ts +116 -0
- package/lib/typescript/social/components/MediaSelector/styles.d.ts.map +1 -0
- package/lib/typescript/social/components/MediaViewer/MediaViewer.d.ts +17 -0
- package/lib/typescript/social/components/MediaViewer/MediaViewer.d.ts.map +1 -0
- package/lib/typescript/social/components/MediaViewer/index.d.ts +3 -0
- package/lib/typescript/social/components/MediaViewer/index.d.ts.map +1 -0
- package/lib/typescript/social/components/MediaViewer/styles.d.ts +48 -0
- package/lib/typescript/social/components/MediaViewer/styles.d.ts.map +1 -0
- package/lib/typescript/social/components/PostContent/index.d.ts.map +1 -1
- package/lib/typescript/social/components/SelectedMediaComponent/SelectedMediaComponent.d.ts +12 -0
- package/lib/typescript/social/components/SelectedMediaComponent/SelectedMediaComponent.d.ts.map +1 -0
- package/lib/typescript/social/components/SelectedMediaComponent/index.d.ts +3 -0
- package/lib/typescript/social/components/SelectedMediaComponent/index.d.ts.map +1 -0
- package/lib/typescript/social/components/SelectedMediaComponent/styles.d.ts +25 -0
- package/lib/typescript/social/components/SelectedMediaComponent/styles.d.ts.map +1 -0
- package/lib/typescript/social/constants/index.d.ts +4 -0
- package/lib/typescript/social/constants/index.d.ts.map +1 -0
- package/lib/typescript/social/elements/PostMediaElement/PostMediaElement.d.ts +15 -0
- package/lib/typescript/social/elements/PostMediaElement/PostMediaElement.d.ts.map +1 -0
- package/lib/typescript/social/elements/PostMediaElement/index.d.ts +3 -0
- package/lib/typescript/social/elements/PostMediaElement/index.d.ts.map +1 -0
- package/lib/typescript/social/elements/PostMediaElement/styles.d.ts +88 -0
- package/lib/typescript/social/elements/PostMediaElement/styles.d.ts.map +1 -0
- package/lib/typescript/social/features/post/Composer/PostComposer.d.ts.map +1 -1
- package/lib/typescript/social/features/post/components/DetailedMediaAttachment/DetailedMediaAttachment.d.ts +3 -0
- package/lib/typescript/social/features/post/components/DetailedMediaAttachment/DetailedMediaAttachment.d.ts.map +1 -1
- package/lib/typescript/social/features/post/components/MediaAttachment/MediaAttachment.d.ts +3 -0
- package/lib/typescript/social/features/post/components/MediaAttachment/MediaAttachment.d.ts.map +1 -1
- package/lib/typescript/social/utils/getFrameRatio.d.ts +21 -0
- package/lib/typescript/social/utils/getFrameRatio.d.ts.map +1 -0
- package/lib/typescript/social/utils/getIndicatorConfig.d.ts +11 -0
- package/lib/typescript/social/utils/getIndicatorConfig.d.ts.map +1 -0
- package/package.json +5 -3
- package/src/core/assets/icons/xml.ts +46 -0
- package/src/social/components/LoadingImage/index.tsx +12 -2
- package/src/social/components/LoadingImage/styles.ts +4 -0
- package/src/social/components/LoadingVideo/index.tsx +28 -4
- package/src/social/components/LoadingVideo/styles.ts +4 -0
- package/src/social/components/MediaSelector/MediaSelector.tsx +231 -0
- package/src/social/components/MediaSelector/index.tsx +1 -0
- package/src/social/components/MediaSelector/styles.ts +124 -0
- package/src/social/components/MediaViewer/MediaViewer.tsx +168 -0
- package/src/social/components/MediaViewer/index.tsx +2 -0
- package/src/social/components/MediaViewer/styles.ts +53 -0
- package/src/social/components/PostContent/index.tsx +86 -159
- package/src/social/components/SelectedMediaComponent/SelectedMediaComponent.tsx +202 -0
- package/src/social/components/SelectedMediaComponent/index.ts +2 -0
- package/src/social/components/SelectedMediaComponent/styles.ts +38 -0
- package/src/social/constants/index.ts +4 -0
- package/src/social/elements/PostMediaElement/PostMediaElement.tsx +318 -0
- package/src/social/elements/PostMediaElement/index.tsx +5 -0
- package/src/social/elements/PostMediaElement/styles.ts +99 -0
- package/src/social/features/post/Composer/PostComposer.tsx +70 -96
- package/src/social/features/post/components/DetailedMediaAttachment/DetailedMediaAttachment.tsx +10 -3
- package/src/social/features/post/components/MediaAttachment/MediaAttachment.tsx +14 -3
- package/src/social/utils/getFrameRatio.ts +42 -0
- package/src/social/utils/getIndicatorConfig.ts +62 -0
|
@@ -9,7 +9,12 @@ import {
|
|
|
9
9
|
import * as Progress from 'react-native-progress';
|
|
10
10
|
import { SvgXml } from 'react-native-svg';
|
|
11
11
|
import { deleteAmityFile, uploadVideoFile } from '../../../core/legacy/file';
|
|
12
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
closeIcon,
|
|
14
|
+
playBtn,
|
|
15
|
+
toastIcon,
|
|
16
|
+
videoControlIcon,
|
|
17
|
+
} from '../../../core/assets/icons/xml';
|
|
13
18
|
import { useStyles } from './styles';
|
|
14
19
|
import Video from 'react-native-video';
|
|
15
20
|
import { useNavigation } from '@react-navigation/native';
|
|
@@ -39,6 +44,7 @@ interface OverlayImageProps {
|
|
|
39
44
|
fileCount?: number;
|
|
40
45
|
postId?: string;
|
|
41
46
|
setIsUploading?: (arg: boolean) => void;
|
|
47
|
+
carousel?: boolean;
|
|
42
48
|
}
|
|
43
49
|
const LoadingVideo = ({
|
|
44
50
|
source,
|
|
@@ -54,6 +60,7 @@ const LoadingVideo = ({
|
|
|
54
60
|
fileCount,
|
|
55
61
|
postId,
|
|
56
62
|
setIsUploading,
|
|
63
|
+
carousel = false,
|
|
57
64
|
}: OverlayImageProps) => {
|
|
58
65
|
const dispatch = useUIKitDispatch();
|
|
59
66
|
const { showToastMessage } = uiSlice.actions;
|
|
@@ -157,8 +164,13 @@ const LoadingVideo = ({
|
|
|
157
164
|
|
|
158
165
|
const handleOnPlay = () => {
|
|
159
166
|
setIsPause(!isPause);
|
|
167
|
+
// In carousel mode the parent (onPlay) opens the full-screen media viewer,
|
|
168
|
+
// so skip the default single-video route navigation.
|
|
169
|
+
if (onPlay) {
|
|
170
|
+
onPlay(source);
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
160
173
|
playVideoFullScreen(source);
|
|
161
|
-
onPlay && onPlay(source);
|
|
162
174
|
};
|
|
163
175
|
|
|
164
176
|
const onRetryUpload = () => {
|
|
@@ -166,10 +178,22 @@ const LoadingVideo = ({
|
|
|
166
178
|
};
|
|
167
179
|
|
|
168
180
|
return (
|
|
169
|
-
<View
|
|
181
|
+
<View
|
|
182
|
+
style={
|
|
183
|
+
carousel
|
|
184
|
+
? styles.carouselContainer
|
|
185
|
+
: fileCount >= 3
|
|
186
|
+
? styles.image3XContainer
|
|
187
|
+
: styles.container
|
|
188
|
+
}
|
|
189
|
+
>
|
|
170
190
|
{!loading && !isUploadError && isPause && (
|
|
171
191
|
<TouchableOpacity style={styles.playButton} onPress={handleOnPlay}>
|
|
172
|
-
<SvgXml
|
|
192
|
+
<SvgXml
|
|
193
|
+
xml={carousel ? videoControlIcon : playBtn}
|
|
194
|
+
width={carousel ? 40 : 50}
|
|
195
|
+
height={carousel ? 40 : 50}
|
|
196
|
+
/>
|
|
173
197
|
</TouchableOpacity>
|
|
174
198
|
)}
|
|
175
199
|
{playingUri && !isPause ? (
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import { useCallback, useEffect, useState } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
FlatList,
|
|
4
|
+
Image,
|
|
5
|
+
Modal,
|
|
6
|
+
PermissionsAndroid,
|
|
7
|
+
Platform,
|
|
8
|
+
Text,
|
|
9
|
+
TouchableOpacity,
|
|
10
|
+
TouchableWithoutFeedback,
|
|
11
|
+
View,
|
|
12
|
+
} from 'react-native';
|
|
13
|
+
import {
|
|
14
|
+
CameraRoll,
|
|
15
|
+
type PhotoIdentifier,
|
|
16
|
+
} from '@react-native-camera-roll/camera-roll';
|
|
17
|
+
import { useStyles } from './styles';
|
|
18
|
+
|
|
19
|
+
type MediaSelectorProps = {
|
|
20
|
+
visible: boolean;
|
|
21
|
+
mediaType: 'photo' | 'video';
|
|
22
|
+
/** URIs already added to the composer — used for silent duplicate detection. */
|
|
23
|
+
existing: string[];
|
|
24
|
+
/** How many more items may be added (10 - current count). */
|
|
25
|
+
remaining: number;
|
|
26
|
+
onClose: () => void;
|
|
27
|
+
/** New, de-duplicated URIs in the order they were selected. */
|
|
28
|
+
onConfirm: (uris: string[]) => void;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const PAGE_SIZE = 90;
|
|
32
|
+
|
|
33
|
+
async function ensureLibraryPermission(
|
|
34
|
+
mediaType: 'photo' | 'video'
|
|
35
|
+
): Promise<boolean> {
|
|
36
|
+
if (Platform.OS !== 'android') return true;
|
|
37
|
+
const version =
|
|
38
|
+
typeof Platform.Version === 'number'
|
|
39
|
+
? Platform.Version
|
|
40
|
+
: parseInt(Platform.Version, 10);
|
|
41
|
+
const perms =
|
|
42
|
+
version >= 33
|
|
43
|
+
? [
|
|
44
|
+
mediaType === 'photo'
|
|
45
|
+
? PermissionsAndroid.PERMISSIONS.READ_MEDIA_IMAGES
|
|
46
|
+
: PermissionsAndroid.PERMISSIONS.READ_MEDIA_VIDEO,
|
|
47
|
+
]
|
|
48
|
+
: [PermissionsAndroid.PERMISSIONS.READ_EXTERNAL_STORAGE];
|
|
49
|
+
try {
|
|
50
|
+
const result = await PermissionsAndroid.requestMultiple(perms);
|
|
51
|
+
return Object.values(result).every(
|
|
52
|
+
(status) => status === PermissionsAndroid.RESULTS.GRANTED
|
|
53
|
+
);
|
|
54
|
+
} catch {
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function formatDuration(seconds?: number): string {
|
|
60
|
+
if (!seconds || seconds <= 0) return '';
|
|
61
|
+
const total = Math.round(seconds);
|
|
62
|
+
const mins = Math.floor(total / 60);
|
|
63
|
+
const secs = total % 60;
|
|
64
|
+
return `${mins}:${secs.toString().padStart(2, '0')}`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function MediaSelector({
|
|
68
|
+
visible,
|
|
69
|
+
mediaType,
|
|
70
|
+
existing,
|
|
71
|
+
remaining,
|
|
72
|
+
onClose,
|
|
73
|
+
onConfirm,
|
|
74
|
+
}: MediaSelectorProps) {
|
|
75
|
+
const { styles } = useStyles();
|
|
76
|
+
const [items, setItems] = useState<PhotoIdentifier[]>([]);
|
|
77
|
+
const [selected, setSelected] = useState<string[]>([]);
|
|
78
|
+
const [cursor, setCursor] = useState<string | undefined>(undefined);
|
|
79
|
+
const [hasNextPage, setHasNextPage] = useState(false);
|
|
80
|
+
const [loading, setLoading] = useState(false);
|
|
81
|
+
|
|
82
|
+
const loadPage = useCallback(
|
|
83
|
+
async (after?: string) => {
|
|
84
|
+
if (loading) return;
|
|
85
|
+
setLoading(true);
|
|
86
|
+
try {
|
|
87
|
+
const res = await CameraRoll.getPhotos({
|
|
88
|
+
first: PAGE_SIZE,
|
|
89
|
+
after,
|
|
90
|
+
assetType: mediaType === 'photo' ? 'Photos' : 'Videos',
|
|
91
|
+
include: ['filename', 'playableDuration'],
|
|
92
|
+
});
|
|
93
|
+
setItems((prev) => (after ? [...prev, ...res.edges] : res.edges));
|
|
94
|
+
setCursor(res.page_info.end_cursor);
|
|
95
|
+
setHasNextPage(res.page_info.has_next_page);
|
|
96
|
+
} catch {
|
|
97
|
+
// permission denied / empty library — leave the list empty
|
|
98
|
+
} finally {
|
|
99
|
+
setLoading(false);
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
[loading, mediaType]
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
// Reset and (re)load whenever the selector opens. A fresh open starts with an
|
|
106
|
+
// empty selection; previously-added items are handled by duplicate detection.
|
|
107
|
+
useEffect(() => {
|
|
108
|
+
if (!visible) return;
|
|
109
|
+
setSelected([]);
|
|
110
|
+
setItems([]);
|
|
111
|
+
setCursor(undefined);
|
|
112
|
+
setHasNextPage(false);
|
|
113
|
+
(async () => {
|
|
114
|
+
const granted = await ensureLibraryPermission(mediaType);
|
|
115
|
+
if (granted) loadPage();
|
|
116
|
+
})();
|
|
117
|
+
}, [visible, mediaType]);
|
|
118
|
+
|
|
119
|
+
const toggle = useCallback(
|
|
120
|
+
(uri: string) => {
|
|
121
|
+
setSelected((prev) => {
|
|
122
|
+
const index = prev.indexOf(uri);
|
|
123
|
+
if (index !== -1) {
|
|
124
|
+
// Deselect: removing from the array renumbers the rest automatically.
|
|
125
|
+
return prev.filter((u) => u !== uri);
|
|
126
|
+
}
|
|
127
|
+
if (prev.length >= remaining) return prev; // respect the cap silently
|
|
128
|
+
return [...prev, uri];
|
|
129
|
+
});
|
|
130
|
+
},
|
|
131
|
+
[remaining]
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
const handleConfirm = useCallback(() => {
|
|
135
|
+
const deduped = selected.filter((uri) => !existing.includes(uri));
|
|
136
|
+
onConfirm(deduped);
|
|
137
|
+
}, [selected, existing, onConfirm]);
|
|
138
|
+
|
|
139
|
+
const onEndReached = useCallback(() => {
|
|
140
|
+
if (hasNextPage && !loading) loadPage(cursor);
|
|
141
|
+
}, [hasNextPage, loading, cursor, loadPage]);
|
|
142
|
+
|
|
143
|
+
const renderItem = useCallback(
|
|
144
|
+
({ item }: { item: PhotoIdentifier }) => {
|
|
145
|
+
const uri = item.node.image.uri;
|
|
146
|
+
const order = selected.indexOf(uri);
|
|
147
|
+
const isSelected = order !== -1;
|
|
148
|
+
const duration =
|
|
149
|
+
mediaType === 'video'
|
|
150
|
+
? formatDuration(item.node.image.playableDuration)
|
|
151
|
+
: '';
|
|
152
|
+
return (
|
|
153
|
+
<TouchableWithoutFeedback onPress={() => toggle(uri)}>
|
|
154
|
+
<View style={styles.cell}>
|
|
155
|
+
<Image style={styles.cell__image} source={{ uri }} />
|
|
156
|
+
{!!duration && (
|
|
157
|
+
<Text style={styles.cell__videoDuration}>{duration}</Text>
|
|
158
|
+
)}
|
|
159
|
+
{isSelected ? (
|
|
160
|
+
<>
|
|
161
|
+
<View style={styles.cell__selectedOverlay} />
|
|
162
|
+
<View style={styles.cell__badge}>
|
|
163
|
+
<Text style={styles.cell__badgeLabel}>{order + 1}</Text>
|
|
164
|
+
</View>
|
|
165
|
+
</>
|
|
166
|
+
) : (
|
|
167
|
+
<View style={styles.cell__badgeEmpty} />
|
|
168
|
+
)}
|
|
169
|
+
</View>
|
|
170
|
+
</TouchableWithoutFeedback>
|
|
171
|
+
);
|
|
172
|
+
},
|
|
173
|
+
[selected, mediaType, toggle, styles]
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
return (
|
|
177
|
+
<Modal
|
|
178
|
+
visible={visible}
|
|
179
|
+
animationType="slide"
|
|
180
|
+
onRequestClose={onClose}
|
|
181
|
+
presentationStyle="fullScreen"
|
|
182
|
+
>
|
|
183
|
+
<View style={styles.container}>
|
|
184
|
+
<View style={styles.header}>
|
|
185
|
+
<TouchableOpacity style={styles.headerButton} onPress={onClose}>
|
|
186
|
+
<Text style={styles.headerClose}>Cancel</Text>
|
|
187
|
+
</TouchableOpacity>
|
|
188
|
+
<Text style={styles.headerTitle}>
|
|
189
|
+
{mediaType === 'photo' ? 'Photos' : 'Videos'}
|
|
190
|
+
</Text>
|
|
191
|
+
<TouchableOpacity
|
|
192
|
+
style={styles.headerButton__right}
|
|
193
|
+
disabled={selected.length === 0}
|
|
194
|
+
onPress={handleConfirm}
|
|
195
|
+
>
|
|
196
|
+
<Text
|
|
197
|
+
style={
|
|
198
|
+
selected.length === 0
|
|
199
|
+
? styles.headerAdd__disabled
|
|
200
|
+
: styles.headerAdd
|
|
201
|
+
}
|
|
202
|
+
>
|
|
203
|
+
{selected.length > 0 ? `Add (${selected.length})` : 'Add'}
|
|
204
|
+
</Text>
|
|
205
|
+
</TouchableOpacity>
|
|
206
|
+
</View>
|
|
207
|
+
<FlatList
|
|
208
|
+
data={items}
|
|
209
|
+
numColumns={3}
|
|
210
|
+
keyExtractor={(item) => item.node.image.uri}
|
|
211
|
+
renderItem={renderItem}
|
|
212
|
+
contentContainerStyle={styles.grid}
|
|
213
|
+
onEndReached={onEndReached}
|
|
214
|
+
onEndReachedThreshold={0.6}
|
|
215
|
+
initialNumToRender={30}
|
|
216
|
+
windowSize={5}
|
|
217
|
+
ListEmptyComponent={
|
|
218
|
+
!loading ? (
|
|
219
|
+
<View style={styles.empty}>
|
|
220
|
+
<Text style={styles.empty__text}>
|
|
221
|
+
No {mediaType === 'photo' ? 'photos' : 'videos'} found on this
|
|
222
|
+
device.
|
|
223
|
+
</Text>
|
|
224
|
+
</View>
|
|
225
|
+
) : null
|
|
226
|
+
}
|
|
227
|
+
/>
|
|
228
|
+
</View>
|
|
229
|
+
</Modal>
|
|
230
|
+
);
|
|
231
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { MediaSelector } from './MediaSelector';
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { StyleSheet, useWindowDimensions } from 'react-native';
|
|
2
|
+
import { useTheme } from 'react-native-paper';
|
|
3
|
+
import type { MyMD3Theme } from '../../../core/providers/AmityUIKitProvider';
|
|
4
|
+
|
|
5
|
+
const NUM_COLUMNS = 3;
|
|
6
|
+
const CELL_GAP = 2;
|
|
7
|
+
|
|
8
|
+
export const useStyles = () => {
|
|
9
|
+
const theme = useTheme<MyMD3Theme>();
|
|
10
|
+
const { width } = useWindowDimensions();
|
|
11
|
+
const cellSize = Math.floor(
|
|
12
|
+
(width - CELL_GAP * (NUM_COLUMNS - 1)) / NUM_COLUMNS
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
const styles = StyleSheet.create({
|
|
16
|
+
container: {
|
|
17
|
+
flex: 1,
|
|
18
|
+
backgroundColor: theme.colors.background,
|
|
19
|
+
},
|
|
20
|
+
header: {
|
|
21
|
+
flexDirection: 'row',
|
|
22
|
+
alignItems: 'center',
|
|
23
|
+
justifyContent: 'space-between',
|
|
24
|
+
paddingHorizontal: 16,
|
|
25
|
+
height: 56,
|
|
26
|
+
},
|
|
27
|
+
headerButton: {
|
|
28
|
+
minWidth: 64,
|
|
29
|
+
},
|
|
30
|
+
headerButton__right: {
|
|
31
|
+
minWidth: 64,
|
|
32
|
+
alignItems: 'flex-end',
|
|
33
|
+
},
|
|
34
|
+
headerTitle: {
|
|
35
|
+
color: theme.colors.base,
|
|
36
|
+
fontSize: 16,
|
|
37
|
+
fontWeight: '600',
|
|
38
|
+
},
|
|
39
|
+
headerClose: {
|
|
40
|
+
color: theme.colors.base,
|
|
41
|
+
fontSize: 16,
|
|
42
|
+
},
|
|
43
|
+
headerAdd: {
|
|
44
|
+
color: theme.colors.primary,
|
|
45
|
+
fontSize: 16,
|
|
46
|
+
fontWeight: '600',
|
|
47
|
+
},
|
|
48
|
+
headerAdd__disabled: {
|
|
49
|
+
color: theme.colors.baseShade3,
|
|
50
|
+
fontSize: 16,
|
|
51
|
+
fontWeight: '600',
|
|
52
|
+
},
|
|
53
|
+
grid: {
|
|
54
|
+
paddingBottom: 24,
|
|
55
|
+
},
|
|
56
|
+
cell: {
|
|
57
|
+
width: cellSize,
|
|
58
|
+
height: cellSize,
|
|
59
|
+
marginRight: CELL_GAP,
|
|
60
|
+
marginBottom: CELL_GAP,
|
|
61
|
+
backgroundColor: theme.colors.baseShade4,
|
|
62
|
+
},
|
|
63
|
+
cell__image: {
|
|
64
|
+
width: '100%',
|
|
65
|
+
height: '100%',
|
|
66
|
+
},
|
|
67
|
+
cell__selectedOverlay: {
|
|
68
|
+
...StyleSheet.absoluteFillObject,
|
|
69
|
+
borderWidth: 2,
|
|
70
|
+
borderColor: theme.colors.primary,
|
|
71
|
+
},
|
|
72
|
+
cell__badge: {
|
|
73
|
+
position: 'absolute',
|
|
74
|
+
top: 6,
|
|
75
|
+
right: 6,
|
|
76
|
+
minWidth: 22,
|
|
77
|
+
height: 22,
|
|
78
|
+
borderRadius: 11,
|
|
79
|
+
paddingHorizontal: 5,
|
|
80
|
+
alignItems: 'center',
|
|
81
|
+
justifyContent: 'center',
|
|
82
|
+
backgroundColor: theme.colors.primary,
|
|
83
|
+
},
|
|
84
|
+
cell__badgeEmpty: {
|
|
85
|
+
position: 'absolute',
|
|
86
|
+
top: 6,
|
|
87
|
+
right: 6,
|
|
88
|
+
width: 22,
|
|
89
|
+
height: 22,
|
|
90
|
+
borderRadius: 11,
|
|
91
|
+
borderWidth: 1.5,
|
|
92
|
+
borderColor: '#FFFFFF',
|
|
93
|
+
backgroundColor: 'rgba(0,0,0,0.2)',
|
|
94
|
+
},
|
|
95
|
+
cell__badgeLabel: {
|
|
96
|
+
color: '#FFFFFF',
|
|
97
|
+
fontSize: 12,
|
|
98
|
+
fontWeight: '700',
|
|
99
|
+
},
|
|
100
|
+
cell__videoDuration: {
|
|
101
|
+
position: 'absolute',
|
|
102
|
+
bottom: 6,
|
|
103
|
+
right: 6,
|
|
104
|
+
color: '#FFFFFF',
|
|
105
|
+
fontSize: 11,
|
|
106
|
+
fontWeight: '600',
|
|
107
|
+
textShadowColor: 'rgba(0,0,0,0.6)',
|
|
108
|
+
textShadowRadius: 2,
|
|
109
|
+
},
|
|
110
|
+
empty: {
|
|
111
|
+
flex: 1,
|
|
112
|
+
alignItems: 'center',
|
|
113
|
+
justifyContent: 'center',
|
|
114
|
+
padding: 32,
|
|
115
|
+
},
|
|
116
|
+
empty__text: {
|
|
117
|
+
color: theme.colors.baseShade1,
|
|
118
|
+
fontSize: 14,
|
|
119
|
+
textAlign: 'center',
|
|
120
|
+
},
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
return { styles, theme };
|
|
124
|
+
};
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
FlatList,
|
|
4
|
+
Image,
|
|
5
|
+
ImageStyle,
|
|
6
|
+
Modal,
|
|
7
|
+
NativeScrollEvent,
|
|
8
|
+
NativeSyntheticEvent,
|
|
9
|
+
StyleProp,
|
|
10
|
+
Text,
|
|
11
|
+
TouchableOpacity,
|
|
12
|
+
useWindowDimensions,
|
|
13
|
+
View,
|
|
14
|
+
} from 'react-native';
|
|
15
|
+
import Video from 'react-native-video';
|
|
16
|
+
import { SvgXml } from 'react-native-svg';
|
|
17
|
+
import {
|
|
18
|
+
clearIcon,
|
|
19
|
+
soundOnIcon,
|
|
20
|
+
soundOffIcon,
|
|
21
|
+
} from '../../../core/assets/icons/xml';
|
|
22
|
+
import { useStyles } from './styles';
|
|
23
|
+
|
|
24
|
+
export type MediaViewerItem = {
|
|
25
|
+
type: 'image' | 'video';
|
|
26
|
+
uri: string;
|
|
27
|
+
/** e.g. 'm3u8' for HLS (published) videos; omit for local files. */
|
|
28
|
+
videoType?: string;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
type MediaViewerProps = {
|
|
32
|
+
visible: boolean;
|
|
33
|
+
items: MediaViewerItem[];
|
|
34
|
+
initialIndex: number;
|
|
35
|
+
/** Called with the last-viewed index so the caller can decide where to
|
|
36
|
+
* return (published: last-viewed; composer: originally-tapped). */
|
|
37
|
+
onClose: (lastIndex: number) => void;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export function MediaViewer({
|
|
41
|
+
visible,
|
|
42
|
+
items,
|
|
43
|
+
initialIndex,
|
|
44
|
+
onClose,
|
|
45
|
+
}: MediaViewerProps) {
|
|
46
|
+
const { styles } = useStyles();
|
|
47
|
+
const { width } = useWindowDimensions();
|
|
48
|
+
const listRef = useRef<FlatList<MediaViewerItem>>(null);
|
|
49
|
+
const [current, setCurrent] = useState(initialIndex);
|
|
50
|
+
// Mute-carry (PDT-4309 / PDT-4312): the first video plays unmuted; each swipe
|
|
51
|
+
// defaults the next video to muted — until the user unmutes, after which
|
|
52
|
+
// subsequent videos stay unmuted.
|
|
53
|
+
const [muted, setMuted] = useState(false);
|
|
54
|
+
const keepUnmutedRef = useRef(false);
|
|
55
|
+
|
|
56
|
+
useEffect(() => {
|
|
57
|
+
if (!visible) return;
|
|
58
|
+
setCurrent(initialIndex);
|
|
59
|
+
setMuted(false);
|
|
60
|
+
keepUnmutedRef.current = false;
|
|
61
|
+
}, [visible, initialIndex]);
|
|
62
|
+
|
|
63
|
+
const onMomentumScrollEnd = useCallback(
|
|
64
|
+
(e: NativeSyntheticEvent<NativeScrollEvent>) => {
|
|
65
|
+
if (!width) return;
|
|
66
|
+
const idx = Math.round(e.nativeEvent.contentOffset.x / width);
|
|
67
|
+
if (idx !== current) {
|
|
68
|
+
setCurrent(idx);
|
|
69
|
+
setMuted(!keepUnmutedRef.current);
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
[width, current]
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
const toggleMute = useCallback(() => {
|
|
76
|
+
setMuted((prev) => {
|
|
77
|
+
const next = !prev;
|
|
78
|
+
keepUnmutedRef.current = !next; // user unmuted -> keep unmuted afterwards
|
|
79
|
+
return next;
|
|
80
|
+
});
|
|
81
|
+
}, []);
|
|
82
|
+
|
|
83
|
+
const renderItem = useCallback(
|
|
84
|
+
({ item, index }: { item: MediaViewerItem; index: number }) => (
|
|
85
|
+
<View style={[styles.page, { width }]}>
|
|
86
|
+
{item.type === 'video' ? (
|
|
87
|
+
// pointerEvents none so horizontal swipes reach the pager instead of
|
|
88
|
+
// being captured by the native video view.
|
|
89
|
+
<View style={styles.media} pointerEvents="none">
|
|
90
|
+
<Video
|
|
91
|
+
source={
|
|
92
|
+
item.videoType
|
|
93
|
+
? { uri: item.uri, type: item.videoType }
|
|
94
|
+
: { uri: item.uri }
|
|
95
|
+
}
|
|
96
|
+
style={styles.media}
|
|
97
|
+
resizeMode="contain"
|
|
98
|
+
paused={index !== current || !visible}
|
|
99
|
+
muted={muted && index === current}
|
|
100
|
+
repeat
|
|
101
|
+
playInBackground={false}
|
|
102
|
+
playWhenInactive={false}
|
|
103
|
+
/>
|
|
104
|
+
</View>
|
|
105
|
+
) : (
|
|
106
|
+
<Image
|
|
107
|
+
source={{ uri: item.uri }}
|
|
108
|
+
style={styles.media as StyleProp<ImageStyle>}
|
|
109
|
+
resizeMode="contain"
|
|
110
|
+
/>
|
|
111
|
+
)}
|
|
112
|
+
</View>
|
|
113
|
+
),
|
|
114
|
+
[styles, width, current, muted, visible]
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
const currentIsVideo = items[current]?.type === 'video';
|
|
118
|
+
|
|
119
|
+
return (
|
|
120
|
+
<Modal
|
|
121
|
+
visible={visible}
|
|
122
|
+
transparent
|
|
123
|
+
animationType="fade"
|
|
124
|
+
onRequestClose={() => onClose(current)}
|
|
125
|
+
>
|
|
126
|
+
<View style={styles.container}>
|
|
127
|
+
<View style={styles.header}>
|
|
128
|
+
<TouchableOpacity
|
|
129
|
+
style={styles.headerBtn}
|
|
130
|
+
onPress={() => onClose(current)}
|
|
131
|
+
>
|
|
132
|
+
<SvgXml xml={clearIcon('white')} width={28} height={28} />
|
|
133
|
+
</TouchableOpacity>
|
|
134
|
+
<Text style={styles.counter}>
|
|
135
|
+
{current + 1}/{items.length}
|
|
136
|
+
</Text>
|
|
137
|
+
<View style={styles.headerBtn__right}>
|
|
138
|
+
{currentIsVideo && (
|
|
139
|
+
<TouchableOpacity onPress={toggleMute}>
|
|
140
|
+
<SvgXml
|
|
141
|
+
xml={muted ? soundOffIcon('#FFFFFF') : soundOnIcon('#FFFFFF')}
|
|
142
|
+
width={24}
|
|
143
|
+
height={24}
|
|
144
|
+
/>
|
|
145
|
+
</TouchableOpacity>
|
|
146
|
+
)}
|
|
147
|
+
</View>
|
|
148
|
+
</View>
|
|
149
|
+
<FlatList
|
|
150
|
+
ref={listRef}
|
|
151
|
+
data={items}
|
|
152
|
+
horizontal
|
|
153
|
+
pagingEnabled
|
|
154
|
+
showsHorizontalScrollIndicator={false}
|
|
155
|
+
keyExtractor={(it, i) => `${it.uri}-${i}`}
|
|
156
|
+
renderItem={renderItem}
|
|
157
|
+
getItemLayout={(_, index) => ({
|
|
158
|
+
length: width,
|
|
159
|
+
offset: width * index,
|
|
160
|
+
index,
|
|
161
|
+
})}
|
|
162
|
+
initialScrollIndex={initialIndex}
|
|
163
|
+
onMomentumScrollEnd={onMomentumScrollEnd}
|
|
164
|
+
/>
|
|
165
|
+
</View>
|
|
166
|
+
</Modal>
|
|
167
|
+
);
|
|
168
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { StyleSheet } from 'react-native';
|
|
2
|
+
import { useTheme } from 'react-native-paper';
|
|
3
|
+
import type { MyMD3Theme } from '../../../core/providers/AmityUIKitProvider';
|
|
4
|
+
|
|
5
|
+
export const useStyles = () => {
|
|
6
|
+
const theme = useTheme<MyMD3Theme>();
|
|
7
|
+
|
|
8
|
+
const styles = StyleSheet.create({
|
|
9
|
+
container: {
|
|
10
|
+
flex: 1,
|
|
11
|
+
backgroundColor: '#000000',
|
|
12
|
+
},
|
|
13
|
+
header: {
|
|
14
|
+
flexDirection: 'row',
|
|
15
|
+
alignItems: 'center',
|
|
16
|
+
justifyContent: 'space-between',
|
|
17
|
+
paddingHorizontal: 16,
|
|
18
|
+
height: 56,
|
|
19
|
+
zIndex: 2,
|
|
20
|
+
},
|
|
21
|
+
headerBtn: {
|
|
22
|
+
minWidth: 44,
|
|
23
|
+
height: 44,
|
|
24
|
+
alignItems: 'flex-start',
|
|
25
|
+
justifyContent: 'center',
|
|
26
|
+
},
|
|
27
|
+
headerBtn__right: {
|
|
28
|
+
minWidth: 44,
|
|
29
|
+
height: 44,
|
|
30
|
+
alignItems: 'flex-end',
|
|
31
|
+
justifyContent: 'center',
|
|
32
|
+
},
|
|
33
|
+
counter: {
|
|
34
|
+
color: '#FFFFFF',
|
|
35
|
+
fontSize: 14,
|
|
36
|
+
fontWeight: '600',
|
|
37
|
+
},
|
|
38
|
+
muteGlyph: {
|
|
39
|
+
fontSize: 20,
|
|
40
|
+
},
|
|
41
|
+
page: {
|
|
42
|
+
flex: 1,
|
|
43
|
+
alignItems: 'center',
|
|
44
|
+
justifyContent: 'center',
|
|
45
|
+
},
|
|
46
|
+
media: {
|
|
47
|
+
width: '100%',
|
|
48
|
+
height: '100%',
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
return { styles, theme };
|
|
53
|
+
};
|