Show notification badge on Threads nav item (#360)
Co-authored-by: Aditya Chaudhary <30+useradityaa@noreply.coracle.social>
This commit is contained in:
parent
0a85983633
commit
f5159a3b5e
3 changed files with 82 additions and 22 deletions
|
|
@ -72,27 +72,27 @@
|
|||
</SecondaryNavItem>
|
||||
{/if}
|
||||
{#if ENABLE_ZAPS && $spaceKinds.has(ZAP_GOAL)}
|
||||
<SecondaryNavItem href={goalsPath}>
|
||||
<SecondaryNavItem href={goalsPath} notification={$notifications.has(goalsPath)}>
|
||||
<Icon icon={StarFallMinimalistic} /> Goals
|
||||
</SecondaryNavItem>
|
||||
{/if}
|
||||
{#if $spaceKinds.has(THREAD)}
|
||||
<SecondaryNavItem href={threadsPath}>
|
||||
<SecondaryNavItem href={threadsPath} notification={$notifications.has(threadsPath)}>
|
||||
<Icon icon={NotesMinimalistic} /> Threads
|
||||
</SecondaryNavItem>
|
||||
{/if}
|
||||
{#if $spaceKinds.has(CLASSIFIED)}
|
||||
<SecondaryNavItem href={classifiedsPath}>
|
||||
<SecondaryNavItem href={classifiedsPath} notification={$notifications.has(classifiedsPath)}>
|
||||
<Icon icon={CaseMinimalistic} /> Classifieds
|
||||
</SecondaryNavItem>
|
||||
{/if}
|
||||
{#if $spaceKinds.has(EVENT_TIME)}
|
||||
<SecondaryNavItem href={calendarPath}>
|
||||
<SecondaryNavItem href={calendarPath} notification={$notifications.has(calendarPath)}>
|
||||
<Icon icon={CalendarMinimalistic} /> Calendar
|
||||
</SecondaryNavItem>
|
||||
{/if}
|
||||
{#if $spaceKinds.has(POLL)}
|
||||
<SecondaryNavItem href={pollsPath}>
|
||||
<SecondaryNavItem href={pollsPath} notification={$notifications.has(pollsPath)}>
|
||||
<Icon icon={Revote} /> Polls
|
||||
</SecondaryNavItem>
|
||||
{/if}
|
||||
|
|
|
|||
|
|
@ -4,9 +4,11 @@ import {page} from "$app/stores"
|
|||
import {assoc, prop, first, identity, groupBy, now, throttle, parseJson, gt} from "@welshman/lib"
|
||||
import type {SignedEvent, TrustedEvent} from "@welshman/util"
|
||||
import {
|
||||
getIdOrAddress,
|
||||
sortEventsDesc,
|
||||
tagSpec,
|
||||
tagValue,
|
||||
COMMENT,
|
||||
MESSAGE,
|
||||
makeHttpAuth,
|
||||
makeHttpAuthHeader,
|
||||
|
|
@ -15,7 +17,13 @@ import {synced, throttled, withGetter} from "@welshman/store"
|
|||
import {Relays, RoomLists} from "@welshman/app"
|
||||
import {deriveEventsByIdByUrl} from "@app/repository"
|
||||
import {app, fromApp} from "@app/core"
|
||||
import {makeSpacePath, makeRoomPath, makeSpaceChatPath, makeChatPath} from "@app/routes"
|
||||
import {
|
||||
makeSpacePath,
|
||||
makeRoomPath,
|
||||
makeSpaceChatPath,
|
||||
makeChatPath,
|
||||
getContentPath,
|
||||
} from "@app/routes"
|
||||
import {CONTENT_KINDS, makeCommentFilter} from "@app/content"
|
||||
import {notificationSettings} from "@app/settings"
|
||||
import {chatsById} from "@app/chats"
|
||||
|
|
@ -174,6 +182,42 @@ export const syncCheckedRemote = () => {
|
|||
|
||||
// Derived notifications state
|
||||
|
||||
// The content item an event belongs to - either the content event itself, or the root of a comment
|
||||
const getContentTarget = (event: TrustedEvent) => {
|
||||
if (CONTENT_KINDS.includes(event.kind)) {
|
||||
return {kind: event.kind, idOrAddress: getIdOrAddress(event)}
|
||||
}
|
||||
|
||||
if (event.kind === COMMENT) {
|
||||
const kind = parseInt(tagValue(tagSpec("K"), event.tags) || "")
|
||||
const idOrAddress = tagValue(tagSpec("A"), event.tags) || tagValue(tagSpec("E"), event.tags)
|
||||
|
||||
if (CONTENT_KINDS.includes(kind) && idOrAddress) {
|
||||
return {kind, idOrAddress}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Assumes `events` is sorted descending, so the first event seen per content item wins.
|
||||
const latestEventByContentPath = (url: string, events: TrustedEvent[]) => {
|
||||
const byPath = new Map<string, {listPath: string; latestEvent: TrustedEvent}>()
|
||||
|
||||
for (const event of events) {
|
||||
const target = getContentTarget(event)
|
||||
|
||||
if (!target) continue
|
||||
|
||||
const path = getContentPath(url, target.kind, target.idOrAddress)
|
||||
const listPath = getContentPath(url, target.kind)
|
||||
|
||||
if (path && listPath && !byPath.has(path)) {
|
||||
byPath.set(path, {listPath, latestEvent: event})
|
||||
}
|
||||
}
|
||||
|
||||
return byPath
|
||||
}
|
||||
|
||||
export const allNotifications = derived(
|
||||
throttled(
|
||||
1000,
|
||||
|
|
@ -256,6 +300,17 @@ export const allNotifications = derived(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const [path, {listPath, latestEvent}] of latestEventByContentPath(url, events)) {
|
||||
if (hasNotification(path, latestEvent)) {
|
||||
paths.add(path)
|
||||
paths.add(listPath)
|
||||
|
||||
if (hasNotification(spacePath, latestEvent)) {
|
||||
paths.add(spacePath)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return paths
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import {
|
|||
THREAD,
|
||||
ZAP_GOAL,
|
||||
getAddress,
|
||||
getIdOrAddress,
|
||||
hexTags,
|
||||
tagSpec,
|
||||
tagValue,
|
||||
|
|
@ -138,6 +139,24 @@ export const makePollPath = (url: string, id?: string) => makeSpacePath(url, "po
|
|||
export const makeLibraryPath = (url: string, address?: string) =>
|
||||
makeSpacePath(url, "library", address)
|
||||
|
||||
/** Path to a content item by kind, or to its listing when `idOrAddress` is omitted. */
|
||||
export const getContentPath = (url: string, kind: number, idOrAddress?: string) => {
|
||||
switch (kind) {
|
||||
case ZAP_GOAL:
|
||||
return makeGoalPath(url, idOrAddress)
|
||||
case THREAD:
|
||||
return makeThreadPath(url, idOrAddress)
|
||||
case CLASSIFIED:
|
||||
return makeClassifiedPath(url, idOrAddress)
|
||||
case EVENT_TIME:
|
||||
return makeCalendarPath(url, idOrAddress)
|
||||
case PINBOARD:
|
||||
return makeLibraryPath(url, idOrAddress)
|
||||
case POLL:
|
||||
return makePollPath(url, idOrAddress)
|
||||
}
|
||||
}
|
||||
|
||||
export const scrollToEvent = (id: string) => {
|
||||
const element = document.querySelector(`[data-event="${id}"]`) as any
|
||||
|
||||
|
|
@ -249,19 +268,5 @@ export const makeEventPermalink = (event: TrustedEvent, url?: string) => {
|
|||
return `${PLATFORM_URL}${path}#${nip19.neventEncode({id: event.id, relays: urls})}`
|
||||
}
|
||||
|
||||
export const getRoomItemPath = (url: string, event: TrustedEvent) => {
|
||||
switch (event.kind) {
|
||||
case THREAD:
|
||||
return makeThreadPath(url, event.id)
|
||||
case CLASSIFIED:
|
||||
return makeClassifiedPath(url, getAddress(event))
|
||||
case ZAP_GOAL:
|
||||
return makeGoalPath(url, event.id)
|
||||
case EVENT_TIME:
|
||||
return makeCalendarPath(url, getAddress(event))
|
||||
case PINBOARD:
|
||||
return makeLibraryPath(url, getAddress(event))
|
||||
case POLL:
|
||||
return makePollPath(url, event.id)
|
||||
}
|
||||
}
|
||||
export const getRoomItemPath = (url: string, event: TrustedEvent) =>
|
||||
getContentPath(url, event.kind, getIdOrAddress(event))
|
||||
|
|
|
|||
Loading…
Reference in a new issue