// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. import RNUtils from '@mattermost/rnutils'; import {Image} from 'expo-image'; import React, {type RefObject} from 'react'; import {DeviceEventEmitter, Keyboard, Platform, View} from 'react-native'; import {Navigation, type Options, type OptionsLayout} from 'react-native-navigation'; import {measure, type AnimatedRef} from 'react-native-reanimated'; import {Events, Screens} from '@constants'; import {SNACK_BAR_TYPE} from '@constants/snack_bar'; import {allOrientations, showOverlay} from '@screens/navigation'; import EphemeralStore from '@store/ephemeral_store'; import {isImage, isVideo} from '@utils/file'; import {generateId} from '@utils/general'; import {urlSafeBase64Encode} from '@utils/security'; import {showSnackBar} from '@utils/snack_bar'; import type {GalleryItemType, GalleryManagerSharedValues} from '@typings/screens/gallery'; export const clamp = (value: number, lowerBound: number, upperBound: number) => { 'worklet'; return Math.min(Math.max(lowerBound, value), upperBound); }; export const clampVelocity = (velocity: number, minVelocity: number, maxVelocity: number) => { 'worklet'; if (velocity > 0) { return Math.min(Math.max(velocity, minVelocity), maxVelocity); } return Math.max(Math.min(velocity, -minVelocity), -maxVelocity); }; export const fileToGalleryItem = (file: FileInfo, authorId?: string, postProps?: Record, lastPictureUpdate = 0, cacheKey: string = file.id || ''): GalleryItemType => { let type: GalleryItemType['type'] = 'file'; if (isVideo(file)) { type = 'video'; } else if (isImage(file)) { type = 'image'; } return { authorId, extension: file.extension, height: file.height, id: file.id || generateId('uid'), lastPictureUpdate, mime_type: file.mime_type, name: file.name, posterUri: type === 'video' ? file.mini_preview : undefined, // set the video poster to the mini_preview postId: file.post_id, size: file.size, type, uri: file.localPath || file.uri || '', width: file.width, postProps: postProps || file.postProps, cacheKey, }; }; export const freezeOtherScreens = (value: boolean) => { DeviceEventEmitter.emit(Events.FREEZE_SCREEN, value); }; export const friction = (value: number) => { 'worklet'; const MAX_FRICTION = 30; const MAX_VALUE = 200; const res = Math.max( 1, Math.min( MAX_FRICTION, (1 + (Math.abs(value) * (MAX_FRICTION - 1))) / MAX_VALUE, ), ); return value > 0 ? res : -res; }; export const galleryItemToFileInfo = (item: GalleryItemType): FileInfo => ({ id: item.id, name: item.name, create_at: 0, delete_at: 0, update_at: 0, width: item.width, height: item.height, extension: item.extension || '', mime_type: item.mime_type, has_preview_image: false, post_id: item.postId!, size: 0, user_id: item.authorId!, }); export const getShouldRender = (index: number, activeIndex: number, diffValue = 3) => { const diff = Math.abs(index - activeIndex); if (diff > diffValue) { return false; } return true; }; export function measureItem(ref: AnimatedRef, sharedValues: GalleryManagerSharedValues) { 'worklet'; try { const measurements = measure(ref); sharedValues.x.value = measurements?.pageX || 999999; sharedValues.y.value = measurements?.pageY || 999999; sharedValues.width.value = measurements?.width || 0; sharedValues.height.value = measurements?.height || 0; } catch (err) { sharedValues.x.value = 999999; sharedValues.y.value = 999999; sharedValues.width.value = 0; sharedValues.height.value = 0; } } export function measureViewInWindow(ref: RefObject): Promise<{x: number; y: number; width: number; height: number}> { return new Promise((resolve) => { if (ref.current) { ref.current.measure((x, y, width, height, pageX, pageY) => { resolve({ x: pageX, y: pageY, width, height, }); }); } else { resolve({ x: 0, y: 0, width: 0, height: 0, }); } }); } export function openGalleryAtIndex(galleryIdentifier: string, initialIndex: number, items: GalleryItemType[], hideActions = false) { // Check if the file at initialIndex is rejected by plugin const initialItem = items[initialIndex]; if (initialItem?.id && EphemeralStore.isFileRejected(initialItem.id)) { // Show error snackbar with the plugin's rejection reason const rejectionReason = EphemeralStore.getRejectionReason(initialItem.id); showSnackBar({ barType: SNACK_BAR_TYPE.FILE_DOWNLOAD_REJECTED, customMessage: rejectionReason || undefined, type: 'error', }); return; } Keyboard.dismiss(); const props = { galleryIdentifier, hideActions, initialIndex, items, }; const layout: OptionsLayout = { orientation: allOrientations, }; const options: Options = { layout, topBar: { background: { color: '#000', }, visible: Platform.OS === 'android', }, statusBar: { backgroundColor: '#000', style: 'light', }, animations: { showModal: { waitForRender: false, enabled: false, }, dismissModal: { enabled: false, }, }, }; if (Platform.OS === 'ios') { // on iOS we need both the navigation & the module Navigation.setDefaultOptions({layout}); RNUtils.unlockOrientation(); } showOverlay(Screens.GALLERY, props, options); setTimeout(() => { freezeOtherScreens(true); }, 500); } export const typedMemo: (c: T) => T = React.memo; export const getImageSize = async (serverUrl: string, uri: string, cacheKey: string) => { const image = await Image.loadAsync({uri, cacheKey, cachePath: urlSafeBase64Encode(serverUrl)}); return {width: image.width, height: image.height}; };