@atlaskit/editor-plugin-mentions
Version:
Mentions plugin for @atlaskit/editor-core
168 lines (167 loc) • 6.65 kB
JavaScript
import _defineProperty from "@babel/runtime/helpers/defineProperty";
import { NodeDataProvider } from '@atlaskit/node-data-provider/node-data-provider';
const getAppType = userType => userType === 'APP' || userType === 'AGENT' ? 'agent' : 'user';
const enrichMentionNodeData = (mention, data) => {
var _data$appType;
return {
...data,
appType: (_data$appType = data === null || data === void 0 ? void 0 : data.appType) !== null && _data$appType !== void 0 ? _data$appType : getAppType(mention.userType)
};
};
const toMentionNode = ({
id,
userType
}) => ({
attrs: {
id,
// NodeDataProvider uses an ADF node internally, so normalize the runtime-only value.
userType: userType === 'AGENT' ? 'APP' : userType
},
type: 'mention'
});
const isMentionNode = node => node.type === 'mention' && node.attrs !== undefined && 'id' in node.attrs && typeof node.attrs.id === 'string';
const SUBSCRIPTION_KEY_PREFIX = 'mention-node-data-provider';
let subscriptionKeyCounter = 0;
const cacheMentionData = (cache, mentions) => {
mentions.forEach(mention => {
if (mention.isPlaceholder || !mention.avatarUrl) {
cache.delete(mention.id);
return;
}
cache.set(mention.id, {
avatarUrl: mention.avatarUrl,
appType: mention.appType
});
});
};
/**
* Page-scoped data provider for mention avatars.
*
* Products inject their resolution strategy so this package remains independent
* of Jira and Confluence APIs. Reuse the instance for the lifetime of a renderer
* or editor rather than creating one for every mention.
*
* Deterministic products can supply `resolveMentionNodeData` to return an
* SSR-safe avatar URL synchronously. It must return the same value on the server
* and the client's first render to preserve hydration parity.
*
* Network-backed products supply `fetchMentionNodeData`. Calls made in the same
* microtask are combined and account IDs are deduplicated before the fetcher is
* invoked. A Relay adapter can query `users(accountIds:)`, then map each user's
* `picture` and app type to a `MentionNodeData` record keyed by account ID. The
* resolved result is cached by the underlying `NodeDataProvider`.
*
* Products that already have a `MentionProvider` result stream can call `connect`
* so emitted mention results populate a synchronous avatar cache.
*/
export class MentionNodeDataProvider extends NodeDataProvider {
constructor(fetchMentionNodeData, resolveMentionNodeData = undefined) {
super();
_defineProperty(this, "name", 'mentionNodeDataProvider');
_defineProperty(this, "abortControllerRef", {
current: new AbortController()
});
_defineProperty(this, "emittedMentionCache", new Map());
_defineProperty(this, "subscriptionKey", `${SUBSCRIPTION_KEY_PREFIX}:${++subscriptionKeyCounter}`);
_defineProperty(this, "queuedFetches", []);
_defineProperty(this, "isFlushScheduled", false);
this.fetchMentionNodeData = fetchMentionNodeData;
this.resolveMentionNodeData = resolveMentionNodeData;
}
isNodeSupported(node) {
return isMentionNode(node) && node.attrs.userType !== 'SPECIAL';
}
nodeDataKey(node) {
return node.attrs.id;
}
async fetchNodesData(nodes) {
const accountIds = Array.from(new Set(nodes.map(({
attrs
}) => attrs.id)));
const dataByAccountId = await this.enqueueFetch(accountIds);
return nodes.map(({
attrs
}) => enrichMentionNodeData(attrs, dataByAccountId[attrs.id]));
}
getMentionData(mention, callback) {
const resolvedData = this.resolveMentionNodeDataFromCache(mention);
if (resolvedData) {
callback({
data: enrichMentionNodeData(mention, resolvedData)
});
return;
}
this.getData(toMentionNode(mention), callback);
}
getMentionDataFromCache(mention) {
var _this$getNodeDataFrom;
const resolvedData = this.resolveMentionNodeDataFromCache(mention);
if (resolvedData) {
return enrichMentionNodeData(mention, resolvedData);
}
const data = (_this$getNodeDataFrom = this.getNodeDataFromCache(toMentionNode(mention))) === null || _this$getNodeDataFrom === void 0 ? void 0 : _this$getNodeDataFrom.data;
return data ? enrichMentionNodeData(mention, data) : undefined;
}
connect(mentionResource) {
if (this.lastMentionResource && this.lastMentionResource !== mentionResource) {
this.emittedMentionCache.clear();
this.resetCache();
}
this.lastMentionResource = mentionResource;
if (this.abortControllerRef.current.signal.aborted) {
this.abortControllerRef.current = new AbortController();
}
let isDisconnected = false;
let subscribedMentionProvider;
void mentionResource.then(mentionProvider => {
if (isDisconnected) {
return;
}
subscribedMentionProvider = mentionProvider;
mentionProvider.subscribe(this.subscriptionKey, mentions => {
cacheMentionData(this.emittedMentionCache, mentions);
});
}).catch(() => undefined);
return () => {
var _subscribedMentionPro;
isDisconnected = true;
this.abortControllerRef.current.abort();
(_subscribedMentionPro = subscribedMentionProvider) === null || _subscribedMentionPro === void 0 ? void 0 : _subscribedMentionPro.unsubscribe(this.subscriptionKey);
subscribedMentionProvider = undefined;
};
}
resolveMentionNodeDataFromCache(mention) {
var _this$resolveMentionN, _this$resolveMentionN2;
return (_this$resolveMentionN = (_this$resolveMentionN2 = this.resolveMentionNodeData) === null || _this$resolveMentionN2 === void 0 ? void 0 : _this$resolveMentionN2.call(this, mention)) !== null && _this$resolveMentionN !== void 0 ? _this$resolveMentionN : this.emittedMentionCache.get(mention.id);
}
enqueueFetch(accountIds) {
const result = new Promise((resolve, reject) => {
this.queuedFetches.push({
accountIds,
reject,
resolve
});
});
if (!this.isFlushScheduled) {
this.isFlushScheduled = true;
void Promise.resolve().then(() => this.flushFetches());
}
return result;
}
async flushFetches() {
this.isFlushScheduled = false;
const queuedFetches = this.queuedFetches;
this.queuedFetches = [];
const accountIds = Array.from(new Set(queuedFetches.flatMap(request => request.accountIds)));
try {
const data = await this.fetchMentionNodeData(accountIds, this.abortControllerRef.current.signal);
queuedFetches.forEach(({
resolve
}) => resolve(data));
} catch (error) {
queuedFetches.forEach(({
reject
}) => reject(error));
}
}
}