Page MenuHomePhabricator

No OneTemporary

diff --git a/lib/types/media-types.js b/lib/types/media-types.js
index a8f8d1636..0bbce54d1 100644
--- a/lib/types/media-types.js
+++ b/lib/types/media-types.js
@@ -1,527 +1,527 @@
// @flow
import type { Shape } from './core';
import { type Platform } from './device-types';
export type Dimensions = $ReadOnly<{
+height: number,
+width: number,
}>;
export type MediaType = 'photo' | 'video';
export type Image = {
+id: string,
+uri: string,
+type: 'photo',
+dimensions: Dimensions,
// stored on native only during creation in case retry needed after state lost
+localMediaSelection?: NativeMediaSelection,
};
export type Video = {
+id: string,
+uri: string,
+type: 'video',
+dimensions: Dimensions,
+loop?: boolean,
+thumbnailID: string,
+thumbnailURI: string,
// stored on native only during creation in case retry needed after state lost
+localMediaSelection?: NativeMediaSelection,
};
export type Media = Image | Video;
export type ClientDBMediaInfo = {
+id: string,
+uri: string,
+type: 'photo' | 'video',
+extras: string,
};
export type Corners = Shape<{
+topLeft: boolean,
+topRight: boolean,
+bottomLeft: boolean,
+bottomRight: boolean,
}>;
export type MediaInfo =
| {
...Image,
+corners: Corners,
+index: number,
}
| {
...Video,
+corners: Corners,
+index: number,
};
export type UploadMultimediaResult = {
+id: string,
+uri: string,
+dimensions: Dimensions,
+mediaType: MediaType,
+loop: boolean,
};
export type UpdateMultimediaMessageMediaPayload = {
+messageID: string,
+currentMediaID: string,
+mediaUpdate: Shape<Media>,
};
export type UploadDeletionRequest = {
+id: string,
};
export type FFmpegStatistics = {
// seconds of video being processed per second
+speed: number,
// total milliseconds of video processed so far
+time: number,
// total result file size in bytes so far
+size: number,
+videoQuality: number,
+videoFrameNumber: number,
+videoFps: number,
+bitrate: number,
};
export type TranscodeVideoMediaMissionStep = {
+step: 'video_ffmpeg_transcode',
+success: boolean,
+exceptionMessage: ?string,
+time: number, // ms
+returnCode: ?number,
+newPath: ?string,
+stats: ?FFmpegStatistics,
};
export type VideoGenerateThumbnailMediaMissionStep = {
+step: 'video_generate_thumbnail',
+success: boolean,
+time: number, // ms
+returnCode: number,
+thumbnailURI: string,
};
export type VideoInfo = {
+codec: ?string,
+dimensions: ?Dimensions,
+duration: number, // seconds
+format: $ReadOnlyArray<string>,
};
export type VideoProbeMediaMissionStep = {
+step: 'video_probe',
+success: boolean,
+exceptionMessage: ?string,
+time: number, // ms
+path: string,
+validFormat: boolean,
+duration: ?number, // seconds
+codec: ?string,
+format: ?$ReadOnlyArray<string>,
+dimensions: ?Dimensions,
};
export type ReadFileHeaderMediaMissionStep = {
+step: 'read_file_header',
+success: boolean,
+exceptionMessage: ?string,
+time: number, // ms
+uri: string,
+mime: ?string,
+mediaType: ?MediaType,
};
export type DetermineFileTypeMediaMissionStep = {
+step: 'determine_file_type',
+success: boolean,
+exceptionMessage: ?string,
+time: number, // ms
+inputFilename: string,
+outputMIME: ?string,
+outputMediaType: ?MediaType,
+outputFilename: ?string,
};
export type FrameCountMediaMissionStep = {
+step: 'frame_count',
+success: boolean,
+exceptionMessage: ?string,
+time: number,
+path: string,
+mime: string,
+hasMultipleFrames: ?boolean,
};
export type DisposeTemporaryFileMediaMissionStep = {
+step: 'dispose_temporary_file',
+success: boolean,
+exceptionMessage: ?string,
+time: number, // ms
+path: string,
};
export type MakeDirectoryMediaMissionStep = {
+step: 'make_directory',
+success: boolean,
+exceptionMessage: ?string,
+time: number, // ms
+path: string,
};
export type AndroidScanFileMediaMissionStep = {
+step: 'android_scan_file',
+success: boolean,
+exceptionMessage: ?string,
+time: number, // ms
+path: string,
};
export type FetchFileHashMediaMissionStep = {
+step: 'fetch_file_hash',
+success: boolean,
+exceptionMessage: ?string,
+time: number, // ms
+path: string,
+hash: ?string,
};
export type CopyFileMediaMissionStep = {
+step: 'copy_file',
+success: boolean,
+exceptionMessage: ?string,
+time: number, // ms
+source: string,
+destination: string,
};
export type GetOrientationMediaMissionStep = {
+step: 'exif_fetch',
+success: boolean,
+exceptionMessage: ?string,
+time: number, // ms
+orientation: ?number,
};
export type MediaLibrarySelection =
| {
+step: 'photo_library',
+dimensions: Dimensions,
+filename: ?string,
+uri: string,
- +mediaNativeID: string,
+ +mediaNativeID: ?string,
+selectTime: number, // ms timestamp
+sendTime: number, // ms timestamp
+retries: number,
}
| {
+step: 'video_library',
+dimensions: Dimensions,
+filename: ?string,
+uri: string,
- +mediaNativeID: string,
+ +mediaNativeID: ?string,
+selectTime: number, // ms timestamp
+sendTime: number, // ms timestamp
+retries: number,
+duration: number, // seconds
};
export type PhotoCapture = {
+step: 'photo_capture',
+time: number, // ms
+dimensions: Dimensions,
+filename: string,
+uri: string,
+captureTime: number, // ms timestamp
+selectTime: number, // ms timestamp
+sendTime: number, // ms timestamp
+retries: number,
};
export type PhotoPaste = {
+step: 'photo_paste',
+dimensions: Dimensions,
+filename: string,
+uri: string,
+selectTime: number, // ms timestamp
+sendTime: number, // ms timestamp
+retries: number,
};
export type NativeMediaSelection =
| MediaLibrarySelection
| PhotoCapture
| PhotoPaste;
export type MediaMissionStep =
| NativeMediaSelection
| {
+step: 'web_selection',
+filename: string,
+size: number, // in bytes
+mime: string,
+selectTime: number, // ms timestamp
}
| {
+step: 'asset_info_fetch',
+success: boolean,
+exceptionMessage: ?string,
+time: number, // ms
+localURI: ?string,
+orientation: ?number,
}
| {
+step: 'stat_file',
+success: boolean,
+exceptionMessage: ?string,
+time: number, // ms
+uri: string,
+fileSize: ?number,
}
| ReadFileHeaderMediaMissionStep
| DetermineFileTypeMediaMissionStep
| FrameCountMediaMissionStep
| {
+step: 'photo_manipulation',
+success: boolean,
+exceptionMessage: ?string,
+time: number, // ms
+manipulation: Object,
+newMIME: ?string,
+newDimensions: ?Dimensions,
+newURI: ?string,
}
| VideoProbeMediaMissionStep
| TranscodeVideoMediaMissionStep
| VideoGenerateThumbnailMediaMissionStep
| DisposeTemporaryFileMediaMissionStep
| {
+step: 'save_media',
+uri: string,
+time: number, // ms timestamp
}
| {
+step: 'permissions_check',
+success: boolean,
+exceptionMessage: ?string,
+time: number, // ms
+platform: Platform,
+permissions: $ReadOnlyArray<string>,
}
| MakeDirectoryMediaMissionStep
| AndroidScanFileMediaMissionStep
| {
+step: 'ios_save_to_library',
+success: boolean,
+exceptionMessage: ?string,
+time: number, // ms
+uri: string,
}
| {
+step: 'fetch_blob',
+success: boolean,
+exceptionMessage: ?string,
+time: number, // ms
+inputURI: string,
+uri: string,
+size: ?number,
+mime: ?string,
}
| {
+step: 'data_uri_from_blob',
+success: boolean,
+exceptionMessage: ?string,
+time: number, // ms
+first255Chars: ?string,
}
| {
+step: 'array_buffer_from_blob',
+success: boolean,
+exceptionMessage: ?string,
+time: number, // ms
}
| {
+step: 'mime_check',
+success: boolean,
+exceptionMessage: ?string,
+time: number, // ms
+mime: ?string,
}
| {
+step: 'write_file',
+success: boolean,
+exceptionMessage: ?string,
+time: number, // ms
+path: string,
+length: number,
}
| FetchFileHashMediaMissionStep
| CopyFileMediaMissionStep
| GetOrientationMediaMissionStep
| {
+step: 'preload_image',
+success: boolean,
+exceptionMessage: ?string,
+time: number, // ms
+uri: string,
+dimensions: ?Dimensions,
}
| {
+step: 'reorient_image',
+success: boolean,
+exceptionMessage: ?string,
+time: number, // ms
+uri: ?string,
}
| {
+step: 'upload',
+success: boolean,
+exceptionMessage: ?string,
+time: number, // ms
+inputFilename: string,
+outputMediaType: ?MediaType,
+outputURI: ?string,
+outputDimensions: ?Dimensions,
+outputLoop: ?boolean,
+hasWiFi?: boolean,
}
| {
+step: 'wait_for_capture_uri_unload',
+success: boolean,
+time: number, // ms
+uri: string,
};
export type MediaMissionFailure =
| {
+success: false,
+reason: 'no_file_path',
}
| {
+success: false,
+reason: 'file_stat_failed',
+uri: string,
}
| {
+success: false,
+reason: 'photo_manipulation_failed',
+size: number, // in bytes
}
| {
+success: false,
+reason: 'media_type_fetch_failed',
+detectedMIME: ?string,
}
| {
+success: false,
+reason: 'mime_type_mismatch',
+reportedMediaType: MediaType,
+reportedMIME: string,
+detectedMIME: string,
}
| {
+success: false,
+reason: 'http_upload_failed',
+exceptionMessage: ?string,
}
| {
+success: false,
+reason: 'video_too_long',
+duration: number, // in seconds
}
| {
+success: false,
+reason: 'video_probe_failed',
}
| {
+success: false,
+reason: 'video_transcode_failed',
}
| {
+success: false,
+reason: 'video_generate_thumbnail_failed',
}
| {
+success: false,
+reason: 'processing_exception',
+time: number, // ms
+exceptionMessage: ?string,
}
| {
+success: false,
+reason: 'save_unsupported',
}
| {
+success: false,
+reason: 'missing_permission',
}
| {
+success: false,
+reason: 'make_directory_failed',
}
| {
+success: false,
+reason: 'resolve_failed',
+uri: string,
}
| {
+success: false,
+reason: 'save_to_library_failed',
+uri: string,
}
| {
+success: false,
+reason: 'fetch_failed',
}
| {
+success: false,
+reason: 'data_uri_failed',
}
| {
+success: false,
+reason: 'array_buffer_failed',
}
| {
+success: false,
+reason: 'mime_check_failed',
+mime: ?string,
}
| {
+success: false,
+reason: 'write_file_failed',
}
| {
+success: false,
+reason: 'fetch_file_hash_failed',
}
| {
+success: false,
+reason: 'copy_file_failed',
}
| {
+success: false,
+reason: 'exif_fetch_failed',
}
| {
+success: false,
+reason: 'reorient_image_failed',
}
| {
+success: false,
+reason: 'web_sibling_validation_failed',
};
export type MediaMissionResult = MediaMissionFailure | { +success: true };
export type MediaMission = {
+steps: $ReadOnlyArray<MediaMissionStep>,
+result: MediaMissionResult,
+userTime: number,
+totalTime: number,
};
diff --git a/native/media/media-gallery-keyboard.react.js b/native/media/media-gallery-keyboard.react.js
index d8552965f..ce639c41b 100644
--- a/native/media/media-gallery-keyboard.react.js
+++ b/native/media/media-gallery-keyboard.react.js
@@ -1,555 +1,638 @@
// @flow
+import * as ImagePicker from 'expo-image-picker';
import * as MediaLibrary from 'expo-media-library';
import invariant from 'invariant';
import * as React from 'react';
import {
View,
Text,
FlatList,
ActivityIndicator,
Animated,
Easing,
Platform,
} from 'react-native';
import { KeyboardRegistry } from 'react-native-keyboard-input';
import { Provider } from 'react-redux';
-import { extensionFromFilename } from 'lib/media/file-utils';
+import {
+ extensionFromFilename,
+ filenameFromPathOrURI,
+} from 'lib/media/file-utils';
import { useIsAppForegrounded } from 'lib/shared/lifecycle-utils';
import type { MediaLibrarySelection } from 'lib/types/media-types';
import type { DimensionsInfo } from '../redux/dimensions-updater.react';
import { store } from '../redux/redux-setup';
import { useSelector } from '../redux/redux-utils';
import { type Colors, useColors, useStyles } from '../themes/colors';
import type { LayoutEvent, ViewableItemsChange } from '../types/react-native';
import type { ViewStyle } from '../types/styles';
import { getCompatibleMediaURI } from './identifier-utils';
import MediaGalleryMedia from './media-gallery-media.react';
import SendMediaButton from './send-media-button.react';
const animationSpec = {
duration: 400,
easing: Easing.inOut(Easing.ease),
useNativeDriver: true,
};
type Props = {
// Redux state
+dimensions: DimensionsInfo,
+foreground: boolean,
+colors: Colors,
+styles: typeof unboundStyles,
};
type State = {
+selections: ?$ReadOnlyArray<MediaLibrarySelection>,
+error: ?string,
+containerHeight: ?number,
// null means end reached; undefined means no fetch yet
+cursor: ?string,
+queuedMediaURIs: ?Set<string>,
+focusedMediaURI: ?string,
+dimensions: DimensionsInfo,
};
class MediaGalleryKeyboard extends React.PureComponent<Props, State> {
mounted = false;
fetchingPhotos = false;
flatList: ?FlatList<MediaLibrarySelection>;
viewableIndices: number[] = [];
queueModeProgress = new Animated.Value(0);
sendButtonStyle: ViewStyle;
mediaSelected = false;
constructor(props: Props) {
super(props);
const sendButtonScale = this.queueModeProgress.interpolate({
inputRange: [0, 1],
outputRange: ([1.3, 1]: number[]), // Flow...
});
this.sendButtonStyle = {
opacity: this.queueModeProgress,
transform: [{ scale: sendButtonScale }],
};
this.state = {
selections: null,
error: null,
containerHeight: null,
cursor: undefined,
queuedMediaURIs: null,
focusedMediaURI: null,
dimensions: props.dimensions,
};
}
static getDerivedStateFromProps(props: Props) {
// We keep this in state since we pass this.state as
// FlatList's extraData prop
return { dimensions: props.dimensions };
}
componentDidMount() {
this.mounted = true;
return this.fetchPhotos();
}
componentWillUnmount() {
this.mounted = false;
}
componentDidUpdate(prevProps: Props, prevState: State) {
const { queuedMediaURIs } = this.state;
const prevQueuedMediaURIs = prevState.queuedMediaURIs;
if (queuedMediaURIs && !prevQueuedMediaURIs) {
Animated.timing(this.queueModeProgress, {
...animationSpec,
toValue: 1,
}).start();
} else if (!queuedMediaURIs && prevQueuedMediaURIs) {
Animated.timing(this.queueModeProgress, {
...animationSpec,
toValue: 0,
}).start();
}
const { flatList, viewableIndices } = this;
const { selections, focusedMediaURI } = this.state;
let scrollingSomewhere = false;
if (flatList && selections) {
let newURI;
if (focusedMediaURI && focusedMediaURI !== prevState.focusedMediaURI) {
newURI = focusedMediaURI;
} else if (
queuedMediaURIs &&
(!prevQueuedMediaURIs ||
queuedMediaURIs.size > prevQueuedMediaURIs.size)
) {
const flowMadeMeDoThis = queuedMediaURIs;
for (const queuedMediaURI of flowMadeMeDoThis) {
if (prevQueuedMediaURIs && prevQueuedMediaURIs.has(queuedMediaURI)) {
continue;
}
newURI = queuedMediaURI;
break;
}
}
let index;
if (newURI !== null && newURI !== undefined) {
index = selections.findIndex(({ uri }) => uri === newURI);
}
if (index !== null && index !== undefined) {
if (index === viewableIndices[0]) {
scrollingSomewhere = true;
flatList.scrollToIndex({ index });
} else if (index === viewableIndices[viewableIndices.length - 1]) {
scrollingSomewhere = true;
flatList.scrollToIndex({ index, viewPosition: 1 });
}
}
}
if (this.props.foreground && !prevProps.foreground) {
this.fetchPhotos();
}
if (
!scrollingSomewhere &&
this.flatList &&
this.state.selections &&
prevState.selections &&
this.state.selections.length > 0 &&
prevState.selections.length > 0 &&
this.state.selections[0].uri !== prevState.selections[0].uri
) {
this.flatList.scrollToIndex({ index: 0 });
}
}
guardedSetState(change) {
if (this.mounted) {
this.setState(change);
}
}
async fetchPhotos(after?: ?string) {
if (this.fetchingPhotos) {
return;
}
this.fetchingPhotos = true;
try {
const hasPermission = await this.getPermissions();
if (!hasPermission) {
return;
}
const {
assets,
endCursor,
hasNextPage,
} = await MediaLibrary.getAssetsAsync({
first: 20,
after,
mediaType: [MediaLibrary.MediaType.photo, MediaLibrary.MediaType.video],
sortBy: [MediaLibrary.SortBy.modificationTime],
});
let firstRemoved = false,
lastRemoved = false;
const mediaURIs = this.state.selections
? this.state.selections.map(({ uri }) => uri)
: [];
const existingURIs = new Set(mediaURIs);
let first = true;
const selections = assets
.map(asset => {
const { id, height, width, filename, mediaType, duration } = asset;
const isVideo = mediaType === MediaLibrary.MediaType.video;
const uri = getCompatibleMediaURI(
asset.uri,
extensionFromFilename(filename),
);
if (existingURIs.has(uri)) {
if (first) {
firstRemoved = true;
}
lastRemoved = true;
first = false;
return null;
}
first = false;
lastRemoved = false;
existingURIs.add(uri);
if (isVideo) {
return {
step: 'video_library',
dimensions: { height, width },
uri,
filename,
mediaNativeID: id,
duration,
selectTime: 0,
sendTime: 0,
retries: 0,
};
} else {
return {
step: 'photo_library',
dimensions: { height, width },
uri,
filename,
mediaNativeID: id,
selectTime: 0,
sendTime: 0,
retries: 0,
};
}
})
.filter(Boolean);
let appendOrPrepend = after ? 'append' : 'prepend';
if (firstRemoved && !lastRemoved) {
appendOrPrepend = 'append';
} else if (!firstRemoved && lastRemoved) {
appendOrPrepend = 'prepend';
}
let newSelections = selections;
if (this.state.selections) {
if (appendOrPrepend === 'prepend') {
newSelections = [...newSelections, ...this.state.selections];
} else {
newSelections = [...this.state.selections, ...newSelections];
}
}
this.guardedSetState({
selections: newSelections,
error: null,
cursor: hasNextPage ? endCursor : null,
});
} catch (e) {
this.guardedSetState({
selections: null,
error: 'something went wrong :(',
});
}
this.fetchingPhotos = false;
}
+ openNativePicker = async () => {
+ try {
+ const { assets, canceled } = await ImagePicker.launchImageLibraryAsync({
+ mediaTypes: ImagePicker.MediaTypeOptions.All,
+ allowsEditing: false,
+ allowsMultipleSelection: true,
+ // maximum quality is 1 - it disables compression
+ quality: 1,
+ // we don't want to compress videos at this point
+ videoExportPreset: ImagePicker.VideoExportPreset.Passthrough,
+ });
+
+ if (canceled || assets.length === 0) {
+ return;
+ }
+
+ const selections = assets.map(asset => {
+ const {
+ width,
+ height,
+ fileName,
+ type,
+ duration,
+ assetId: mediaNativeID,
+ } = asset;
+ const isVideo = type === 'video';
+ const filename = fileName || filenameFromPathOrURI(asset.uri) || '';
+ const uri = getCompatibleMediaURI(
+ asset.uri,
+ extensionFromFilename(filename),
+ );
+
+ if (isVideo) {
+ return {
+ step: 'video_library',
+ dimensions: { height, width },
+ uri,
+ filename,
+ mediaNativeID,
+ duration,
+ selectTime: 0,
+ sendTime: 0,
+ retries: 0,
+ };
+ } else {
+ return {
+ step: 'photo_library',
+ dimensions: { height, width },
+ uri,
+ filename,
+ mediaNativeID,
+ selectTime: 0,
+ sendTime: 0,
+ retries: 0,
+ };
+ }
+ });
+
+ const selectionURIs = selections.map(({ uri }) => uri);
+ this.guardedSetState(prevState => ({
+ error: null,
+ selections: [...selections, ...(prevState.selections ?? [])],
+ queuedMediaURIs: new Set([
+ ...selectionURIs,
+ ...(prevState.queuedMediaURIs ?? []),
+ ]),
+ focusedMediaURI: null,
+ }));
+ } catch (e) {
+ if (__DEV__) {
+ console.warn(e);
+ }
+ this.guardedSetState({
+ selections: null,
+ error: 'something went wrong :(',
+ });
+ }
+ };
+
async getPermissions(): Promise<boolean> {
const { granted } = await MediaLibrary.requestPermissionsAsync();
if (!granted) {
this.guardedSetState({ error: "don't have permission :(" });
}
return granted;
}
get queueModeActive() {
return !!this.state.queuedMediaURIs;
}
renderItem = (row: { item: MediaLibrarySelection, ... }) => {
const { containerHeight, queuedMediaURIs } = this.state;
invariant(containerHeight, 'should be set');
const { uri } = row.item;
const isQueued = !!(queuedMediaURIs && queuedMediaURIs.has(uri));
const { queueModeActive } = this;
return (
<MediaGalleryMedia
selection={row.item}
containerHeight={containerHeight}
queueModeActive={queueModeActive}
isQueued={isQueued}
setMediaQueued={this.setMediaQueued}
sendMedia={this.sendSingleMedia}
isFocused={this.state.focusedMediaURI === uri}
setFocus={this.setFocus}
dimensions={this.state.dimensions}
/>
);
};
ItemSeparator = () => {
return <View style={this.props.styles.separator} />;
};
static keyExtractor = (item: MediaLibrarySelection) => {
return item.uri;
};
render() {
let content;
const { selections, error, containerHeight } = this.state;
const bottomOffsetStyle: ViewStyle = {
marginBottom: this.props.dimensions.bottomInset,
};
if (selections && selections.length > 0 && containerHeight) {
content = (
<FlatList
horizontal={true}
data={selections}
renderItem={this.renderItem}
ItemSeparatorComponent={this.ItemSeparator}
keyExtractor={MediaGalleryKeyboard.keyExtractor}
scrollsToTop={false}
showsHorizontalScrollIndicator={false}
onEndReached={this.onEndReached}
onEndReachedThreshold={5}
onViewableItemsChanged={this.onViewableItemsChanged}
extraData={this.state}
ref={this.flatListRef}
/>
);
} else if (selections && containerHeight) {
content = (
<Text style={[this.props.styles.error, bottomOffsetStyle]}>
no media was found!
</Text>
);
} else if (error) {
content = (
<Text style={[this.props.styles.error, bottomOffsetStyle]}>
{error}
</Text>
);
} else {
content = (
<ActivityIndicator
color={this.props.colors.listSeparatorLabel}
size="large"
style={[this.props.styles.loadingIndicator, bottomOffsetStyle]}
/>
);
}
const { queuedMediaURIs } = this.state;
const queueCount = queuedMediaURIs ? queuedMediaURIs.size : 0;
const bottomInset = Platform.select({
ios: -1 * this.props.dimensions.bottomInset,
default: 0,
});
const containerStyle = { bottom: bottomInset };
return (
<View
style={[this.props.styles.container, containerStyle]}
onLayout={this.onContainerLayout}
>
{content}
<SendMediaButton
onPress={this.sendQueuedMedia}
queueCount={queueCount}
pointerEvents={queuedMediaURIs ? 'auto' : 'none'}
containerStyle={[
this.props.styles.sendButtonContainer,
bottomOffsetStyle,
]}
style={this.sendButtonStyle}
/>
</View>
);
}
flatListRef = (flatList: ?FlatList<MediaLibrarySelection>) => {
this.flatList = flatList;
};
onContainerLayout = (event: LayoutEvent) => {
this.guardedSetState({ containerHeight: event.nativeEvent.layout.height });
};
onEndReached = () => {
const { cursor } = this.state;
if (cursor !== null) {
this.fetchPhotos(cursor);
}
};
onViewableItemsChanged = (info: ViewableItemsChange) => {
const viewableIndices = [];
for (const { index } of info.viewableItems) {
if (index !== null && index !== undefined) {
viewableIndices.push(index);
}
}
this.viewableIndices = viewableIndices;
};
setMediaQueued = (selection: MediaLibrarySelection, isQueued: boolean) => {
this.setState((prevState: State) => {
const prevQueuedMediaURIs = prevState.queuedMediaURIs
? [...prevState.queuedMediaURIs]
: [];
if (isQueued) {
return {
queuedMediaURIs: new Set([...prevQueuedMediaURIs, selection.uri]),
focusedMediaURI: null,
};
}
const queuedMediaURIs = prevQueuedMediaURIs.filter(
uri => uri !== selection.uri,
);
if (queuedMediaURIs.length < prevQueuedMediaURIs.length) {
return {
queuedMediaURIs: new Set(queuedMediaURIs),
focusedMediaURI: null,
};
}
return null;
});
};
setFocus = (selection: MediaLibrarySelection, isFocused: boolean) => {
const { uri } = selection;
if (isFocused) {
this.setState({ focusedMediaURI: uri });
} else if (this.state.focusedMediaURI === uri) {
this.setState({ focusedMediaURI: null });
}
};
sendSingleMedia = (selection: MediaLibrarySelection) => {
this.sendMedia([selection]);
};
sendQueuedMedia = () => {
const { selections, queuedMediaURIs } = this.state;
if (!selections || !queuedMediaURIs) {
return;
}
const queuedSelections = [];
for (const uri of queuedMediaURIs) {
for (const selection of selections) {
if (selection.uri === uri) {
queuedSelections.push(selection);
break;
}
}
}
this.sendMedia(queuedSelections);
};
sendMedia(selections: $ReadOnlyArray<MediaLibrarySelection>) {
if (this.mediaSelected) {
return;
}
this.mediaSelected = true;
const now = Date.now();
const timeProps = {
selectTime: now,
sendTime: now,
};
const selectionsWithTime = selections.map(selection => ({
...selection,
...timeProps,
}));
KeyboardRegistry.onItemSelected(
mediaGalleryKeyboardName,
selectionsWithTime,
);
}
}
const mediaGalleryKeyboardName = 'MediaGalleryKeyboard';
const unboundStyles = {
container: {
alignItems: 'center',
backgroundColor: 'listBackground',
flexDirection: 'row',
left: 0,
position: 'absolute',
right: 0,
top: 0,
},
error: {
color: 'listBackgroundLabel',
flex: 1,
fontSize: 28,
textAlign: 'center',
},
loadingIndicator: {
flex: 1,
},
sendButtonContainer: {
bottom: 20,
position: 'absolute',
right: 30,
},
separator: {
width: 2,
},
};
function ConnectedMediaGalleryKeyboard() {
const dimensions = useSelector(state => state.dimensions);
const foreground = useIsAppForegrounded();
const colors = useColors();
const styles = useStyles(unboundStyles);
return (
<MediaGalleryKeyboard
dimensions={dimensions}
foreground={foreground}
colors={colors}
styles={styles}
/>
);
}
function ReduxMediaGalleryKeyboard() {
return (
<Provider store={store}>
<ConnectedMediaGalleryKeyboard />
</Provider>
);
}
KeyboardRegistry.registerKeyboard(
mediaGalleryKeyboardName,
() => ReduxMediaGalleryKeyboard,
);
export { mediaGalleryKeyboardName };

File Metadata

Mime Type
text/x-diff
Expires
Wed, Dec 25, 5:37 PM (6 h, 44 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
2700595
Default Alt Text
(30 KB)

Event Timeline