UNPKG

@amityco/ts-sdk-react-native

Version:

Amity Social Cloud Typescript SDK

1,499 lines (1,462 loc) 1.62 MB
import { encode, decode, btoa, atob } from 'js-base64'; import mitt from 'mitt'; import debug from 'debug'; import axios from 'axios'; import HttpAgent, { HttpsAgent } from 'agentkeepalive'; import AsyncStorage from '@react-native-async-storage/async-storage'; import uuid$1 from 'react-native-uuid'; import jwtDecode from 'jwt-decode'; import { Platform } from 'react-native'; import hash from 'object-hash'; import Hls from 'hls.js'; var MembershipAcceptanceTypeEnum; (function (MembershipAcceptanceTypeEnum) { MembershipAcceptanceTypeEnum["AUTOMATIC"] = "automatic"; MembershipAcceptanceTypeEnum["INVITATION"] = "invitation"; })(MembershipAcceptanceTypeEnum || (MembershipAcceptanceTypeEnum = {})); const FileType = Object.freeze({ FILE: 'file', IMAGE: 'image', VIDEO: 'video', CLIP: 'clip', }); const VideoResolution = Object.freeze({ '1080P': '1080p', '720P': '720p', '480P': '480p', '360P': '360p', ORIGINAL: 'original', }); const VideoTranscodingStatus = Object.freeze({ UPLOADED: 'uploaded', TRANSCODING: 'transcoding', TRANSCODED: 'transcoded', TRANSCODE_FAILED: 'transcodeFailed', }); const VideoSize = Object.freeze({ LOW: 'low', MEDIUM: 'medium', HIGH: 'high', ORIGINAL: 'original', }); var FileAccessTypeEnum; (function (FileAccessTypeEnum) { FileAccessTypeEnum["PUBLIC"] = "public"; FileAccessTypeEnum["NETWORK"] = "network"; })(FileAccessTypeEnum || (FileAccessTypeEnum = {})); const CommunityPostSettings = Object.freeze({ ONLY_ADMIN_CAN_POST: 'ONLY_ADMIN_CAN_POST', ADMIN_REVIEW_POST_REQUIRED: 'ADMIN_REVIEW_POST_REQUIRED', ANYONE_CAN_POST: 'ANYONE_CAN_POST', }); const CommunityPostSettingMaps = Object.freeze({ ONLY_ADMIN_CAN_POST: { needApprovalOnPostCreation: false, onlyAdminCanPost: true, }, ADMIN_REVIEW_POST_REQUIRED: { needApprovalOnPostCreation: true, onlyAdminCanPost: false, }, ANYONE_CAN_POST: { needApprovalOnPostCreation: false, onlyAdminCanPost: false, }, }); const DefaultCommunityPostSetting = 'ONLY_ADMIN_CAN_POST'; const ContentFeedType = Object.freeze({ STORY: 'story', CLIP: 'clip', CHAT: 'chat', POST: 'post', MESSAGE: 'message', }); var ContentFlagReasonEnum; (function (ContentFlagReasonEnum) { ContentFlagReasonEnum["CommunityGuidelines"] = "Against community guidelines"; ContentFlagReasonEnum["HarassmentOrBullying"] = "Harassment or bullying"; ContentFlagReasonEnum["SelfHarmOrSuicide"] = "Self-harm or suicide"; ContentFlagReasonEnum["ViolenceOrThreateningContent"] = "Violence or threatening content"; ContentFlagReasonEnum["SellingRestrictedItems"] = "Selling and promoting restricted items"; ContentFlagReasonEnum["SexualContentOrNudity"] = "Sexual message or nudity"; ContentFlagReasonEnum["SpamOrScams"] = "Spam or scams"; ContentFlagReasonEnum["FalseInformation"] = "False information or misinformation"; ContentFlagReasonEnum["Others"] = "Others"; })(ContentFlagReasonEnum || (ContentFlagReasonEnum = {})); const MessageContentType = Object.freeze({ TEXT: 'text', IMAGE: 'image', FILE: 'file', VIDEO: 'video', AUDIO: 'audio', CUSTOM: 'custom', }); const PostContentType = Object.freeze({ TEXT: 'text', IMAGE: 'image', FILE: 'file', VIDEO: 'video', LIVESTREAM: 'liveStream', POLL: 'poll', CLIP: 'clip', }); var InvitationTypeEnum; (function (InvitationTypeEnum) { InvitationTypeEnum["CommunityMemberInvite"] = "communityMemberInvite"; InvitationTypeEnum["LivestreamInvite"] = "livestreamInvite"; })(InvitationTypeEnum || (InvitationTypeEnum = {})); var InvitationStatusEnum; (function (InvitationStatusEnum) { InvitationStatusEnum["Pending"] = "pending"; InvitationStatusEnum["Approved"] = "approved"; InvitationStatusEnum["Rejected"] = "rejected"; InvitationStatusEnum["Cancelled"] = "cancelled"; })(InvitationStatusEnum || (InvitationStatusEnum = {})); var InvitationSortByEnum; (function (InvitationSortByEnum) { InvitationSortByEnum["FirstCreated"] = "firstCreated"; InvitationSortByEnum["LastCreated"] = "lastCreated"; })(InvitationSortByEnum || (InvitationSortByEnum = {})); var JoinRequestStatusEnum; (function (JoinRequestStatusEnum) { JoinRequestStatusEnum["Pending"] = "pending"; JoinRequestStatusEnum["Approved"] = "approved"; JoinRequestStatusEnum["Rejected"] = "rejected"; JoinRequestStatusEnum["Cancelled"] = "cancelled"; })(JoinRequestStatusEnum || (JoinRequestStatusEnum = {})); var JoinResultStatusEnum; (function (JoinResultStatusEnum) { JoinResultStatusEnum["Success"] = "success"; JoinResultStatusEnum["Pending"] = "pending"; })(JoinResultStatusEnum || (JoinResultStatusEnum = {})); var FeedDataTypeEnum; (function (FeedDataTypeEnum) { FeedDataTypeEnum["Text"] = "text"; FeedDataTypeEnum["Video"] = "video"; FeedDataTypeEnum["Image"] = "image"; FeedDataTypeEnum["File"] = "file"; FeedDataTypeEnum["LiveStream"] = "liveStream"; FeedDataTypeEnum["Clip"] = "clip"; FeedDataTypeEnum["Poll"] = "poll"; })(FeedDataTypeEnum || (FeedDataTypeEnum = {})); var FeedSortByEnum; (function (FeedSortByEnum) { FeedSortByEnum["LastCreated"] = "lastCreated"; FeedSortByEnum["FirstCreated"] = "firstCreated"; FeedSortByEnum["LastUpdated"] = "lastUpdated"; FeedSortByEnum["FirstUpdated"] = "firstUpdated"; })(FeedSortByEnum || (FeedSortByEnum = {})); var FeedSourceEnum; (function (FeedSourceEnum) { FeedSourceEnum["Community"] = "community"; FeedSourceEnum["User"] = "user"; })(FeedSourceEnum || (FeedSourceEnum = {})); function getVersion() { try { // the string ''v7.9.1-esm'' should be replaced by actual value by @rollup/plugin-replace return 'v7.9.1-esm'; } catch (error) { return '__dev__'; } } const VERSION = getVersion(); const COLLECTION_DEFAULT_PAGINATION_LIMIT = 5; const COLLECTION_DEFAULT_CACHING_POLICY = 'cache_then_server'; const ENABLE_CACHE_MESSAGE = 'For using Live Collection feature you need to enable Cache!'; const LIVE_OBJECT_ENABLE_CACHE_MESSAGE = 'For using Live Object feature you need to enable Cache!'; const UNSYNCED_OBJECT_CACHED_AT_MESSAGE = 'Observing unsynced object is not supported by Live Object.'; const UNSYNCED_OBJECT_CACHED_AT_VALUE = -5; const SECOND$1 = 1000; const MINUTE = 60 * SECOND$1; const HOUR = 60 * MINUTE; const DAY = 24 * HOUR; const WEEK = 7 * DAY; const YEAR = 365 * DAY; const ACCESS_TOKEN_WATCHER_INTERVAL = 10 * MINUTE; // cache constants const CACHE_KEY_GET = 'get'; const CACHE_KEY_TOMBSTONE = 'dead'; const CACHE_SHORTEN_LIFESPAN = 2 * SECOND$1; const CACHE_LIFESPAN = 1 * MINUTE; // 1 minute const CACHE_LIFESPAN_TOMBSTONE = 3 * MINUTE; // 3 minutes /** * Shallow-clones an object and sort its keys * * @param source a plain object to clone * @returns a clone of source, with keys sorted with default javascript sorting * * @category Cache * @hidden */ const normalize = (source) => Object.keys(source) .sort() .reduce((acc, key) => (Object.assign(Object.assign({}, acc), { [key]: source[key] })), {}); /** * Encodes a given {@link Amity.CacheKey} to a plain string * * @param key the key to encode * @returns an encoded string * * @category Cache * @hidden */ const encodeKey = (key) => JSON.stringify(key, (_, val) => (typeof val === 'object' ? normalize(val) : val)); /** * Decodes a string back into a {@link Amity.CacheKey} * * @param key the string key to decode * @returns a plain Amity.CacheKey object. * * @category Cache * @hidden */ const decodeKey = (key) => JSON.parse(key); /** * Performs a recursive partial deep equal check on two objects. * * @param predicate the reference object containing the partial information * @param candidate the object to check against * @returns true if the candidate contains all the values of the predicate * * @category Cache */ const partialMatch = (predicate, candidate) => { if (predicate === candidate) return true; // if one or the other is nullish, there can't be equality if (!predicate || !candidate) return false; // we only perform recursive check on objects if (typeof predicate !== 'object') { return false; } // recursively call for partial match based on the predicate return Object.keys(predicate).every(key => partialMatch(predicate[key], candidate[key])); }; /** * checks if passed error code is included in * Tombstone errors list * * @param errorCode as {@link Amity.ServerError} * @returns success boolean if the given errorCode * is included in Tombstone errors list * * @category Cache */ const checkIfShouldGoesToTombstone = (errorCode) => [400400 /* Amity.ServerError.ITEM_NOT_FOUND */, 400300 /* Amity.ServerError.FORBIDDEN */].includes(errorCode); /** * Type guard to find if the a given decoded backend token uses "skip" pagination style * * @param json any json object as extracted from the backend * @returns success boolean if the token has either "skip" or "limit" props * * @hidden */ const isSkip = (json) => ['skip'].some(prop => prop in json); /** * Type guard to find if the a given decoded backend token uses "after/before" pagination style * * @param json any json object as extracted from the backend * @returns success boolean if the token has either "after" or "before" props * * @hidden */ const isAfterBefore = (json) => ['after', 'before', 'first', 'last', 'limit'].some(prop => prop in json); /** * Type guard to find if the a given decoded backend token uses v4 or newer "after/before" pagination style * * @param json any json object as extracted from the backend * @returns success boolean if the token has either "after" or "before" props * * @hidden */ const isAfterBeforeRaw = (json) => 'limit' in json; /** * Type guard to find if the a given object is wrapped around Amity.Paged<> * * @param payload any object as passed from query functions * @returns success boolean if the object is an object shaped as { data: T[] } & Amity.Pages * * @hidden */ const isPaged = (payload) => { return ((payload === null || payload === void 0 ? void 0 : payload.hasOwnProperty('data')) && (payload === null || payload === void 0 ? void 0 : payload.hasOwnProperty('nextPage')) && (payload === null || payload === void 0 ? void 0 : payload.hasOwnProperty('prevPage'))); }; /** * Converts a paging object into a b64 string token * * @param paging the sdk-friendly paging object * @param style the style of token to produce * @returns a backend's b64 encoded token * * @hidden */ const toToken = (paging, style) => { var _a; if (!paging || !Object.keys(paging).length) return; let payload = {}; // TODO: refactor this clean if (style === 'skiplimit') { payload = { skip: (_a = paging.after) !== null && _a !== void 0 ? _a : 0, limit: paging.limit, }; } else if (style === 'afterbefore' && isAfterBefore(paging)) { /* Caution: this testing style is only valid because our backend expects nothing else than a number as "before" or "after" value. if that would change, we'd need to move toward a more simple: `!paging.before` */ if (paging === null || paging === void 0 ? void 0 : paging.before) { payload = Object.assign(Object.assign({}, payload), { before: paging.before }); } if (paging === null || paging === void 0 ? void 0 : paging.after) { payload = Object.assign(Object.assign({}, payload), { after: paging.after }); } if (!Number.isNaN(Number(paging === null || paging === void 0 ? void 0 : paging.limit))) { payload = Object.assign(Object.assign({}, payload), { limit: paging.limit }); } } else if (style === 'afterbeforeraw') { payload = paging; } // avoid sending {} as it seems backend flips when we do if (!Object.keys(payload).length) return; // don't try catch, let it throw return encode(JSON.stringify(payload)); }; /** * Converts a b64 string token into a paging object * * @param token the backend's b64 encoded token * @returns a sdk-friendly paging object * * @hidden */ const toPage = (token) => { if (!token) return undefined; // don't try catch, let it throw const json = JSON.parse(decode(token)); if (isSkip(json)) { return { after: json.skip, limit: json.limit, }; } if (isAfterBefore(json)) { if ('before' in json) { return { before: json.before, limit: json.last, }; } if ('after' in json) { return { after: json.after, limit: json.first, }; } } return undefined; }; /** * Converts a b64 string token into a paging object * * @param token the backend's b64 encoded token * @returns a sdk-friendly paging object * * @hidden */ const toPageRaw = (token) => { if (!token) return undefined; // don't try catch, let it throw const json = JSON.parse(decode(token)); if (isAfterBeforeRaw(json)) { return json; } return undefined; }; /* eslint-disable no-use-before-define */ /** * Type guard to check and cast that a given async function is has the ".locally" property * * @param func any SDK APi function * @returns success boolean if the function has a 'locally' twin * * @hidden */ const isFetcher = (func) => 'locally' in func; /** * Type guard to check and cast that a given async function is has the ".optimistically" property * * @param func any SDK APi function * @returns success boolean if the function has an 'optimistically' twin * * @hidden */ const isMutator = (func) => 'optimistically' in func; /** * Type guard to check and cast that a given async function is has * the ".locally" or ".optimistically" property * * @param func any SDK APi function * @returns success boolean if the function has an offline twin * * @hidden */ const isOffline = (func) => isFetcher(func) || isMutator(func); /** * Type guard to check and cast that a given object has the "cachedAt" property * * @param model any object to check on * @returns success boolean if the object has property "cachedAt" * * @hidden */ const isCachable = (model) => model === null || model === void 0 ? void 0 : model.hasOwnProperty('cachedAt'); /** * Checks if a model is considered local (cachedAt === -1) * * @param model any cachable object to check * @returns success boolean if the object is marked as local * * @hidden */ const isLocal = (model) => { return isCachable(model) && (model === null || model === void 0 ? void 0 : model.cachedAt) === -1; }; /** * Checks if a model is considered fresh * * @param model any cachable object to check * @param lifeSpan the supposedly duration for which the object is considered synced * @returns success boolean if the object is below the given lifespan * * @hidden */ const isFresh = (model, lifeSpan = CACHE_LIFESPAN) => { var _a; return Date.now() - ((_a = model === null || model === void 0 ? void 0 : model.cachedAt) !== null && _a !== void 0 ? _a : 0) <= lifeSpan; }; /** * ```js * import { createQuery, getUser } from '@amityco/ts-sdk-react-native' * const query = createQuery(getUser, 'foobar') * ``` * * Creates a wrapper for the API call you wish to call. * This wrapper is necessary to create for optimistically calls * * * @param func A compatible API function from the ts sdk * @param args The arguments to pass to the function passed as `fn` * @returns A wrapper containing both the function and its future arguments * * @category Query */ const createQuery = (func, ...args) => ({ func, args }); /** * ```js * import { queryOptions } from '@amityco/ts-sdk-react-native' * const options = queryOptions('no_fetch', lifeSpan) * ``` * * Creates a query options object based on the query policy passed * * @param policy The policy to apply to a query * @returns A properly set query options object * * @category Query */ const queryOptions = (policy, lifeSpan = CACHE_LIFESPAN) => { if (policy === 'cache_only') return { lifeSpan: Infinity }; return { lifeSpan: lifeSpan < CACHE_LIFESPAN ? CACHE_LIFESPAN : lifeSpan }; }; /** * ```js * import { createQuery, getUser, runQuery } from '@amityco/ts-sdk-react-native' * const query = createQuery(getUser, client, 'foobar') * runQuery(query, user => console.log(user)) * ``` * * Calls an API function wrapped around a Amity.Query, and executes the callback whenever * a value is available. The value can be picked either from the local cache and/or * from the server afterwards depending on the query options passed * * @param query A query object wrapping the call to be made * @param callback A function to execute when a value is available * @param options the query options * * @category Query */ const runQuery = ({ func, args }, callback, options = queryOptions('cache_then_server')) => { let local; const { lifeSpan } = queryOptions('cache_then_server', options.lifeSpan); // offline first if (isOffline(func)) { try { local = isMutator(func) ? func.optimistically(...args) : func.locally(...args); } catch (error) { callback === null || callback === void 0 ? void 0 : callback(createSnapshot(undefined, { origin: 'local', loading: false, error, })); } const shouldAbort = isCachable(local) && isFresh(local, lifeSpan); callback === null || callback === void 0 ? void 0 : callback(createSnapshot(local, { origin: 'local', loading: !(isFetcher(func) && shouldAbort), })); if (shouldAbort) return; } else { callback === null || callback === void 0 ? void 0 : callback(createSnapshot(undefined, { origin: 'local', loading: true, })); } func(...args) .then(fresh => { callback === null || callback === void 0 ? void 0 : callback(createSnapshot(fresh, { origin: 'server', loading: false, })); }) .catch(error => { callback === null || callback === void 0 ? void 0 : callback(createSnapshot(undefined, { origin: 'server', loading: false, error, })); }); }; // eslint-disable-next-line no-redeclare function createSnapshot(data, options) { if (isPaged(data) || isCachable(data)) return Object.assign(Object.assign({}, options), data); return Object.assign(Object.assign({}, options), { data }); } /** @hidden */ const idResolvers = { user: ({ userId }) => userId, file: ({ fileId }) => fileId, role: ({ roleId }) => roleId, channel: ({ channelInternalId }) => channelInternalId, subChannel: ({ subChannelId }) => subChannelId, channelUsers: ({ channelId, userId }) => `${channelId}#${userId}`, message: ({ messageId, referenceId }) => referenceId !== null && referenceId !== void 0 ? referenceId : messageId, messagePreviewChannel: ({ channelId }) => `${channelId}`, messagePreviewSubChannel: ({ subChannelId }) => `${subChannelId}`, channelUnreadInfo: ({ channelId }) => channelId, subChannelUnreadInfo: ({ subChannelId }) => subChannelId, channelUnread: ({ channelId }) => channelId, channelMarker: ({ entityId, userId }) => `${entityId}#${userId}`, subChannelMarker: ({ entityId, feedId, userId }) => `${entityId}#${feedId}#${userId}`, messageMarker: ({ feedId, contentId, creatorId }) => `${feedId}#${contentId}#${creatorId}`, feedMarker: ({ feedId, entityId }) => `${feedId}#${entityId}`, userMarker: ({ userId }) => userId, community: ({ communityId }) => communityId, category: ({ categoryId }) => categoryId, communityUsers: ({ communityId, userId }) => `${communityId}#${userId}`, post: ({ postId }) => postId, comment: ({ commentId }) => commentId, commentChildren: ({ commentId }) => commentId, poll: ({ pollId }) => pollId, reaction: ({ referenceType, referenceId }) => `${referenceType}#${referenceId}`, reactor: ({ reactionId }) => reactionId, stream: ({ streamId }) => streamId, streamModeration: ({ streamId }) => streamId, follow: ({ from, to }) => `${from}#${to}`, followInfo: ({ userId }) => userId, followCount: ({ userId }) => userId, feed: ({ targetId, feedId }) => `${targetId}#${feedId}`, story: ({ referenceId }) => referenceId, storyTarget: ({ targetId }) => targetId, ad: ({ adId }) => adId, advertiser: ({ advertiserId }) => advertiserId, pin: ({ placement, referenceId }) => `${placement}#${referenceId}`, pinTarget: ({ targetId }) => targetId, notificationTrayItem: ({ _id }) => _id, notificationTraySeen: ({ userId }) => userId, invitation: ({ _id }) => _id, joinRequest: ({ joinRequestId }) => joinRequestId, }; /** * Retrieve the id resolver matching a domain name * * @param name the domain name for the resolve * @returns an idResolver function for the given domain name */ const getResolver = (name) => idResolvers[name]; /** * A map of v3 response keys to a store name. * @hidden */ const PAYLOAD2MODEL = { users: 'user', files: 'file', roles: 'role', stories: 'story', storyTargets: 'storyTarget', channels: 'channel', messageFeeds: 'subChannel', channelUsers: 'channelUsers', messages: 'message', messagePreviewChannel: 'messagePreviewChannel', messagePreviewSubChannel: 'messagePreviewSubChannel', channelUnreadInfo: 'channelUnreadInfo', subChannelUnreadInfo: 'subChannelUnreadInfo', userEntityMarkers: 'channelMarker', userFeedMarkers: 'subChannelMarker', contentMarkers: 'messageMarker', feedMarkers: 'feedMarker', userMarkers: 'userMarker', communities: 'community', categories: 'category', communityUsers: 'communityUsers', posts: 'post', postChildren: 'post', comments: 'comment', commentChildren: 'comment', polls: 'poll', reactors: 'reactor', reactions: 'reaction', videoStreamings: 'stream', videoStreamingChildren: 'stream', videoStreamModerations: 'streamModeration', follows: 'follow', followCounts: 'followCount', feeds: 'feed', ads: 'ad', advertisers: 'advertiser', pinTargets: 'pinTarget', pins: 'pin', notificationTrayItems: 'notificationTrayItem', invitations: 'invitation', joinRequests: 'joinRequest', }; /** hidden */ const isOutdated = (prevData, nextData) => { // Check if the new value is outdated. if ('updatedAt' in nextData && 'updatedAt' in prevData) { return new Date(nextData.updatedAt) < new Date(prevData.updatedAt); } return false; }; /** hidden */ function getFutureDate(date = new Date().toISOString()) { return new Date(new Date(date).getTime() + 1).toISOString(); } /* eslint-disable max-classes-per-file */ /** * Generic ASC error * @category Errors */ class ASCError extends Error { /** * @param message A custom error message * @param code A normalized error code * @param level A normalized failure level descriptor */ constructor(message, code, level) { super(`Amity SDK (${code}): ${message}`); this.code = code; this.level = level; this.type = 'ASC'; this.timestamp = Date.now(); if (Error.captureStackTrace) Error.captureStackTrace(this, ASCError); } } /** * API level error * @category Errors */ class ASCApiError extends ASCError { /** * @param code A normalized error code * @param level A normalized failure level descriptor */ // eslint-disable-next-line no-useless-constructor constructor(message, code, level) { super(message, code, level); } } /** * Unexpected error * @category Errors */ class ASCUnknownError extends ASCError { /** * @param code A normalized error code * @param level A normalized failure level descriptor */ constructor(code = 800000 /* Amity.ClientError.UNKNOWN_ERROR */, level = "fatal" /* Amity.ErrorLevel.FATAL */) { super('Unexpected error', code, level); } } /** * Network related error * @category Errors */ class ASCConnectionError extends ASCError { /** * @param message A custom error message */ constructor(event, message = 'SDK client is having connection issues') { super(`${message} (${event})`, event === 'disconnected' ? 800211 /* Amity.ClientError.DISCONNECTED */ : 800210 /* Amity.ClientError.CONNECTION_ERROR */, "error" /* Amity.ErrorLevel.ERROR */); this.event = event; } } /** * Input sanitization related error * @category Errors */ class ASCInvalidParameterError extends ASCError { /** * @param message A custom error message */ constructor(message) { super(message, 800110 /* Amity.ClientError.INVALID_PARAMETERS */, "error" /* Amity.ErrorLevel.ERROR */); } } let activeClient = null; /** * Get the active client * * @returns the active client instance * * @hidden */ const getActiveClient = () => { if (!activeClient) { throw new ASCError('There is no active client', 800000 /* Amity.ClientError.UNKNOWN_ERROR */, "fatal" /* Amity.ErrorLevel.FATAL */); } return activeClient; }; /** * Sets the active client * * @param client the client to assume as currently active client * * @hidden */ const setActiveClient = (client) => { activeClient = client; }; /** * ```js * import { enableCache } from '@amityco/ts-sdk-react-native' * enableCache() * ``` * * Adds a new {@link Amity.Cache} object to * an {@link Amity.Client} instance * * @param prevState a previous state of cache instance (useful for SSR) * @param persistIf a function to determine if an entry inserted in cache * is destined to be also saved in the persistent storage when * calling {@link backupCache} * * @category Cache API */ const enableCache = (prevState = {}, persistIf) => { const client = getActiveClient(); if (client.cache) return; client.log('cache/api/enableCache'); client.cache = { data: prevState, persistIf }; }; /** * ```js * import { disableCache } from '@amityco/ts-sdk-react-native' * disableCache() * ``` * * Wipes the existing {@link Amity.Cache} object attached to * an {@link Amity.Client} instance * * @category Cache API */ const disableCache = () => { const client = getActiveClient(); if (!client.cache) return; client.log('cache/api/disableCache'); // we do this so that testing if cache is enabled // is only `if (client.cache) delete client.cache; }; /** * ```js * import { restoreCache } from '@amityco/ts-sdk-react-native' * const success = await restoreCache() * ``` * * Reads a previously saved {@link Amity.Cache} from a persistent storage, * and inserts it into the current {@link Amity.Cache} instance. * * The strategy for persistent storage will depend on the runtime, * which is supported by @react-native-async-storage/async-storage. * * The current userId will be appended to the given storageKey to ensures * the cached data concerns only the current user. * * @param storageKey the name of the persistent storage * @returns a success boolean if the cache was dumped to persistent storage * * @category Cache API */ const restoreCache = async (storageKey = 'amitySdk') => { var _a; const client = getActiveClient(); if (!client.cache) return false; client.log('cache/api/restoreCache', { storageKey }); const serializedData = localStorage ? (_a = (await localStorage.getItem(`${storageKey}#${client.userId}`))) !== null && _a !== void 0 ? _a : '{}' : '{}'; let cache = {}; try { cache = JSON.parse(serializedData); } catch (err) { // } // current cache should override. in case there's something fresher. client.cache.data = Object.assign(Object.assign({}, cache), client.cache.data); return true; }; /** * ```js * import { backupCache } from '@amityco/ts-sdk-react-native' * const success = await backupCache() * ``` * * Writes the {@link Amity.Cache} to a persistent storage. * * The strategy for persistent storage will depend on the runtime, * which is supported by @react-native-async-storage/async-storage. * * The current userId will be appended to the given storageKey to avoid * collision between multiple client instances over time. * * @param storageKey the name of the persistent storage * @param persistIf a custom function to define the persistence policy. Default * will check the value of {@link Amity.CacheEntry["offline"]}, which can be * defined globally when customizing {@link Amity.Cache["persistIf"]} * @returns a success boolean if the cache was dumped to persistent storage * * @category Cache API */ const backupCache = async (storageKey = 'amitySdk', persistIf = (entry) => entry.offline) => { const { log, cache, userId } = getActiveClient(); if (!cache) return false; log('cache/api/backupCache', { storageKey }); // prepare a subset of the cache where only // objects to backup are there const offlineEntries = Object.fromEntries(Object.entries(cache.data).filter(([_, entry]) => persistIf(entry))); // nothing to backup, abort if (!Object.keys(offlineEntries).length) return false; if (localStorage) { await localStorage.setItem(`${storageKey}#${userId}`, JSON.stringify(offlineEntries)); } return true; }; /** * ```js * import { wipeCache } from '@amityco/ts-sdk-react-native' * const success = await wipeCache() * ``` * * Wipes a persistent storage for the current {@link Amity.Cache} instance. * * The strategy for persistent storage will depend on the runtime, * which is supported by @react-native-async-storage/async-storage. * * The current userId will be appended to the given storageKey to avoid * collision between multiple client instances over time. * * @param storageKey the name of the persistent storage * @returns a success boolean if the persistent cache was wiped. * * @category Cache API */ const wipeCache = async (storageKey = 'amitySdk') => { const { log, cache, userId } = getActiveClient(); if (!cache) return false; log('cache/api/wipeCache', { storageKey }); cache.data = {}; if (localStorage) { await localStorage.setItem(`${storageKey}#${userId}`, '{}'); } return true; }; /** * ```js * import { queryCache } from '@amityco/ts-sdk-react-native' * const entries = queryCache(["user"]) * ``` * * Retrieves a list of {@link Amity.CacheEntry} objects matching a * partial {@link Amity.CacheKey}. The cache entries can't be typed, * but the expected returned type can be passed manually. * * @param partialKey the partial key matching the objects to retrieve from cache * @returns the matching cache entries, or empty array. * * @category Cache API */ const queryCache = (key) => { const { log, cache } = getActiveClient(); if (!cache) return; log('cache/api/queryCache', { key }); return Object.keys(cache.data) .filter(stringKey => { const decodedKey = decodeKey(stringKey); return partialMatch(key, decodedKey); }) .map(stringKey => cache.data[stringKey]); }; /** * ```js * import { pullFromCache } from '@amityco/ts-sdk-react-native' * const user = pullFromCache<Amity.User>(["user", "foobar"]) * ``` * * Retrieves a {@link Amity.CacheEntry} object matching a given * {@link Amity.CacheKey}. The cache entry is not typed, so the * expected returned type must be passed manually. * * @param key the key matching the object to retrieve from cache * @returns the matching cache entry, or undefined. * * @category Cache API */ const pullFromCache = (key) => { const { log, cache } = getActiveClient(); if (!cache) return; log('cache/api/pullFromCache', key); const str = encodeKey(key); return cache.data[str] ? cache.data[str] : undefined; }; /** * ```js * import { pushToCache } from '@amityco/ts-sdk-react-native' * pushToCache<Amity.InternalUser>(["user", "foobar"], user) * ``` * * Saves any provided value as {@link Amity.CacheEntry} for the matching {@link Amity.CacheKey} * * @param key the key to save the object to * @param data the object to save * @param options customisation object around cache behavior (default gives a 2mn lifespan) * @returns a success boolean if the object was saved in cache * * @category Cache API */ const pushToCache = (key, data, options = { cachedAt: Date.now() }) => { const { log, cache } = getActiveClient(); if (!cache) return false; log('cache/api/pushToCache', { key, data, options }); // if consumer did not pass offline but a offline policy is // defined, use the fn to determine if the object needs to // be saved in persistent storage or not. if (!(options === null || options === void 0 ? void 0 : options.hasOwnProperty('offline')) && cache.persistIf) { // eslint-disable-next-line no-param-reassign options.offline = cache.persistIf(key, data); } const encodedKey = encodeKey(key); cache.data[encodedKey] = Object.assign({ key, data }, options); return true; }; /** * ```js * import { mergeInCache } from '@amityco/ts-sdk-react-native' * * mergeInCache( * ["foo", "bar"], * (oldVal) => ({ ...oldVal, ...newVal }). * ) * ``` * * Merges a new {@link Amity.Cache} object to an {@link Amity.Client} instance * * @param key the key matching the object to retrieve from cache * @param mutation either a plain object to shallow merge, or a function. * @returns a success boolean if the object was updated * * @category Cache API */ const mergeInCache = (key, mutation, options) => { const { log, cache } = getActiveClient(); if (!cache) return false; log('cache/api/mergeInCache', { key, mutation }); const oldVal = pullFromCache(key); if (!oldVal) return false; const newVal = typeof mutation === 'function' ? mutation(oldVal.data) : Object.assign(Object.assign({}, oldVal.data), mutation); if (isOutdated(oldVal.data, newVal)) { return false; } pushToCache(key, newVal, options); return true; }; /** * ```js * import { upsertInCache } from '@amityco/ts-sdk-react-native' * upsertInCache<Amity.InternalUser>(["user", "foobar"], user) * ``` * * Insert or update any provided value as {@link Amity.CacheEntry} for * the matching {@link Amity.CacheKey} * * @param key the key to save the object to * @param data the object to save * @param options customisation object around cache behavior (default gives a 2mn lifespan) * @returns a success boolean if the object was saved in cache * * @category Cache API * @hidden */ const upsertInCache = (key, data, options = { cachedAt: Date.now() }) => { const { log, cache } = getActiveClient(); if (!cache) return false; log('cache/api/upsertInCache', { key, data, options }); const cached = pullFromCache(key); return cached ? mergeInCache(key, data, options) : pushToCache(key, data, options); }; /** * ```js * import { dropFromCache } from '@amityco/ts-sdk-react-native' * const success = dropFromCache(['user', 'foobar']) * ``` * * Removes an existing {@link Amity.CacheEntry} from the {@link Amity.Client}'s * {@link Amity.Cache} from a given {@link Amity.CacheKey} * * @param key The key of the object to delete * @param exact If false, the function will delete all keys satisfying the given key * @returns A success boolean if the object was deleted * * @category Cache API */ const dropFromCache = (key, exact = false) => { const { log, cache } = getActiveClient(); if (!cache) return false; log('cache/api/dropFromCache', { key, exact }); if (!exact) { return Object.keys(cache.data) .map(stringKey => decodeKey(stringKey)) .filter(candidate => partialMatch(key, candidate)) .map(filteredKey => dropFromCache(filteredKey, true)) .every(returned => returned); } const encodedKey = encodeKey(key); if (encodedKey in cache.data) { delete cache.data[encodedKey]; return true; } return false; }; // Note: // this file should contain a suite of filtering utilities to help the // local version of the query functions. /** * Filter a given collection with strict equality against a param * * @param collection the collection to filter * @param key the key of the collection's items to challenge * @param value the expected value * @returns a filtered collection with items only matching the criteria * * @hidden */ const filterByPropEquality = (collection, key, value) => value !== undefined ? collection.filter(item => JSON.stringify(item[key]) === JSON.stringify(value)) : collection; const filterByStringComparePartially = (collection, key, value) => value !== undefined ? collection.filter(item => { if (typeof item[key] === 'string' && typeof value === 'string') { return String(item[key]).toLowerCase().match(value.toLowerCase()); } return false; }) : collection; const filterByPropInclusion = (collection, key, value) => (value !== undefined ? collection.filter(item => value.includes(item[key])) : collection); const filterByPropIntersection = (collection, key, values) => { if (!(values === null || values === void 0 ? void 0 : values.length)) return collection; return collection.filter(item => Array.isArray(item[key]) && values.some(value => item[key].includes(value))); }; /** * Filter a channel collection by membership of the userId * * @param collection the channel collection to filter * @param membership the membership to be filtered by * @param userId user id to be filtered by * @returns a filtered collection with items only matching the criteria * * @hidden */ const filterByChannelMembership = (collection, membership, userId) => { if (membership === 'all') { return collection; } return collection.filter(c => { var _a; // if channel is a community, only member user must receive realtime event if (c.type === 'community') return true; // get resolver for the channel by user const channelUserCacheKey = getResolver('channelUsers')({ channelId: c.channelPublicId, userId, }); const channelUser = (_a = pullFromCache([ 'channelUsers', 'get', channelUserCacheKey, ])) === null || _a === void 0 ? void 0 : _a.data; if (membership === 'member') { return channelUser && channelUser.membership !== 'none'; } // only membership value remainging is 'notMember' return !channelUser || channelUser.membership === 'none'; }); }; /** * Filter a channel collection by membership of the userId * * @param collection the channel collection to filter * @param feedType to be filtered by * @returns a filtered collection with items only matching the criteria * * @hidden */ const filterByFeedType = (collection, feedType) => { /* * It is possible that the targetId & feedId are the same for most of the posts * But since cache is in-memory, i've avoided memoizing, to avoid premature * optimization. Can be revisited if performance issues arise */ return collection.filter(({ targetId, feedId }) => { var _a; const feed = (_a = pullFromCache([ 'feed', 'get', getResolver('feed')({ targetId, feedId }), ])) === null || _a === void 0 ? void 0 : _a.data; return feed && feed.feedType === feedType; }); }; /** * Filter a community collection by membership of the userId * * @param collection the community to filter * @param membership the membership to be filtered by * @param userId user id to be filtered by * @returns a filtered collection with items only matching the criteria * * @hidden */ const filterByCommunityMembership = (collection, membership, userId) => { if (membership === 'all') { return collection; } return collection.filter(c => { var _a; // get resolver for the community by user const communityUserCacheKey = getResolver('communityUsers')({ communityId: c.communityId, userId, }); const communityUser = (_a = pullFromCache([ 'communityUsers', 'get', communityUserCacheKey, ])) === null || _a === void 0 ? void 0 : _a.data; if (membership === 'member') { return communityUser && communityUser.communityMembership === 'member'; } // only membership value remainging is 'notMember' return !communityUser || communityUser.communityMembership !== 'none'; }); }; /** * Filter a post collection by dataType * * @param collection the post to filter * @param dataTypes of the post to be filtered by * @returns a filtered collection with items only matching the criteria * * @hidden */ const filterByPostDataTypes = (collection, dataTypes) => { return collection.reduce((acc, post) => { var _a; // Check dataType for current post if (dataTypes === null || dataTypes === void 0 ? void 0 : dataTypes.includes(post.dataType)) { return [...acc, post]; } if (((post === null || post === void 0 ? void 0 : post.children) || []).length > 0) { const childPost = (_a = pullFromCache(['post', 'get', post.children[0]])) === null || _a === void 0 ? void 0 : _a.data; if (!(dataTypes === null || dataTypes === void 0 ? void 0 : dataTypes.includes(childPost === null || childPost === void 0 ? void 0 : childPost.dataType))) return [...acc, post]; return acc; } return acc; }, []); }; /** * Filter a collection by search term * Check if userId matches first if not filter by displayName * * @param collection to be filtered * @param searchTerm to filter collection by * @returns a filtered collection with items only matching the search term * * @hidden */ const filterBySearchTerm = (collection, searchTerm) => { /* * Search term should match regardless of the case. * Hence, the flag "i", is passed to the created regex */ const containsMatcher = new RegExp(searchTerm, 'i'); return collection.filter(m => { var _a; if (m.userId.match(containsMatcher)) return true; return m.user && ((_a = m.user.displayName) === null || _a === void 0 ? void 0 : _a.match(containsMatcher)); }); }; // Note: // this file should contain a suite of sorting utilities to help the // local version of the query functions. /** * Alphabetic sorting of objects having a displayName */ const sortByDisplayName = ({ displayName: a }, { displayName: b }) => { if (a && b) return a.localeCompare(b); return a ? -1 : 1; }; /** * Alphabetic sorting of objects having a name */ const sortByName = ({ name: a }, { name: b }) => { if (a && b) return a.localeCompare(b); return a ? -1 : 1; }; /** * Sorting a collection by their apparition order (oldest first) */ const sortByChannelSegment = ({ channelSegment: a }, { channelSegment: b }) => a - b; /** * Sorting a collection by their apparition order (oldest first) */ const sortBySegmentNumber = ({ segmentNumber: a }, { segmentNumber: b }) => a - b; /** * Sorting a collection by its oldest items */ const sortByFirstCreated = ({ createdAt: a }, { createdAt: b }) => new Date(a).valueOf() - new Date(b).valueOf(); /** * Sorting a story-collection by its localSortingDate */ const sortByLocalSortingDate = ({ localSortingDate: a }, { localSortingDate: b }) => new Date(b).getTime() - new Date(a).getTime(); /** * Sorting a collection by its newest items */ const sortByLastCreated = ({ createdAt: a }, { createdAt: b }) => new Date(b).valueOf() - new Date(a).valueOf(); /** * Sorting a collection by its oldest items * -- Due to Amity.UpdatedAt is an optional type, we need to define a default value to 0 to prevent error */ const sortByFirstUpdated = ({ updatedAt: a = 0 }, { updatedAt: b = 0 }) => new Date(a).valueOf() - new Date(b).valueOf(); /** * Sorting a collection by its newest items * -- Due to Amity.UpdatedAt is an optional type, we need to define a default value to 0 to prevent error */ const sortByLastUpdated = ({ updatedAt: a = 0 }, { updatedAt: b = 0 }) => new Date(b).valueOf() - new Date(a).valueOf(); /** * Sorting a collection by the items with most recent activity */ const sortByLastActivity = ({ lastActivity: a }, { lastActivity: b }) => new Date(b).valueOf() - new Date(a).valueOf(); let activeUser = null; /* begin_public_function id: client.get_current_user */ /** * for internal use */ const getActiveUser = () => { if (!activeUser) { throw new ASCError('Connect client first', 800000 /* Amity.ClientError.UNKNOWN_ERROR */, "fatal" /* Amity.ErrorLevel.FATAL */); } return activeUser; }; /* end_public_function */ const setActiveUser = (user) => { activeUser = { _id: user._id, userId: user.userId, path: user.path, }; }; /* eslint-disable @typescript-eslint/ban-types */ let tasks = []; let timer; /** * Add a function to run just before the next tick * * @param task function to schedule for later execution */ const scheduleTask = (task) => { clearTimeout(timer); tasks.push(task); timer = setTimeout(() => { tasks.forEach(fn => fn()); tasks = []; }, 0); }; /* eslint-disable @typescript-eslint/no-unused-vars */ const MQTT_EVENTS = [ 'connect', 'message', 'disconnect', 'error', 'close', 'end', 'reconnect', 'video-streaming.didStart', 'video-streaming.didRecord', 'video-streaming.didStop', 'video-streaming.didFlag', 'video-streaming.didTerminate', 'video-streaming.viewerDidBan', 'video-streaming.viewerDidUnban', 'liveReaction.created', ]; /** @hidden */ const createEventEmitter = () => { return mitt(); }; const proxyMqttEvents = (mqttClient, emitter) => { MQTT_EVENTS.forEach(event => { mqttClient === null || mqttClient === void 0 ? void 0 : mqttClient.on(event, (...params) => { emitter.emit(event, params.length === 1 ? params[0] : params); }); }); // @ts-ignore mqttClient.on('message', (topic, payload) => { const message = JSON.parse(payload.toString()); emitter.emit(message.eventType, message.data); }); }; /** * Standardize the subscription of SSE through web sockets * * @param client The current client for which to subscribe the event to * @param namespace A unique name for the logger * @param event The websocket event name * @param fn A wrapper for the callback. * @returns A dispose function to unsubscribe to the event * * @category Transport * @hidden */ const createEventSubscriber = (client, namespace, event, fn) => { const { log, emitter } = client; const timestamp = Date.now(); log(`${namespace}(tmpid: ${timestamp}) > listen`); const handler = (...payload) => { log(`${namespace}(tmpid: ${timestamp}) > trigger`, payload); try { fn(...payload); } catch (e) { log(`${namespace}(tmpid: ${timestamp}) > error`, e); } }; emitter.on(event, handler); return () => { log(`${namespace}(tmpid: ${timestamp}) > dispose`); emitter.off(event, handler); }; }; /** * Wrapper around dispatch event * * @hidden */ const fireEvent = (event, payload) => { const { emitter } = getActiveClient(); scheduleTask(() => { emitter.emit(event, payload); }); }; let mqttAccessToken; let mqttUserId; async function modifyMqttConnection() { var _a; const { mqtt, emitter, token } = getActiveClient(); if (!mqtt) return; const accessToken = (_a = token === null || token === void 0 ? void 0 : token.accessToken) !== null && _a !== void 0 ? _a : ''; const user = getActiveUser(); if (mqttAccessToken !== accessToken || mqttUserId !== user._id) { mqttAccessToken = accessToken; mqttUserId = user._id; await mqtt.connect({ accessToken: mqttAccessToken, userId: mqttUserId }); proxyMqttEvents(mqtt, emitter); } } var SubscriptionLevels; (function (SubscriptionLevels) { SubscriptionLevels["COMMUNITY"] = "community"; SubscriptionLevels["POST"] = "post"; SubscriptionLevels["COMMENT"] = "comment"; SubscriptionLevels["POST_AND_COMMENT"] = "post_and_comment"; SubscriptionLevels["USER"] = "user"; })(SubscriptionLevels || (SubscriptionLevels = {})); const getCommunityUserTopic = (path, level) => { switch (level) { case 'post': return `${path}/post/+`; case 'comment': return `${path}/post/+/comment/+`; case 'post_and_comment': return `${path}/post/#`; default: return path; } }; const getNetworkId = (user) => user.path.split('/user/')[0]; const getCommunityTopic = ({ path }, level = SubscriptionLevels.COMMUNITY) => getCommunityUserTopic(path, level); const getUserTopic = ({ path }, level = SubscriptionLevels.USER