flotilla/src/app/util/notifications.ts

216 lines
6.1 KiB
TypeScript
Raw Normal View History

2025-09-23 15:34:53 +00:00
import {derived, get} from "svelte/store"
import {synced, throttled} from "@welshman/store"
import {pubkey, relaysByUrl} from "@welshman/app"
2025-01-03 00:58:04 +00:00
import {prop, spec, identity, now, groupBy} from "@welshman/lib"
2024-12-16 19:24:10 +00:00
import type {TrustedEvent} from "@welshman/util"
2025-10-06 18:23:19 +00:00
import {ZAP_GOAL, EVENT_TIME, MESSAGE, THREAD, COMMENT, getTagValue} from "@welshman/util"
2025-02-06 17:29:30 +00:00
import {
makeSpacePath,
makeChatPath,
2025-10-06 18:23:19 +00:00
makeGoalPath,
2025-02-06 17:29:30 +00:00
makeThreadPath,
makeCalendarPath,
2025-06-25 00:36:14 +00:00
makeSpaceChatPath,
2025-02-06 17:29:30 +00:00
makeRoomPath,
} from "@app/util/routes"
2025-09-23 15:34:53 +00:00
import {
chats,
hasNip29,
getUrlsForEvent,
repositoryStore,
userSettingsValues,
userGroupSelections,
getSpaceUrlsFromGroupSelections,
getSpaceRoomsFromGroupSelections,
2025-09-23 15:34:53 +00:00
} from "@app/core/state"
import {preferencesStorageProvider} from "@src/lib/storage"
2025-09-23 15:34:53 +00:00
import {Badge} from "@capawesome/capacitor-badge"
2024-11-14 18:53:13 +00:00
// Checked state
export const checked = synced<Record<string, number>>({
key: "checked",
defaultValue: {},
storage: preferencesStorageProvider,
})
2024-11-14 18:53:13 +00:00
export const deriveChecked = (key: string) => derived(checked, prop(key))
2024-12-16 19:49:57 +00:00
export const setChecked = (key: string) => checked.update(state => ({...state, [key]: now()}))
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
2024-12-16 19:24:10 +00:00
export const notifications = derived(
2025-01-02 18:04:28 +00:00
throttled(
1000,
derived(
[pubkey, checked, chats, userGroupSelections, repositoryStore, getUrlsForEvent, relaysByUrl],
identity,
),
2025-01-02 18:04:28 +00:00
),
([
$pubkey,
$checked,
$chats,
$userGroupSelections,
$repository,
$getUrlsForEvent,
$relaysByUrl,
]) => {
2025-01-02 18:04:28 +00:00
const hasNotification = (path: string, latestEvent: TrustedEvent | undefined) => {
2024-12-16 19:24:10 +00:00
if (!latestEvent || latestEvent.pubkey === $pubkey) {
return false
}
2024-11-14 18:53:13 +00:00
2024-12-16 19:49:57 +00:00
for (const [entryPath, ts] of Object.entries($checked)) {
2025-01-15 19:07:21 +00:00
const isMatch =
entryPath === "*" ||
entryPath.startsWith(path) ||
(entryPath === "/chat/*" && path.startsWith("/chat/"))
2024-11-15 22:03:54 +00:00
2024-12-16 19:49:57 +00:00
if (isMatch && ts > latestEvent.created_at) {
return false
}
2024-12-16 19:24:10 +00:00
}
2024-11-15 22:03:54 +00:00
2024-12-16 19:49:57 +00:00
return true
2024-12-16 19:24:10 +00:00
}
2024-11-14 18:53:13 +00:00
2024-12-16 19:24:10 +00:00
const paths = new Set<string>()
2024-11-14 20:16:44 +00:00
2024-12-16 19:24:10 +00:00
for (const {pubkeys, messages} of $chats) {
const chatPath = makeChatPath(pubkeys)
2024-12-16 18:30:42 +00:00
2025-01-02 18:04:28 +00:00
if (hasNotification(chatPath, messages[0])) {
2024-12-16 19:24:10 +00:00
paths.add("/chat")
paths.add(chatPath)
}
2024-12-16 18:30:42 +00:00
}
2025-10-06 18:23:19 +00:00
const allGoalComments = $repository.query([{kinds: [COMMENT], "#K": [String(ZAP_GOAL)]}])
2025-02-06 17:29:30 +00:00
2025-10-06 18:23:19 +00:00
const allThreadComments = $repository.query([{kinds: [COMMENT], "#K": [String(THREAD)]}])
2025-02-06 17:29:30 +00:00
2025-10-06 18:23:19 +00:00
const allCalendarComments = $repository.query([{kinds: [COMMENT], "#K": [String(EVENT_TIME)]}])
const allMessages = $repository.query([{kinds: [MESSAGE, THREAD, ZAP_GOAL, EVENT_TIME]}])
2025-01-02 18:04:28 +00:00
for (const url of getSpaceUrlsFromGroupSelections($userGroupSelections)) {
2024-12-16 19:24:10 +00:00
const spacePath = makeSpacePath(url)
2025-10-06 18:23:19 +00:00
const spacePathMobile = spacePath + ":mobile"
2025-10-06 18:23:19 +00:00
const goalPath = makeGoalPath(url)
2024-12-16 19:24:10 +00:00
const threadPath = makeThreadPath(url)
2025-02-06 17:29:30 +00:00
const calendarPath = makeCalendarPath(url)
2025-06-25 00:36:14 +00:00
const messagesPath = makeSpaceChatPath(url)
2025-10-06 18:23:19 +00:00
const goalComments = allGoalComments.filter(e => $getUrlsForEvent(e.id).includes(url))
const threadComments = allThreadComments.filter(e => $getUrlsForEvent(e.id).includes(url))
const calendarComments = allCalendarComments.filter(e => $getUrlsForEvent(e.id).includes(url))
const messages = allMessages.filter(e => $getUrlsForEvent(e.id).includes(url))
2024-12-16 19:24:10 +00:00
2025-10-06 18:23:19 +00:00
const commentsByGoalId = groupBy(
e => getTagValue("E", e.tags),
goalComments.filter(spec({kind: COMMENT})),
)
for (const [goalId, [comment]] of commentsByGoalId.entries()) {
const goalItemPath = makeGoalPath(url, goalId)
2024-12-16 19:24:10 +00:00
2025-10-06 18:23:19 +00:00
if (hasNotification(spacePathMobile, comment)) {
paths.add(spacePathMobile)
}
2025-10-06 18:23:19 +00:00
if (hasNotification(goalPath, comment)) {
paths.add(goalPath)
}
2025-10-06 18:23:19 +00:00
2025-10-06 18:23:19 +00:00
if (hasNotification(goalItemPath, comment)) {
paths.add(goalItemPath)
}
2025-02-06 17:29:30 +00:00
}
2025-01-03 00:58:04 +00:00
const commentsByThreadId = groupBy(
e => getTagValue("E", e.tags),
2025-10-06 18:23:19 +00:00
threadComments.filter(spec({kind: COMMENT})),
2025-01-03 00:58:04 +00:00
)
for (const [threadId, [comment]] of commentsByThreadId.entries()) {
const threadItemPath = makeThreadPath(url, threadId)
2025-10-06 18:23:19 +00:00
if (hasNotification(spacePathMobile, comment)) {
paths.add(spacePathMobile)
}
2025-10-06 18:23:19 +00:00
if (hasNotification(threadPath, comment)) {
paths.add(threadPath)
}
2025-10-06 18:23:19 +00:00
2025-01-03 00:58:04 +00:00
if (hasNotification(threadItemPath, comment)) {
paths.add(threadItemPath)
}
}
2025-02-06 17:29:30 +00:00
const commentsByEventId = groupBy(
e => getTagValue("E", e.tags),
2025-10-06 18:23:19 +00:00
calendarComments.filter(spec({kind: COMMENT})),
2025-02-06 17:29:30 +00:00
)
for (const [eventId, [comment]] of commentsByEventId.entries()) {
2025-10-06 18:23:19 +00:00
const calendarItemPath = makeCalendarPath(url, eventId)
2025-10-06 18:23:19 +00:00
if (hasNotification(spacePathMobile, comment)) {
paths.add(spacePathMobile)
}
2025-10-06 18:23:19 +00:00
if (hasNotification(calendarPath, comment)) {
paths.add(calendarPath)
}
2025-02-06 17:29:30 +00:00
2025-10-06 18:23:19 +00:00
if (hasNotification(calendarItemPath, comment)) {
paths.add(calendarItemPath)
2025-02-06 17:29:30 +00:00
}
}
if (hasNip29($relaysByUrl.get(url))) {
2025-10-30 22:33:34 +00:00
for (const h of getSpaceRoomsFromGroupSelections(url, $userGroupSelections)) {
const roomPath = makeRoomPath(url, h)
2025-11-03 22:43:27 +00:00
const latestEvent = messages.find(e => e.tags.some(spec(["h", h])))
if (hasNotification(roomPath, latestEvent)) {
2025-10-06 18:23:19 +00:00
paths.add(spacePathMobile)
paths.add(spacePath)
paths.add(roomPath)
}
}
} else {
2025-10-06 18:23:19 +00:00
if (hasNotification(messagesPath, messages[0])) {
2025-10-06 18:23:19 +00:00
paths.add(spacePathMobile)
2024-12-16 19:24:10 +00:00
paths.add(spacePath)
paths.add(messagesPath)
2024-12-16 18:30:42 +00:00
}
2024-12-16 19:24:10 +00:00
}
}
2024-12-16 18:30:42 +00:00
2024-12-16 19:24:10 +00:00
return paths
2024-11-14 20:16:44 +00:00
},
)
2025-09-23 15:34:53 +00:00
export const badgeCount = derived(notifications, notifications => {
return notifications.size
})
export const handleBadgeCountChanges = async (count: number) => {
if (get(userSettingsValues).show_notifications_badge) {
try {
await Badge.set({count})
} catch (err) {
// failed to set badge
}
2025-09-23 15:34:53 +00:00
} else {
await clearBadges()
2025-09-23 15:34:53 +00:00
}
}
export const clearBadges = async () => {
await Badge.clear()
2025-09-23 15:34:53 +00:00
}