diff --git a/lib/types/media-types.js b/lib/types/media-types.js index 36858dab0..c189ffe5a 100644 --- a/lib/types/media-types.js +++ b/lib/types/media-types.js @@ -1,918 +1,496 @@ // @flow -import PropTypes from 'prop-types'; - import type { Shape } from './core'; -import { type Platform, platformPropType } from './device-types'; +import { type Platform } from './device-types'; export type Dimensions = $ReadOnly<{| height: number, width: number, |}>; -export const dimensionsPropType = PropTypes.shape({ - height: PropTypes.number.isRequired, - width: PropTypes.number.isRequired, -}); - 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, // stored on native only during creation in case retry needed after state lost localMediaSelection?: NativeMediaSelection, |}; export type Media = Image | Video; 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 const cornersPropType = PropTypes.shape({ - topLeft: PropTypes.bool, - topRight: PropTypes.bool, - bottomLeft: PropTypes.bool, - bottomRight: PropTypes.bool, -}); - -export const mediaTypePropType = PropTypes.oneOf(['photo', 'video']); - -export const mediaInfoPropType = PropTypes.shape({ - id: PropTypes.string.isRequired, - uri: PropTypes.string.isRequired, - type: mediaTypePropType.isRequired, - dimensions: dimensionsPropType.isRequired, - filename: PropTypes.string, - corners: cornersPropType.isRequired, - index: PropTypes.number.isRequired, -}); - export type UploadMultimediaResult = {| id: string, uri: string, dimensions: Dimensions, mediaType: MediaType, loop: boolean, |}; export type UpdateMultimediaMessageMediaPayload = {| messageID: string, currentMediaID: string, mediaUpdate: Shape, |}; 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 VideoProbeMediaMissionStep = {| step: 'video_probe', success: boolean, exceptionMessage: ?string, time: number, // ms path: string, validFormat: boolean, duration: ?number, // seconds codec: ?string, format: ?$ReadOnlyArray, 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, selectTime: number, // ms timestamp sendTime: number, // ms timestamp retries: number, |} | {| step: 'video_library', dimensions: Dimensions, filename: string, uri: string, mediaNativeID: string, selectTime: number, // ms timestamp sendTime: number, // ms timestamp retries: number, duration: number, // seconds |}; -const photoLibrarySelectionPropType = PropTypes.shape({ - step: PropTypes.oneOf(['photo_library']).isRequired, - dimensions: dimensionsPropType.isRequired, - filename: PropTypes.string.isRequired, - uri: PropTypes.string.isRequired, - mediaNativeID: PropTypes.string.isRequired, - selectTime: PropTypes.number.isRequired, - sendTime: PropTypes.number.isRequired, - retries: PropTypes.number.isRequired, -}); - -const videoLibrarySelectionPropType = PropTypes.shape({ - step: PropTypes.oneOf(['video_library']).isRequired, - dimensions: dimensionsPropType.isRequired, - filename: PropTypes.string.isRequired, - uri: PropTypes.string.isRequired, - mediaNativeID: PropTypes.string.isRequired, - selectTime: PropTypes.number.isRequired, - sendTime: PropTypes.number.isRequired, - retries: PropTypes.number.isRequired, - duration: PropTypes.number.isRequired, -}); - -export const mediaLibrarySelectionPropType = PropTypes.oneOfType([ - photoLibrarySelectionPropType, - videoLibrarySelectionPropType, -]); - 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, |}; -const photoPasteSelectionPropType = PropTypes.exact({ - step: PropTypes.oneOf(['photo_paste']).isRequired, - dimensions: dimensionsPropType.isRequired, - filename: PropTypes.string.isRequired, - uri: PropTypes.string.isRequired, - selectTime: PropTypes.number.isRequired, - sendTime: PropTypes.number.isRequired, - retries: PropTypes.number.isRequired, -}); - 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 | {| step: 'video_ffmpeg_transcode', success: boolean, exceptionMessage: ?string, time: number, // ms returnCode: ?number, newPath: ?string, stats: ?FFmpegStatistics, |} | DisposeTemporaryFileMediaMissionStep | {| step: 'save_media', uri: string, time: number, // ms timestamp |} | {| step: 'permissions_check', success: boolean, exceptionMessage: ?string, time: number, // ms platform: Platform, permissions: $ReadOnlyArray, |} | 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: '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, result: MediaMissionResult, userTime: number, totalTime: number, |}; - -export const mediaMissionStepPropType = PropTypes.oneOfType([ - photoLibrarySelectionPropType, - videoLibrarySelectionPropType, - photoPasteSelectionPropType, - PropTypes.shape({ - step: PropTypes.oneOf(['photo_capture']).isRequired, - time: PropTypes.number.isRequired, - dimensions: dimensionsPropType.isRequired, - filename: PropTypes.string.isRequired, - uri: PropTypes.string.isRequired, - captureTime: PropTypes.number.isRequired, - selectTime: PropTypes.number.isRequired, - sendTime: PropTypes.number.isRequired, - retries: PropTypes.number.isRequired, - }), - PropTypes.shape({ - step: PropTypes.oneOf(['web_selection']).isRequired, - filename: PropTypes.string.isRequired, - size: PropTypes.number.isRequired, - mime: PropTypes.string.isRequired, - selectTime: PropTypes.number.isRequired, - }), - PropTypes.shape({ - step: PropTypes.oneOf(['asset_info_fetch']).isRequired, - success: PropTypes.bool.isRequired, - exceptionMessage: PropTypes.string, - time: PropTypes.number.isRequired, - localURI: PropTypes.string, - orientation: PropTypes.number, - }), - PropTypes.shape({ - step: PropTypes.oneOf(['stat_file']).isRequired, - success: PropTypes.bool.isRequired, - exceptionMessage: PropTypes.string, - time: PropTypes.number.isRequired, - uri: PropTypes.string.isRequired, - fileSize: PropTypes.number, - }), - PropTypes.shape({ - step: PropTypes.oneOf(['read_file_header']).isRequired, - success: PropTypes.bool.isRequired, - exceptionMessage: PropTypes.string, - time: PropTypes.number.isRequired, - uri: PropTypes.string.isRequired, - mime: PropTypes.string, - mediaType: mediaTypePropType, - }), - PropTypes.shape({ - step: PropTypes.oneOf(['determine_file_type']).isRequired, - success: PropTypes.bool.isRequired, - exceptionMessage: PropTypes.string, - time: PropTypes.number.isRequired, - inputFilename: PropTypes.string.isRequired, - outputMIME: PropTypes.string, - outputMediaType: mediaTypePropType, - outputFilename: PropTypes.string, - }), - PropTypes.shape({ - step: PropTypes.oneOf(['frame_count']).isRequired, - success: PropTypes.bool.isRequired, - exceptionMessage: PropTypes.string, - time: PropTypes.number.isRequired, - path: PropTypes.string.isRequired, - mime: PropTypes.string.isRequired, - hasMultipleFrames: PropTypes.bool, - }), - PropTypes.shape({ - step: PropTypes.oneOf(['photo_manipulation']).isRequired, - success: PropTypes.bool.isRequired, - exceptionMessage: PropTypes.string, - time: PropTypes.number.isRequired, - manipulation: PropTypes.object.isRequired, - newMIME: PropTypes.string, - newDimensions: dimensionsPropType, - newURI: PropTypes.string, - }), - PropTypes.shape({ - step: PropTypes.oneOf(['video_probe']).isRequired, - success: PropTypes.bool.isRequired, - exceptionMessage: PropTypes.string, - time: PropTypes.number.isRequired, - path: PropTypes.string.isRequired, - validFormat: PropTypes.bool.isRequired, - duration: PropTypes.number, - codec: PropTypes.string, - format: PropTypes.arrayOf(PropTypes.string), - dimensions: dimensionsPropType, - }), - PropTypes.shape({ - step: PropTypes.oneOf(['video_ffmpeg_transcode']).isRequired, - success: PropTypes.bool.isRequired, - exceptionMessage: PropTypes.string, - time: PropTypes.number.isRequired, - returnCode: PropTypes.number, - newPath: PropTypes.string, - stats: PropTypes.object, - }), - PropTypes.shape({ - step: PropTypes.oneOf(['dispose_temporary_file']).isRequired, - success: PropTypes.bool.isRequired, - exceptionMessage: PropTypes.string, - time: PropTypes.number.isRequired, - path: PropTypes.string.isRequired, - }), - PropTypes.shape({ - step: PropTypes.oneOf(['save_media']).isRequired, - uri: PropTypes.string.isRequired, - time: PropTypes.number.isRequired, - }), - PropTypes.shape({ - step: PropTypes.oneOf(['permissions_check']).isRequired, - success: PropTypes.bool.isRequired, - exceptionMessage: PropTypes.string, - time: PropTypes.number.isRequired, - platform: platformPropType.isRequired, - permissions: PropTypes.arrayOf(PropTypes.string).isRequired, - }), - PropTypes.shape({ - step: PropTypes.oneOf(['make_directory']).isRequired, - success: PropTypes.bool.isRequired, - exceptionMessage: PropTypes.string, - time: PropTypes.number.isRequired, - path: PropTypes.string.isRequired, - }), - PropTypes.shape({ - step: PropTypes.oneOf(['android_scan_file']).isRequired, - success: PropTypes.bool.isRequired, - exceptionMessage: PropTypes.string, - time: PropTypes.number.isRequired, - path: PropTypes.string.isRequired, - }), - PropTypes.shape({ - step: PropTypes.oneOf(['fetch_file_hash']).isRequired, - success: PropTypes.bool.isRequired, - exceptionMessage: PropTypes.string, - time: PropTypes.number.isRequired, - path: PropTypes.string.isRequired, - hash: PropTypes.string, - }), - PropTypes.shape({ - step: PropTypes.oneOf(['copy_file']).isRequired, - success: PropTypes.bool.isRequired, - exceptionMessage: PropTypes.string, - time: PropTypes.number.isRequired, - source: PropTypes.string.isRequired, - destination: PropTypes.string.isRequired, - }), - PropTypes.shape({ - step: PropTypes.oneOf(['ios_save_to_library']).isRequired, - success: PropTypes.bool.isRequired, - exceptionMessage: PropTypes.string, - time: PropTypes.number.isRequired, - uri: PropTypes.string.isRequired, - }), - PropTypes.shape({ - step: PropTypes.oneOf(['fetch_blob']).isRequired, - success: PropTypes.bool.isRequired, - exceptionMessage: PropTypes.string, - time: PropTypes.number.isRequired, - inputURI: PropTypes.string.isRequired, - uri: PropTypes.string.isRequired, - size: PropTypes.number, - mime: PropTypes.string, - }), - PropTypes.shape({ - step: PropTypes.oneOf(['data_uri_from_blob']).isRequired, - success: PropTypes.bool.isRequired, - exceptionMessage: PropTypes.string, - time: PropTypes.number.isRequired, - first255Chars: PropTypes.string, - }), - PropTypes.shape({ - step: PropTypes.oneOf(['array_buffer_from_blob']).isRequired, - success: PropTypes.bool.isRequired, - exceptionMessage: PropTypes.string, - time: PropTypes.number.isRequired, - }), - PropTypes.shape({ - step: PropTypes.oneOf(['mime_check']).isRequired, - success: PropTypes.bool.isRequired, - exceptionMessage: PropTypes.string, - time: PropTypes.number.isRequired, - mime: PropTypes.string, - }), - PropTypes.shape({ - step: PropTypes.oneOf(['write_file']).isRequired, - success: PropTypes.bool.isRequired, - exceptionMessage: PropTypes.string, - time: PropTypes.number.isRequired, - path: PropTypes.string.isRequired, - length: PropTypes.number.isRequired, - }), - PropTypes.shape({ - step: PropTypes.oneOf(['exif_fetch']).isRequired, - success: PropTypes.bool.isRequired, - exceptionMessage: PropTypes.string, - time: PropTypes.number.isRequired, - orientation: PropTypes.number, - }), - PropTypes.shape({ - step: PropTypes.oneOf(['preload_image']).isRequired, - success: PropTypes.bool.isRequired, - exceptionMessage: PropTypes.string, - time: PropTypes.number.isRequired, - uri: PropTypes.string.isRequired, - dimensions: dimensionsPropType, - }), - PropTypes.shape({ - step: PropTypes.oneOf(['reorient_image']).isRequired, - success: PropTypes.bool.isRequired, - exceptionMessage: PropTypes.string, - time: PropTypes.number.isRequired, - uri: PropTypes.string, - }), - PropTypes.shape({ - step: PropTypes.oneOf(['upload']).isRequired, - success: PropTypes.bool.isRequired, - exceptionMessage: PropTypes.string, - time: PropTypes.number.isRequired, - inputFilename: PropTypes.string.isRequired, - outputMediaType: mediaTypePropType, - outputURI: PropTypes.string, - outputDimensions: dimensionsPropType, - outputLoop: PropTypes.bool, - hasWiFi: PropTypes.bool, - }), - PropTypes.shape({ - step: PropTypes.oneOf(['wait_for_capture_uri_unload']).isRequired, - success: PropTypes.bool.isRequired, - time: PropTypes.number.isRequired, - uri: PropTypes.string.isRequired, - }), -]); - -export const mediaMissionPropType = PropTypes.shape({ - steps: PropTypes.arrayOf(mediaMissionStepPropType).isRequired, - result: PropTypes.oneOfType([ - PropTypes.shape({ - success: PropTypes.oneOf([true]).isRequired, - }), - PropTypes.shape({ - success: PropTypes.oneOf([false]).isRequired, - reason: PropTypes.oneOf(['no_file_path']).isRequired, - }), - PropTypes.shape({ - success: PropTypes.oneOf([false]).isRequired, - reason: PropTypes.oneOf(['file_stat_failed']).isRequired, - uri: PropTypes.string.isRequired, - }), - PropTypes.shape({ - success: PropTypes.oneOf([false]).isRequired, - reason: PropTypes.oneOf(['photo_manipulation_failed']).isRequired, - size: PropTypes.number.isRequired, - }), - PropTypes.shape({ - success: PropTypes.oneOf([false]).isRequired, - reason: PropTypes.oneOf(['media_type_fetch_failed']).isRequired, - detectedMIME: PropTypes.string, - }), - PropTypes.shape({ - success: PropTypes.oneOf([false]).isRequired, - reason: PropTypes.oneOf(['mime_type_mismatch']).isRequired, - reportedMediaType: mediaTypePropType.isRequired, - reportedMIME: PropTypes.string.isRequired, - detectedMIME: PropTypes.string.isRequired, - }), - PropTypes.shape({ - success: PropTypes.oneOf([false]).isRequired, - reason: PropTypes.oneOf(['http_upload_failed']).isRequired, - exceptionMessage: PropTypes.string, - }), - PropTypes.shape({ - success: PropTypes.oneOf([false]).isRequired, - reason: PropTypes.oneOf(['video_too_long']).isRequired, - duration: PropTypes.number.isRequired, - }), - PropTypes.shape({ - success: PropTypes.oneOf([false]).isRequired, - reason: PropTypes.oneOf(['video_probe_failed']).isRequired, - }), - PropTypes.shape({ - success: PropTypes.oneOf([false]).isRequired, - reason: PropTypes.oneOf(['video_transcode_failed']).isRequired, - }), - PropTypes.shape({ - success: PropTypes.oneOf([false]).isRequired, - reason: PropTypes.oneOf(['processing_exception']).isRequired, - time: PropTypes.number.isRequired, - exceptionMessage: PropTypes.string, - }), - PropTypes.shape({ - success: PropTypes.oneOf([false]).isRequired, - reason: PropTypes.oneOf(['save_unsupported']).isRequired, - }), - PropTypes.shape({ - success: PropTypes.oneOf([false]).isRequired, - reason: PropTypes.oneOf(['missing_permission']).isRequired, - }), - PropTypes.shape({ - success: PropTypes.oneOf([false]).isRequired, - reason: PropTypes.oneOf(['make_directory_failed']).isRequired, - }), - PropTypes.shape({ - success: PropTypes.oneOf([false]).isRequired, - reason: PropTypes.oneOf(['resolve_failed']).isRequired, - uri: PropTypes.string.isRequired, - }), - PropTypes.shape({ - success: PropTypes.oneOf([false]).isRequired, - reason: PropTypes.oneOf(['save_to_library_failed']).isRequired, - uri: PropTypes.string.isRequired, - }), - PropTypes.shape({ - success: PropTypes.oneOf([false]).isRequired, - reason: PropTypes.oneOf(['fetch_failed']).isRequired, - }), - PropTypes.shape({ - success: PropTypes.oneOf([false]).isRequired, - reason: PropTypes.oneOf(['data_uri_failed']).isRequired, - }), - PropTypes.shape({ - success: PropTypes.oneOf([false]).isRequired, - reason: PropTypes.oneOf(['array_buffer_failed']).isRequired, - }), - PropTypes.shape({ - success: PropTypes.oneOf([false]).isRequired, - reason: PropTypes.oneOf(['mime_check_failed']).isRequired, - mime: PropTypes.string, - }), - PropTypes.shape({ - success: PropTypes.oneOf([false]).isRequired, - reason: PropTypes.oneOf(['write_file_failed']).isRequired, - }), - PropTypes.shape({ - success: PropTypes.oneOf([false]).isRequired, - reason: PropTypes.oneOf(['fetch_file_hash_failed']).isRequired, - }), - PropTypes.shape({ - success: PropTypes.oneOf([false]).isRequired, - reason: PropTypes.oneOf(['copy_file_failed']).isRequired, - }), - PropTypes.shape({ - success: PropTypes.oneOf([false]).isRequired, - reason: PropTypes.oneOf(['exif_fetch_failed']).isRequired, - }), - PropTypes.shape({ - success: PropTypes.oneOf([false]).isRequired, - reason: PropTypes.oneOf(['reorient_image_failed']).isRequired, - }), - PropTypes.shape({ - success: PropTypes.oneOf([false]).isRequired, - reason: PropTypes.oneOf(['web_sibling_validation_failed']).isRequired, - }), - ]), - userTime: PropTypes.number.isRequired, - totalTime: PropTypes.number.isRequired, -}); diff --git a/lib/types/report-types.js b/lib/types/report-types.js index e00d0dd09..09db4bd36 100644 --- a/lib/types/report-types.js +++ b/lib/types/report-types.js @@ -1,233 +1,178 @@ // @flow import invariant from 'invariant'; -import PropTypes from 'prop-types'; - -import { type PlatformDetails, platformDetailsPropType } from './device-types'; -import { - type RawEntryInfo, - type CalendarQuery, - rawEntryInfoPropType, - calendarQueryPropType, -} from './entry-types'; -import { type MediaMission, mediaMissionPropType } from './media-types'; + +import { type PlatformDetails } from './device-types'; +import { type RawEntryInfo, type CalendarQuery } from './entry-types'; +import { type MediaMission } from './media-types'; import type { AppState, BaseAction } from './redux-types'; -import { type RawThreadInfo, rawThreadInfoPropType } from './thread-types'; +import { type RawThreadInfo } from './thread-types'; import type { UserInfo, UserInfos } from './user-types'; export const reportTypes = Object.freeze({ ERROR: 0, THREAD_INCONSISTENCY: 1, ENTRY_INCONSISTENCY: 2, MEDIA_MISSION: 3, USER_INCONSISTENCY: 4, }); type ReportType = $Values; export function assertReportType(reportType: number): ReportType { invariant( reportType === 0 || reportType === 1 || reportType === 2 || reportType === 3 || reportType === 4, 'number is not ReportType enum', ); return reportType; } export type ErrorInfo = { componentStack: string }; export type ErrorData = {| error: Error, info?: ErrorInfo |}; export type FlatErrorData = {| errorMessage: string, stack?: string, componentStack?: ?string, |}; export type ActionSummary = {| type: $PropertyType, time: number, summary: string, |}; export type ThreadInconsistencyReportShape = {| platformDetails: PlatformDetails, beforeAction: { [id: string]: RawThreadInfo }, action: BaseAction, pollResult?: { [id: string]: RawThreadInfo }, pushResult: { [id: string]: RawThreadInfo }, lastActionTypes?: $ReadOnlyArray<$PropertyType>, lastActions?: $ReadOnlyArray, time?: number, |}; export type EntryInconsistencyReportShape = {| platformDetails: PlatformDetails, beforeAction: { [id: string]: RawEntryInfo }, action: BaseAction, calendarQuery: CalendarQuery, pollResult?: { [id: string]: RawEntryInfo }, pushResult: { [id: string]: RawEntryInfo }, lastActionTypes?: $ReadOnlyArray<$PropertyType>, lastActions?: $ReadOnlyArray, time: number, |}; export type UserInconsistencyReportShape = {| platformDetails: PlatformDetails, action: BaseAction, beforeStateCheck: UserInfos, afterStateCheck: UserInfos, lastActions: $ReadOnlyArray, time: number, |}; type ErrorReportCreationRequest = {| type: 0, platformDetails: PlatformDetails, errors: $ReadOnlyArray, preloadedState: AppState, currentState: AppState, actions: $ReadOnlyArray, |}; export type ThreadInconsistencyReportCreationRequest = {| ...ThreadInconsistencyReportShape, type: 1, |}; export type EntryInconsistencyReportCreationRequest = {| ...EntryInconsistencyReportShape, type: 2, |}; export type MediaMissionReportCreationRequest = {| type: 3, platformDetails: PlatformDetails, time: number, // ms mediaMission: MediaMission, uploadServerID?: ?string, uploadLocalID?: ?string, mediaLocalID?: ?string, // deprecated messageServerID?: ?string, messageLocalID?: ?string, |}; export type UserInconsistencyReportCreationRequest = {| ...UserInconsistencyReportShape, type: 4, |}; export type ReportCreationRequest = | ErrorReportCreationRequest | ThreadInconsistencyReportCreationRequest | EntryInconsistencyReportCreationRequest | MediaMissionReportCreationRequest | UserInconsistencyReportCreationRequest; export type ClientThreadInconsistencyReportShape = {| platformDetails: PlatformDetails, beforeAction: { [id: string]: RawThreadInfo }, action: BaseAction, pushResult: { [id: string]: RawThreadInfo }, lastActions: $ReadOnlyArray, time: number, |}; export type ClientEntryInconsistencyReportShape = {| platformDetails: PlatformDetails, beforeAction: { [id: string]: RawEntryInfo }, action: BaseAction, calendarQuery: CalendarQuery, pushResult: { [id: string]: RawEntryInfo }, lastActions: $ReadOnlyArray, time: number, |}; export type ClientThreadInconsistencyReportCreationRequest = {| ...ClientThreadInconsistencyReportShape, type: 1, |}; export type ClientEntryInconsistencyReportCreationRequest = {| ...ClientEntryInconsistencyReportShape, type: 2, |}; export type ClientReportCreationRequest = | ErrorReportCreationRequest | ClientThreadInconsistencyReportCreationRequest | ClientEntryInconsistencyReportCreationRequest | MediaMissionReportCreationRequest | UserInconsistencyReportCreationRequest; export type QueueReportsPayload = {| reports: $ReadOnlyArray, |}; export type ClearDeliveredReportsPayload = {| reports: $ReadOnlyArray, |}; -const actionSummaryPropType = PropTypes.shape({ - type: PropTypes.string.isRequired, - time: PropTypes.number.isRequired, - summary: PropTypes.string.isRequired, -}); -export const queuedClientReportCreationRequestPropType = PropTypes.oneOfType([ - PropTypes.shape({ - type: PropTypes.oneOf([reportTypes.THREAD_INCONSISTENCY]).isRequired, - platformDetails: platformDetailsPropType.isRequired, - beforeAction: PropTypes.objectOf(rawThreadInfoPropType).isRequired, - action: PropTypes.object.isRequired, - pollResult: PropTypes.objectOf(rawThreadInfoPropType), - pushResult: PropTypes.objectOf(rawThreadInfoPropType).isRequired, - lastActions: PropTypes.arrayOf(actionSummaryPropType).isRequired, - time: PropTypes.number.isRequired, - }), - PropTypes.shape({ - type: PropTypes.oneOf([reportTypes.ENTRY_INCONSISTENCY]).isRequired, - platformDetails: platformDetailsPropType.isRequired, - beforeAction: PropTypes.objectOf(rawEntryInfoPropType).isRequired, - action: PropTypes.object.isRequired, - calendarQuery: calendarQueryPropType.isRequired, - pollResult: PropTypes.objectOf(rawEntryInfoPropType), - pushResult: PropTypes.objectOf(rawEntryInfoPropType).isRequired, - lastActions: PropTypes.arrayOf(actionSummaryPropType).isRequired, - time: PropTypes.number.isRequired, - }), - PropTypes.shape({ - type: PropTypes.oneOf([reportTypes.MEDIA_MISSION]).isRequired, - platformDetails: platformDetailsPropType.isRequired, - time: PropTypes.number.isRequired, - mediaMission: mediaMissionPropType.isRequired, - uploadServerID: PropTypes.string, - uploadLocalID: PropTypes.string, - mediaLocalID: PropTypes.string, - messageServerID: PropTypes.string, - messageLocalID: PropTypes.string, - }), - PropTypes.shape({ - type: PropTypes.oneOf([reportTypes.USER_INCONSISTENCY]).isRequired, - platformDetails: platformDetailsPropType.isRequired, - action: PropTypes.object.isRequired, - beforeStateCheck: PropTypes.objectOf(rawThreadInfoPropType).isRequired, - afterStateCheck: PropTypes.objectOf(rawThreadInfoPropType).isRequired, - lastActions: PropTypes.arrayOf(actionSummaryPropType).isRequired, - time: PropTypes.number.isRequired, - }), -]); - export type ReportCreationResponse = {| id: string, |}; type ReportInfo = {| id: string, viewerID: string, platformDetails: PlatformDetails, creationTime: number, |}; export type FetchErrorReportInfosRequest = {| cursor: ?string, |}; export type FetchErrorReportInfosResponse = {| reports: $ReadOnlyArray, userInfos: $ReadOnlyArray, |}; export type ReduxToolsImport = {| preloadedState: AppState, payload: $ReadOnlyArray, |}; diff --git a/native/media/media-gallery-media.react.js b/native/media/media-gallery-media.react.js index 855898f66..89cc130ca 100644 --- a/native/media/media-gallery-media.react.js +++ b/native/media/media-gallery-media.react.js @@ -1,306 +1,288 @@ // @flow import LottieView from 'lottie-react-native'; -import PropTypes from 'prop-types'; import * as React from 'react'; import { TouchableOpacity, StyleSheet, View, Text, Platform, Animated, Easing, } from 'react-native'; import Reanimated, { Easing as ReanimatedEasing, } from 'react-native-reanimated'; import Icon from 'react-native-vector-icons/FontAwesome'; import MaterialIcon from 'react-native-vector-icons/MaterialIcons'; import Video from 'react-native-video'; -import { - type MediaLibrarySelection, - mediaLibrarySelectionPropType, -} from 'lib/types/media-types'; +import { type MediaLibrarySelection } from 'lib/types/media-types'; import GestureTouchableOpacity from '../components/gesture-touchable-opacity.react'; -import { - type DimensionsInfo, - dimensionsInfoPropType, -} from '../redux/dimensions-updater.react'; +import { type DimensionsInfo } from '../redux/dimensions-updater.react'; import type { ViewStyle, ImageStyle } from '../types/styles'; const animatedSpec = { duration: 400, easing: Easing.inOut(Easing.ease), useNativeDriver: true, }; const reanimatedSpec = { duration: 400, easing: ReanimatedEasing.inOut(ReanimatedEasing.ease), }; type Props = {| +selection: MediaLibrarySelection, +containerHeight: number, +queueModeActive: boolean, +isQueued: boolean, +setMediaQueued: (media: MediaLibrarySelection, isQueued: boolean) => void, +sendMedia: (media: MediaLibrarySelection) => void, +isFocused: boolean, +setFocus: (media: MediaLibrarySelection, isFocused: boolean) => void, +dimensions: DimensionsInfo, |}; class MediaGalleryMedia extends React.PureComponent { - static propTypes = { - selection: mediaLibrarySelectionPropType.isRequired, - containerHeight: PropTypes.number.isRequired, - queueModeActive: PropTypes.bool.isRequired, - isQueued: PropTypes.bool.isRequired, - setMediaQueued: PropTypes.func.isRequired, - sendMedia: PropTypes.func.isRequired, - isFocused: PropTypes.bool.isRequired, - setFocus: PropTypes.func.isRequired, - dimensions: dimensionsInfoPropType.isRequired, - }; // eslint-disable-next-line import/no-named-as-default-member focusProgress = new Reanimated.Value(0); buttonsStyle: ViewStyle; mediaStyle: ImageStyle; checkProgress = new Animated.Value(0); constructor(props: Props) { super(props); // eslint-disable-next-line import/no-named-as-default-member const buttonsScale = Reanimated.interpolate(this.focusProgress, { inputRange: [0, 1], outputRange: [1.3, 1], }); this.buttonsStyle = { ...styles.buttons, opacity: this.focusProgress, transform: [{ scale: buttonsScale }], marginBottom: this.props.dimensions.bottomInset, }; // eslint-disable-next-line import/no-named-as-default-member const mediaScale = Reanimated.interpolate(this.focusProgress, { inputRange: [0, 1], outputRange: [1, 1.3], }); this.mediaStyle = { transform: [{ scale: mediaScale }], }; } static isActive(props: Props) { return props.isFocused || props.isQueued; } componentDidUpdate(prevProps: Props) { const isActive = MediaGalleryMedia.isActive(this.props); const wasActive = MediaGalleryMedia.isActive(prevProps); if (isActive && !wasActive) { // eslint-disable-next-line import/no-named-as-default-member Reanimated.timing(this.focusProgress, { ...reanimatedSpec, toValue: 1, }).start(); } else if (!isActive && wasActive) { // eslint-disable-next-line import/no-named-as-default-member Reanimated.timing(this.focusProgress, { ...reanimatedSpec, toValue: 0, }).start(); } if (this.props.isQueued && !prevProps.isQueued) { // When I updated to React Native 0.60, I also updated Lottie. At that // time, on iOS the last frame of the animation drops the circle outlining // the checkmark. This is a hack to get around that const maxValue = Platform.OS === 'ios' ? 0.99 : 1; Animated.timing(this.checkProgress, { ...animatedSpec, toValue: maxValue, }).start(); } else if (!this.props.isQueued && prevProps.isQueued) { Animated.timing(this.checkProgress, { ...animatedSpec, toValue: 0, }).start(); } } render() { const { selection, containerHeight } = this.props; const { uri, dimensions: { width, height }, step, } = selection; const active = MediaGalleryMedia.isActive(this.props); const scaledWidth = height ? (width * containerHeight) / height : 0; const dimensionsStyle = { height: containerHeight, width: Math.max(Math.min(scaledWidth, this.props.dimensions.width), 150), }; let buttons = null; const { queueModeActive } = this.props; if (!queueModeActive) { buttons = ( <> Send Queue ); } let media; const source = { uri }; if (step === 'video_library') { let resizeMode = 'contain'; if (Platform.OS === 'ios') { const [major, minor] = Platform.Version.split('.'); if (parseInt(major, 10) === 14 && parseInt(minor, 10) < 2) { resizeMode = 'stretch'; } } media = ( ); } else { media = ( ); } const overlay = ( ); return ( {media} {buttons} ); } onPressBackdrop = () => { if (this.props.isQueued) { this.props.setMediaQueued(this.props.selection, false); } else if (this.props.queueModeActive) { this.props.setMediaQueued(this.props.selection, true); } else { this.props.setFocus(this.props.selection, !this.props.isFocused); } }; onPressSend = () => { this.props.sendMedia(this.props.selection); }; onPressEnqueue = () => { this.props.setMediaQueued(this.props.selection, true); }; } const buttonStyle = { flexDirection: 'row', alignItems: 'flex-start', margin: 10, borderRadius: 20, paddingLeft: 20, paddingRight: 20, paddingTop: 10, paddingBottom: 10, }; const styles = StyleSheet.create({ buttonIcon: { alignSelf: Platform.OS === 'android' ? 'center' : 'flex-end', color: 'white', fontSize: 18, marginRight: 6, paddingRight: 5, }, buttonText: { color: 'white', fontSize: 16, }, buttons: { alignItems: 'center', bottom: 0, justifyContent: 'center', left: 0, position: 'absolute', right: 0, top: 0, }, checkAnimation: { position: 'absolute', width: 128, }, container: { flex: 1, overflow: 'hidden', }, enqueueButton: { ...buttonStyle, backgroundColor: '#2A78E5', }, sendButton: { ...buttonStyle, backgroundColor: '#7ED321', paddingLeft: 18, }, }); export default MediaGalleryMedia; diff --git a/native/media/multimedia.react.js b/native/media/multimedia.react.js index a5b1bd2be..bd422d427 100644 --- a/native/media/multimedia.react.js +++ b/native/media/multimedia.react.js @@ -1,154 +1,144 @@ // @flow import invariant from 'invariant'; -import PropTypes from 'prop-types'; import * as React from 'react'; import { View, Image, StyleSheet } from 'react-native'; -import { type MediaInfo, mediaInfoPropType } from 'lib/types/media-types'; +import { type MediaInfo } from 'lib/types/media-types'; -import { - type InputState, - inputStatePropType, - InputStateContext, -} from '../input/input-state'; +import { type InputState, InputStateContext } from '../input/input-state'; import RemoteImage from './remote-image.react'; type BaseProps = {| +mediaInfo: MediaInfo, +spinnerColor: string, |}; type Props = {| ...BaseProps, // withInputState +inputState: ?InputState, |}; type State = {| +currentURI: string, +departingURI: ?string, |}; class Multimedia extends React.PureComponent { - static propTypes = { - mediaInfo: mediaInfoPropType.isRequired, - spinnerColor: PropTypes.string.isRequired, - inputState: inputStatePropType, - }; static defaultProps = { spinnerColor: 'black', }; constructor(props: Props) { super(props); this.state = { currentURI: props.mediaInfo.uri, departingURI: null, }; } get inputState() { const { inputState } = this.props; invariant(inputState, 'inputState should be set in Multimedia'); return inputState; } componentDidMount() { this.inputState.reportURIDisplayed(this.state.currentURI, true); } componentWillUnmount() { const { inputState } = this; const { currentURI, departingURI } = this.state; inputState.reportURIDisplayed(currentURI, false); if (departingURI) { inputState.reportURIDisplayed(departingURI, false); } } componentDidUpdate(prevProps: Props, prevState: State) { const { inputState } = this; const newURI = this.props.mediaInfo.uri; const oldURI = this.state.currentURI; if (newURI !== oldURI) { inputState.reportURIDisplayed(newURI, true); const { departingURI } = this.state; if (departingURI && oldURI !== departingURI) { // If there's currently an existing departingURI, that means that oldURI // hasn't loaded yet. Since it's being replaced anyways we don't need to // display it anymore, so we can unlink it now inputState.reportURIDisplayed(oldURI, false); this.setState({ currentURI: newURI }); } else { this.setState({ currentURI: newURI, departingURI: oldURI }); } } const newDepartingURI = this.state.departingURI; const oldDepartingURI = prevState.departingURI; if (oldDepartingURI && oldDepartingURI !== newDepartingURI) { inputState.reportURIDisplayed(oldDepartingURI, false); } } render() { const images = []; const { currentURI, departingURI } = this.state; if (departingURI) { images.push(this.renderURI(currentURI, true)); images.push(this.renderURI(departingURI, true)); } else { images.push(this.renderURI(currentURI)); } return {images}; } renderURI(uri: string, invisibleLoad?: boolean = false) { if (uri.startsWith('http')) { return ( ); } else { const source = { uri }; return ( ); } } onLoad = () => { this.setState({ departingURI: null }); }; } const styles = StyleSheet.create({ container: { flex: 1, }, image: { bottom: 0, left: 0, position: 'absolute', right: 0, top: 0, }, }); export default React.memo(function ConnectedMultimedia( props: BaseProps, ) { const inputState = React.useContext(InputStateContext); return ; }); diff --git a/web/chat/chat-input-bar.react.js b/web/chat/chat-input-bar.react.js index b54c80852..034a0d895 100644 --- a/web/chat/chat-input-bar.react.js +++ b/web/chat/chat-input-bar.react.js @@ -1,480 +1,462 @@ // @flow import { faFileImage } from '@fortawesome/free-regular-svg-icons'; import { faChevronRight } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import invariant from 'invariant'; import _difference from 'lodash/fp/difference'; -import PropTypes from 'prop-types'; import * as React from 'react'; import { joinThreadActionTypes, joinThread } from 'lib/actions/thread-actions'; import { createLoadingStatusSelector } from 'lib/selectors/loading-selectors'; import { trimMessage } from 'lib/shared/message-utils'; import { threadHasPermission, viewerIsMember, threadFrozenDueToViewerBlock, threadActualMembers, useRealThreadCreator, } from 'lib/shared/thread-utils'; import type { CalendarQuery } from 'lib/types/entry-types'; -import { loadingStatusPropType } from 'lib/types/loading-types'; import type { LoadingStatus } from 'lib/types/loading-types'; import { messageTypes } from 'lib/types/message-types'; import { type ThreadInfo, - threadInfoPropType, threadPermissions, type ClientThreadJoinRequest, type ThreadJoinPayload, } from 'lib/types/thread-types'; -import { type UserInfos, userInfoPropType } from 'lib/types/user-types'; +import { type UserInfos } from 'lib/types/user-types'; import { type DispatchActionPromise, useServerCall, useDispatchActionPromise, } from 'lib/utils/action-utils'; import { - inputStatePropType, type InputState, type PendingMultimediaUpload, } from '../input/input-state'; import LoadingIndicator from '../loading-indicator.react'; import { allowedMimeTypeString } from '../media/file-utils'; import Multimedia from '../media/multimedia.react'; import FailedSendModal from '../modals/chat/failed-send.react'; import { useSelector } from '../redux/redux-utils'; import { nonThreadCalendarQuery } from '../selectors/nav-selectors'; import css from './chat-message-list.css'; type BaseProps = {| +threadInfo: ThreadInfo, +inputState: InputState, +setModal: (modal: ?React.Node) => void, |}; type Props = {| ...BaseProps, // Redux state +viewerID: ?string, +joinThreadLoadingStatus: LoadingStatus, +calendarQuery: () => CalendarQuery, +nextLocalID: number, +isThreadActive: boolean, +userInfos: UserInfos, // Redux dispatch functions +dispatchActionPromise: DispatchActionPromise, // async functions that hit server APIs +joinThread: (request: ClientThreadJoinRequest) => Promise, +getServerThreadID: () => Promise, |}; class ChatInputBar extends React.PureComponent { - static propTypes = { - threadInfo: threadInfoPropType.isRequired, - inputState: inputStatePropType.isRequired, - setModal: PropTypes.func.isRequired, - viewerID: PropTypes.string, - joinThreadLoadingStatus: loadingStatusPropType.isRequired, - calendarQuery: PropTypes.func.isRequired, - nextLocalID: PropTypes.number.isRequired, - isThreadActive: PropTypes.bool.isRequired, - userInfos: PropTypes.objectOf(userInfoPropType).isRequired, - dispatchActionPromise: PropTypes.func.isRequired, - joinThread: PropTypes.func.isRequired, - getServerThreadID: PropTypes.func.isRequired, - }; textarea: ?HTMLTextAreaElement; multimediaInput: ?HTMLInputElement; componentDidMount() { this.updateHeight(); if (this.props.isThreadActive) { this.addReplyListener(); } } componentWillUnmount() { if (this.props.isThreadActive) { this.removeReplyListener(); } } componentDidUpdate(prevProps: Props) { if (this.props.isThreadActive && !prevProps.isThreadActive) { this.addReplyListener(); } else if (!this.props.isThreadActive && prevProps.isThreadActive) { this.removeReplyListener(); } const { inputState } = this.props; const prevInputState = prevProps.inputState; if (inputState.draft !== prevInputState.draft) { this.updateHeight(); } const curUploadIDs = ChatInputBar.unassignedUploadIDs( inputState.pendingUploads, ); const prevUploadIDs = ChatInputBar.unassignedUploadIDs( prevInputState.pendingUploads, ); if ( this.multimediaInput && _difference(prevUploadIDs)(curUploadIDs).length > 0 ) { // Whenever a pending upload is removed, we reset the file // HTMLInputElement's value field, so that if the same upload occurs again // the onChange call doesn't get filtered this.multimediaInput.value = ''; } else if ( this.textarea && _difference(curUploadIDs)(prevUploadIDs).length > 0 ) { // Whenever a pending upload is added, we focus the textarea this.textarea.focus(); return; } if (this.props.threadInfo.id !== prevProps.threadInfo.id && this.textarea) { this.textarea.focus(); } } static unassignedUploadIDs( pendingUploads: $ReadOnlyArray, ) { return pendingUploads .filter( (pendingUpload: PendingMultimediaUpload) => !pendingUpload.messageID, ) .map((pendingUpload: PendingMultimediaUpload) => pendingUpload.localID); } updateHeight() { const textarea = this.textarea; if (textarea) { textarea.style.height = 'auto'; const newHeight = Math.min(textarea.scrollHeight, 150); textarea.style.height = `${newHeight}px`; } } addReplyListener() { invariant( this.props.inputState, 'inputState should be set in addReplyListener', ); this.props.inputState.addReplyListener(this.focusAndUpdateText); } removeReplyListener() { invariant( this.props.inputState, 'inputState should be set in removeReplyListener', ); this.props.inputState.removeReplyListener(this.focusAndUpdateText); } render() { const isMember = viewerIsMember(this.props.threadInfo); const canJoin = threadHasPermission( this.props.threadInfo, threadPermissions.JOIN_THREAD, ); let joinButton = null; if (!isMember && canJoin) { let buttonContent; if (this.props.joinThreadLoadingStatus === 'loading') { buttonContent = ( ); } else { buttonContent = Join Thread; } joinButton = ( ); } const { pendingUploads, cancelPendingUpload } = this.props.inputState; const multimediaPreviews = pendingUploads.map((pendingUpload) => ( )); const previews = multimediaPreviews.length > 0 ? (
{multimediaPreviews}
) : null; let content; if (threadHasPermission(this.props.threadInfo, threadPermissions.VOICED)) { const sendIconStyle = { color: `#${this.props.threadInfo.color}` }; content = (