@amityco/ts-sdk-react-native
Version:
Amity Social Cloud Typescript SDK
1,499 lines (1,462 loc) • 1.62 MB
JavaScript
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