BangSo/apps/mobile/lib/live-notifications.ts

146 lines
4.8 KiB
TypeScript

import { requireNativeModule } from "expo-modules-core";
import * as Notifications from "expo-notifications";
import { Platform } from "react-native";
import { apiBaseWarning, normalizeApiBase } from "./endpoint";
export interface LiveNotificationSettings {
liveConnection: boolean;
messages: boolean;
scheduledTasks: boolean;
needsAttention: boolean;
}
export const DEFAULT_LIVE_NOTIFICATION_SETTINGS: LiveNotificationSettings = {
liveConnection: false,
messages: true,
scheduledTasks: true,
needsAttention: true,
};
type NativeNotifications = {
getSettings(): Promise<LiveNotificationSettings>;
setSettings(
settings: LiveNotificationSettings,
endpoint: string,
token: string,
spaceId: string,
): Promise<void>;
resume(endpoint: string, token: string, spaceId: string): Promise<void>;
stop(clearSession: boolean): Promise<void>;
setOpenThread(botId: string | null, threadId: string | null): Promise<void>;
openSettings(): Promise<void>;
canPostPromotedNotifications(): Promise<boolean>;
openPromotedSettings(): Promise<void>;
};
const nativeNotifications =
Platform.OS === "android"
? requireNativeModule<NativeNotifications>("RakazoNotifications")
: null;
export type NotificationThreadTarget = { botId?: string; threadId?: string };
let openThread: NotificationThreadTarget | null = null;
let foregroundHandlerConfigured = false;
export function notificationTargetsThread(
data: Record<string, unknown> | null | undefined,
target: NotificationThreadTarget | null,
): boolean {
if (!data || !target) return false;
const dataThreadId = data.threadId ?? data["rakazo.threadId"];
if (target.threadId && dataThreadId) return dataThreadId === target.threadId;
return Boolean(
target.botId && (data.botId === target.botId || data["rakazo.botId"] === target.botId),
);
}
export function configureForegroundNotifications(): void {
if (foregroundHandlerConfigured) return;
foregroundHandlerConfigured = true;
Notifications.setNotificationHandler({
handleNotification: async ({ request }) => {
const show = !notificationTargetsThread(request.content.data, openThread);
return {
shouldShowBanner: show,
shouldShowList: show,
shouldPlaySound: false,
shouldSetBadge: false,
};
},
});
}
export async function getLiveNotificationSettings(): Promise<LiveNotificationSettings> {
return nativeNotifications?.getSettings() ?? DEFAULT_LIVE_NOTIFICATION_SETTINGS;
}
export async function setLiveNotificationSettings(
settings: LiveNotificationSettings,
endpoint: string,
token: string,
spaceId: string,
): Promise<void> {
if (!nativeNotifications) return;
const parsed = normalizeApiBase(endpoint);
if (!parsed.ok) throw new Error(parsed.error);
const endpointWarning = apiBaseWarning(parsed.url);
if (endpointWarning) throw new Error(endpointWarning);
if (settings.liveConnection) {
const existing = await Notifications.getPermissionsAsync();
const granted = existing.granted || (await Notifications.requestPermissionsAsync()).granted;
if (!granted) throw new Error("Android blocked notifications.");
}
await nativeNotifications.setSettings(settings, parsed.url, token, spaceId);
}
export async function resumeLiveNotifications(
endpoint: string,
token: string,
spaceId: string,
): Promise<void> {
if (!nativeNotifications || !token) return;
const parsed = normalizeApiBase(endpoint);
if (!parsed.ok || apiBaseWarning(parsed.url)) return;
await nativeNotifications.resume(parsed.url, token, spaceId);
}
export async function stopLiveNotifications(clearSession = false): Promise<void> {
await nativeNotifications?.stop(clearSession);
}
export async function setOpenNotificationThread(
target: NotificationThreadTarget | null,
): Promise<void> {
openThread = target;
await nativeNotifications?.setOpenThread(target?.botId ?? null, target?.threadId ?? null);
}
export async function dismissThreadNotifications(target: {
botId?: string;
threadId?: string;
}): Promise<void> {
if (!target.botId && !target.threadId) return;
const presented = await Notifications.getPresentedNotificationsAsync();
await Promise.all(
presented
.filter(({ request }) => {
const data = request.content.data ?? {};
return notificationTargetsThread(data, target);
})
.map(({ request }) => Notifications.dismissNotificationAsync(request.identifier)),
);
}
export async function openLiveNotificationSettings(): Promise<void> {
await nativeNotifications?.openSettings();
}
export async function canPostPromotedNotifications(): Promise<boolean> {
return nativeNotifications?.canPostPromotedNotifications() ?? true;
}
export async function openPromotedNotificationSettings(): Promise<void> {
await nativeNotifications?.openPromotedSettings();
}