UNPKG

@atlaskit/editor-plugin-mentions

Version:

Mentions plugin for @atlaskit/editor-core

666 lines (650 loc) 33.6 kB
import { ACTION, ACTION_SUBJECT, ACTION_SUBJECT_ID, EVENT_TYPE } from '@atlaskit/editor-common/analytics'; import { SafePlugin } from '@atlaskit/editor-common/safe-plugin'; import { insm } from '@atlaskit/insm'; import { SLI_EVENT_TYPE, SMART_EVENT_TYPE } from '@atlaskit/mention/resource'; import { ComponentNames } from '@atlaskit/mention/types'; import { fg } from '@atlaskit/platform-feature-flags'; import { editorExperiment } from '@atlaskit/tmp-editor-statsig/experiments'; import { MentionNodeView } from '../nodeviews/mentionNodeView'; import { MENTION_PROVIDER_REJECTED, MENTION_PROVIDER_UNDEFINED } from '../types'; import { mentionPluginKey } from './key'; import { canMentionBeCreatedInRange } from './utils'; export const ACTIONS = { COMMIT_PENDING_TYPED_AGENT_MENTION: 'COMMIT_PENDING_TYPED_AGENT_MENTION', SET_PENDING_TYPED_AGENT_MENTION: 'SET_PENDING_TYPED_AGENT_MENTION', SET_PROVIDER: 'SET_PROVIDER' }; // 'AGENT' is not in the ADF schema UserType enum but is used at runtime. const AGENT_USER_TYPES = new Set(['APP', 'AGENT']); const isAgentUserType = userType => { return typeof userType === 'string' && AGENT_USER_TYPES.has(userType); }; const getAgentMentionName = (text, fallbackName) => { const trimmedFallbackName = typeof fallbackName === 'string' ? fallbackName.trim() : ''; const normalizedFallbackName = (trimmedFallbackName.startsWith('@') ? trimmedFallbackName.slice(1).trim() : trimmedFallbackName) || null; if (typeof text !== 'string') { return normalizedFallbackName; } const trimmedText = text.trim(); const displayName = trimmedText.startsWith('@') ? trimmedText.slice(1).trim() : trimmedText; const normalizedName = displayName || normalizedFallbackName; return normalizedName; }; const AI_STREAMING_TRANSFORMATION_META_KEY = 'isAIStreamingTransformation'; const AGENT_MENTION_INACTIVITY_MS = 3000; const PACKAGE_NAME = "@atlaskit/editor-plugin-mentions"; const PACKAGE_VERSION = "14.4.5"; const setProvider = provider => (state, dispatch) => { if (dispatch) { dispatch(state.tr.setMeta(mentionPluginKey, { action: ACTIONS.SET_PROVIDER, params: { provider } })); } return true; }; /** * Returns true when a transaction represents a local user document edit that * should restart pending agent-mention inactivity tracking. * * Remote/collab updates, replace-document transactions, AI streaming transforms, * selection-only movements, and metadata-only transactions are intentionally ignored. */ const isQualifyingLocalUserDocChange = tr => { const isAIStreaming = Boolean(tr.getMeta(AI_STREAMING_TRANSFORMATION_META_KEY)); return tr.docChanged && !tr.getMeta('isRemote') && !tr.getMeta('replaceDocument') && !isAIStreaming; }; const isLocalSelectionChange = (tr, hasPositionChanged) => { const isAIStreaming = Boolean(tr.getMeta(AI_STREAMING_TRANSFORMATION_META_KEY)); // Pressing Enter can move selection through a doc split without setting tr.selectionSet // or changing from/to numerically, so local doc changes are checked against the // pending mention's current parent before publishing. return (hasPositionChanged || tr.docChanged) && !tr.getMeta('isRemote') && !tr.getMeta('replaceDocument') && !isAIStreaming; }; /** * Reads agent-mention details from a known document position without traversing * the document. Callers pass a matcher so mapped positions are only accepted * when they still point at the same pending/tracked mention. */ const getAgentMentionDetailsAtPos = (state, pos, matchesMention, fallbackName) => { var _parentNode$type$name; if (pos < 0 || pos > state.doc.content.size) { return null; } const node = state.doc.nodeAt(pos); const mentionSchema = state.schema.nodes.mention; if ((node === null || node === void 0 ? void 0 : node.type) !== mentionSchema || !isAgentUserType(node.attrs.userType) || !matchesMention(node.attrs) || !node.attrs.id) { return null; } const $mentionPos = state.doc.resolve(Math.min(pos + node.nodeSize, state.doc.content.size)); const parentNode = $mentionPos.node($mentionPos.depth); return { id: node.attrs.id, context: parentNode.textContent.trim() || null, name: getAgentMentionName(node.attrs.text, fallbackName), nodeSize: node.nodeSize, parentEnd: $mentionPos.end($mentionPos.depth), parentNodeType: (_parentNode$type$name = parentNode.type.name) !== null && _parentNode$type$name !== void 0 ? _parentNode$type$name : null, parentStart: $mentionPos.start($mentionPos.depth), pos }; }; /** * Finds an agent mention that survived a document change when the changed-range * scan did not find one. Prefers the previously tracked mention ID when present; * otherwise returns a surviving agent mention using the existing traversal order. */ const getSurvivingAgentMentionDetails = (state, preferredId, preferredName) => { const mentionSchema = state.schema.nodes.mention; let result = null; state.doc.descendants((node, pos) => { var _result, _result2; if (((_result = result) === null || _result === void 0 ? void 0 : _result.id) === preferredId) { return false; } if (node.type !== mentionSchema || !isAgentUserType(node.attrs.userType) || !node.attrs.id) { return true; } result = getAgentMentionDetailsAtPos(state, pos, attrs => attrs.id === node.attrs.id, node.attrs.id === preferredId ? preferredName : undefined); return ((_result2 = result) === null || _result2 === void 0 ? void 0 : _result2.id) !== preferredId; }); return result; }; /** * Maps a pending typed agent mention through a document-changing transaction and * returns the updated pending state. If the mapped position was deleted or no * longer points at the same local mention, the pending mention is cleared. */ const getPendingTypedAgentMentionAfterDocChange = (state, tr, pendingTypedAgentMention, { resetTimer }) => { const mappedPos = tr.mapping.mapResult(pendingTypedAgentMention.pos, 1); const resetCount = resetTimer ? pendingTypedAgentMention.resetCount + 1 : pendingTypedAgentMention.resetCount; if (mappedPos.deleted) { return null; } const pendingMentionDetails = getAgentMentionDetailsAtPos(state, mappedPos.pos, attrs => attrs.localId === pendingTypedAgentMention.localId, pendingTypedAgentMention.name); return pendingMentionDetails ? { id: pendingMentionDetails.id, localId: pendingTypedAgentMention.localId, name: pendingMentionDetails.name, nodeSize: pendingMentionDetails.nodeSize, pos: pendingMentionDetails.pos, resetCount } : null; }; const hasPendingMentionMovedToNewParent = (oldState, tr, previousPendingTypedAgentMention, pendingMentionDetails) => { if (!previousPendingTypedAgentMention) { return false; } const previousMentionDetails = getAgentMentionDetailsAtPos(oldState, previousPendingTypedAgentMention.pos, attrs => attrs.localId === previousPendingTypedAgentMention.localId); // Keep the previous parent boundary associated with the left side of an // insertion at that boundary, so typing at the start of the parent does not // look like the pending mention moved into a new parent. const mappedPreviousParentStart = previousMentionDetails && tr.mapping.map(previousMentionDetails.parentStart, -1); return Boolean(previousMentionDetails && mappedPreviousParentStart !== pendingMentionDetails.parentStart); }; const isSelectionOutsideDirectParent = (state, pendingMentionDetails) => { return state.selection.from < pendingMentionDetails.parentStart || state.selection.to > pendingMentionDetails.parentEnd; }; /** * Finalises a pending typed agent mention by copying its details into the * public lastInserted* plugin state after the caller has already resolved the * pending mention from the current document. */ const commitResolvedPendingTypedAgentMention = (pluginState, pendingMentionDetails) => { var _pluginState$lastAgen; return { hasPublicPluginStateChanged: true, pluginState: { ...pluginState, pendingTypedAgentMention: null, lastInsertedAgentMentionId: pendingMentionDetails.id, lastInsertedAgentMentionContext: pendingMentionDetails.context, lastInsertedAgentMentionName: pendingMentionDetails.name, lastInsertedAgentMentionParentNodeType: pendingMentionDetails.parentNodeType, lastAgentMentionInsertionCount: ((_pluginState$lastAgen = pluginState.lastAgentMentionInsertionCount) !== null && _pluginState$lastAgen !== void 0 ? _pluginState$lastAgen : 0) + 1 } }; }; /** * Resolves and finalises a pending typed agent mention. If the tracked mention * no longer resolves, the stale pending state is cleared without dispatching a * public update. */ const commitPendingTypedAgentMention = (state, pluginState, pendingTypedAgentMention) => { const pendingMentionDetails = getAgentMentionDetailsAtPos(state, pendingTypedAgentMention.pos, attrs => attrs.localId === pendingTypedAgentMention.localId, pendingTypedAgentMention.name); if (!pendingMentionDetails) { return { hasPublicPluginStateChanged: false, pluginState: { ...pluginState, pendingTypedAgentMention: null } }; } return commitResolvedPendingTypedAgentMention(pluginState, pendingMentionDetails); }; const hasTrackedAgentMentionState = pluginState => Boolean(pluginState.pendingTypedAgentMention) || pluginState.lastInsertedAgentMentionId != null || pluginState.lastInsertedAgentMentionContext != null || pluginState.lastInsertedAgentMentionName != null || pluginState.lastInsertedAgentMentionParentNodeType != null; /** * Clears agent mention state that points at a specific document snapshot. * replaceDocument swaps content wholesale, so pending typed mentions and * lastInserted* details from the previous document must be cleared together. */ const clearTrackedAgentMentionState = pluginState => { return { ...pluginState, pendingTypedAgentMention: null, lastInsertedAgentMentionId: null, lastInsertedAgentMentionContext: null, lastInsertedAgentMentionName: null, lastInsertedAgentMentionParentNodeType: null }; }; export function createMentionPlugin({ pmPluginFactoryParams, fireEvent, options, api }) { let mentionProvider; const sendAnalytics = (event, actionSubject, action, attributes) => { if (event === SLI_EVENT_TYPE || event === SMART_EVENT_TYPE) { fireEvent({ action: action, actionSubject: actionSubject, eventType: EVENT_TYPE.OPERATIONAL, attributes: { packageName: PACKAGE_NAME, packageVersion: PACKAGE_VERSION, componentName: ComponentNames.MENTION, ...attributes } }, 'fabricElements'); } }; return new SafePlugin({ key: mentionPluginKey, state: { init(_, state) { const canInsertMention = canMentionBeCreatedInRange(state.selection.from, state.selection.to)(state); return { canInsertMention }; }, apply(tr, pluginState, oldState, newState) { const { action, params } = tr.getMeta(mentionPluginKey) || { action: null, params: null }; let hasPublicPluginStateChanged = false; let newPluginState = pluginState; const isAgentMentionsExperimentEnabled = editorExperiment('platform_editor_agent_mentions', true); const hasPositionChanged = oldState.selection.from !== newState.selection.from || oldState.selection.to !== newState.selection.to; if (tr.docChanged || tr.selectionSet && hasPositionChanged) { newPluginState = { ...pluginState, canInsertMention: canMentionBeCreatedInRange(newState.selection.from, newState.selection.to)(newState) }; hasPublicPluginStateChanged = true; } switch (action) { case ACTIONS.COMMIT_PENDING_TYPED_AGENT_MENTION: { const pendingTypedAgentMention = newPluginState.pendingTypedAgentMention; // Ignore stale timer callbacks. The localId and resetCount must still match the // current pending mention so older timers cannot publish after later user edits. if (!isAgentMentionsExperimentEnabled || !pendingTypedAgentMention || pendingTypedAgentMention.localId !== (params === null || params === void 0 ? void 0 : params.localId) || pendingTypedAgentMention.resetCount !== (params === null || params === void 0 ? void 0 : params.resetCount)) { break; } const commitResult = commitPendingTypedAgentMention(newState, newPluginState, pendingTypedAgentMention); newPluginState = commitResult.pluginState; hasPublicPluginStateChanged = hasPublicPluginStateChanged || commitResult.hasPublicPluginStateChanged; break; } case ACTIONS.SET_PROVIDER: newPluginState = { ...newPluginState, mentionProvider: params.provider }; hasPublicPluginStateChanged = true; break; } // When the agent mentions experiment is off, dispatch immediately (original behaviour). // When it's on, defer dispatch to after the agent tracking block below so that // agent-mention state changes are included in the notification. if (hasPublicPluginStateChanged && !isAgentMentionsExperimentEnabled) { pmPluginFactoryParams.dispatch(mentionPluginKey, newPluginState); } if (options !== null && options !== void 0 && options.handleMentionsChanged && tr.docChanged) { var _insm$session, _insm$session2; (_insm$session = insm.session) === null || _insm$session === void 0 ? void 0 : _insm$session.startFeature('mentionDeletionDetection'); const mentionSchema = newState.schema.nodes.mention; const mentionsRemoved = new Map(); tr.steps.forEach((step, index) => { step.getMap().forEach((from, to) => { const newStart = tr.mapping.slice(index).map(from, -1); const newEnd = tr.mapping.slice(index).map(to); const oldStart = tr.mapping.invert().map(newStart, -1); const oldEnd = tr.mapping.invert().map(newEnd); const oldSlice = oldState.doc.slice(oldStart, oldEnd); const newSlice = newState.doc.slice(newStart, newEnd); const mentionsBefore = new Map(); const mentionsAfter = new Map(); oldSlice.content.descendants(node => { if (node.type.name === mentionSchema.name && node.attrs.localId) { mentionsBefore.set(node.attrs.localId, { id: node.attrs.id, localId: node.attrs.localId }); } }); newSlice.content.descendants(node => { if (node.type.name === mentionSchema.name && node.attrs.localId) { mentionsAfter.set(node.attrs.localId, { id: node.attrs.id, localId: node.attrs.localId }); } }); // Determine which mentions were removed in this step mentionsBefore.forEach((mention, localId) => { if (!mentionsAfter.has(localId)) { mentionsRemoved.set(localId, mention); } }); // Adjust mentionsRemoved by removing any that reappear mentionsAfter.forEach((_, localId) => { if (mentionsRemoved.has(localId)) { mentionsRemoved.delete(localId); } }); }); }); if (mentionsRemoved.size > 0) { const changes = Array.from(mentionsRemoved.values()).map(mention => ({ id: mention.id, localId: mention.localId, type: 'deleted' })); options.handleMentionsChanged(changes); } (_insm$session2 = insm.session) === null || _insm$session2 === void 0 ? void 0 : _insm$session2.endFeature('mentionDeletionDetection'); } if (isAgentMentionsExperimentEnabled && isQualifyingLocalUserDocChange(tr)) { const mentionSchema = newState.schema.nodes.mention; const newDocRanges = []; const oldDocRanges = []; let stepsTouchMentions = false; tr.steps.forEach(step => { let found = false; // Only merge a step's ranges if it actually touched an agent mention, // so unrelated steps (e.g. mark-only changes) don't inflate the scan area. const stepNewRanges = []; const stepOldRanges = []; step.getMap().forEach((oldFrom, oldTo, newFrom, newTo) => { stepOldRanges.push([oldFrom, oldTo]); stepNewRanges.push([newFrom, newTo]); if (!found) { // Clamp positions: delete-only steps can produce newTo > doc.content.size. const clampedNewFrom = Math.min(newFrom, newState.doc.content.size); const clampedNewTo = Math.min(newTo, newState.doc.content.size); if (clampedNewFrom < clampedNewTo) { newState.doc.nodesBetween(clampedNewFrom, clampedNewTo, node => { if (node.type === mentionSchema && isAgentUserType(node.attrs.userType)) { found = true; } return !found; }); } if (!found) { const clampedOldFrom = Math.min(oldFrom, oldState.doc.content.size); const clampedOldTo = Math.min(oldTo, oldState.doc.content.size); if (clampedOldFrom < clampedOldTo) { oldState.doc.nodesBetween(clampedOldFrom, clampedOldTo, node => { if (node.type === mentionSchema && AGENT_USER_TYPES.has(node.attrs.userType)) { found = true; } return !found; }); } } } }); if (found) { stepsTouchMentions = true; newDocRanges.push(...stepNewRanges); oldDocRanges.push(...stepOldRanges); } }); const shouldResolveAgentMentionState = stepsTouchMentions || Boolean(newPluginState.lastInsertedAgentMentionId); if (shouldResolveAgentMentionState) { var _newPluginState$lastA, _newPluginState$lastI, _newPluginState$lastI2, _newPluginState$lastI3, _newPluginState$lastI4; let agentMentionId = null; let agentMentionContext = null; let agentMentionName = null; let agentMentionParentNodeType = null; let newCount = 0; let oldAgentMentionId = null; let oldCount = 0; let pendingTypedAgentMentionDetails = null; if (stepsTouchMentions) { for (const [from, to] of newDocRanges) { const clampedTo = Math.min(to, newState.doc.content.size); if (from >= clampedTo) continue; newState.doc.nodesBetween(from, clampedTo, (node, pos) => { if (node.type !== mentionSchema || !isAgentUserType(node.attrs.userType)) { return true; } newCount++; if (pendingTypedAgentMentionDetails === null && action === ACTIONS.SET_PENDING_TYPED_AGENT_MENTION && node.attrs.localId === (params === null || params === void 0 ? void 0 : params.localId)) { pendingTypedAgentMentionDetails = getAgentMentionDetailsAtPos(newState, pos, attrs => attrs.localId === params.localId, params.name); } if (agentMentionId === null && node.attrs.id) { const agentMentionDetails = getAgentMentionDetailsAtPos(newState, pos, attrs => attrs.id === node.attrs.id, params === null || params === void 0 ? void 0 : params.name); if (agentMentionDetails) { agentMentionId = agentMentionDetails.id; agentMentionContext = agentMentionDetails.context; agentMentionName = agentMentionDetails.name; agentMentionParentNodeType = agentMentionDetails.parentNodeType; } } return true; }); } for (const [from, to] of oldDocRanges) { const clampedOldTo = Math.min(to, oldState.doc.content.size); if (from >= clampedOldTo) continue; oldState.doc.nodesBetween(from, clampedOldTo, node => { if (node.type !== mentionSchema || !isAgentUserType(node.attrs.userType)) { return true; } oldCount++; if (oldAgentMentionId === null && node.attrs.id) { oldAgentMentionId = node.attrs.id; } return true; }); } } // When a deletion collapses the new-doc range to a zero-width point, or when // the doc changed but no step covered the tracked mention, the new-doc scan // above finds nothing. Check whether any agent mention survived in the document. let resolvedFromFullDocFallback = false; if (agentMentionId === null && newPluginState.lastInsertedAgentMentionId) { const survivorDetails = getSurvivingAgentMentionDetails(newState, newPluginState.lastInsertedAgentMentionId, newPluginState.lastInsertedAgentMentionName); if (survivorDetails) { agentMentionId = survivorDetails.id; agentMentionContext = survivorDetails.context; agentMentionName = survivorDetails.name; agentMentionParentNodeType = survivorDetails.parentNodeType; resolvedFromFullDocFallback = true; } } const isNewInsertion = agentMentionId !== null && !resolvedFromFullDocFallback && (oldAgentMentionId !== agentMentionId || newCount > oldCount); const isPendingTypedAgentMentionInsertion = isNewInsertion && action === ACTIONS.SET_PENDING_TYPED_AGENT_MENTION && typeof (params === null || params === void 0 ? void 0 : params.localId) === 'string'; const newInsertionCount = isNewInsertion ? ((_newPluginState$lastA = newPluginState.lastAgentMentionInsertionCount) !== null && _newPluginState$lastA !== void 0 ? _newPluginState$lastA : 0) + 1 : undefined; const pendingTypedAgentMentionDetailsForState = pendingTypedAgentMentionDetails; if (isPendingTypedAgentMentionInsertion && pendingTypedAgentMentionDetailsForState) { const pendingTypedAgentMentionLocalId = params === null || params === void 0 ? void 0 : params.localId; newPluginState = { ...newPluginState, pendingTypedAgentMention: { id: pendingTypedAgentMentionDetailsForState.id, localId: pendingTypedAgentMentionLocalId, name: pendingTypedAgentMentionDetailsForState.name, nodeSize: pendingTypedAgentMentionDetailsForState.nodeSize, pos: pendingTypedAgentMentionDetailsForState.pos, resetCount: 1 } }; } else if (isPendingTypedAgentMentionInsertion) { // Fallback: if the localId-specific scan missed the typed mention, // publish immediately so the insertion is not dropped. newPluginState = { ...newPluginState, pendingTypedAgentMention: null, lastInsertedAgentMentionId: agentMentionId, lastInsertedAgentMentionContext: agentMentionContext, lastInsertedAgentMentionName: agentMentionName, lastInsertedAgentMentionParentNodeType: agentMentionParentNodeType, ...(newInsertionCount !== undefined ? { lastAgentMentionInsertionCount: newInsertionCount } : {}) }; hasPublicPluginStateChanged = true; } else if (agentMentionId !== ((_newPluginState$lastI = newPluginState.lastInsertedAgentMentionId) !== null && _newPluginState$lastI !== void 0 ? _newPluginState$lastI : null) || agentMentionContext !== ((_newPluginState$lastI2 = newPluginState.lastInsertedAgentMentionContext) !== null && _newPluginState$lastI2 !== void 0 ? _newPluginState$lastI2 : null) || agentMentionName !== ((_newPluginState$lastI3 = newPluginState.lastInsertedAgentMentionName) !== null && _newPluginState$lastI3 !== void 0 ? _newPluginState$lastI3 : null) || agentMentionParentNodeType !== ((_newPluginState$lastI4 = newPluginState.lastInsertedAgentMentionParentNodeType) !== null && _newPluginState$lastI4 !== void 0 ? _newPluginState$lastI4 : null) || newInsertionCount !== undefined) { newPluginState = { ...newPluginState, lastInsertedAgentMentionId: agentMentionId, lastInsertedAgentMentionContext: agentMentionContext, lastInsertedAgentMentionName: agentMentionName, lastInsertedAgentMentionParentNodeType: agentMentionParentNodeType, ...(newInsertionCount !== undefined ? { lastAgentMentionInsertionCount: newInsertionCount } : {}) }; hasPublicPluginStateChanged = true; } } } if (isAgentMentionsExperimentEnabled && tr.docChanged && tr.getMeta('replaceDocument') && hasTrackedAgentMentionState(newPluginState)) { newPluginState = clearTrackedAgentMentionState(newPluginState); hasPublicPluginStateChanged = true; } if (isAgentMentionsExperimentEnabled && newPluginState.pendingTypedAgentMention && action !== ACTIONS.SET_PENDING_TYPED_AGENT_MENTION && action !== ACTIONS.COMMIT_PENDING_TYPED_AGENT_MENTION && tr.docChanged) { newPluginState = { ...newPluginState, pendingTypedAgentMention: getPendingTypedAgentMentionAfterDocChange(newState, tr, newPluginState.pendingTypedAgentMention, { resetTimer: isQualifyingLocalUserDocChange(tr) }) }; } // Typed agent mentions stay pending while the user is still editing around them, // but leaving the mention's direct parent means they have moved on from that // paragraph/block. Publish immediately in that case instead of waiting for the // inactivity timer. const shouldCheckPendingTypedAgentMentionParent = isLocalSelectionChange(tr, hasPositionChanged); if (isAgentMentionsExperimentEnabled && newPluginState.pendingTypedAgentMention && action !== ACTIONS.SET_PENDING_TYPED_AGENT_MENTION && action !== ACTIONS.COMMIT_PENDING_TYPED_AGENT_MENTION && shouldCheckPendingTypedAgentMentionParent) { const pendingTypedAgentMention = newPluginState.pendingTypedAgentMention; const pendingMentionDetails = getAgentMentionDetailsAtPos(newState, pendingTypedAgentMention.pos, attrs => attrs.localId === pendingTypedAgentMention.localId, pendingTypedAgentMention.name); if (!pendingMentionDetails) { newPluginState = { ...newPluginState, pendingTypedAgentMention: null }; } else if (hasPendingMentionMovedToNewParent(oldState, tr, pluginState.pendingTypedAgentMention, pendingMentionDetails) || isSelectionOutsideDirectParent(newState, pendingMentionDetails)) { const commitResult = commitResolvedPendingTypedAgentMention(newPluginState, pendingMentionDetails); newPluginState = commitResult.pluginState; hasPublicPluginStateChanged = hasPublicPluginStateChanged || commitResult.hasPublicPluginStateChanged; } } if (hasPublicPluginStateChanged && isAgentMentionsExperimentEnabled) { pmPluginFactoryParams.dispatch(mentionPluginKey, newPluginState); } return newPluginState; } }, props: { nodeViews: { mention: node => { return new MentionNodeView(node, { options, api, portalProviderAPI: pmPluginFactoryParams.portalProviderAPI }); } } }, view(editorView) { const isAgentMentionsEnabled = editorExperiment('platform_editor_agent_mentions', true); let pendingTypedAgentMentionTimer; let pendingTypedAgentMentionTimerKey = null; const clearPendingTypedAgentMentionTimer = () => { if (pendingTypedAgentMentionTimer) { clearTimeout(pendingTypedAgentMentionTimer); pendingTypedAgentMentionTimer = undefined; } pendingTypedAgentMentionTimerKey = null; }; const schedulePendingTypedAgentMentionTimer = mentionPluginState => { if (!isAgentMentionsEnabled) { clearPendingTypedAgentMentionTimer(); return; } const pendingTypedAgentMention = mentionPluginState === null || mentionPluginState === void 0 ? void 0 : mentionPluginState.pendingTypedAgentMention; if (!pendingTypedAgentMention) { clearPendingTypedAgentMentionTimer(); return; } const timerKey = `${pendingTypedAgentMention.localId}:${pendingTypedAgentMention.resetCount}`; if (timerKey === pendingTypedAgentMentionTimerKey) { return; } clearPendingTypedAgentMentionTimer(); pendingTypedAgentMentionTimerKey = timerKey; pendingTypedAgentMentionTimer = setTimeout(() => { var _mentionPluginKey$get; const latestPendingTypedAgentMention = (_mentionPluginKey$get = mentionPluginKey.getState(editorView.state)) === null || _mentionPluginKey$get === void 0 ? void 0 : _mentionPluginKey$get.pendingTypedAgentMention; if (!latestPendingTypedAgentMention || latestPendingTypedAgentMention.localId !== pendingTypedAgentMention.localId || latestPendingTypedAgentMention.resetCount !== pendingTypedAgentMention.resetCount) { return; } editorView.dispatch(editorView.state.tr.setMeta(mentionPluginKey, { action: ACTIONS.COMMIT_PENDING_TYPED_AGENT_MENTION, params: { localId: pendingTypedAgentMention.localId, resetCount: pendingTypedAgentMention.resetCount } })); }, AGENT_MENTION_INACTIVITY_MS); }; const providerHandler = (name, providerPromise) => { switch (name) { case 'mentionProvider': if (!providerPromise) { fireEvent({ action: ACTION.ERRORED, actionSubject: ACTION_SUBJECT.MENTION, actionSubjectId: ACTION_SUBJECT_ID.MENTION_PROVIDER, eventType: EVENT_TYPE.OPERATIONAL, attributes: { reason: MENTION_PROVIDER_UNDEFINED } }); return setProvider(undefined)(editorView.state, editorView.dispatch); } providerPromise.then(provider => { if (mentionProvider) { mentionProvider.unsubscribe('mentionPlugin'); } mentionProvider = provider; setProvider(provider)(editorView.state, editorView.dispatch); provider.subscribe('mentionPlugin', undefined, undefined, undefined, undefined, sendAnalytics); }).catch(() => { fireEvent({ action: ACTION.ERRORED, actionSubject: ACTION_SUBJECT.MENTION, actionSubjectId: ACTION_SUBJECT_ID.MENTION_PROVIDER, eventType: EVENT_TYPE.OPERATIONAL, attributes: { reason: MENTION_PROVIDER_REJECTED } }); return setProvider(undefined)(editorView.state, editorView.dispatch); }); break; } return; }; const providerViaConfig = fg('platform_editor_mention_provider_via_plugin_config'); if (providerViaConfig && options !== null && options !== void 0 && options.mentionProvider) { providerHandler('mentionProvider', options === null || options === void 0 ? void 0 : options.mentionProvider); } else { pmPluginFactoryParams.providerFactory.subscribe('mentionProvider', providerHandler); } return { update(view, prevState) { const mentionPluginState = mentionPluginKey.getState(view.state); if (mentionPluginState === mentionPluginKey.getState(prevState)) { return; } schedulePendingTypedAgentMentionTimer(mentionPluginState); }, destroy() { clearPendingTypedAgentMentionTimer(); if (pmPluginFactoryParams.providerFactory) { pmPluginFactoryParams.providerFactory.unsubscribe('mentionProvider', providerHandler); } if (mentionProvider) { mentionProvider.unsubscribe('mentionPlugin'); } } }; } }); }