@amityco/react-native-social-uikit 4.1.1-dfb439b1.0 → 4.1.1-e85011d9.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 +1 -15
- package/lib/commonjs/core/assets/icons/xml.js.map +1 -1
- package/lib/commonjs/core/hooks/usePostByIds.js +38 -0
- package/lib/commonjs/core/hooks/usePostByIds.js.map +1 -0
- package/lib/commonjs/social/components/MediaViewer/MediaViewer.js +3 -24
- package/lib/commonjs/social/components/MediaViewer/MediaViewer.js.map +1 -1
- package/lib/commonjs/social/features/post/Composer/PostComposer.js +32 -39
- package/lib/commonjs/social/features/post/Composer/PostComposer.js.map +1 -1
- package/lib/module/core/assets/icons/xml.js +0 -12
- package/lib/module/core/assets/icons/xml.js.map +1 -1
- package/lib/module/core/hooks/usePostByIds.js +32 -0
- package/lib/module/core/hooks/usePostByIds.js.map +1 -0
- package/lib/module/social/components/MediaViewer/MediaViewer.js +4 -25
- package/lib/module/social/components/MediaViewer/MediaViewer.js.map +1 -1
- package/lib/module/social/features/post/Composer/PostComposer.js +34 -41
- package/lib/module/social/features/post/Composer/PostComposer.js.map +1 -1
- package/lib/typescript/core/assets/icons/xml.d.ts +0 -2
- package/lib/typescript/core/assets/icons/xml.d.ts.map +1 -1
- package/lib/typescript/core/hooks/usePostByIds.d.ts +8 -0
- package/lib/typescript/core/hooks/usePostByIds.d.ts.map +1 -0
- package/lib/typescript/social/components/MediaViewer/MediaViewer.d.ts.map +1 -1
- package/lib/typescript/social/features/post/Composer/PostComposer.d.ts.map +1 -1
- package/package.json +1 -3
- package/src/core/assets/icons/xml.ts +0 -16
- package/src/core/hooks/usePostByIds.ts +33 -0
- package/src/social/components/MediaViewer/MediaViewer.tsx +1 -30
- package/src/social/features/post/Composer/PostComposer.tsx +48 -65
- package/lib/commonjs/social/components/MediaSelector/MediaSelector.js +0 -164
- package/lib/commonjs/social/components/MediaSelector/MediaSelector.js.map +0 -1
- package/lib/commonjs/social/components/MediaSelector/index.js +0 -13
- package/lib/commonjs/social/components/MediaSelector/index.js.map +0 -1
- package/lib/commonjs/social/components/MediaSelector/styles.js +0 -130
- package/lib/commonjs/social/components/MediaSelector/styles.js.map +0 -1
- package/lib/module/social/components/MediaSelector/MediaSelector.js +0 -158
- package/lib/module/social/components/MediaSelector/MediaSelector.js.map +0 -1
- package/lib/module/social/components/MediaSelector/index.js +0 -2
- package/lib/module/social/components/MediaSelector/index.js.map +0 -1
- package/lib/module/social/components/MediaSelector/styles.js +0 -123
- package/lib/module/social/components/MediaSelector/styles.js.map +0 -1
- package/lib/typescript/social/components/MediaSelector/MediaSelector.d.ts +0 -14
- package/lib/typescript/social/components/MediaSelector/MediaSelector.d.ts.map +0 -1
- package/lib/typescript/social/components/MediaSelector/index.d.ts +0 -2
- package/lib/typescript/social/components/MediaSelector/index.d.ts.map +0 -1
- package/lib/typescript/social/components/MediaSelector/styles.d.ts +0 -116
- package/lib/typescript/social/components/MediaSelector/styles.d.ts.map +0 -1
- package/src/social/components/MediaSelector/MediaSelector.tsx +0 -231
- package/src/social/components/MediaSelector/index.tsx +0 -1
- package/src/social/components/MediaSelector/styles.ts +0 -124
|
@@ -1,231 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export { MediaSelector } from './MediaSelector';
|
|
@@ -1,124 +0,0 @@
|
|
|
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
|
-
};
|