flotilla/src/app/util/notifications.ts

175 lines
4.5 KiB
TypeScript
Raw Normal View History

import {derived} from "svelte/store"
2025-11-12 01:49:22 +00:00
import {Badge} from "@capawesome/capacitor-badge"
import {synced, throttled, withGetter} from "@welshman/store"
import {pubkey, tracker, repository, relaysByUrl} from "@welshman/app"
import {assoc, prop, spec, first, identity, now} from "@welshman/lib"
import type {TrustedEvent} from "@welshman/util"
import {deriveEventsByIdByUrl} from "@welshman/store"
import {sortEventsDesc} from "@welshman/util"
import {makeSpacePath, makeRoomPath, makeSpaceChatPath, makeChatPath} from "@app/util/routes"
import {
MESSAGE_KINDS,
2026-01-24 00:51:02 +00:00
notificationSettings,
2025-11-20 23:54:06 +00:00
chatsById,
2025-11-20 23:12:16 +00:00
userGroupList,
getSpaceUrlsFromGroupList,
2026-02-27 20:25:16 +00:00
getSpaceRoomsFromGroupList,
makeCommentFilter,
2026-02-27 20:25:16 +00:00
hasNip29,
2025-09-23 15:34:53 +00:00
} from "@app/core/state"
2025-11-12 01:49:22 +00:00
import {kv} from "@app/core/storage"
import {page} from "$app/stores"
export {Push} from "@app/util/push"
2026-01-28 01:15:22 +00:00
2024-11-14 18:53:13 +00:00
// Checked state
export const checked = withGetter(
synced<Record<string, number>>({
key: "checked",
defaultValue: {},
storage: kv,
}),
)
export const getChecked = (key: string) => checked.get()[key]
export const deriveChecked = (key: string) => derived(checked, prop<number>(key))
export const setChecked = (key: string) => checked.update(assoc(key, now()))
export const syncChecked = () => {
let prev = ""
2024-11-14 18:53:13 +00:00
const getPaths = (path: string) =>
path
.split("/")
.map((_, i, segments) => segments.slice(0, i + 1).join("/"))
.slice(1)
2024-11-14 18:53:13 +00:00
// Set checked when we enter and when we leave a given page
return page.subscribe($page => {
checked.update($checked => {
for (const path of getPaths($page.url.pathname)) {
$checked[path] = now()
}
for (const path of getPaths(prev)) {
$checked[path] = now()
}
return $checked
})
prev = $page.url.pathname
})
}
2024-11-14 18:53:13 +00:00
2024-12-16 19:24:10 +00:00
// Derived notifications state
2024-11-14 18:53:13 +00:00
export const allNotifications = derived(
2026-01-20 18:40:33 +00:00
throttled(
2000,
2026-01-20 18:40:33 +00:00
derived(
[
pubkey,
checked,
chatsById,
2026-02-27 20:25:16 +00:00
relaysByUrl,
2026-01-20 18:40:33 +00:00
userGroupList,
deriveEventsByIdByUrl({
tracker,
repository,
filters: [{kinds: MESSAGE_KINDS}, makeCommentFilter(MESSAGE_KINDS)],
}),
2026-01-20 18:40:33 +00:00
],
identity,
),
2026-01-20 18:40:33 +00:00
),
2026-02-27 20:25:16 +00:00
([$pubkey, $checked, $chatsById, $relaysByUrl, $userGroupList, eventsByIdByUrl]) => {
const hasNotification = (path: string, latestEvent?: TrustedEvent) => {
2026-01-20 18:40:33 +00:00
if (!latestEvent || latestEvent.pubkey === $pubkey) {
return false
}
2024-11-14 18:53:13 +00:00
2026-01-20 18:40:33 +00:00
for (const [entryPath, ts] of Object.entries($checked)) {
const isMatch =
entryPath === "*" ||
entryPath.startsWith(path) ||
(entryPath === "/chat/*" && path.startsWith("/chat/"))
2024-11-14 20:16:44 +00:00
2026-01-20 18:40:33 +00:00
if (isMatch && ts > latestEvent.created_at) {
return false
2025-11-20 23:54:06 +00:00
}
2024-12-16 19:24:10 +00:00
}
2025-02-06 17:29:30 +00:00
2026-01-20 18:40:33 +00:00
return true
}
2025-02-06 17:29:30 +00:00
2026-01-20 18:40:33 +00:00
const paths = new Set<string>()
2025-10-06 18:23:19 +00:00
2026-01-20 18:40:33 +00:00
for (const {pubkeys, messages} of $chatsById.values()) {
const chatPath = makeChatPath(pubkeys)
if (hasNotification(chatPath, messages[0])) {
paths.add("/chat")
paths.add(chatPath)
2025-11-20 23:54:06 +00:00
}
2026-01-20 18:40:33 +00:00
}
2025-10-06 18:23:19 +00:00
2026-01-20 18:40:33 +00:00
for (const url of getSpaceUrlsFromGroupList($userGroupList)) {
const spacePath = makeSpacePath(url)
2026-03-16 20:46:00 +00:00
const events = sortEventsDesc((eventsByIdByUrl.get(url) || new Map()).values())
2026-02-27 20:25:16 +00:00
if (hasNip29($relaysByUrl.get(url))) {
for (const h of getSpaceRoomsFromGroupList(url, $userGroupList)) {
const roomPath = makeRoomPath(url, h)
2026-03-16 20:46:00 +00:00
const latestEvent = events.find(e => e.tags.some(spec(["h", h])))
2026-02-27 20:25:16 +00:00
if (hasNotification(roomPath, latestEvent)) {
paths.add(spacePath)
paths.add(roomPath)
}
}
} else {
const messagesPath = makeSpaceChatPath(url)
2026-03-16 20:46:00 +00:00
if (hasNotification(messagesPath, first(events))) {
2026-02-27 20:25:16 +00:00
paths.add(spacePath)
paths.add(messagesPath)
}
}
2026-01-20 18:40:33 +00:00
}
2024-12-16 18:30:42 +00:00
2026-01-20 18:40:33 +00:00
return paths
},
)
export const notifications = derived([page, allNotifications], ([$page, $allNotifications]) => {
2026-02-10 01:40:10 +00:00
return new Set([...$allNotifications].filter(p => !$page.url.pathname.startsWith(p)))
})
// Badges
export const syncBadges = () =>
2026-01-28 01:15:22 +00:00
derived([notifications, notificationSettings], identity).subscribe(
async ([$notifications, $notificationSettings]) => {
if ($notificationSettings.badge) {
try {
await Badge.set({count: $notifications.size})
} catch (err) {
// pass - firefox doesn't support badges
}
} else {
await clearBadges()
}
},
)
2025-09-23 15:34:53 +00:00
export const clearBadges = async () => {
2026-01-22 20:37:30 +00:00
try {
await Badge.clear()
} catch (e) {
// pass - firefox doesn't support this
2026-01-22 20:37:30 +00:00
}
2025-09-23 15:34:53 +00:00
}