UNPKG

@atlaskit/mention

Version:

A React component used to display user profiles in a list for 'Mention' functionality

249 lines (240 loc) 7.66 kB
import _defineProperty from "@babel/runtime/helpers/defineProperty"; import debug from '../util/logger'; import { AbstractResource } from './MentionResource'; class CacheEntry { constructor(pres, timeout) { this.presence = pres; this.expiry = Date.now() + timeout; } expired() { return Date.now() > this.expiry; } } class AbstractPresenceResource extends AbstractResource { refreshPresence(userIds) { throw new Error(`not yet implemented.\nParams: userIds=${userIds}`); } notifyListeners(presences) { this.changeListeners.forEach((listener, key) => { try { listener(presences); } catch (e) { // ignore error from listener debug(`error from listener '${key}', ignoring`, e); } }); } } class PresenceResource extends AbstractPresenceResource { constructor(config) { super(); if (!config.url) { throw new Error('config.url is a required parameter'); } if (!config.cloudId) { throw new Error('config.cloudId is a required parameter'); } this.config = config; this.config.url = PresenceResource.cleanUrl(config.url); this.presenceCache = config.cache || new DefaultPresenceCache(config.cacheExpiry); this.presenceParser = config.parser || new DefaultPresenceParser(); } refreshPresence(userIds) { const cacheHits = this.presenceCache.getBulk(userIds); this.notifyListeners(cacheHits); const cacheMisses = this.presenceCache.getMissingUserIds(userIds); if (cacheMisses.length) { this.retrievePresence(cacheMisses); } } retrievePresence(userIds) { this.queryDirectoryForPresences(userIds).then(res => this.presenceParser.parse(res)).then(presences => { this.notifyListeners(presences); this.presenceCache.update(presences); }); } queryDirectoryForPresences(userIds) { const query = { query: `query getPresenceForMentions($organizationId: String!, $userIds: [String!], $productId: String) { PresenceBulk(organizationId: $organizationId, product: $productId, userIds: $userIds) { userId state stateMetadata } }`, variables: { organizationId: this.config.cloudId, userIds: userIds } }; if (this.config.productId) { query.variables['productId'] = this.config.productId; } const options = { method: 'POST', headers: { 'Content-Type': 'application/json' }, credentials: 'include', body: JSON.stringify(query) }; return fetch(this.config.url, options).then(response => response.json()); } static cleanUrl(url) { if (url.substr(-1) !== '/') { url += '/'; } return url; } } export class DefaultPresenceCache { constructor(cacheTimeout, cacheTrigger) { this.expiryInMillis = cacheTimeout ? cacheTimeout : DefaultPresenceCache.defaultTimeout; this.flushTrigger = cacheTrigger ? cacheTrigger : DefaultPresenceCache.defaultFlushTrigger; this.cache = {}; this.size = 0; } /** * Precondition: _delete is only called internally if userId exists in cache * Removes cache entry * @param userId */ _delete(userId) { delete this.cache[userId]; this.size--; } /** * Checks a cache entry and calls delete if the info has expired * @param userId */ _deleteIfExpired(userId) { if (this.contains(userId) && this.cache[userId].expired()) { this._delete(userId); } } /** * Cleans expired entries from cache */ _removeExpired() { Object.keys(this.cache).forEach(id => { this._deleteIfExpired(id); }); } /** * Checks if a user exists in the cache * @param userId */ contains(userId) { return this.cache.hasOwnProperty(userId); } /** * Retrieves a presence from the cache after checking for expired entries * @param userId - to index the cache * @returns Presence - the presence that matches the userId */ get(userId) { this._deleteIfExpired(userId); if (!this.contains(userId)) { return {}; } return this.cache[userId].presence; } /** * Retrieve multiple presences at once from the cache * @param userIds - to index the cache * @returns PresenceMap - A map of userIds to cached Presences */ getBulk(userIds) { const presences = {}; for (const userId of userIds) { if (this.contains(userId)) { presences[userId] = this.get(userId); } } return presences; } /** * For a given list of ids, returns a subset * of all the ids with missing cache entries. * @param userIds - to index the cache * @returns string[] - ids missing from the cache */ getMissingUserIds(userIds) { return userIds.filter(id => !this.contains(id)); } /** * Precondition: presMap only contains ids of users not in cache * expired users must first be removed then reinserted with updated presence * Updates the cache by adding the new Presence entries and setting the expiry time * @param presMap */ update(presMap) { if (this.size >= this.flushTrigger) { this._removeExpired(); } Object.keys(presMap).forEach(userId => { this.cache[userId] = new CacheEntry(presMap[userId], this.expiryInMillis); this.size++; }); } } _defineProperty(DefaultPresenceCache, "defaultTimeout", 20000); _defineProperty(DefaultPresenceCache, "defaultFlushTrigger", 50); export class DefaultPresenceParser { mapState(state) { if (state === 'unavailable') { return 'offline'; } else if (state === 'available') { return 'online'; } else { return state; } } parse(response) { const presences = {}; if (response.hasOwnProperty('data') && response['data'].hasOwnProperty('PresenceBulk')) { const results = response['data'].PresenceBulk; // Store map of state and time indexed by userId. Ignore null results. for (const user of results) { if (user.userId && user.state) { const state = DefaultPresenceParser.extractState(user) || user.state; presences[user.userId] = { status: this.mapState(state) }; } else if (!user.hasOwnProperty('userId') || !user.hasOwnProperty('state')) { // eslint-disable-next-line no-console console.error('Unexpected response from presence service contains keys: ' + Object.keys(user)); } } } return presences; } static extractState(presence) { if (DefaultPresenceParser.isFocusState(presence)) { return DefaultPresenceParser.FOCUS_STATE; } return presence.state; } /* This is a bit of an odd exception. In the case where a user is in "Focus Mode", their presence state is returned as 'busy' along with a `stateMetadata` object containing a `focus` field. In this case we ignore the value of the `state` field and treat the presence as a 'focus' state. */ static isFocusState(presence) { if (presence.stateMetadata) { try { const metadata = JSON.parse(presence.stateMetadata); return metadata && !!metadata.focus; } catch (e) { // eslint-disable-next-line no-console console.error(`Failed to parse presence's stateMetadata for user with id ${presence.userId}: ${presence.stateMetadata}`); // eslint-disable-next-line no-console console.error(e); } } return false; } } _defineProperty(DefaultPresenceParser, "FOCUS_STATE", 'focus'); export { AbstractPresenceResource }; export default PresenceResource;