stream-chat-react-native-core
Version:
The official React Native and Expo components for Stream Chat, a service for building chat applications
312 lines (249 loc) • 11.6 kB
text/typescript
import type { Notification } from 'stream-chat';
const NOTIFICATION_TARGET_PANELS = [
'channel',
'thread',
'channel-list',
'thread-list',
'channel-details',
] as const;
const NOTIFICATION_TARGET_TAG_PREFIX = 'target:' as const;
/** Built-in SDK surfaces that can host snackbar notifications. */
export type BuiltInNotificationTargetPanel = (typeof NOTIFICATION_TARGET_PANELS)[number];
/** Notification surface name. Custom panel strings are supported for integrator-defined hosts. */
export type NotificationTargetPanel = BuiltInNotificationTargetPanel | (string & {});
/** Exact notification host within a panel. */
export type NotificationTarget = {
/** Stable id for a mounted notification host, for example a channel cid-derived id. */
hostId: string;
/** Surface that owns the host. */
panel: NotificationTargetPanel;
};
/** Owner object used to keep target stacks scoped to a notification manager instance. */
export type NotificationTargetOwner = object;
type ParsedNotificationTargetTag = {
hostId?: string;
panel: NotificationTargetPanel;
};
type NotificationTargetStackItem = {
target: NotificationTarget;
token: symbol;
};
const activeNotificationTargets = new WeakMap<
NotificationTargetOwner,
NotificationTargetStackItem[]
>();
const actionNotificationTargets = new WeakMap<
NotificationTargetOwner,
NotificationTargetStackItem[]
>();
/** Returns whether a value can be encoded as a notification target panel segment. */
export const isNotificationTargetPanel = (value: unknown): value is NotificationTargetPanel =>
typeof value === 'string' && value.length > 0 && !value.includes(':');
/** Returns whether a value is one of the SDK's built-in notification panels. */
export const isBuiltInNotificationTargetPanel = (
value: unknown,
): value is BuiltInNotificationTargetPanel =>
typeof value === 'string' && (NOTIFICATION_TARGET_PANELS as readonly string[]).includes(value);
/** Builds a notification target tag. Without `hostId`, the tag targets every host in the panel. */
export const getNotificationTargetTag = (panel: NotificationTargetPanel, hostId?: string) =>
`${NOTIFICATION_TARGET_TAG_PREFIX}${panel}${hostId ? `:${hostId}` : ''}`;
/** Adds a broad or exact target tag to a notification tag list without duplicating tags. */
export const addNotificationTargetTag = (
panel: NotificationTargetPanel | undefined,
tags?: string[],
hostId?: string,
) => {
if (!panel) return tags ?? [];
return Array.from(new Set([getNotificationTargetTag(panel, hostId), ...(tags ?? [])]));
};
/** Adds an exact panel and host target tag to a notification tag list. */
export const addExactNotificationTargetTag = (target: NotificationTarget, tags?: string[]) =>
addNotificationTargetTag(target.panel, tags, target.hostId);
/** Host id helper for channel notification targets. */
export const getChannelNotificationHostId = (cid: string) => `channel:${cid}` as const;
/** Host id helper for thread notification targets. */
export const getThreadNotificationHostId = (threadId: string) => `thread:${threadId}` as const;
/** Compares notification targets by panel and host id. */
export const isNotificationTargetEqual = (
left: NotificationTarget | undefined,
right: NotificationTarget | undefined,
) => !!left && !!right && left.panel === right.panel && left.hostId === right.hostId;
const isRecord = (value: unknown): value is Record<string, unknown> =>
typeof value === 'object' && value !== null;
const getStringValue = (value: unknown) => (typeof value === 'string' ? value : undefined);
const parseNotificationTargetTag = (tag: string): ParsedNotificationTargetTag | undefined => {
if (!tag.startsWith(NOTIFICATION_TARGET_TAG_PREFIX)) return undefined;
const tagValue = tag.slice(NOTIFICATION_TARGET_TAG_PREFIX.length);
const hostSeparatorIndex = tagValue.indexOf(':');
const panel = hostSeparatorIndex === -1 ? tagValue : tagValue.slice(0, hostSeparatorIndex);
if (!isNotificationTargetPanel(panel)) return undefined;
const hostId = hostSeparatorIndex === -1 ? undefined : tagValue.slice(hostSeparatorIndex + 1);
return hostId ? { hostId, panel } : { panel };
};
const getNotificationTargetTags = (notification: Notification) =>
(notification.tags ?? [])
.map(parseNotificationTargetTag)
.filter((target): target is ParsedNotificationTargetTag => !!target);
/** Returns whether a tag list already contains a notification target tag. */
export const hasNotificationTargetTag = (tags: string[] | undefined) =>
tags?.some((tag) => !!parseNotificationTargetTag(tag)) ?? false;
const getExactNotificationTargetTags = (notification: Notification): NotificationTarget[] =>
getNotificationTargetTags(notification).filter(
(target): target is NotificationTarget => !!target.hostId,
);
const getNotificationTargetFromTags = (
notification: Notification,
): NotificationTarget | undefined => getExactNotificationTargetTags(notification)[0];
const getNotificationTargetFromComposer = (
notification: Notification,
): NotificationTarget | undefined => {
const composer = notification.origin.context?.composer;
if (!isRecord(composer)) return undefined;
const threadId = getStringValue(composer.threadId);
if (threadId) {
return { hostId: getThreadNotificationHostId(threadId), panel: 'thread' };
}
const channel = composer.channel;
if (!isRecord(channel)) return undefined;
const cid = getStringValue(channel.cid);
return cid ? { hostId: getChannelNotificationHostId(cid), panel: 'channel' } : undefined;
};
/** Resolves an exact notification target from tags or composer origin metadata. */
export const getNotificationTarget = (notification: Notification): NotificationTarget | undefined =>
getNotificationTargetFromTags(notification) ?? getNotificationTargetFromComposer(notification);
const getExplicitNotificationTargetPanels = (
notification: Notification,
): NotificationTargetPanel[] => {
const targetPanels = getNotificationTargetTags(notification).map(({ panel }) => panel);
if (targetPanels.length > 0) {
return Array.from(new Set(targetPanels));
}
const panel = notification.origin.context?.panel;
return isNotificationTargetPanel(panel) ? [panel] : [];
};
/** Resolves the first target panel declared or inferred for a notification. */
export const getNotificationTargetPanel = (
notification: Notification,
): NotificationTargetPanel | undefined => {
const explicitPanels = getExplicitNotificationTargetPanels(notification);
if (explicitPanels.length > 0) return explicitPanels[0];
return getNotificationTarget(notification)?.panel;
};
/** Resolves all target panels declared or inferred for a notification. */
export const getNotificationTargetPanels = (
notification: Notification,
): NotificationTargetPanel[] => {
const explicitPanels = getExplicitNotificationTargetPanels(notification);
if (explicitPanels.length > 0) return explicitPanels;
const target = getNotificationTarget(notification);
return target ? [target.panel] : [];
};
const getNotificationTargetStack = (
stackMap: WeakMap<NotificationTargetOwner, NotificationTargetStackItem[]>,
owner: NotificationTargetOwner,
) => {
const stack = stackMap.get(owner) ?? [];
stackMap.set(owner, stack);
return stack;
};
const registerNotificationTarget = (
stackMap: WeakMap<NotificationTargetOwner, NotificationTargetStackItem[]>,
owner: NotificationTargetOwner,
target: NotificationTarget,
) => {
const token = Symbol('notification-target');
const stack = getNotificationTargetStack(stackMap, owner);
stack.push({ target, token });
return () => {
const currentStack = stackMap.get(owner);
if (!currentStack) return;
const itemIndex = currentStack.findIndex((item) => item.token === token);
if (itemIndex !== -1) {
currentStack.splice(itemIndex, 1);
}
if (currentStack.length === 0) {
stackMap.delete(owner);
}
};
};
const getLastNotificationTarget = (
stackMap: WeakMap<NotificationTargetOwner, NotificationTargetStackItem[]>,
owner: NotificationTargetOwner,
) => {
const stack = stackMap.get(owner);
return stack?.[stack.length - 1]?.target;
};
/** Registers a mounted notification host as the latest active fallback target. */
export const registerActiveNotificationTarget = (
owner: NotificationTargetOwner,
target: NotificationTarget,
) => registerNotificationTarget(activeNotificationTargets, owner, target);
/** Registers the target for notifications emitted while an action is running. */
export const registerNotificationActionTarget = (
owner: NotificationTargetOwner,
target: NotificationTarget,
) => registerNotificationTarget(actionNotificationTargets, owner, target);
/** Returns the latest mounted fallback target for a notification manager owner. */
export const getActiveNotificationTarget = (owner: NotificationTargetOwner) =>
getLastNotificationTarget(activeNotificationTargets, owner);
/** Returns the latest action-scoped target for a notification manager owner. */
export const getNotificationActionTarget = (owner: NotificationTargetOwner) =>
getLastNotificationTarget(actionNotificationTargets, owner);
const hasExactNotificationTargetTag = (notification: Notification) =>
getExactNotificationTargetTags(notification).length > 0;
const hasExplicitNotificationTargetPanel = (notification: Notification) =>
getExplicitNotificationTargetPanels(notification).length > 0;
const setNotificationTargetTag = (notification: Notification, target: NotificationTarget) => {
notification.tags = addExactNotificationTargetTag(target, notification.tags);
};
/** Mutates an unscoped notification by adding the best available exact target tag. */
export const resolveNotificationTargetTagIfNeeded = (
owner: NotificationTargetOwner,
notification: Notification,
) => {
if (hasExactNotificationTargetTag(notification)) return false;
if (hasExplicitNotificationTargetPanel(notification)) return false;
const composerTarget = getNotificationTargetFromComposer(notification);
if (composerTarget) {
setNotificationTargetTag(notification, composerTarget);
return true;
}
const actionTarget = getNotificationActionTarget(owner);
if (actionTarget) {
setNotificationTargetTag(notification, actionTarget);
return true;
}
const activeTarget = getActiveNotificationTarget(owner);
if (activeTarget) {
setNotificationTargetTag(notification, activeTarget);
return true;
}
return false;
};
/** Returns whether a notification should be consumed by an exact target host. */
export const isNotificationForTarget = (notification: Notification, target: NotificationTarget) => {
const exactTargets = getExactNotificationTargetTags(notification);
if (exactTargets.length > 0) {
return exactTargets.some((exactTarget) => isNotificationTargetEqual(exactTarget, target));
}
const explicitTargetPanels = getExplicitNotificationTargetPanels(notification);
if (explicitTargetPanels.length > 0) {
return explicitTargetPanels.includes(target.panel);
}
const inferredTarget = getNotificationTargetFromComposer(notification);
if (inferredTarget) {
return isNotificationTargetEqual(inferredTarget, target);
}
return false;
};
/** Returns whether a notification should be consumed by any host in a panel. */
export const isNotificationForPanel = (
notification: Notification,
panel: NotificationTargetPanel,
) => {
const explicitTargetPanels = getNotificationTargetPanels(notification);
if (explicitTargetPanels.length > 0) {
return explicitTargetPanels.includes(panel);
}
return false;
};