Browse Source

make the notifications page more advanced

imwald
Silberengel 1 month ago
parent
commit
579b1e533b
  1. 5
      src/i18n/locales/de.ts
  2. 5
      src/i18n/locales/en.ts
  3. 108
      src/pages/primary/SpellsPage/index.tsx
  4. 6
      src/providers/NostrProvider/index.tsx
  5. 3
      src/providers/nostr-context.tsx

5
src/i18n/locales/de.ts

@ -596,6 +596,11 @@ export default {
Pay: 'Bezahlen', Pay: 'Bezahlen',
interactions: 'Interaktionen', interactions: 'Interaktionen',
notifications: 'Benachrichtigungen', notifications: 'Benachrichtigungen',
notificationsViewAsAccount: 'Anzeigen als',
notificationsViewAsAccountAria:
'Gespeichertes Konto wechseln. Benachrichtigungen, Antworten, Zaps, Reaktionen und Boosts nutzen dieses Konto.',
notificationsSwitchAccountFailed:
'Kontowechsel fehlgeschlagen. Signierer, Passwort oder Erweiterung prüfen und erneut versuchen.',
'Show untrusted {type}': 'Untrusted {{type}} anzeigen', 'Show untrusted {type}': 'Untrusted {{type}} anzeigen',
'Hide untrusted {type}': 'Untrusted {{type}} ausblenden', 'Hide untrusted {type}': 'Untrusted {{type}} ausblenden',
'Currently hiding {type} from untrusted users.': 'Currently hiding {type} from untrusted users.':

5
src/i18n/locales/en.ts

@ -617,6 +617,11 @@ export default {
Pay: 'Pay', Pay: 'Pay',
interactions: 'interactions', interactions: 'interactions',
notifications: 'notifications', notifications: 'notifications',
notificationsViewAsAccount: 'View as',
notificationsViewAsAccountAria:
'Switch stored account. Notifications, replies, zaps, reactions, and boosts use this account.',
notificationsSwitchAccountFailed:
'Could not switch account. Check your signer, password, or extension and try again.',
'Show untrusted {type}': 'Show untrusted {{type}}', 'Show untrusted {type}': 'Show untrusted {{type}}',
'Hide untrusted {type}': 'Hide untrusted {{type}}', 'Hide untrusted {type}': 'Hide untrusted {{type}}',
'Currently hiding {type} from untrusted users.': 'Currently hiding {type} from untrusted users.':

108
src/pages/primary/SpellsPage/index.tsx

@ -15,6 +15,13 @@ import {
DropdownMenuSeparator, DropdownMenuSeparator,
DropdownMenuTrigger DropdownMenuTrigger
} from '@/components/ui/dropdown-menu' } from '@/components/ui/dropdown-menu'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue
} from '@/components/ui/select'
import { Separator } from '@/components/ui/separator' import { Separator } from '@/components/ui/separator'
import { Drawer, DrawerContent, DrawerHeader, DrawerTitle } from '@/components/ui/drawer' import { Drawer, DrawerContent, DrawerHeader, DrawerTitle } from '@/components/ui/drawer'
import UserAvatar from '@/components/UserAvatar' import UserAvatar from '@/components/UserAvatar'
@ -50,7 +57,7 @@ import {
FIRST_RELAY_RESULT_GRACE_MS, FIRST_RELAY_RESULT_GRACE_MS,
} from '@/constants' } from '@/constants'
import { filterEventsExcludingTombstones, isUserInEventMentions } from '@/lib/event' import { filterEventsExcludingTombstones, isUserInEventMentions } from '@/lib/event'
import { formatPubkey } from '@/lib/pubkey' import { formatPubkey, hexPubkeysEqual, normalizeHexPubkey } from '@/lib/pubkey'
import { import {
augmentSubRequestsWithFavoritesFastReadAndInbox, augmentSubRequestsWithFavoritesFastReadAndInbox,
getRelayUrlsWithFavoritesFastReadAndInbox getRelayUrlsWithFavoritesFastReadAndInbox
@ -98,6 +105,7 @@ import type { Event } from 'nostr-tools'
import { kinds as nostrKinds, verifyEvent } from 'nostr-tools' import { kinds as nostrKinds, verifyEvent } from 'nostr-tools'
import { forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react' import { forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { toast } from 'sonner'
import CreateSpellDialog from './CreateSpellDialog' import CreateSpellDialog from './CreateSpellDialog'
import { import {
appendCuratedReadOnlyRelays, appendCuratedReadOnlyRelays,
@ -310,8 +318,17 @@ const SpellsPage = forwardRef<TPageRef>(function SpellsPage(
) { ) {
const { t } = useTranslation() const { t } = useTranslation()
const { navigate: navigatePrimary } = usePrimaryPage() const { navigate: navigatePrimary } = usePrimaryPage()
const { pubkey, account, relayList, attemptDelete, bookmarkListEvent, interestListEvent } = const {
useNostr() pubkey,
account,
accounts,
relayList,
attemptDelete,
bookmarkListEvent,
interestListEvent,
switchAccount,
isAccountSessionHydrating
} = useNostr()
const { addBookmark, removeBookmark } = useBookmarks() const { addBookmark, removeBookmark } = useBookmarks()
const { hideUntrustedNotifications } = useUserTrust() const { hideUntrustedNotifications } = useUserTrust()
const { isSmallScreen } = useScreenSize() const { isSmallScreen } = useScreenSize()
@ -352,6 +369,43 @@ const SpellsPage = forwardRef<TPageRef>(function SpellsPage(
const spellFeedInstrLabelRef = useRef('') const spellFeedInstrLabelRef = useRef('')
const [spellFeedInstrumentToken, setSpellFeedInstrumentToken] = useState(0) const [spellFeedInstrumentToken, setSpellFeedInstrumentToken] = useState(0)
const [followSetManualRefreshKey, setFollowSetManualRefreshKey] = useState(0) const [followSetManualRefreshKey, setFollowSetManualRefreshKey] = useState(0)
/** Notifications `#p` + mention filter track the active session; changing the dropdown calls {@link switchAccount}. */
const notificationsFeedPubkey = useMemo(() => {
const cur = pubkey?.trim()
return cur ? normalizeHexPubkey(cur) : null
}, [pubkey])
const storedAccountPubkeys = useMemo(() => {
const seen = new Set<string>()
const out: string[] = []
for (const a of accounts) {
const raw = a.pubkey?.trim()
if (!raw) continue
const p = normalizeHexPubkey(raw)
if (!seen.has(p)) {
seen.add(p)
out.push(p)
}
}
return out
}, [accounts])
const handleNotificationsAccountPick = useCallback(
async (v: string) => {
const target = normalizeHexPubkey(v)
if (pubkey && hexPubkeysEqual(target, pubkey)) return
const nextAccount = accounts.find((a) => hexPubkeysEqual(a.pubkey, target))
if (!nextAccount) {
toast.error(t('notificationsSwitchAccountFailed'))
return
}
const switched = await switchAccount(nextAccount)
if (!switched || !hexPubkeysEqual(normalizeHexPubkey(switched), target)) {
toast.error(t('notificationsSwitchAccountFailed'))
}
},
[pubkey, accounts, switchAccount, t]
)
const logSpellFeedPickerSelection = useCallback((label: string, extra?: Record<string, unknown>) => { const logSpellFeedPickerSelection = useCallback((label: string, extra?: Record<string, unknown>) => {
spellFeedInstrT0Ref.current = performance.now() spellFeedInstrT0Ref.current = performance.now()
@ -835,8 +889,8 @@ const SpellsPage = forwardRef<TPageRef>(function SpellsPage(
) )
if (selectedFauxSpell === 'notifications') { if (selectedFauxSpell === 'notifications') {
if (!pubkey || !feedUrls.length) return [] if (!notificationsFeedPubkey || !feedUrls.length) return []
return buildNotificationsSpellSubRequests(feedUrls, pubkey) return buildNotificationsSpellSubRequests(feedUrls, notificationsFeedPubkey)
} }
if (selectedFauxSpell === 'discussions') { if (selectedFauxSpell === 'discussions') {
// Read-only prepended in appendCuratedReadOnlyRelays so FAUX_SPELL_MAX_RELAYS still includes aggr. // Read-only prepended in appendCuratedReadOnlyRelays so FAUX_SPELL_MAX_RELAYS still includes aggr.
@ -876,7 +930,7 @@ const SpellsPage = forwardRef<TPageRef>(function SpellsPage(
] ]
} }
return [] return []
}, [selectedFauxSpell, pubkey, fauxFeedRelaysDepsKey, relayMailboxStableKey]) }, [selectedFauxSpell, pubkey, notificationsFeedPubkey, fauxFeedRelaysDepsKey, relayMailboxStableKey])
const fauxSubRequests = useMemo<TFeedSubRequest[]>(() => { const fauxSubRequests = useMemo<TFeedSubRequest[]>(() => {
const base = isFollowFeedFauxSpellId(selectedFauxSpell ?? '') const base = isFollowFeedFauxSpellId(selectedFauxSpell ?? '')
@ -1205,8 +1259,9 @@ const SpellsPage = forwardRef<TPageRef>(function SpellsPage(
}, [selectedFauxSpell]) }, [selectedFauxSpell])
const notificationsMentionExtraHide = useCallback( const notificationsMentionExtraHide = useCallback(
(evt: Event) => (pubkey ? !isUserInEventMentions(evt, pubkey) : false), (evt: Event) =>
[pubkey] notificationsFeedPubkey ? !isUserInEventMentions(evt, notificationsFeedPubkey) : false,
[notificationsFeedPubkey]
) )
const fauxFeedEmptyMessage = useMemo(() => { const fauxFeedEmptyMessage = useMemo(() => {
@ -1670,7 +1725,40 @@ const SpellsPage = forwardRef<TPageRef>(function SpellsPage(
) : selectedFauxSpell && fauxSubRequests.length > 0 ? ( ) : selectedFauxSpell && fauxSubRequests.length > 0 ? (
<> <>
{selectedFauxSpell === 'notifications' ? ( {selectedFauxSpell === 'notifications' ? (
<div className="flex shrink-0 justify-end px-1 pb-2"> <div className="flex shrink-0 flex-wrap items-center justify-end gap-2 px-1 pb-2 sm:justify-between">
{storedAccountPubkeys.length > 1 && notificationsFeedPubkey ? (
<div className="flex min-w-0 flex-1 items-center gap-2 sm:max-w-[min(100%,20rem)]">
<span className="hidden shrink-0 text-xs text-muted-foreground sm:inline">
{t('notificationsViewAsAccount')}
</span>
<Select
value={notificationsFeedPubkey}
disabled={isAccountSessionHydrating}
onValueChange={(v) => void handleNotificationsAccountPick(v)}
>
<SelectTrigger
className="h-9 min-w-0 flex-1"
aria-label={t('notificationsViewAsAccountAria')}
>
<SelectValue />
</SelectTrigger>
<SelectContent position="popper">
{storedAccountPubkeys.map((pk) => (
<SelectItem key={pk} value={pk}>
<span className="flex min-w-0 items-center gap-2">
<UserAvatar userId={pk} size="small" className="shrink-0" />
<Username
userId={pk}
className="min-w-0 truncate text-left font-normal"
skeletonClassName="h-4 w-24"
/>
</span>
</SelectItem>
))}
</SelectContent>
</Select>
</div>
) : null}
<HideUntrustedContentButton type="notifications" size="titlebar-icon" /> <HideUntrustedContentButton type="notifications" size="titlebar-icon" />
</div> </div>
) : null} ) : null}
@ -1712,7 +1800,7 @@ const SpellsPage = forwardRef<TPageRef>(function SpellsPage(
: false : false
} }
extraShouldHideEvent={ extraShouldHideEvent={
selectedFauxSpell === 'notifications' && pubkey selectedFauxSpell === 'notifications' && notificationsFeedPubkey
? notificationsMentionExtraHide ? notificationsMentionExtraHide
: undefined : undefined
} }

6
src/providers/NostrProvider/index.tsx

@ -840,14 +840,14 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
} }
} }
const switchAccount = async (act: TAccountPointer | null) => { const switchAccount = async (act: TAccountPointer | null): Promise<string | null> => {
if (!act) { if (!act) {
storage.switchAccount(null) storage.switchAccount(null)
setAccount(null) setAccount(null)
setSigner(null) setSigner(null)
return return null
} }
await loginWithAccountPointer(act) return await loginWithAccountPointer(act)
} }
const finishNcryptsecPasswordPrompt = useCallback((password: string | null) => { const finishNcryptsecPasswordPrompt = useCallback((password: string | null) => {

3
src/providers/nostr-context.tsx

@ -35,7 +35,8 @@ export type TNostrContext = {
accounts: TAccountPointer[] accounts: TAccountPointer[]
nsec: string | null nsec: string | null
ncryptsec: string | null ncryptsec: string | null
switchAccount: (account: TAccountPointer | null) => Promise<void> /** Returns the new session pubkey on success, or `null` if logout / switch failed. */
switchAccount: (account: TAccountPointer | null) => Promise<string | null>
nsecLogin: (nsec: string, password?: string, needSetup?: boolean) => Promise<string> nsecLogin: (nsec: string, password?: string, needSetup?: boolean) => Promise<string>
ncryptsecLogin: (ncryptsec: string) => Promise<string> ncryptsecLogin: (ncryptsec: string) => Promise<string>
nip07Login: () => Promise<string> nip07Login: () => Promise<string>

Loading…
Cancel
Save