import type { Message, Room } from '../../../../shared-kernel'; import type { NotificationDeliveryContext, NotificationDisplayPayload, NotificationsSettings, RoomUnreadCounts } from '../models/notification.model'; export const DEFAULT_TEXT_CHANNEL_ID = 'general'; const MESSAGE_PREVIEW_LIMIT = 140; export type AppTranslateFn = (key: string, params?: Record) => string; export function resolveMessageChannelId(message: Pick): string { return message.channelId || DEFAULT_TEXT_CHANNEL_ID; } export function getRoomTextChannelIds(room: Room): string[] { const textChannelIds = (room.channels ?? []) .filter((channel) => channel.type === 'text') .map((channel) => channel.id); return textChannelIds.length > 0 ? textChannelIds : [DEFAULT_TEXT_CHANNEL_ID]; } export function getRoomById(rooms: Room[], roomId: string): Room | null { return rooms.find((room) => room.id === roomId) ?? null; } export function getChannelLabel(room: Room | null, channelId: string): string { const channelName = room?.channels?.find((channel) => channel.id === channelId)?.name; return channelName || DEFAULT_TEXT_CHANNEL_ID; } export function getChannelLastReadAt( settings: NotificationsSettings, roomId: string, channelId: string ): number { return settings.lastReadByChannel[roomId]?.[channelId] ?? settings.roomBaselines[roomId] ?? 0; } export function getRoomTrackingBaseline(settings: NotificationsSettings, room: Room): number { const trackedChannels = getRoomTextChannelIds(room).map((channelId) => getChannelLastReadAt(settings, room.id, channelId) ); return Math.min(...trackedChannels, settings.roomBaselines[room.id] ?? Date.now()); } export function isRoomMuted(settings: NotificationsSettings, roomId: string): boolean { return settings.mutedRooms[roomId] === true; } export function isChannelMuted( settings: NotificationsSettings, roomId: string, channelId: string ): boolean { return settings.mutedChannels[roomId]?.[channelId] === true; } export function isMessageVisibleInActiveView( message: Pick, context: NotificationDeliveryContext ): boolean { return context.currentRoomId === message.roomId && context.activeChannelId === resolveMessageChannelId(message) && context.isWindowFocused && context.isDocumentVisible; } export function shouldDeliverNotification( settings: NotificationsSettings, message: Pick, context: NotificationDeliveryContext ): boolean { const channelId = resolveMessageChannelId(message); if (!settings.enabled) { return false; } if (context.currentUser?.status === 'busy') { return false; } if (isRoomMuted(settings, message.roomId) || isChannelMuted(settings, message.roomId, channelId)) { return false; } return !isMessageVisibleInActiveView(message, context); } export function buildNotificationDisplayPayload( message: Pick, room: Room | null, settings: NotificationsSettings, requestAttention: boolean, translate: AppTranslateFn ): NotificationDisplayPayload { const channelId = resolveMessageChannelId(message); const roomName = room?.name || translate('notifications.display.defaultServerName'); const channelLabel = getChannelLabel(room, channelId); return { title: `${roomName} ยท #${channelLabel}`, body: settings.showPreview ? formatMessagePreview(message.senderName, message.content, translate) : translate('notifications.display.newMessageHidden', { sender: message.senderName }), requestAttention }; } export function calculateUnreadForRoom( room: Room, messages: Message[], settings: NotificationsSettings, currentUserIds: Set ): RoomUnreadCounts { const channelCounts = Object.fromEntries( getRoomTextChannelIds(room).map((channelId) => [channelId, 0]) ) as Record; for (const message of messages) { if (message.isDeleted || currentUserIds.has(message.senderId)) { continue; } const channelId = resolveMessageChannelId(message); if (!(channelId in channelCounts)) { continue; } if (message.timestamp <= getChannelLastReadAt(settings, room.id, channelId)) { continue; } channelCounts[channelId] += 1; } return { channelCounts, roomCount: Object.values(channelCounts).reduce((total, count) => total + count, 0) }; } function formatMessagePreview(senderName: string, content: string, translate: AppTranslateFn): string { const normalisedContent = content.replace(/\s+/g, ' ').trim(); if (!normalisedContent) { return translate('notifications.display.newMessageEmpty', { sender: senderName }); } const preview = normalisedContent.length > MESSAGE_PREVIEW_LIMIT ? `${normalisedContent.slice(0, MESSAGE_PREVIEW_LIMIT - 1)}...` : normalisedContent; return translate('notifications.display.preview', { sender: senderName, content: preview }); }