UNPKG

@atlaskit/editor-plugin-collab-edit

Version:

Collab Edit plugin for @atlaskit/editor-core

285 lines (283 loc) 13.8 kB
import { ACTION, ACTION_SUBJECT, EVENT_TYPE } from '@atlaskit/editor-common/analytics'; import { isEmptyDocument } from '@atlaskit/editor-common/utils'; import { JSONTransformer } from '@atlaskit/editor-json-transformer'; import { AddMarkStep, AddNodeMarkStep } from '@atlaskit/editor-prosemirror/transform'; import { fg } from '@atlaskit/platform-feature-flags'; import { collab, getCollabState, sendableSteps } from '@atlaskit/prosemirror-collab'; import { expValEquals } from '@atlaskit/tmp-editor-statsig/exp-val-equals'; import { editorExperiment } from '@atlaskit/tmp-editor-statsig/experiments'; import { addSynchronyErrorAnalytics } from './pm-plugins/analytics'; import { sendTransaction } from './pm-plugins/events/send-transaction'; import { filterAnalyticsSteps } from './pm-plugins/filterAnalytics'; import { createPlugin } from './pm-plugins/main'; import { pluginKey as mainPluginKey } from './pm-plugins/main/plugin-key'; import { mergeUnconfirmedSteps } from './pm-plugins/mergeUnconfirmed'; import { monitorOrganic } from './pm-plugins/monitor-organic-changes'; import { nativeCollabProviderPlugin } from './pm-plugins/native-collab-provider-plugin'; import { sanitizeFilteredStep, createPlugin as trackSpammingStepsPlugin } from './pm-plugins/track-and-filter-spamming-steps'; import { createPlugin as createLastOrganicChangePlugin, trackLastOrganicChangePluginKey } from './pm-plugins/track-last-organic-change'; import { createPlugin as createTrackNCSInitializationPlugin, trackNCSInitializationPluginKey } from './pm-plugins/track-ncs-initialization'; import { createPlugin as createTrackReconnectionConflictPlugin, trackLastRemoteConflictPluginKey } from './pm-plugins/track-reconnection-conflict'; import { track } from './pm-plugins/track-steps'; import { getAvatarColor } from './pm-plugins/utils'; const providerBuilder = collabEditProviderPromise => async (codeToExecute, onError) => { try { const provider = await collabEditProviderPromise; if (provider) { return codeToExecute(provider); } } catch (err) { if (onError) { onError(err); } else { // eslint-disable-next-line no-console console.error(err); } } }; const createAddInlineCommentMark = providerPromise => ({ from, to, mark }) => { providerPromise.then(provider => { var _provider$api; const commentMark = new AddMarkStep(Math.min(from, to), Math.max(from, to), mark); // @ts-expect-error 2339: Property 'api' does not exist on type 'CollabEditProvider<CollabEvents>'. (_provider$api = provider.api) === null || _provider$api === void 0 ? void 0 : _provider$api.addComment([commentMark]); }); return false; }; const createAddInlineCommentNodeMark = providerPromise => ({ pos, mark }) => { providerPromise.then(provider => { var _provider$api2; const commentMark = new AddNodeMarkStep(pos, mark); // @ts-expect-error 2339: Property 'api' does not exist on type 'CollabEditProvider<CollabEvents>'. (_provider$api2 = provider.api) === null || _provider$api2 === void 0 ? void 0 : _provider$api2.addComment([commentMark]); }); return false; }; export const collabEditPlugin = ({ config: options, api }) => { var _api$featureFlags; const featureFlags = (api === null || api === void 0 ? void 0 : (_api$featureFlags = api.featureFlags) === null || _api$featureFlags === void 0 ? void 0 : _api$featureFlags.sharedState.currentState()) || {}; let providerResolver = () => {}; const editorViewRef = { current: null }; const collabEditProviderPromise = new Promise(_providerResolver => { providerResolver = _providerResolver; }); const executeProviderCode = providerBuilder(collabEditProviderPromise); return { name: 'collabEdit', getSharedState(state) { if (!state) { return { initialised: { collabInitialisedAt: null, firstChangeAfterInitAt: null, firstContentBodyChangeAfterInitAt: null, lastLocalOrganicChangeAt: null, lastRemoteOrganicChangeAt: null, lastLocalOrganicBodyChangeAt: null, lastRemoteOrganicBodyChangeAt: null }, activeParticipants: undefined, sessionId: undefined, lastReconnectionConflictMetadata: undefined }; } const collabPluginState = mainPluginKey.getState(state); const metadata = trackNCSInitializationPluginKey.getState(state); const lastOrganicChangeState = trackLastOrganicChangePluginKey.getState(state); const lastRemoteConflict = trackLastRemoteConflictPluginKey.getState(state); return { activeParticipants: collabPluginState === null || collabPluginState === void 0 ? void 0 : collabPluginState.activeParticipants, sessionId: collabPluginState === null || collabPluginState === void 0 ? void 0 : collabPluginState.sessionId, initialised: { collabInitialisedAt: (metadata === null || metadata === void 0 ? void 0 : metadata.collabInitialisedAt) || null, firstChangeAfterInitAt: (metadata === null || metadata === void 0 ? void 0 : metadata.firstChangeAfterInitAt) || null, firstContentBodyChangeAfterInitAt: (metadata === null || metadata === void 0 ? void 0 : metadata.firstContentBodyChangeAfterInitAt) || null, lastLocalOrganicChangeAt: (lastOrganicChangeState === null || lastOrganicChangeState === void 0 ? void 0 : lastOrganicChangeState.lastLocalOrganicChangeAt) || null, lastRemoteOrganicChangeAt: (lastOrganicChangeState === null || lastOrganicChangeState === void 0 ? void 0 : lastOrganicChangeState.lastRemoteOrganicChangeAt) || null, lastLocalOrganicBodyChangeAt: (lastOrganicChangeState === null || lastOrganicChangeState === void 0 ? void 0 : lastOrganicChangeState.lastLocalOrganicBodyChangeAt) || null, lastRemoteOrganicBodyChangeAt: (lastOrganicChangeState === null || lastOrganicChangeState === void 0 ? void 0 : lastOrganicChangeState.lastRemoteOrganicBodyChangeAt) || null }, lastReconnectionConflictMetadata: lastRemoteConflict }; }, actions: { getAvatarColor, addInlineCommentMark: createAddInlineCommentMark(collabEditProviderPromise), addInlineCommentNodeMark: createAddInlineCommentNodeMark(collabEditProviderPromise), isRemoteReplaceDocumentTransaction: tr => tr.getMeta('isRemote') && tr.getMeta('replaceDocument'), getCurrentCollabState: () => { var _getCollabState; // Ignored via go/ees005 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const adfDocument = new JSONTransformer().encode(editorViewRef.current.state.doc); return { content: adfDocument, // Ignored via go/ees005 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion version: ((_getCollabState = getCollabState(editorViewRef.current.state)) === null || _getCollabState === void 0 ? void 0 : _getCollabState.version) || 0, // Ignored via go/ees005 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion sendableSteps: sendableSteps(editorViewRef.current.state) }; }, // Ignored via go/ees005 // eslint-disable-next-line @typescript-eslint/no-explicit-any validatePMJSONDocument: doc => { // Ignored via go/ees005 // eslint-disable-next-line @typescript-eslint/no-explicit-any const content = (doc.content || []).map(child => // Ignored via go/ees005 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion // Ignored via go/ees005 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion editorViewRef.current.state.schema.nodeFromJSON(child)); return content.every(node => { try { node.check(); // this will throw an error if the node is invalid } catch { return false; } return true; }); } }, pmPlugins() { const { useNativePlugin = false, userId = null } = options || {}; const transformUnconfirmed = steps => { let transformed = steps; if (editorExperiment('platform_editor_reduce_noisy_steps_ncs', true, { exposure: true })) { transformed = filterAnalyticsSteps(transformed); } if (editorExperiment('platform_editor_offline_editing_web', true) || expValEquals('platform_editor_enable_single_player_step_merging', 'isEnabled', true)) { transformed = mergeUnconfirmedSteps(transformed, api); } return transformed; }; const plugins = [...(useNativePlugin ? [{ name: 'pmCollab', plugin: () => collab({ clientID: userId, transformUnconfirmed }) }, { name: 'nativeCollabProviderPlugin', plugin: () => nativeCollabProviderPlugin({ providerPromise: collabEditProviderPromise }) }] : []), { name: 'collab', plugin: ({ dispatch, providerFactory }) => { return createPlugin(dispatch, providerFactory, providerResolver, executeProviderCode, options, featureFlags, api); } }, { name: 'collabTrackNCSInitializationPlugin', plugin: createTrackNCSInitializationPlugin }]; plugins.push({ name: 'trackAndFilterSpammingSteps', plugin: () => trackSpammingStepsPlugin(tr => { var _api$analytics, _api$analytics$action; const sanitizedSteps = tr.steps.map(step => sanitizeFilteredStep(step)); api === null || api === void 0 ? void 0 : (_api$analytics = api.analytics) === null || _api$analytics === void 0 ? void 0 : (_api$analytics$action = _api$analytics.actions) === null || _api$analytics$action === void 0 ? void 0 : _api$analytics$action.fireAnalyticsEvent({ action: ACTION.STEPS_FILTERED, actionSubject: ACTION_SUBJECT.COLLAB, attributes: { steps: sanitizedSteps }, eventType: EVENT_TYPE.OPERATIONAL }); }) }); plugins.push({ name: 'collabTrackLastOrganicChangePlugin', plugin: createLastOrganicChangePlugin }); if (editorExperiment('platform_editor_offline_editing_web', true)) { plugins.push({ name: 'trackLastRemoteConflictPlugin', plugin: createTrackReconnectionConflictPlugin }); } return plugins; }, onEditorViewStateUpdated(props) { var _api$analytics2, _api$editorViewMode, _api$editorViewMode$s, _options$useNativePlu, _options$hideTelecurs; const addErrorAnalytics = addSynchronyErrorAnalytics(props.newEditorState, props.newEditorState.tr, featureFlags, api === null || api === void 0 ? void 0 : (_api$analytics2 = api.analytics) === null || _api$analytics2 === void 0 ? void 0 : _api$analytics2.actions); const isEmptyDoc = isEmptyDocument(props.newEditorState.doc); const viewMode = api === null || api === void 0 ? void 0 : (_api$editorViewMode = api.editorViewMode) === null || _api$editorViewMode === void 0 ? void 0 : (_api$editorViewMode$s = _api$editorViewMode.sharedState.currentState()) === null || _api$editorViewMode$s === void 0 ? void 0 : _api$editorViewMode$s.mode; executeProviderCode(sendTransaction({ originalTransaction: props.originalTransaction, transactions: props.transactions, oldEditorState: props.oldEditorState, newEditorState: props.newEditorState, useNativePlugin: (_options$useNativePlu = options.useNativePlugin) !== null && _options$useNativePlu !== void 0 ? _options$useNativePlu : false, hideTelecursorOnLoad: !isEmptyDoc && ((_options$hideTelecurs = options.hideTelecursorOnLoad) !== null && _options$hideTelecurs !== void 0 ? _options$hideTelecurs : false), viewMode }), addErrorAnalytics); if (!expValEquals('platform_editor_remove_collab_step_metrics', 'isEnabled', true)) { track({ api, ...props, onTrackDataProcessed: steps => { var _api$analytics3, _api$analytics3$actio; api === null || api === void 0 ? void 0 : (_api$analytics3 = api.analytics) === null || _api$analytics3 === void 0 ? void 0 : (_api$analytics3$actio = _api$analytics3.actions) === null || _api$analytics3$actio === void 0 ? void 0 : _api$analytics3$actio.fireAnalyticsEvent({ action: ACTION.STEPS_TRACKED, actionSubject: ACTION_SUBJECT.COLLAB, attributes: { steps }, eventType: EVENT_TYPE.OPERATIONAL }); } }); } if (fg('platform_editor_collab_organic_change_reporting')) { monitorOrganic({ api, ...props, onDataProcessed: data => { var _api$analytics4, _api$analytics4$actio; api === null || api === void 0 ? void 0 : (_api$analytics4 = api.analytics) === null || _api$analytics4 === void 0 ? void 0 : (_api$analytics4$actio = _api$analytics4.actions) === null || _api$analytics4$actio === void 0 ? void 0 : _api$analytics4$actio.fireAnalyticsEvent({ action: ACTION.ORGANIC_CHANGES_TRACKED, actionSubject: ACTION_SUBJECT.COLLAB, attributes: { organicChanges: data }, eventType: EVENT_TYPE.OPERATIONAL }); } }); } }, commands: { nudgeTelepointer: sessionId => ({ tr }) => { tr.setMeta('nudgeTelepointer', { sessionId }); return tr; } } }; };