UNPKG

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
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; };