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>
|
</SecondaryNavItem>
|
||||||
{/if}
|
{/if}
|
||||||
{#if ENABLE_ZAPS && $spaceKinds.has(ZAP_GOAL)}
|
{#if ENABLE_ZAPS && $spaceKinds.has(ZAP_GOAL)}
|
||||||
<SecondaryNavItem href={goalsPath}>
|
<SecondaryNavItem href={goalsPath} notification={$notifications.has(goalsPath)}>
|
||||||
<Icon icon={StarFallMinimalistic} /> Goals
|
<Icon icon={StarFallMinimalistic} /> Goals
|
||||||
</SecondaryNavItem>
|
</SecondaryNavItem>
|
||||||
{/if}
|
{/if}
|
||||||
{#if $spaceKinds.has(THREAD)}
|
{#if $spaceKinds.has(THREAD)}
|
||||||
<SecondaryNavItem href={threadsPath}>
|
<SecondaryNavItem href={threadsPath} notification={$notifications.has(threadsPath)}>
|
||||||
<Icon icon={NotesMinimalistic} /> Threads
|
<Icon icon={NotesMinimalistic} /> Threads
|
||||||
</SecondaryNavItem>
|
</SecondaryNavItem>
|
||||||
{/if}
|
{/if}
|
||||||
{#if $spaceKinds.has(CLASSIFIED)}
|
{#if $spaceKinds.has(CLASSIFIED)}
|
||||||
<SecondaryNavItem href={classifiedsPath}>
|
<SecondaryNavItem href={classifiedsPath} notification={$notifications.has(classifiedsPath)}>
|
||||||
<Icon icon={CaseMinimalistic} /> Classifieds
|
<Icon icon={CaseMinimalistic} /> Classifieds
|
||||||
</SecondaryNavItem>
|
</SecondaryNavItem>
|
||||||
{/if}
|
{/if}
|
||||||
{#if $spaceKinds.has(EVENT_TIME)}
|
{#if $spaceKinds.has(EVENT_TIME)}
|
||||||
<SecondaryNavItem href={calendarPath}>
|
<SecondaryNavItem href={calendarPath} notification={$notifications.has(calendarPath)}>
|
||||||
<Icon icon={CalendarMinimalistic} /> Calendar
|
<Icon icon={CalendarMinimalistic} /> Calendar
|
||||||
</SecondaryNavItem>
|
</SecondaryNavItem>
|
||||||
{/if}
|
{/if}
|
||||||
{#if $spaceKinds.has(POLL)}
|
{#if $spaceKinds.has(POLL)}
|
||||||
<SecondaryNavItem href={pollsPath}>
|
<SecondaryNavItem href={pollsPath} notification={$notifications.has(pollsPath)}>
|
||||||
<Icon icon={Revote} /> Polls
|
<Icon icon={Revote} /> Polls
|
||||||
</SecondaryNavItem>
|
</SecondaryNavItem>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
||||||
|
|
@ -4,9 +4,11 @@ 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, throttle, parseJson, gt} from "@welshman/lib"
|
||||||
import type {SignedEvent, TrustedEvent} from "@welshman/util"
|
import type {SignedEvent, TrustedEvent} from "@welshman/util"
|
||||||
import {
|
import {
|
||||||
|
getIdOrAddress,
|
||||||
sortEventsDesc,
|
sortEventsDesc,
|
||||||
tagSpec,
|
tagSpec,
|
||||||
tagValue,
|
tagValue,
|
||||||
|
COMMENT,
|
||||||
MESSAGE,
|
MESSAGE,
|
||||||
makeHttpAuth,
|
makeHttpAuth,
|
||||||
makeHttpAuthHeader,
|
makeHttpAuthHeader,
|
||||||
|
|
@ -15,7 +17,13 @@ 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 {makeSpacePath, makeRoomPath, makeSpaceChatPath, makeChatPath} from "@app/routes"
|
import {
|
||||||
|
makeSpacePath,
|
||||||
|
makeRoomPath,
|
||||||
|
makeSpaceChatPath,
|
||||||
|
makeChatPath,
|
||||||
|
getContentPath,
|
||||||
|
} 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"
|
||||||
|
|
@ -174,6 +182,42 @@ export const syncCheckedRemote = () => {
|
||||||
|
|
||||||
// Derived notifications state
|
// 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(
|
export const allNotifications = derived(
|
||||||
throttled(
|
throttled(
|
||||||
1000,
|
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
|
return paths
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ import {
|
||||||
THREAD,
|
THREAD,
|
||||||
ZAP_GOAL,
|
ZAP_GOAL,
|
||||||
getAddress,
|
getAddress,
|
||||||
|
getIdOrAddress,
|
||||||
hexTags,
|
hexTags,
|
||||||
tagSpec,
|
tagSpec,
|
||||||
tagValue,
|
tagValue,
|
||||||
|
|
@ -138,6 +139,24 @@ export const makePollPath = (url: string, id?: string) => makeSpacePath(url, "po
|
||||||
export const makeLibraryPath = (url: string, address?: string) =>
|
export const makeLibraryPath = (url: string, address?: string) =>
|
||||||
makeSpacePath(url, "library", address)
|
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) => {
|
export const scrollToEvent = (id: string) => {
|
||||||
const element = document.querySelector(`[data-event="${id}"]`) as any
|
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})}`
|
return `${PLATFORM_URL}${path}#${nip19.neventEncode({id: event.id, relays: urls})}`
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getRoomItemPath = (url: string, event: TrustedEvent) => {
|
export const getRoomItemPath = (url: string, event: TrustedEvent) =>
|
||||||
switch (event.kind) {
|
getContentPath(url, event.kind, getIdOrAddress(event))
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue