Changeset View
Changeset View
Standalone View
Standalone View
web/input/input-state-container.react.js
// @flow | // @flow | ||||
import { detect as detectBrowser } from 'detect-browser'; | import { detect as detectBrowser } from 'detect-browser'; | ||||
import invariant from 'invariant'; | import invariant from 'invariant'; | ||||
import _groupBy from 'lodash/fp/groupBy.js'; | import _groupBy from 'lodash/fp/groupBy.js'; | ||||
import _keyBy from 'lodash/fp/keyBy.js'; | import _keyBy from 'lodash/fp/keyBy.js'; | ||||
import _omit from 'lodash/fp/omit.js'; | import _omit from 'lodash/fp/omit.js'; | ||||
import _partition from 'lodash/fp/partition.js'; | import _partition from 'lodash/fp/partition.js'; | ||||
import _sortBy from 'lodash/fp/sortBy.js'; | import _sortBy from 'lodash/fp/sortBy.js'; | ||||
import _memoize from 'lodash/memoize.js'; | import _memoize from 'lodash/memoize.js'; | ||||
import _throttle from 'lodash/throttle.js'; | |||||
import * as React from 'react'; | import * as React from 'react'; | ||||
import { useDispatch } from 'react-redux'; | import { useDispatch } from 'react-redux'; | ||||
import { createSelector } from 'reselect'; | import { createSelector } from 'reselect'; | ||||
import uuid from 'uuid'; | |||||
import { | import { | ||||
createLocalMessageActionType, | createLocalMessageActionType, | ||||
sendMultimediaMessageActionTypes, | sendMultimediaMessageActionTypes, | ||||
legacySendMultimediaMessage, | legacySendMultimediaMessage, | ||||
sendTextMessageActionTypes, | sendTextMessageActionTypes, | ||||
sendTextMessage, | sendTextMessage, | ||||
} from 'lib/actions/message-actions.js'; | } from 'lib/actions/message-actions.js'; | ||||
import { queueReportsActionType } from 'lib/actions/report-actions.js'; | import { queueReportsActionType } from 'lib/actions/report-actions.js'; | ||||
import { newThread } from 'lib/actions/thread-actions.js'; | import { newThread } from 'lib/actions/thread-actions.js'; | ||||
import { | import { | ||||
uploadMultimedia, | uploadMultimedia, | ||||
uploadMediaMetadata, | |||||
updateMultimediaMessageMediaActionType, | updateMultimediaMessageMediaActionType, | ||||
deleteUpload, | deleteUpload, | ||||
type MultimediaUploadCallbacks, | type MultimediaUploadCallbacks, | ||||
type MultimediaUploadExtras, | type MultimediaUploadExtras, | ||||
} from 'lib/actions/upload-actions.js'; | } from 'lib/actions/upload-actions.js'; | ||||
import { | import { | ||||
useModalContext, | useModalContext, | ||||
type PushModal, | type PushModal, | ||||
} from 'lib/components/modal-provider.react.js'; | } from 'lib/components/modal-provider.react.js'; | ||||
import blobService from 'lib/facts/blob-service.js'; | |||||
import commStaffCommunity from 'lib/facts/comm-staff-community.js'; | import commStaffCommunity from 'lib/facts/comm-staff-community.js'; | ||||
import { getNextLocalUploadID } from 'lib/media/media-utils.js'; | import { getNextLocalUploadID } from 'lib/media/media-utils.js'; | ||||
import { pendingToRealizedThreadIDsSelector } from 'lib/selectors/thread-selectors.js'; | import { pendingToRealizedThreadIDsSelector } from 'lib/selectors/thread-selectors.js'; | ||||
import { | import { | ||||
createMediaMessageInfo, | createMediaMessageInfo, | ||||
localIDPrefix, | localIDPrefix, | ||||
useMessageCreationSideEffectsFunc, | useMessageCreationSideEffectsFunc, | ||||
} from 'lib/shared/message-utils.js'; | } from 'lib/shared/message-utils.js'; | ||||
import type { CreationSideEffectsFunc } from 'lib/shared/messages/message-spec.js'; | import type { CreationSideEffectsFunc } from 'lib/shared/messages/message-spec.js'; | ||||
import { | import { | ||||
createRealThreadFromPendingThread, | createRealThreadFromPendingThread, | ||||
draftKeyFromThreadID, | draftKeyFromThreadID, | ||||
threadIsPending, | threadIsPending, | ||||
threadIsPendingSidebar, | threadIsPendingSidebar, | ||||
patchThreadInfoToIncludeMentionedMembersOfParent, | patchThreadInfoToIncludeMentionedMembersOfParent, | ||||
threadInfoInsideCommunity, | threadInfoInsideCommunity, | ||||
} from 'lib/shared/thread-utils.js'; | } from 'lib/shared/thread-utils.js'; | ||||
import type { CalendarQuery } from 'lib/types/entry-types.js'; | import type { CalendarQuery } from 'lib/types/entry-types.js'; | ||||
import type { | import type { | ||||
UploadMultimediaResult, | UploadMultimediaResult, | ||||
UploadMediaMetadataRequest, | |||||
MediaMissionStep, | MediaMissionStep, | ||||
MediaMissionFailure, | MediaMissionFailure, | ||||
MediaMissionResult, | MediaMissionResult, | ||||
MediaMission, | MediaMission, | ||||
Dimensions, | |||||
} from 'lib/types/media-types.js'; | } from 'lib/types/media-types.js'; | ||||
import { messageTypes } from 'lib/types/message-types-enum.js'; | import { messageTypes } from 'lib/types/message-types-enum.js'; | ||||
import { | import { | ||||
type RawMessageInfo, | type RawMessageInfo, | ||||
type RawMultimediaMessageInfo, | type RawMultimediaMessageInfo, | ||||
type SendMessageResult, | type SendMessageResult, | ||||
type SendMessagePayload, | type SendMessagePayload, | ||||
} from 'lib/types/message-types.js'; | } from 'lib/types/message-types.js'; | ||||
Show All 9 Lines | import { | ||||
type RawThreadInfo, | type RawThreadInfo, | ||||
threadTypes, | threadTypes, | ||||
} from 'lib/types/thread-types.js'; | } from 'lib/types/thread-types.js'; | ||||
import { | import { | ||||
type DispatchActionPromise, | type DispatchActionPromise, | ||||
useServerCall, | useServerCall, | ||||
useDispatchActionPromise, | useDispatchActionPromise, | ||||
} from 'lib/utils/action-utils.js'; | } from 'lib/utils/action-utils.js'; | ||||
import { toBase64URL } from 'lib/utils/base64.js'; | |||||
import { makeBlobServiceEndpointURL } from 'lib/utils/blob-service.js'; | |||||
import type { CallServerEndpointOptions } from 'lib/utils/call-server-endpoint.js'; | |||||
import { getConfig } from 'lib/utils/config.js'; | import { getConfig } from 'lib/utils/config.js'; | ||||
import { getMessageForException, cloneError } from 'lib/utils/errors.js'; | import { getMessageForException, cloneError } from 'lib/utils/errors.js'; | ||||
import { | import { | ||||
type PendingMultimediaUpload, | type PendingMultimediaUpload, | ||||
type TypeaheadState, | type TypeaheadState, | ||||
InputStateContext, | InputStateContext, | ||||
} from './input-state.js'; | } from './input-state.js'; | ||||
Show All 22 Lines | type Props = { | ||||
+dispatch: Dispatch, | +dispatch: Dispatch, | ||||
+dispatchActionPromise: DispatchActionPromise, | +dispatchActionPromise: DispatchActionPromise, | ||||
+calendarQuery: () => CalendarQuery, | +calendarQuery: () => CalendarQuery, | ||||
+uploadMultimedia: ( | +uploadMultimedia: ( | ||||
multimedia: Object, | multimedia: Object, | ||||
extras: MultimediaUploadExtras, | extras: MultimediaUploadExtras, | ||||
callbacks: MultimediaUploadCallbacks, | callbacks: MultimediaUploadCallbacks, | ||||
) => Promise<UploadMultimediaResult>, | ) => Promise<UploadMultimediaResult>, | ||||
+uploadMediaMetadata: ( | |||||
input: UploadMediaMetadataRequest, | |||||
) => Promise<UploadMultimediaResult>, | |||||
+deleteUpload: (id: string) => Promise<void>, | +deleteUpload: (id: string) => Promise<void>, | ||||
+sendMultimediaMessage: ( | +sendMultimediaMessage: ( | ||||
threadID: string, | threadID: string, | ||||
localID: string, | localID: string, | ||||
mediaIDs: $ReadOnlyArray<string>, | mediaIDs: $ReadOnlyArray<string>, | ||||
sidebarCreation?: boolean, | sidebarCreation?: boolean, | ||||
) => Promise<SendMessageResult>, | ) => Promise<SendMessageResult>, | ||||
+sendTextMessage: ( | +sendTextMessage: ( | ||||
▲ Show 20 Lines • Show All 857 Lines • ▼ Show 20 Lines | this.setState(prevState => { | ||||
loop: result.loop, | loop: result.loop, | ||||
}, | }, | ||||
}, | }, | ||||
}, | }, | ||||
}; | }; | ||||
}); | }); | ||||
} | } | ||||
async blobServiceUpload( | |||||
input: { | |||||
file: File, | |||||
blobHash: string, | |||||
encryptionKey: string, | |||||
dimensions: Dimensions, | |||||
loop?: boolean, | |||||
}, | |||||
options?: ?CallServerEndpointOptions, | |||||
): Promise<void> { | |||||
const newHolder = uuid.v4(); | |||||
const blobHash = toBase64URL(input.blobHash); | |||||
// 1. Assign new holder for blob with given blobHash | |||||
let blobAlreadyExists: boolean; | |||||
try { | |||||
const assignHolderEndpoint = blobService.httpEndpoints.ASSIGN_HOLDER; | |||||
const assignHolderResponse = await fetch( | |||||
makeBlobServiceEndpointURL(assignHolderEndpoint), | |||||
{ | |||||
method: assignHolderEndpoint.method, | |||||
body: JSON.stringify({ | |||||
holder: newHolder, | |||||
blob_hash: blobHash, | |||||
}), | |||||
headers: { | |||||
'content-type': 'application/json', | |||||
}, | |||||
}, | |||||
); | |||||
if (!assignHolderResponse.ok) { | |||||
const { status, statusText } = assignHolderResponse; | |||||
throw new Error(`Server responded with HTTP ${status}: ${statusText}`); | |||||
} | |||||
const { data_exists: dataExistsResponse } = | |||||
await assignHolderResponse.json(); | |||||
blobAlreadyExists = dataExistsResponse; | |||||
} catch (e) { | |||||
throw new Error( | |||||
`Failed to assign holder: ${ | |||||
getMessageForException(e) ?? 'unknown error' | |||||
}`, | |||||
); | |||||
} | |||||
// 2. Upload blob contents if blob doesn't exist | |||||
if (!blobAlreadyExists) { | |||||
const formData = new FormData(); | |||||
formData.append('blob_hash', blobHash); | |||||
formData.append('blob_data', input.file); | |||||
const xhr = new XMLHttpRequest(); | |||||
const uploadEndpoint = blobService.httpEndpoints.UPLOAD_BLOB; | |||||
xhr.open( | |||||
uploadEndpoint.method, | |||||
makeBlobServiceEndpointURL(uploadEndpoint), | |||||
); | |||||
if (options?.timeout) { | |||||
xhr.timeout = options.timeout; | |||||
} | |||||
if (options && options.onProgress) { | |||||
const { onProgress } = options; | |||||
xhr.upload.onprogress = _throttle( | |||||
({ loaded, total }) => onProgress(loaded / total), | |||||
50, | |||||
); | |||||
} | |||||
let failed = false; | |||||
const responsePromise = new Promise((resolve, reject) => { | |||||
xhr.onload = () => { | |||||
if (failed) { | |||||
return; | |||||
} | |||||
resolve(); | |||||
}; | |||||
xhr.onabort = () => { | |||||
failed = true; | |||||
reject(new Error('request aborted')); | |||||
}; | |||||
xhr.onerror = event => { | |||||
failed = true; | |||||
reject(event); | |||||
}; | |||||
if (options && options.timeout) { | |||||
xhr.ontimeout = event => { | |||||
failed = true; | |||||
reject(event); | |||||
}; | |||||
} | |||||
if (options && options.abortHandler) { | |||||
options.abortHandler(() => { | |||||
failed = true; | |||||
reject(new Error('request aborted')); | |||||
xhr.abort(); | |||||
}); | |||||
} | |||||
}); | |||||
if (!failed) { | |||||
xhr.send(formData); | |||||
} | |||||
await responsePromise; | |||||
} | |||||
// 3. Send upload metadata to the keyserver, return response | |||||
return await this.props.uploadMediaMetadata({ | |||||
...input.dimensions, | |||||
loop: input.loop ?? false, | |||||
blobHolder: newHolder, | |||||
encryptionKey: input.encryptionKey, | |||||
mimeType: input.file.type, | |||||
filename: input.file.name, | |||||
}); | |||||
} | |||||
handleAbortCallback( | handleAbortCallback( | ||||
threadID: string, | threadID: string, | ||||
localUploadID: string, | localUploadID: string, | ||||
abort: () => void, | abort: () => void, | ||||
) { | ) { | ||||
this.setState(prevState => { | this.setState(prevState => { | ||||
const newThreadID = this.getRealizedOrPendingThreadID(threadID); | const newThreadID = this.getRealizedOrPendingThreadID(threadID); | ||||
const uploads = prevState.pendingUploads[newThreadID]; | const uploads = prevState.pendingUploads[newThreadID]; | ||||
▲ Show 20 Lines • Show All 513 Lines • ▼ Show 20 Lines | React.memo<BaseProps>(function ConnectedInputStateContainer(props) { | ||||
const threadStoreThreadInfos = useSelector( | const threadStoreThreadInfos = useSelector( | ||||
state => state.threadStore.threadInfos, | state => state.threadStore.threadInfos, | ||||
); | ); | ||||
const pendingToRealizedThreadIDs = useSelector(state => | const pendingToRealizedThreadIDs = useSelector(state => | ||||
pendingToRealizedThreadIDsSelector(state.threadStore.threadInfos), | pendingToRealizedThreadIDsSelector(state.threadStore.threadInfos), | ||||
); | ); | ||||
const calendarQuery = useSelector(nonThreadCalendarQuery); | const calendarQuery = useSelector(nonThreadCalendarQuery); | ||||
const callUploadMultimedia = useServerCall(uploadMultimedia); | const callUploadMultimedia = useServerCall(uploadMultimedia); | ||||
const callUploadMediaMetadata = useServerCall(uploadMediaMetadata); | |||||
const callDeleteUpload = useServerCall(deleteUpload); | const callDeleteUpload = useServerCall(deleteUpload); | ||||
const callSendMultimediaMessage = useServerCall( | const callSendMultimediaMessage = useServerCall( | ||||
legacySendMultimediaMessage, | legacySendMultimediaMessage, | ||||
); | ); | ||||
const callSendTextMessage = useServerCall(sendTextMessage); | const callSendTextMessage = useServerCall(sendTextMessage); | ||||
const callNewThread = useServerCall(newThread); | const callNewThread = useServerCall(newThread); | ||||
const dispatch = useDispatch(); | const dispatch = useDispatch(); | ||||
const dispatchActionPromise = useDispatchActionPromise(); | const dispatchActionPromise = useDispatchActionPromise(); | ||||
Show All 22 Lines | return ( | ||||
activeChatThreadID={activeChatThreadID} | activeChatThreadID={activeChatThreadID} | ||||
drafts={drafts} | drafts={drafts} | ||||
viewerID={viewerID} | viewerID={viewerID} | ||||
messageStoreMessages={messageStoreMessages} | messageStoreMessages={messageStoreMessages} | ||||
threadStoreThreadInfos={threadStoreThreadInfos} | threadStoreThreadInfos={threadStoreThreadInfos} | ||||
pendingRealizedThreadIDs={pendingToRealizedThreadIDs} | pendingRealizedThreadIDs={pendingToRealizedThreadIDs} | ||||
calendarQuery={calendarQuery} | calendarQuery={calendarQuery} | ||||
uploadMultimedia={callUploadMultimedia} | uploadMultimedia={callUploadMultimedia} | ||||
uploadMediaMetadata={callUploadMediaMetadata} | |||||
deleteUpload={callDeleteUpload} | deleteUpload={callDeleteUpload} | ||||
sendMultimediaMessage={callSendMultimediaMessage} | sendMultimediaMessage={callSendMultimediaMessage} | ||||
sendTextMessage={callSendTextMessage} | sendTextMessage={callSendTextMessage} | ||||
newThread={callNewThread} | newThread={callNewThread} | ||||
dispatch={dispatch} | dispatch={dispatch} | ||||
dispatchActionPromise={dispatchActionPromise} | dispatchActionPromise={dispatchActionPromise} | ||||
pushModal={modalContext.pushModal} | pushModal={modalContext.pushModal} | ||||
sendCallbacks={sendCallbacks} | sendCallbacks={sendCallbacks} | ||||
registerSendCallback={registerSendCallback} | registerSendCallback={registerSendCallback} | ||||
unregisterSendCallback={unregisterSendCallback} | unregisterSendCallback={unregisterSendCallback} | ||||
textMessageCreationSideEffectsFunc={textMessageCreationSideEffectsFunc} | textMessageCreationSideEffectsFunc={textMessageCreationSideEffectsFunc} | ||||
/> | /> | ||||
); | ); | ||||
}); | }); | ||||
export default ConnectedInputStateContainer; | export default ConnectedInputStateContainer; |