@atlaskit/mention
Version:
A React component used to display user profiles in a list for 'Mention' functionality
249 lines (240 loc) • 7.66 kB
JavaScript
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;