Fix unread notification count
This commit is contained in:
parent
3d66fb3109
commit
4ee05fd21c
2 changed files with 89 additions and 84 deletions
|
|
@ -1,7 +1,18 @@
|
||||||
import {derived, get, writable} from "svelte/store"
|
import {derived, get, writable} from "svelte/store"
|
||||||
import {Badge} from "@capawesome/capacitor-badge"
|
import {Badge} from "@capawesome/capacitor-badge"
|
||||||
import {page} from "$app/stores"
|
import {page} from "$app/stores"
|
||||||
import {assoc, prop, first, identity, groupBy, now, throttle, parseJson, gt} from "@welshman/lib"
|
import {
|
||||||
|
assoc,
|
||||||
|
prop,
|
||||||
|
first,
|
||||||
|
identity,
|
||||||
|
groupBy,
|
||||||
|
now,
|
||||||
|
remove,
|
||||||
|
throttle,
|
||||||
|
parseJson,
|
||||||
|
gt,
|
||||||
|
} from "@welshman/lib"
|
||||||
import type {SignedEvent, TrustedEvent} from "@welshman/util"
|
import type {SignedEvent, TrustedEvent} from "@welshman/util"
|
||||||
import {
|
import {
|
||||||
getIdOrAddress,
|
getIdOrAddress,
|
||||||
|
|
@ -17,13 +28,7 @@ import {synced, throttled, withGetter} from "@welshman/store"
|
||||||
import {Relays, RoomLists} from "@welshman/app"
|
import {Relays, RoomLists} from "@welshman/app"
|
||||||
import {deriveEventsByIdByUrl} from "@app/repository"
|
import {deriveEventsByIdByUrl} from "@app/repository"
|
||||||
import {app, fromApp} from "@app/core"
|
import {app, fromApp} from "@app/core"
|
||||||
import {
|
import {makeRoomPath, makeSpaceChatPath, makeChatPath, makeContentPath} from "@app/routes"
|
||||||
makeSpacePath,
|
|
||||||
makeRoomPath,
|
|
||||||
makeSpaceChatPath,
|
|
||||||
makeChatPath,
|
|
||||||
makeContentPath,
|
|
||||||
} from "@app/routes"
|
|
||||||
import {CONTENT_KINDS, makeCommentFilter} from "@app/content"
|
import {CONTENT_KINDS, makeCommentFilter} from "@app/content"
|
||||||
import {notificationSettings} from "@app/settings"
|
import {notificationSettings} from "@app/settings"
|
||||||
import {chatsById} from "@app/chats"
|
import {chatsById} from "@app/chats"
|
||||||
|
|
@ -49,15 +54,15 @@ export const setChecked = (key: string) => checked.update(assoc(key, now()))
|
||||||
/** Room path while video call UI hides chat; checked + badge stay active until chat is shown. */
|
/** Room path while video call UI hides chat; checked + badge stay active until chat is shown. */
|
||||||
export const deferredRoomPath = writable<string | undefined>(undefined)
|
export const deferredRoomPath = writable<string | undefined>(undefined)
|
||||||
|
|
||||||
|
const getPaths = (path: string) =>
|
||||||
|
path
|
||||||
|
.split("/")
|
||||||
|
.map((_, i, segments) => segments.slice(0, i + 1).join("/"))
|
||||||
|
.slice(1)
|
||||||
|
|
||||||
export const syncChecked = () => {
|
export const syncChecked = () => {
|
||||||
let prev = ""
|
let prev = ""
|
||||||
|
|
||||||
const getPaths = (path: string) =>
|
|
||||||
path
|
|
||||||
.split("/")
|
|
||||||
.map((_, i, segments) => segments.slice(0, i + 1).join("/"))
|
|
||||||
.slice(1)
|
|
||||||
|
|
||||||
return page.subscribe($page => {
|
return page.subscribe($page => {
|
||||||
// Set checked when we leave a given page
|
// Set checked when we leave a given page
|
||||||
checked.update($checked => {
|
checked.update($checked => {
|
||||||
|
|
@ -200,7 +205,7 @@ const getContentTarget = (event: TrustedEvent) => {
|
||||||
|
|
||||||
// Assumes `events` is sorted descending, so the first event seen per content item wins.
|
// Assumes `events` is sorted descending, so the first event seen per content item wins.
|
||||||
const latestEventByContentPath = (url: string, events: TrustedEvent[]) => {
|
const latestEventByContentPath = (url: string, events: TrustedEvent[]) => {
|
||||||
const byPath = new Map<string, {listPath: string; latestEvent: TrustedEvent}>()
|
const byPath = new Map<string, TrustedEvent>()
|
||||||
|
|
||||||
for (const event of events) {
|
for (const event of events) {
|
||||||
const target = getContentTarget(event)
|
const target = getContentTarget(event)
|
||||||
|
|
@ -208,23 +213,21 @@ const latestEventByContentPath = (url: string, events: TrustedEvent[]) => {
|
||||||
if (!target) continue
|
if (!target) continue
|
||||||
|
|
||||||
const path = makeContentPath(url, target.kind, target.idOrAddress)
|
const path = makeContentPath(url, target.kind, target.idOrAddress)
|
||||||
const listPath = makeContentPath(url, target.kind)
|
|
||||||
|
|
||||||
if (path && listPath && !byPath.has(path)) {
|
if (path && !byPath.has(path)) {
|
||||||
byPath.set(path, {listPath, latestEvent: event})
|
byPath.set(path, event)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return byPath
|
return byPath
|
||||||
}
|
}
|
||||||
|
|
||||||
export const allNotifications = derived(
|
export const latestActivityByPath = derived(
|
||||||
throttled(
|
throttled(
|
||||||
1000,
|
1000,
|
||||||
derived(
|
derived(
|
||||||
[
|
[
|
||||||
app,
|
app,
|
||||||
checked,
|
|
||||||
chatsById,
|
chatsById,
|
||||||
fromApp($app => $app.use(Relays).index.$),
|
fromApp($app => $app.use(Relays).index.$),
|
||||||
fromApp($app => $app.use(RoomLists).index.$),
|
fromApp($app => $app.use(RoomLists).index.$),
|
||||||
|
|
@ -236,17 +239,56 @@ export const allNotifications = derived(
|
||||||
identity,
|
identity,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
([$app, $checked, $chatsById, $relays, $roomLists, eventsByIdByUrl]) => {
|
([$app, $chatsById, $relays, $roomLists, eventsByIdByUrl]) => {
|
||||||
const hasNotification = (path: string, latestEvent?: TrustedEvent) => {
|
const activity = new Map<string, TrustedEvent>()
|
||||||
if (!latestEvent || latestEvent.pubkey === $app.user?.pubkey) {
|
|
||||||
|
for (const {pubkeys, messages} of $chatsById.values()) {
|
||||||
|
if (messages[0]) {
|
||||||
|
activity.set(makeChatPath(pubkeys), messages[0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const roomList = $app.user?.pubkey ? $roomLists.get($app.user.pubkey) : undefined
|
||||||
|
const urls = PLATFORM_RELAYS.length > 0 ? PLATFORM_RELAYS : (roomList?.urls() ?? [])
|
||||||
|
|
||||||
|
for (const url of urls) {
|
||||||
|
const events = sortEventsDesc((eventsByIdByUrl.get(url) || new Map()).values())
|
||||||
|
|
||||||
|
if ($relays.get(url)?.hasNip(29)) {
|
||||||
|
for (const [h, [latestEvent]] of groupBy(e => tagValue(tagSpec("h"), e.tags), events)) {
|
||||||
|
if (h) {
|
||||||
|
activity.set(makeRoomPath(url, h), latestEvent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const latestEvent = first(events)
|
||||||
|
|
||||||
|
if (latestEvent) {
|
||||||
|
activity.set(makeSpaceChatPath(url), latestEvent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [path, latestEvent] of latestEventByContentPath(url, events)) {
|
||||||
|
activity.set(path, latestEvent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return activity
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
export const allNotifications = derived(
|
||||||
|
[app, latestActivityByPath, checked],
|
||||||
|
([$app, $latestActivityByPath, $checked]) => {
|
||||||
|
const hasNotification = (path: string, latestEvent: TrustedEvent) => {
|
||||||
|
if (latestEvent.pubkey === $app.user?.pubkey) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const [entryPath, ts] of Object.entries($checked)) {
|
for (const [entryPath, ts] of Object.entries($checked)) {
|
||||||
const isMatch =
|
const isMatch = entryPath.endsWith("*")
|
||||||
entryPath === "*" ||
|
? path.startsWith(entryPath.slice(0, -1))
|
||||||
entryPath.startsWith(path) ||
|
: entryPath.startsWith(path)
|
||||||
(entryPath === "/chat/*" && path.startsWith("/chat/"))
|
|
||||||
|
|
||||||
if (isMatch && ts > latestEvent.created_at) {
|
if (isMatch && ts > latestEvent.created_at) {
|
||||||
return false
|
return false
|
||||||
|
|
@ -258,59 +300,13 @@ export const allNotifications = derived(
|
||||||
|
|
||||||
const paths = new Set<string>()
|
const paths = new Set<string>()
|
||||||
|
|
||||||
for (const {pubkeys, messages} of $chatsById.values()) {
|
for (const [path, latestEvent] of $latestActivityByPath) {
|
||||||
const chatPath = makeChatPath(pubkeys)
|
if (hasNotification(path, latestEvent)) {
|
||||||
|
paths.add(path)
|
||||||
|
|
||||||
if (hasNotification(chatPath, messages[0])) {
|
for (const branchPath of remove(path, getPaths(path.split("?")[0]))) {
|
||||||
paths.add("/chat")
|
if (hasNotification(branchPath, latestEvent)) {
|
||||||
paths.add(chatPath)
|
paths.add(branchPath)
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const roomList = $app.user?.pubkey ? $roomLists.get($app.user?.pubkey) : undefined
|
|
||||||
const urls = PLATFORM_RELAYS.length > 0 ? PLATFORM_RELAYS : (roomList?.urls() ?? [])
|
|
||||||
|
|
||||||
for (const url of urls) {
|
|
||||||
const spacePath = makeSpacePath(url)
|
|
||||||
const events = sortEventsDesc((eventsByIdByUrl.get(url) || new Map()).values())
|
|
||||||
|
|
||||||
if ($relays.get(url)?.hasNip(29)) {
|
|
||||||
for (const [h, [latestEvent]] of groupBy(e => tagValue(tagSpec("h"), e.tags), events)) {
|
|
||||||
if (h) {
|
|
||||||
const roomPath = makeRoomPath(url, h)
|
|
||||||
|
|
||||||
if (hasNotification(roomPath, latestEvent)) {
|
|
||||||
paths.add(roomPath)
|
|
||||||
|
|
||||||
if (hasNotification(spacePath, latestEvent)) {
|
|
||||||
paths.add(spacePath)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const messagesPath = makeSpaceChatPath(url)
|
|
||||||
const latestEvent = first(events)
|
|
||||||
|
|
||||||
if (hasNotification(messagesPath, latestEvent)) {
|
|
||||||
paths.add(messagesPath)
|
|
||||||
|
|
||||||
if (hasNotification(spacePath, latestEvent)) {
|
|
||||||
paths.add(spacePath)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const [path, {listPath, latestEvent}] of latestEventByContentPath(url, events)) {
|
|
||||||
if (hasNotification(path, latestEvent)) {
|
|
||||||
paths.add(path)
|
|
||||||
|
|
||||||
if (hasNotification(listPath, latestEvent)) {
|
|
||||||
paths.add(listPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hasNotification(spacePath, latestEvent)) {
|
|
||||||
paths.add(spacePath)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -335,11 +331,15 @@ export const notifications = derived(
|
||||||
// Badges
|
// Badges
|
||||||
|
|
||||||
export const syncBadges = () =>
|
export const syncBadges = () =>
|
||||||
derived([notifications, notificationSettings], identity).subscribe(
|
derived([latestActivityByPath, notifications, notificationSettings], identity).subscribe(
|
||||||
async ([$notifications, $notificationSettings]) => {
|
async ([$latestActivityByPath, $notifications, $notificationSettings]) => {
|
||||||
if ($notificationSettings.badge) {
|
if ($notificationSettings.badge) {
|
||||||
|
const count = [...$latestActivityByPath.keys()].filter(path =>
|
||||||
|
$notifications.has(path),
|
||||||
|
).length
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await Badge.set({count: $notifications.size})
|
await Badge.set({count})
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// pass - firefox doesn't support badges
|
// pass - firefox doesn't support badges
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,11 @@
|
||||||
import {activeTheme, flTheme, theme} from "@app/theme"
|
import {activeTheme, flTheme, theme} from "@app/theme"
|
||||||
import {toast, pushToast} from "@app/toast"
|
import {toast, pushToast} from "@app/toast"
|
||||||
import * as notifications from "@app/notifications"
|
import * as notifications from "@app/notifications"
|
||||||
import {notifications as notificationPaths, allNotifications} from "@app/notifications"
|
import {
|
||||||
|
notifications as notificationPaths,
|
||||||
|
allNotifications,
|
||||||
|
latestActivityByPath,
|
||||||
|
} from "@app/notifications"
|
||||||
import {Push} from "@app/push"
|
import {Push} from "@app/push"
|
||||||
import {onPushNotificationAction, pushState} from "@app/push/adapters/common"
|
import {onPushNotificationAction, pushState} from "@app/push/adapters/common"
|
||||||
import {syncKeyboard} from "@app/keyboard"
|
import {syncKeyboard} from "@app/keyboard"
|
||||||
|
|
@ -274,7 +278,8 @@
|
||||||
const title = getPageTitle({page: $page, pubkey: $app.user?.pubkey})
|
const title = getPageTitle({page: $page, pubkey: $app.user?.pubkey})
|
||||||
// While the tab isn't actively focused the user isn't actually looking at the
|
// While the tab isn't actively focused the user isn't actually looking at the
|
||||||
// active page, so count notifications for it too rather than treating it as read.
|
// active page, so count notifications for it too rather than treating it as read.
|
||||||
const unreadCount = $documentActive ? $notificationPaths.size : $allNotifications.size
|
const paths = $documentActive ? $notificationPaths : $allNotifications
|
||||||
|
const unreadCount = [...$latestActivityByPath.keys()].filter(path => paths.has(path)).length
|
||||||
|
|
||||||
document.title = unreadCount > 0 ? `(${unreadCount}) ${title}` : title
|
document.title = unreadCount > 0 ? `(${unreadCount}) ${title}` : title
|
||||||
})
|
})
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue