stream-chat-react-native 9.0.0-beta.8 → 9.0.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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "stream-chat-react-native",
3
3
  "description": "The official React Native SDK for Stream Chat, a service for building chat applications",
4
- "version": "9.0.0-beta.8",
4
+ "version": "9.0.0",
5
5
  "homepage": "https://www.npmjs.com/package/stream-chat-react-native",
6
6
  "author": {
7
7
  "company": "Stream.io Inc",
@@ -21,6 +21,7 @@
21
21
  "android/gradle",
22
22
  "ios",
23
23
  "*.podspec",
24
+ "react-native.config.js",
24
25
  "package.json"
25
26
  ],
26
27
  "license": "SEE LICENSE IN LICENSE",
@@ -29,21 +30,20 @@
29
30
  "dependencies": {
30
31
  "es6-symbol": "^3.1.3",
31
32
  "mime": "^4.0.7",
32
- "stream-chat-react-native-core": "9.0.0-beta.8"
33
+ "stream-chat-react-native-core": "9.0.0"
33
34
  },
34
35
  "peerDependencies": {
35
- "@react-native-camera-roll/camera-roll": ">=7.8.0",
36
- "@react-native-clipboard/clipboard": ">=1.14.1",
36
+ "@react-native-camera-roll/camera-roll": ">=7.9.0",
37
+ "@react-native-clipboard/clipboard": ">=1.16.0",
37
38
  "@react-native-documents/picker": ">=10.1.1",
38
- "@stream-io/flat-list-mvcp": ">=0.10.3",
39
- "react-native": ">=0.73.0",
39
+ "react-native": ">=0.76.0",
40
40
  "react-native-audio-recorder-player": ">=3.6.13",
41
41
  "react-native-nitro-sound": ">=0.2.9",
42
- "react-native-blob-util": ">=0.21.1",
42
+ "react-native-blob-util": ">=0.22.0",
43
43
  "react-native-haptic-feedback": ">=2.3.0",
44
44
  "react-native-image-picker": ">=7.1.2",
45
45
  "react-native-share": ">=11.0.0",
46
- "react-native-video": ">=6.4.2"
46
+ "react-native-video": ">=6.18.0"
47
47
  },
48
48
  "peerDependenciesMeta": {
49
49
  "@react-native-camera-roll/camera-roll": {
@@ -52,9 +52,6 @@
52
52
  "@react-native-clipboard/clipboard": {
53
53
  "optional": true
54
54
  },
55
- "@stream-io/flat-list-mvcp": {
56
- "optional": true
57
- },
58
55
  "react-native-share": {
59
56
  "optional": true
60
57
  },
@@ -81,7 +78,6 @@
81
78
  }
82
79
  },
83
80
  "scripts": {
84
- "postinstall": "if [ -f ../scripts/sync-shared-native.sh ] && [ -d ../shared-native/ios ]; then bash ../scripts/sync-shared-native.sh native-package; fi",
85
81
  "prepack": "bash ../scripts/sync-shared-native.sh native-package && cp ../../README.md .",
86
82
  "postpack": "rm README.md && bash ../scripts/clean-shared-native-copies.sh native-package"
87
83
  },
@@ -92,7 +88,14 @@
92
88
  "name": "StreamChatReactNativeSpec",
93
89
  "type": "all",
94
90
  "jsSrcsDir": "src/native",
91
+ "android": {
92
+ "javaPackageName": "com.streamchatreactnative"
93
+ },
95
94
  "ios": {
95
+ "modulesProvider": {
96
+ "StreamChatReactNative": "StreamChatReactNative",
97
+ "StreamVideoThumbnail": "StreamVideoThumbnail"
98
+ },
96
99
  "componentProvider": {
97
100
  "StreamShimmerView": "StreamShimmerViewComponentView"
98
101
  }
@@ -0,0 +1,13 @@
1
+ module.exports = {
2
+ dependency: {
3
+ platforms: {
4
+ android: {
5
+ packageImportPath: 'import com.streamchatreactnative.StreamChatReactNativePackage;',
6
+ packageInstance: 'new StreamChatReactNativePackage()',
7
+ },
8
+ ios: {
9
+ podspecPath: 'stream-chat-react-native.podspec',
10
+ },
11
+ },
12
+ },
13
+ };
@@ -22,7 +22,6 @@ export const compressImage = async ({
22
22
  Math.min(Math.max(0, compressImageQuality), 1) * 100,
23
23
  0,
24
24
  undefined,
25
- false,
26
25
  { mode: 'cover' },
27
26
  );
28
27
  return compressedUri;
@@ -13,7 +13,6 @@ export interface Spec extends TurboModule {
13
13
  onlyScaleDown: boolean,
14
14
  rotation?: number,
15
15
  outputPath?: string | null,
16
- keepMeta?: boolean,
17
16
  ): Promise<{
18
17
  base64: string;
19
18
  height: number;
@@ -0,0 +1,14 @@
1
+ import type { TurboModule } from 'react-native';
2
+
3
+ import { TurboModuleRegistry } from 'react-native';
4
+
5
+ export type VideoThumbnailResult = {
6
+ error?: string | null;
7
+ uri?: string | null;
8
+ };
9
+
10
+ export interface Spec extends TurboModule {
11
+ createVideoThumbnails(urls: ReadonlyArray<string>): Promise<ReadonlyArray<VideoThumbnailResult>>;
12
+ }
13
+
14
+ export default TurboModuleRegistry.getEnforcing<Spec>('StreamVideoThumbnail');
@@ -24,7 +24,6 @@ async function createResizedImage(
24
24
  quality: number,
25
25
  rotation: number = 0,
26
26
  outputPath?: string | null,
27
- keepMeta = false,
28
27
  options: Options = defaultOptions,
29
28
  ): Promise<Response> {
30
29
  const { mode, onlyScaleDown } = { ...defaultOptions, ...options };
@@ -39,7 +38,6 @@ async function createResizedImage(
39
38
  onlyScaleDown,
40
39
  rotation,
41
40
  outputPath,
42
- keepMeta,
43
41
  );
44
42
  }
45
43
 
@@ -7,6 +7,8 @@ export interface Response {
7
7
  width: number;
8
8
  }
9
9
 
10
+ export interface VideoThumbnailResponse extends Response {}
11
+
10
12
  export type ResizeFormat = 'PNG' | 'JPEG' | 'WEBP';
11
13
  export type ResizeMode = 'contain' | 'cover' | 'stretch';
12
14
 
@@ -0,0 +1,8 @@
1
+ import NativeStreamVideoThumbnail, { type VideoThumbnailResult } from './NativeStreamVideoThumbnail';
2
+
3
+ export type { VideoThumbnailResult } from './NativeStreamVideoThumbnail';
4
+
5
+ export const createVideoThumbnails = async (urls: string[]): Promise<VideoThumbnailResult[]> => {
6
+ const results = await NativeStreamVideoThumbnail.createVideoThumbnails(urls);
7
+ return Array.from(results);
8
+ };
@@ -136,7 +136,7 @@ class _Audio {
136
136
  };
137
137
  startPlayer = async (uri, _, onPlaybackStatusUpdate) => {
138
138
  try {
139
- const playback = await audioRecorderPlayer.startPlayer(uri);
139
+ await audioRecorderPlayer.startPlayer(uri);
140
140
  audioRecorderPlayer.addPlayBackListener((status) => {
141
141
  onPlaybackStatusUpdate(status);
142
142
  });
@@ -1,47 +1,353 @@
1
- import React from 'react';
1
+ import type { PlaybackStatus, SoundReturnType } from 'stream-chat-react-native-core';
2
2
 
3
- import AudioVideoPlayer from './AudioVideo';
3
+ type InitialPlaybackStatus = {
4
+ positionMillis?: number;
5
+ progressUpdateIntervalMillis?: number;
6
+ rate?: number;
7
+ };
8
+
9
+ type LegacyAudioRecorderPlayerConstructor = new () => NativePlaybackInstance;
10
+
11
+ let LegacyAudioRecorderPlayer: LegacyAudioRecorderPlayerConstructor | undefined;
12
+ let createNitroSound: (() => NativePlaybackInstance) | undefined;
13
+
14
+ try {
15
+ ({ createSound: createNitroSound } = require('react-native-nitro-sound'));
16
+ } catch (e) {
17
+ // do nothing
18
+ }
19
+
20
+ try {
21
+ LegacyAudioRecorderPlayer = require('react-native-audio-recorder-player').default;
22
+ } catch (e) {
23
+ // do nothing
24
+ }
25
+
26
+ const PROGRESS_UPDATE_INTERVAL_MILLIS = 100;
27
+
28
+ type NativePlaybackMeta = {
29
+ currentPosition?: number;
30
+ duration?: number;
31
+ isFinished?: boolean;
32
+ };
33
+
34
+ type NativePlaybackEndMeta = {
35
+ currentPosition: number;
36
+ duration: number;
37
+ };
38
+
39
+ type NativePlaybackInstance = {
40
+ addPlayBackListener: (callback: (meta: NativePlaybackMeta) => void) => void;
41
+ pausePlayer: () => Promise<string>;
42
+ removePlayBackListener: () => void;
43
+ resumePlayer: () => Promise<string>;
44
+ seekToPlayer: (time: number) => Promise<string>;
45
+ setPlaybackSpeed?: (playbackSpeed: number) => Promise<string>;
46
+ setSubscriptionDuration?: (seconds: number) => void | Promise<string>;
47
+ startPlayer: (uri?: string, httpHeaders?: Record<string, string>) => Promise<string>;
48
+ stopPlayer: () => Promise<string>;
49
+ addPlaybackEndListener?: (callback: (meta: NativePlaybackEndMeta) => void) => void;
50
+ dispose?: () => void;
51
+ removePlaybackEndListener?: () => void;
52
+ };
53
+
54
+ const createPlaybackInstance = (): NativePlaybackInstance | null => {
55
+ if (createNitroSound) {
56
+ return createNitroSound();
57
+ }
58
+
59
+ if (LegacyAudioRecorderPlayer) {
60
+ return new LegacyAudioRecorderPlayer();
61
+ }
62
+
63
+ return null;
64
+ };
65
+
66
+ const createPlaybackStatus = ({
67
+ didJustFinish = false,
68
+ durationMillis,
69
+ error = '',
70
+ isLoaded,
71
+ isPlaying,
72
+ positionMillis,
73
+ }: {
74
+ didJustFinish?: boolean;
75
+ durationMillis: number;
76
+ error?: string;
77
+ isLoaded: boolean;
78
+ isPlaying: boolean;
79
+ positionMillis: number;
80
+ }): PlaybackStatus => ({
81
+ currentPosition: positionMillis,
82
+ didJustFinish,
83
+ duration: durationMillis,
84
+ durationMillis,
85
+ error,
86
+ isBuffering: false,
87
+ isLoaded,
88
+ isLooping: false,
89
+ isMuted: false,
90
+ isPlaying,
91
+ isSeeking: false,
92
+ positionMillis,
93
+ shouldPlay: isPlaying,
94
+ });
95
+
96
+ class NativeAudioSoundAdapter implements SoundReturnType {
97
+ testID = 'native-audio-sound';
98
+ onPlaybackStatusUpdate?: (playbackStatus: PlaybackStatus) => void;
99
+ private playbackInstance: NativePlaybackInstance | null;
100
+ private sourceUri?: string;
101
+ private isDisposed = false;
102
+ private isLoaded = false;
103
+ private playing = false;
104
+ private durationMillis = 0;
105
+ private positionMillis = 0;
106
+ private playbackRate = 1;
107
+ private hasProgressListener = false;
108
+ private hasPlaybackEndListener = false;
109
+
110
+ constructor({
111
+ source,
112
+ initialStatus,
113
+ onPlaybackStatusUpdate,
114
+ }: {
115
+ source?: { uri: string };
116
+ initialStatus?: InitialPlaybackStatus;
117
+ onPlaybackStatusUpdate?: (playbackStatus: PlaybackStatus) => void;
118
+ }) {
119
+ this.playbackInstance = createPlaybackInstance();
120
+ this.sourceUri = source?.uri;
121
+ this.onPlaybackStatusUpdate = onPlaybackStatusUpdate;
122
+ this.playbackRate = initialStatus?.rate ?? 1;
123
+ this.positionMillis = initialStatus?.positionMillis ?? 0;
124
+ const progressUpdateIntervalMillis =
125
+ initialStatus?.progressUpdateIntervalMillis ?? PROGRESS_UPDATE_INTERVAL_MILLIS;
126
+
127
+ this.playbackInstance?.setSubscriptionDuration?.(progressUpdateIntervalMillis / 1000);
128
+ }
129
+
130
+ private emitPlaybackStatus({
131
+ didJustFinish = false,
132
+ error = '',
133
+ }: {
134
+ didJustFinish?: boolean;
135
+ error?: string;
136
+ } = {}) {
137
+ this.onPlaybackStatusUpdate?.(
138
+ createPlaybackStatus({
139
+ didJustFinish,
140
+ durationMillis: this.durationMillis,
141
+ error,
142
+ isLoaded: this.isLoaded,
143
+ isPlaying: this.playing,
144
+ positionMillis: this.positionMillis,
145
+ }),
146
+ );
147
+ }
148
+
149
+ private attachListeners() {
150
+ if (!this.playbackInstance || this.hasProgressListener) {
151
+ return;
152
+ }
153
+
154
+ this.playbackInstance.addPlayBackListener(this.handlePlaybackProgress);
155
+ this.hasProgressListener = true;
156
+
157
+ if (this.playbackInstance.addPlaybackEndListener) {
158
+ this.playbackInstance.addPlaybackEndListener(this.handlePlaybackEnd);
159
+ this.hasPlaybackEndListener = true;
160
+ }
161
+ }
162
+
163
+ private detachListeners() {
164
+ if (!this.playbackInstance) {
165
+ return;
166
+ }
167
+
168
+ if (this.hasProgressListener) {
169
+ this.playbackInstance.removePlayBackListener();
170
+ this.hasProgressListener = false;
171
+ }
172
+
173
+ if (this.hasPlaybackEndListener && this.playbackInstance.removePlaybackEndListener) {
174
+ this.playbackInstance.removePlaybackEndListener();
175
+ this.hasPlaybackEndListener = false;
176
+ }
177
+ }
178
+
179
+ private handlePlaybackProgress = ({
180
+ currentPosition,
181
+ duration,
182
+ isFinished,
183
+ }: NativePlaybackMeta) => {
184
+ this.positionMillis = currentPosition ?? this.positionMillis;
185
+ this.durationMillis = duration ?? this.durationMillis;
186
+
187
+ const didJustFinish =
188
+ isFinished === true && this.durationMillis > 0 && this.positionMillis >= this.durationMillis;
189
+
190
+ if (didJustFinish) {
191
+ this.playing = false;
192
+ }
193
+
194
+ this.emitPlaybackStatus({ didJustFinish });
195
+ };
196
+
197
+ private handlePlaybackEnd = ({ currentPosition, duration }: NativePlaybackEndMeta) => {
198
+ this.positionMillis = currentPosition ?? this.positionMillis;
199
+ this.durationMillis = duration ?? this.durationMillis;
200
+ this.playing = false;
201
+ this.emitPlaybackStatus({ didJustFinish: true });
202
+ };
203
+
204
+ private async ensureLoaded({ shouldPlay }: { shouldPlay: boolean }) {
205
+ if (!this.playbackInstance || this.isDisposed || !this.sourceUri) {
206
+ return false;
207
+ }
208
+
209
+ if (!this.isLoaded) {
210
+ this.attachListeners();
211
+ await this.playbackInstance.startPlayer(this.sourceUri);
212
+ this.isLoaded = true;
213
+
214
+ if (this.playbackRate !== 1 && this.playbackInstance.setPlaybackSpeed) {
215
+ await this.playbackInstance.setPlaybackSpeed(this.playbackRate);
216
+ }
217
+
218
+ if (this.positionMillis > 0) {
219
+ await this.playbackInstance.seekToPlayer(this.positionMillis);
220
+ }
221
+
222
+ if (!shouldPlay) {
223
+ await this.playbackInstance.pausePlayer();
224
+ }
225
+ } else if (shouldPlay) {
226
+ await this.playbackInstance.resumePlayer();
227
+ }
228
+
229
+ return true;
230
+ }
231
+
232
+ playAsync: SoundReturnType['playAsync'] = async () => {
233
+ const loaded = await this.ensureLoaded({ shouldPlay: true });
234
+ if (!loaded) {
235
+ return;
236
+ }
237
+
238
+ this.playing = true;
239
+ this.emitPlaybackStatus();
240
+ };
241
+
242
+ resume: SoundReturnType['resume'] = () => {
243
+ void this.playAsync?.();
244
+ };
245
+
246
+ pauseAsync: SoundReturnType['pauseAsync'] = async () => {
247
+ if (!this.playbackInstance || !this.isLoaded || this.isDisposed) {
248
+ return;
249
+ }
250
+
251
+ await this.playbackInstance.pausePlayer();
252
+ this.playing = false;
253
+ this.emitPlaybackStatus();
254
+ };
255
+
256
+ pause: SoundReturnType['pause'] = () => {
257
+ void this.pauseAsync?.();
258
+ };
259
+
260
+ seek: SoundReturnType['seek'] = async (progress) => {
261
+ const loaded = await this.ensureLoaded({ shouldPlay: false });
262
+ if (!loaded || !this.playbackInstance) {
263
+ return;
264
+ }
265
+
266
+ this.positionMillis = progress * 1000;
267
+ await this.playbackInstance.seekToPlayer(this.positionMillis);
268
+ this.emitPlaybackStatus();
269
+ };
270
+
271
+ setPositionAsync: SoundReturnType['setPositionAsync'] = async (millis) => {
272
+ await this.seek?.(millis / 1000);
273
+ };
274
+
275
+ setRateAsync: SoundReturnType['setRateAsync'] = async (rate) => {
276
+ this.playbackRate = rate;
277
+
278
+ if (this.playbackInstance?.setPlaybackSpeed && this.isLoaded) {
279
+ await this.playbackInstance.setPlaybackSpeed(rate);
280
+
281
+ // Some Android backends resume playback as a side effect of changing speed.
282
+ // Preserve the previous paused state explicitly so rate changes stay silent.
283
+ if (!this.playing) {
284
+ await this.playbackInstance.pausePlayer();
285
+ this.emitPlaybackStatus();
286
+ }
287
+ }
288
+ };
289
+
290
+ replayAsync: SoundReturnType['replayAsync'] = async () => {
291
+ await this.stopAsync?.();
292
+ this.positionMillis = 0;
293
+ await this.playAsync?.();
294
+ };
295
+
296
+ stopAsync: SoundReturnType['stopAsync'] = async () => {
297
+ if (!this.playbackInstance || !this.isLoaded || this.isDisposed) {
298
+ return;
299
+ }
300
+
301
+ await this.playbackInstance.stopPlayer();
302
+ this.isLoaded = false;
303
+ this.playing = false;
304
+ this.positionMillis = 0;
305
+ this.emitPlaybackStatus();
306
+ };
307
+
308
+ unloadAsync: SoundReturnType['unloadAsync'] = async () => {
309
+ if (this.isDisposed) {
310
+ return;
311
+ }
312
+
313
+ try {
314
+ if (this.isLoaded && this.playbackInstance) {
315
+ await this.playbackInstance.stopPlayer();
316
+ }
317
+ } catch {
318
+ // Best effort cleanup.
319
+ }
320
+
321
+ this.detachListeners();
322
+ this.playbackInstance?.dispose?.();
323
+ this.isLoaded = false;
324
+ this.playing = false;
325
+ this.isDisposed = true;
326
+ };
327
+ }
328
+
329
+ const initializeSound =
330
+ createNitroSound || LegacyAudioRecorderPlayer
331
+ ? (
332
+ source?: { uri: string },
333
+ initialStatus?: InitialPlaybackStatus,
334
+ onPlaybackStatusUpdate?: (playbackStatus: PlaybackStatus) => void,
335
+ ) => {
336
+ if (!source?.uri) {
337
+ return null;
338
+ }
339
+
340
+ const sound = new NativeAudioSoundAdapter({
341
+ initialStatus,
342
+ onPlaybackStatusUpdate,
343
+ source,
344
+ });
345
+
346
+ return sound;
347
+ }
348
+ : null;
4
349
 
5
350
  export const Sound = {
6
- initializeSound: null,
7
- // eslint-disable-next-line react/display-name
8
- Player: AudioVideoPlayer
9
- ? ({
10
- onBuffer,
11
- onEnd,
12
- onLoad,
13
- onLoadStart,
14
- onPlaybackStateChanged,
15
- onProgress,
16
- onSeek,
17
- paused,
18
- rate,
19
- soundRef,
20
- style,
21
- uri,
22
- }) => (
23
- <AudioVideoPlayer
24
- audioOnly={true}
25
- ignoreSilentSwitch={'ignore'}
26
- onBuffer={onBuffer}
27
- onEnd={onEnd}
28
- onError={(error: Error) => {
29
- console.log(error);
30
- }}
31
- onLoad={onLoad}
32
- onLoadStart={onLoadStart}
33
- onPlaybackStateChanged={onPlaybackStateChanged}
34
- onProgress={onProgress}
35
- onSeek={onSeek}
36
- paused={paused}
37
- rate={rate}
38
- ref={soundRef}
39
- progressUpdateInterval={100}
40
- source={{
41
- uri,
42
- }}
43
- style={style}
44
- />
45
- )
46
- : null,
351
+ initializeSound,
352
+ Player: null,
47
353
  };
@@ -0,0 +1,8 @@
1
+ import { createGenerateVideoThumbnails } from 'stream-chat-react-native-core';
2
+
3
+ import { createVideoThumbnails, type VideoThumbnailResult } from '../native/videoThumbnail';
4
+
5
+ export const generateThumbnails: (uris: string[]) => Promise<Record<string, VideoThumbnailResult>> =
6
+ createGenerateVideoThumbnails({
7
+ createVideoThumbnails,
8
+ });
@@ -1,8 +1,12 @@
1
1
  import { PermissionsAndroid, Platform } from 'react-native';
2
+
2
3
  import mime from 'mime';
3
4
 
4
5
  import type { File } from 'stream-chat-react-native-core';
5
6
 
7
+ import { generateThumbnails } from './generateThumbnail';
8
+ import { getLocalAssetUri } from './getLocalAssetUri';
9
+
6
10
  let CameraRollDependency;
7
11
 
8
12
  try {
@@ -14,8 +18,6 @@ try {
14
18
  );
15
19
  }
16
20
 
17
- import { getLocalAssetUri } from './getLocalAssetUri';
18
-
19
21
  type ReturnType = {
20
22
  assets: File[];
21
23
  endCursor: string | undefined;
@@ -88,7 +90,7 @@ export const getPhotos = CameraRollDependency
88
90
  first,
89
91
  include: ['fileSize', 'filename', 'imageSize', 'playableDuration'],
90
92
  });
91
- const assets = await Promise.all(
93
+ const assetEntries = await Promise.all(
92
94
  results.edges.map(async (edge) => {
93
95
  const originalUri = edge.node?.image?.uri;
94
96
  const type =
@@ -100,20 +102,34 @@ export const getPhotos = CameraRollDependency
100
102
  (edge.node.image.playableDuration ? 'video/*' : 'image/*');
101
103
  const isImage = type.includes('image');
102
104
 
103
- const uri =
104
- isImage && getLocalAssetUri ? await getLocalAssetUri(originalUri) : originalUri;
105
-
106
105
  return {
107
- ...edge.node.image,
108
- name: edge.node.image.filename as string,
109
- duration: edge.node.image.playableDuration * 1000,
110
- thumb_url: isImage ? undefined : originalUri,
111
- size: edge.node.image.fileSize as number,
106
+ edge,
107
+ isImage,
108
+ originalUri,
112
109
  type,
113
- uri,
110
+ uri: isImage && getLocalAssetUri ? await getLocalAssetUri(originalUri) : originalUri,
114
111
  };
115
112
  }),
116
113
  );
114
+ const videoUris = assetEntries
115
+ .filter(({ isImage, originalUri }) => !isImage && !!originalUri)
116
+ .map(({ originalUri }) => originalUri);
117
+ const videoThumbnailResults = await generateThumbnails(videoUris);
118
+
119
+ const assets = assetEntries.map(({ edge, isImage, originalUri, type, uri }) => {
120
+ const thumbnailResult =
121
+ !isImage && originalUri ? videoThumbnailResults[originalUri] : undefined;
122
+
123
+ return {
124
+ ...edge.node.image,
125
+ name: edge.node.image.filename as string,
126
+ duration: edge.node.image.playableDuration * 1000,
127
+ thumb_url: thumbnailResult?.uri || undefined,
128
+ size: edge.node.image.fileSize as number,
129
+ type,
130
+ uri,
131
+ };
132
+ });
117
133
  const hasNextPage = results.page_info.has_next_page;
118
134
  const endCursor = results.page_info.end_cursor;
119
135
  return { assets, endCursor, hasNextPage, iOSLimited: !!results.limited };