UNPKG

@atlaskit/editor-plugin-media

Version:

Media plugin for @atlaskit/editor-core

481 lines (480 loc) 17.3 kB
import _defineProperty from "@babel/runtime/helpers/defineProperty"; // eslint-disable-next-line @atlaskit/platform/prefer-crypto-random-uuid -- Use crypto.randomUUID instead import uuidV4 from 'uuid/v4'; import { ACTION, ACTION_SUBJECT, EVENT_TYPE } from '@atlaskit/editor-common/analytics'; import { DEFAULT_IMAGE_HEIGHT, DEFAULT_IMAGE_WIDTH } from '@atlaskit/editor-common/media-single'; import { getAttrsFromUrl, isImageRepresentationReady, isMediaBlobUrl } from '@atlaskit/media-client'; import { getMediaClient } from '@atlaskit/media-client-react'; import { getClientIdForFile } from '@atlaskit/media-common'; import { fg } from '@atlaskit/platform-feature-flags'; import { replaceExternalMedia, updateCurrentMediaNodeAttrs, updateMediaNodeAttrs } from '../pm-plugins/commands/helpers'; import { stateKey as mediaStateKey } from '../pm-plugins/plugin-key'; import { batchMediaNodeAttrsUpdate } from '../pm-plugins/utils/batchMediaNodeAttrs'; import { getIdentifier } from '../pm-plugins/utils/media-common'; const isMediaTypeSupported = type => { if (type) { return ['image', 'file'].includes(type); } return false; }; export class MediaNodeUpdater { constructor(props) { // Updates the node with contextId if it doesn't have one already _defineProperty(this, "updateContextId", async () => { const attrs = this.getAttrs(); if (!attrs || attrs && !isMediaTypeSupported(attrs.type)) { return; } const { id } = attrs; const objectId = await this.getObjectId(); batchMediaNodeAttrsUpdate(this.props.view, { id: id, nextAttributes: { __contextId: objectId } }); }); _defineProperty(this, "updateNodeContextId", async getPos => { const attrs = this.getAttrs(); if (!attrs || attrs && !isMediaTypeSupported(attrs.type)) { return; } const objectId = await this.getObjectId(); updateCurrentMediaNodeAttrs({ __contextId: objectId }, { node: this.props.node, getPos })(this.props.view.state, this.props.view.dispatch); }); _defineProperty(this, "hasFileAttributesDefined", attrs => { return attrs && attrs.type === 'file' && attrs.__fileName && attrs.__fileMimeType && attrs.__fileSize && attrs.__contextId; }); _defineProperty(this, "getNewFileAttrsForNode", async () => { const attrs = this.getAttrs(); const mediaProvider = await this.props.mediaProvider; if (!mediaProvider || !mediaProvider.uploadParams || !attrs || !isMediaTypeSupported(attrs.type) || this.hasFileAttributesDefined(attrs)) { return; } const mediaClientConfig = mediaProvider.viewMediaClientConfig; const mediaClient = getMediaClient(mediaClientConfig); let fileState; const { id, collection: collectionName } = attrs; try { fileState = await mediaClient.file.getCurrentState(id, { collectionName }); if (fileState.status === 'error') { return; } } catch { return; } const contextId = this.getNodeContextId() || (await this.getObjectId()); const { name, mimeType, size } = fileState; const newAttrs = { __fileName: name, __fileMimeType: mimeType, __fileSize: size, __contextId: contextId }; if (!hasPrivateAttrsChanged(attrs, newAttrs)) { return; } return newAttrs; }); _defineProperty(this, "updateMediaSingleFileAttrs", async () => { const newAttrs = await this.getNewFileAttrsForNode(); const { id } = this.getAttrs(); if (id && newAttrs) { batchMediaNodeAttrsUpdate(this.props.view, { id: id, nextAttributes: newAttrs }); } }); _defineProperty(this, "updateNodeAttrs", async getPos => { const newAttrs = await this.getNewFileAttrsForNode(); if (newAttrs) { updateCurrentMediaNodeAttrs(newAttrs, { node: this.props.node, getPos })(this.props.view.state, this.props.view.dispatch); } }); _defineProperty(this, "getAttrs", () => { const { attrs } = this.props.node; if (attrs) { return attrs; } return undefined; }); _defineProperty(this, "getObjectId", async () => { const contextIdentifierProvider = await this.props.contextIdentifierProvider; return (contextIdentifierProvider === null || contextIdentifierProvider === void 0 ? void 0 : contextIdentifierProvider.objectId) || null; }); _defineProperty(this, "uploadExternalMedia", async getPos => { const { node, mediaOptions } = this.props; if (mediaOptions !== null && mediaOptions !== void 0 && mediaOptions.isExternalMediaUploadDisabled) { return; } const mediaProvider = await this.props.mediaProvider; if (node && mediaProvider) { const uploadMediaClientConfig = mediaProvider.uploadMediaClientConfig; if (!uploadMediaClientConfig || !node.attrs.url) { return; } const mediaClient = getMediaClient(uploadMediaClientConfig); const collection = mediaProvider.uploadParams && mediaProvider.uploadParams.collection; try { const uploader = await mediaClient.file.uploadExternal(node.attrs.url, collection); const { uploadableFileUpfrontIds, dimensions } = uploader; const pos = getPos(); if (typeof pos !== 'number') { return; } replaceExternalMedia(pos + 1, { id: uploadableFileUpfrontIds.id, collection, height: dimensions.height, width: dimensions.width, occurrenceKey: uploadableFileUpfrontIds.occurrenceKey })(this.props.view.state, this.props.view.dispatch); } catch { //keep it as external media if (this.props.dispatchAnalyticsEvent) { this.props.dispatchAnalyticsEvent({ action: ACTION.UPLOAD_EXTERNAL_FAIL, actionSubject: ACTION_SUBJECT.EDITOR, eventType: EVENT_TYPE.OPERATIONAL }); } } } }); _defineProperty(this, "getNodeContextId", () => { const attrs = this.getAttrs(); if (!attrs || attrs && !isMediaTypeSupported(attrs.type)) { return null; } return attrs.__contextId || null; }); _defineProperty(this, "updateDimensions", dimensions => { batchMediaNodeAttrsUpdate(this.props.view, { id: dimensions.id, nextAttributes: { height: dimensions.height, width: dimensions.width } }); }); _defineProperty(this, "shouldNodeBeDeepCopied", async () => { var _ref, _this$props$mediaOpti, _this$props$mediaOpti2, _this$mediaPluginStat, _this$mediaPluginStat2; const scope = (_ref = (_this$props$mediaOpti = (_this$props$mediaOpti2 = this.props.mediaOptions) === null || _this$props$mediaOpti2 === void 0 ? void 0 : _this$props$mediaOpti2.mediaShallowCopyScope) !== null && _this$props$mediaOpti !== void 0 ? _this$props$mediaOpti : (_this$mediaPluginStat = this.mediaPluginState) === null || _this$mediaPluginStat === void 0 ? void 0 : (_this$mediaPluginStat2 = _this$mediaPluginStat.mediaOptions) === null || _this$mediaPluginStat2 === void 0 ? void 0 : _this$mediaPluginStat2.mediaShallowCopyScope) !== null && _ref !== void 0 ? _ref : 'context'; if (scope === 'context') { return await this.hasDifferentContextId(); } else { const attrs = this.getAttrs(); if (!attrs || !this.mediaPluginState) { return false; } const id = getIdentifier(attrs); const isIdentifierOutsideEditorScope = !this.mediaPluginState.isIdentifierInEditorScope(id); return isIdentifierOutsideEditorScope; } }); _defineProperty(this, "hasDifferentContextId", async () => { const nodeContextId = this.getNodeContextId(); const currentContextId = await this.getObjectId(); if (nodeContextId && currentContextId && nodeContextId !== currentContextId) { return true; } return false; }); _defineProperty(this, "isNodeFromDifferentCollection", async () => { const mediaProvider = await this.props.mediaProvider; if (!mediaProvider || !mediaProvider.uploadParams) { return false; } const currentCollectionName = mediaProvider.uploadParams.collection; const attrs = this.getAttrs(); if (!attrs || attrs && !isMediaTypeSupported(attrs.type)) { return false; } const { collection: nodeCollection, __contextId } = attrs; const contextId = __contextId || (await this.getObjectId()); if (contextId && currentCollectionName !== nodeCollection) { return true; } return false; }); _defineProperty(this, "handleCopyFileSwitcher", async attrs => { const { mediaClient, source, destination, traceContext } = attrs; try { // calling copyWithToken by passing the auth providers const { id } = await mediaClient.file.copyFile(source, destination, undefined, traceContext); return id; } catch (err) { if (fg('platform_media_cross_client_copy')) { // calling /v2/file/copy by removing the auth tokens to make cross product copy and pastes const { authProvider: _sourceAP, ...copyV2Source } = source; const { authProvider: _destAP, ...copyV2Destination } = destination; const { id } = await mediaClient.file.copyFile(copyV2Source, copyV2Destination, undefined, traceContext); return id; } else { throw err; } } }); _defineProperty(this, "copyNodeFromBlobUrl", async getPos => { const attrs = this.getAttrs(); if (!attrs || attrs.type !== 'external') { return; } const { url } = attrs; const mediaAttrs = getAttrsFromUrl(url); if (!mediaAttrs) { return; } const mediaProvider = await this.props.mediaProvider; if (!mediaProvider || !mediaProvider.uploadParams) { return; } const currentCollectionName = mediaProvider.uploadParams.collection; const { contextId, clientId, id, collection, height, width, mimeType, name, size } = mediaAttrs; const uploadMediaClientConfig = mediaProvider.uploadMediaClientConfig; if (!uploadMediaClientConfig || !uploadMediaClientConfig.getAuthFromContext) { return; } const mediaClient = getMediaClient(uploadMediaClientConfig); const getAuthFromContext = uploadMediaClientConfig.getAuthFromContext; const mediaFileId = await this.handleCopyFileSwitcher({ mediaClient, source: { id, collection, authProvider: () => getAuthFromContext(contextId), clientId: fg('platform_media_cross_client_copy_with_auth') ? clientId : undefined }, destination: { collection: currentCollectionName, authProvider: uploadMediaClientConfig.authProvider, // eslint-disable-next-line @atlaskit/platform/prefer-crypto-random-uuid -- Use crypto.randomUUID instead occurrenceKey: uuidV4() } }); const pos = getPos(); if (typeof pos !== 'number') { return; } replaceExternalMedia(pos + 1, { id: mediaFileId, collection: currentCollectionName, height, width, __fileName: name, __fileMimeType: mimeType, __fileSize: size })(this.props.view.state, this.props.view.dispatch); }); // Copies the pasted node into the current collection using a getPos handler _defineProperty(this, "copyNodeFromPos", async (getPos, traceContext) => { const attrs = this.getAttrs(); if (!attrs || attrs && !isMediaTypeSupported(attrs.type)) { return; } const copiedAttrs = await this.copyFile(attrs.id, attrs.collection, traceContext); if (!copiedAttrs) { return; } updateCurrentMediaNodeAttrs(copiedAttrs, { node: this.props.node, getPos })(this.props.view.state, this.props.view.dispatch); }); // Copies the pasted node into the current collection _defineProperty(this, "copyNode", async traceContext => { const attrs = this.getAttrs(); const { view } = this.props; if (!attrs || attrs && !isMediaTypeSupported(attrs.type)) { return; } const copiedAttrs = await this.copyFile(attrs.id, attrs.collection, traceContext); if (!copiedAttrs) { return; } updateMediaNodeAttrs(attrs.id, copiedAttrs)(view.state, view.dispatch); }); _defineProperty(this, "copyFile", async (id, collection, traceContext) => { const mediaProvider = await this.props.mediaProvider; if (!(mediaProvider !== null && mediaProvider !== void 0 && mediaProvider.uploadParams)) { return; } const nodeContextId = this.getNodeContextId(); const uploadMediaClientConfig = mediaProvider.uploadMediaClientConfig; if (!(uploadMediaClientConfig !== null && uploadMediaClientConfig !== void 0 && uploadMediaClientConfig.getAuthFromContext) || !nodeContextId) { return; } const mediaClient = getMediaClient(uploadMediaClientConfig); const clientId = fg('platform_media_cross_client_copy_with_auth') ? getClientIdForFile(id) : undefined; const currentCollectionName = mediaProvider.uploadParams.collection; const objectId = await this.getObjectId(); const getAuthFromContext = uploadMediaClientConfig.getAuthFromContext; const mediaFileId = await this.handleCopyFileSwitcher({ mediaClient, source: { id, collection, authProvider: () => getAuthFromContext(nodeContextId), clientId }, destination: { collection: currentCollectionName, authProvider: uploadMediaClientConfig.authProvider, // eslint-disable-next-line @atlaskit/platform/prefer-crypto-random-uuid -- Use crypto.randomUUID instead occurrenceKey: uuidV4() }, traceContext }); return { id: mediaFileId, collection: currentCollectionName, __contextId: objectId }; }); this.props = props; this.mediaPluginState = mediaStateKey.getState(props.view.state); } setProps(newComponentProps) { this.props = { ...this.props, ...newComponentProps }; } isMediaBlobUrl() { const attrs = this.getAttrs(); return !!(attrs && attrs.type === 'external' && isMediaBlobUrl(attrs.url)); } async getRemoteDimensions() { const mediaProvider = await this.props.mediaProvider; const { mediaOptions } = this.props; const attrs = this.getAttrs(); if (!mediaProvider || !attrs) { return false; } const { height, width } = attrs; if (attrs.type === 'external' || !attrs.id) { return false; } const { id, collection } = attrs; if (height && width) { return false; } // can't fetch remote dimensions on mobile, so we'll default them if (mediaOptions && !mediaOptions.allowRemoteDimensionsFetch) { return { id, height: DEFAULT_IMAGE_HEIGHT, width: DEFAULT_IMAGE_WIDTH }; } const viewMediaClientConfig = mediaProvider.viewMediaClientConfig; const mediaClient = getMediaClient(viewMediaClientConfig); const currentState = await mediaClient.file.getCurrentState(id, { collectionName: collection }); if (!isImageRepresentationReady(currentState)) { return false; } const imageMetadata = await mediaClient.getImageMetadata(id, { collection }); if (!imageMetadata || !imageMetadata.original) { return false; } return { id, height: imageMetadata.original.height || DEFAULT_IMAGE_HEIGHT, width: imageMetadata.original.width || DEFAULT_IMAGE_WIDTH }; } async handleExternalMedia(getPos) { if (this.isMediaBlobUrl()) { try { await this.copyNodeFromBlobUrl(getPos); } catch { await this.uploadExternalMedia(getPos); } } else { await this.uploadExternalMedia(getPos); } } } const hasPrivateAttrsChanged = (currentAttrs, newAttrs) => { return currentAttrs.__fileName !== newAttrs.__fileName || currentAttrs.__fileMimeType !== newAttrs.__fileMimeType || currentAttrs.__fileSize !== newAttrs.__fileSize || currentAttrs.__contextId !== newAttrs.__contextId; }; export const createMediaNodeUpdater = props => { const updaterProps = { ...props }; return new MediaNodeUpdater(updaterProps); };