Browse Source

bug-fixes

remove emojito link
remove quiet tag functionality
remove untrusted interactions functionality
imwald
Silberengel 3 weeks ago
parent
commit
6538fd0f26
  1. 3
      src/App.tsx
  2. 77
      src/components/HideUntrustedContentButton/index.tsx
  3. 83
      src/components/InBrowserCacheSetting/index.tsx
  4. 3
      src/components/NormalFeed/index.tsx
  5. 4
      src/components/Note/MoneroTip.tsx
  6. 7
      src/components/Note/Zap.tsx
  7. 4
      src/components/Note/index.tsx
  8. 3
      src/components/NoteBoostBadges/index.tsx
  9. 8
      src/components/NoteInteractions/index.tsx
  10. 6
      src/components/NoteList/index.tsx
  11. 12
      src/components/NoteStats/LikeButton.tsx
  12. 23
      src/components/NoteStats/NoteStatsCountHover.tsx
  13. 8
      src/components/NoteStats/ReplyButton.tsx
  14. 8
      src/components/NoteStats/RepostButton.tsx
  15. 11
      src/components/NoteStats/index.tsx
  16. 41
      src/components/PostEditor/PostContent.tsx
  17. 1
      src/components/Profile/ProfileFeed.tsx
  18. 6
      src/components/RelayInfo/RelayReviewsPreview.tsx
  19. 16
      src/components/ReplyNoteList/index.tsx
  20. 19
      src/components/RssUrlThreadStatsBar/index.tsx
  21. 14
      src/components/Sidebar/index.tsx
  22. 7
      src/constants.ts
  23. 27
      src/contexts/user-trust-context.tsx
  24. 19
      src/hooks/useConsoleLogBuffer.ts
  25. 21
      src/index.css
  26. 131
      src/lib/console-log-buffer.ts
  27. 76
      src/lib/draft-event.ts
  28. 36
      src/lib/event-filtering.ts
  29. 10
      src/lib/note-stats-interactors.ts
  30. 1
      src/main.tsx
  31. 7
      src/pages/primary/SpellsPage/index.tsx
  32. 29
      src/pages/secondary/GeneralSettingsPage/index.tsx
  33. 108
      src/pages/secondary/PostSettingsPage/QuietSettings.tsx
  34. 5
      src/pages/secondary/PostSettingsPage/index.tsx
  35. 100
      src/providers/UserTrustProvider.tsx
  36. 138
      src/services/local-storage.service.ts

3
src/App.tsx

@ -23,7 +23,6 @@ import { ScreenSizeProvider } from '@/providers/ScreenSizeProvider' @@ -23,7 +23,6 @@ import { ScreenSizeProvider } from '@/providers/ScreenSizeProvider'
import { ThemeProvider } from '@/providers/ThemeProvider'
import { LiveActivitiesProvider } from '@/providers/LiveActivitiesProvider'
import { UserPreferencesProvider } from '@/providers/UserPreferencesProvider'
import { UserTrustProvider } from '@/providers/UserTrustProvider'
import { ZapProvider } from '@/providers/ZapProvider'
import SlowConnectionHint from '@/components/SlowConnectionHint'
import StartupSessionBanner from '@/components/StartupSessionBanner'
@ -51,7 +50,6 @@ export default function App(): JSX.Element { @@ -51,7 +50,6 @@ export default function App(): JSX.Element {
<MuteListProvider>
<FavoriteRelaysActivityProvider>
<InterestListProvider>
<UserTrustProvider>
<BookmarksProvider>
<NotificationThreadWatchProvider>
<FeedProvider>
@ -71,7 +69,6 @@ export default function App(): JSX.Element { @@ -71,7 +69,6 @@ export default function App(): JSX.Element {
</FeedProvider>
</NotificationThreadWatchProvider>
</BookmarksProvider>
</UserTrustProvider>
</InterestListProvider>
</FavoriteRelaysActivityProvider>
</MuteListProvider>

77
src/components/HideUntrustedContentButton/index.tsx

@ -1,77 +0,0 @@ @@ -1,77 +0,0 @@
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger
} from '@/components/ui/alert-dialog'
import { Button, buttonVariants } from '@/components/ui/button'
import { useUserTrust } from '@/contexts/user-trust-context'
import { VariantProps } from 'class-variance-authority'
import { Shield, ShieldCheck } from 'lucide-react'
import { useTranslation } from 'react-i18next'
export default function HideUntrustedContentButton({
type,
size = 'icon'
}: {
type: 'interactions' | 'notifications'
size?: VariantProps<typeof buttonVariants>['size']
}) {
const { t } = useTranslation()
const {
hideUntrustedInteractions,
hideUntrustedNotifications,
updateHideUntrustedInteractions,
updateHideUntrustedNotifications
} = useUserTrust()
const enabled = type === 'interactions' ? hideUntrustedInteractions : hideUntrustedNotifications
const updateEnabled =
type === 'interactions' ? updateHideUntrustedInteractions : updateHideUntrustedNotifications
const typeText = t(type)
return (
<AlertDialog>
<AlertDialogTrigger asChild>
<Button variant="ghost" size={size} className="[&_svg]:size-3.5">
{enabled ? (
<ShieldCheck className="text-green-400" />
) : (
<Shield className="text-muted-foreground" />
)}
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>
{enabled
? t('Show untrusted {type}', { type: typeText })
: t('Hide untrusted {type}', { type: typeText })}
</AlertDialogTitle>
<AlertDialogDescription>
{enabled
? t('Currently hiding {type} from untrusted users.', { type: typeText })
: t('Currently showing all {type}.', { type: typeText })}{' '}
{t('Trusted users include people you follow and people they follow.')}{' '}
{enabled
? t('Click continue to show all {type}.', { type: typeText })
: t('Click continue to hide {type} from untrusted users.', { type: typeText })}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>{t('Cancel')}</AlertDialogCancel>
<AlertDialogAction onClick={() => updateEnabled(!enabled)}>
{t('Continue')}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
)
}

83
src/components/InBrowserCacheSetting/index.tsx

@ -1,7 +1,9 @@ @@ -1,7 +1,9 @@
import { Button } from '@/components/ui/button'
import { clearConsoleLogBuffer } from '@/lib/console-log-buffer'
import { useConsoleLogBuffer } from '@/hooks/useConsoleLogBuffer'
import logger from '@/lib/logger'
import { useNostr } from '@/providers/NostrProvider'
import { useEffect, useState, useMemo, useRef } from 'react'
import { useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { Trash2, RefreshCw, Database, X, Terminal, XCircle } from 'lucide-react'
import { Input } from '@/components/ui/input'
@ -25,12 +27,11 @@ export default function InBrowserCacheSetting() { @@ -25,12 +27,11 @@ export default function InBrowserCacheSetting() {
requestAccountNetworkHydrate
} = useNostr()
const { openBrowseCache } = useCacheBrowser()
const [consoleLogs, setConsoleLogs] = useState<Array<{ type: string; message: string; formattedParts?: Array<{ text: string; style?: string }>; timestamp: number }>>([])
const consoleLogs = useConsoleLogBuffer()
const [showConsoleLogs, setShowConsoleLogs] = useState(false)
const [consoleLogSearch, setConsoleLogSearch] = useState('')
const [consoleLogLevel, setConsoleLogLevel] = useState<'errors-warnings' | 'all'>('all')
const [cacheRefreshBusy, setCacheRefreshBusy] = useState(false)
const consoleLogRef = useRef<Array<{ type: string; message: string; formattedParts?: Array<{ text: string; style?: string }>; timestamp: number }>>([])
const handleClearCache = async () => {
if (!confirm(t('Are you sure you want to clear all cached data? This will delete all stored events and settings from your browser.'))) {
@ -189,86 +190,14 @@ export default function InBrowserCacheSetting() { @@ -189,86 +190,14 @@ export default function InBrowserCacheSetting() {
}
}
useEffect(() => {
const originalLog = console.log
const originalError = console.error
const originalWarn = console.warn
const originalInfo = console.info
const captureLog = (type: string, ...args: any[]) => {
let message = ''
let formattedParts: Array<{ text: string; style?: string }> = []
if (args.length > 0 && typeof args[0] === 'string' && args[0].includes('%c')) {
const formatString = args[0]
const parts = formatString.split(/%c/g)
formattedParts = []
for (let i = 0; i < parts.length; i++) {
const text = parts[i]
const style = i < args.length - 1 && typeof args[i + 1] === 'string' ? args[i + 1] : undefined
formattedParts.push({ text, style })
}
const remainingArgs = args.slice(parts.length)
if (remainingArgs.length > 0) {
const remainingText = remainingArgs.map(arg => {
if (typeof arg === 'object') {
try { return JSON.stringify(arg, null, 2) } catch { return String(arg) }
}
return String(arg)
}).join(' ')
if (formattedParts.length > 0) {
formattedParts[formattedParts.length - 1].text += ' ' + remainingText
} else {
formattedParts.push({ text: remainingText })
}
}
message = formattedParts.map(p => p.text).join('')
} else {
message = args.map(arg => {
if (typeof arg === 'object') {
try { return JSON.stringify(arg, null, 2) } catch { return String(arg) }
}
return String(arg)
}).join(' ')
formattedParts = [{ text: message }]
}
const logEntry = { type, message, formattedParts, timestamp: Date.now() }
consoleLogRef.current.push(logEntry)
if (consoleLogRef.current.length > 1000) {
consoleLogRef.current = consoleLogRef.current.slice(-1000)
}
if (showConsoleLogs) {
setConsoleLogs([...consoleLogRef.current])
}
}
console.log = (...args: any[]) => { captureLog('log', ...args); originalLog.apply(console, args) }
console.error = (...args: any[]) => { captureLog('error', ...args); originalError.apply(console, args) }
console.warn = (...args: any[]) => { captureLog('warn', ...args); originalWarn.apply(console, args) }
console.info = (...args: any[]) => { captureLog('info', ...args); originalInfo.apply(console, args) }
return () => {
console.log = originalLog
console.error = originalError
console.warn = originalWarn
console.info = originalInfo
}
}, [showConsoleLogs])
const handleShowConsoleLogs = () => {
setConsoleLogs([...consoleLogRef.current])
setShowConsoleLogs(true)
setConsoleLogSearch('')
setConsoleLogLevel('all')
}
const handleClearConsoleLogs = () => {
consoleLogRef.current = []
setConsoleLogs([])
clearConsoleLogBuffer()
toast.success(t('Console logs cleared'))
}
@ -406,7 +335,7 @@ export default function InBrowserCacheSetting() { @@ -406,7 +335,7 @@ export default function InBrowserCacheSetting() {
</Button>
<Button variant="outline" className="shrink-0" onClick={handleShowConsoleLogs}>
<Terminal className="mr-2 h-4 w-4" />
{t('View Console Logs')} ({consoleLogRef.current.length})
{t('View Console Logs')} ({consoleLogs.length})
</Button>
</div>

3
src/components/NormalFeed/index.tsx

@ -4,7 +4,6 @@ import { RefreshButton } from '@/components/RefreshButton' @@ -4,7 +4,6 @@ import { RefreshButton } from '@/components/RefreshButton'
import Tabs, { TabDefinition } from '@/components/Tabs'
import { useGlobalRelayBootstrapDefaults } from '@/hooks/use-global-relay-bootstrap-defaults'
import { useKindFilterOrDefaults } from '@/providers/KindFilterProvider'
import { useUserTrust } from '@/contexts/user-trust-context'
import { PROFILE_MEDIA_TAB_KINDS, FAST_READ_RELAY_URLS } from '@/constants'
import { isWispTrendingNotesRelayUrl } from '@/lib/wisp-trending-relay'
import type { TPrimaryPageName } from '@/PageManager'
@ -170,7 +169,6 @@ const NormalFeed = forwardRef<TNoteListRef, { @@ -170,7 +169,6 @@ const NormalFeed = forwardRef<TNoteListRef, {
},
ref
) {
const { hideUntrustedNotes } = useUserTrust()
const useGlobalRelayBootstrap = useGlobalRelayBootstrapDefaults()
const { showKinds, showKind1OPs, showKind1Replies, showKind1111, feedKindFilterBypass } =
useKindFilterOrDefaults()
@ -397,7 +395,6 @@ const NormalFeed = forwardRef<TNoteListRef, { @@ -397,7 +395,6 @@ const NormalFeed = forwardRef<TNoteListRef, {
withKindFilter={withKindFilter}
subRequests={effectiveSubRequests}
hideReplies={listMode === 'posts'}
hideUntrustedNotes={hideUntrustedNotes}
areAlgoRelays={areAlgoRelays}
relayCapabilityReady={relayCapabilityReady}
feedSubscriptionKey={feedSubscriptionKey}

4
src/components/Note/MoneroTip.tsx

@ -1,6 +1,5 @@ @@ -1,6 +1,5 @@
import { useFetchEvent } from '@/hooks'
import { usePaymentAttestationStatus } from '@/hooks/usePaymentAttestationStatus'
import { shouldHideInteractions } from '@/lib/event-filtering'
import {
formatXmrAmount,
getMoneroTipInfo,
@ -64,9 +63,6 @@ export default function MoneroTip({ @@ -64,9 +63,6 @@ export default function MoneroTip({
const secondaryPage = useSecondaryPageOptional()
const push = secondaryPage?.push ?? ((url: string) => { window.location.href = url })
const inQuietMode = targetEvent ? shouldHideInteractions(targetEvent) : false
if (inQuietMode) return null
if (!tipInfo || !tipInfo.senderPubkey) {
return (
<div className={cn('py-0.5 text-sm text-muted-foreground', className)}>

7
src/components/Note/Zap.tsx

@ -1,7 +1,6 @@ @@ -1,7 +1,6 @@
import { useFetchEvent } from '@/hooks'
import { usePaymentAttestationStatus } from '@/hooks/usePaymentAttestationStatus'
import { getZapInfoFromEvent } from '@/lib/event-metadata'
import { shouldHideInteractions } from '@/lib/event-filtering'
import { formatAmount } from '@/lib/lightning'
import { openNoteFromFetchOrCache } from '@/lib/navigation-related-events'
import { relayHintsFromEventTags } from '@/lib/relay-list-builder'
@ -64,12 +63,6 @@ export default function Zap({ @@ -64,12 +63,6 @@ export default function Zap({
const secondaryPage = useSecondaryPageOptional()
const push = secondaryPage?.push ?? ((url: string) => { window.location.href = url })
const inQuietMode = targetEvent ? shouldHideInteractions(targetEvent) : false
if (inQuietMode) {
return null
}
if (!zapInfo || !zapInfo.senderPubkey) {
return (
<div className={cn('py-0.5 text-sm text-muted-foreground', className)}>

4
src/components/Note/index.tsx

@ -8,7 +8,6 @@ import { @@ -8,7 +8,6 @@ import {
isNip25ReactionKind,
isNsfwEvent
} from '@/lib/event'
import { shouldHideInteractions } from '@/lib/event-filtering'
import { mergeNip84MarkedIntervals, renderPlaintextWithNip84MergedMarks } from '@/lib/nip84-op-body-marks'
import { getCachedThreadContextEvents } from '@/lib/navigation-related-events'
import { relayHintsFromEventTags } from '@/lib/relay-list-builder'
@ -383,8 +382,7 @@ export default function Note({ @@ -383,8 +382,7 @@ export default function Note({
}
if (
nip84HighlightEvents?.length &&
displayEvent.kind === kinds.ShortTextNote &&
!shouldHideInteractions(displayEvent)
displayEvent.kind === kinds.ShortTextNote
) {
const merged = mergeNip84MarkedIntervals(
displayEvent.content ?? '',

3
src/components/NoteBoostBadges/index.tsx

@ -1,6 +1,5 @@ @@ -1,6 +1,5 @@
import { ExtendedKind } from '@/constants'
import { useNoteStatsById } from '@/hooks/useNoteStatsById'
import { shouldHideInteractions } from '@/lib/event-filtering'
import { cn } from '@/lib/utils'
import { Event } from 'nostr-tools'
import { useMemo } from 'react'
@ -22,7 +21,7 @@ export default function NoteBoostBadges({ event, className }: { event: Event; cl @@ -22,7 +21,7 @@ export default function NoteBoostBadges({ event, className }: { event: Event; cl
return [...(noteStats?.reposts ?? [])].sort((a, b) => b.created_at - a.created_at)
}, [noteStats, event.kind])
if (shouldHideInteractions(event) || boosters.length === 0) {
if (boosters.length === 0) {
return null
}

8
src/components/NoteInteractions/index.tsx

@ -1,10 +1,8 @@ @@ -1,10 +1,8 @@
import { Separator } from '@/components/ui/separator'
import { ExtendedKind } from '@/constants'
import { shouldHideInteractions } from '@/lib/event-filtering'
import { Event } from 'nostr-tools'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import HideUntrustedContentButton from '../HideUntrustedContentButton'
import ReplyNoteList from '../ReplyNoteList'
import ReplySort, { ReplySortOption } from './ReplySort'
@ -32,11 +30,6 @@ export default function NoteInteractions({ @@ -32,11 +30,6 @@ export default function NoteInteractions({
const isDiscussion = event.kind === ExtendedKind.DISCUSSION
const showQuotes = showQuotesProp ?? !isDiscussion
// Hide interactions if event is in quiet mode
if (shouldHideInteractions(event)) {
return null
}
return (
<>
<div className="flex items-center gap-2 min-w-0 px-2 sm:px-4 md:px-6 py-2">
@ -47,7 +40,6 @@ export default function NoteInteractions({ @@ -47,7 +40,6 @@ export default function NoteInteractions({
{isDiscussion && (
<ReplySort selectedSort={replySort} onSortChange={setReplySort} />
)}
<HideUntrustedContentButton type="interactions" size="icon" />
</div>
</div>
<Separator />

6
src/components/NoteList/index.tsx

@ -38,7 +38,6 @@ import { useDeletedEventSafe } from '@/providers/DeletedEventProvider' @@ -38,7 +38,6 @@ import { useDeletedEventSafe } from '@/providers/DeletedEventProvider'
import { useMuteList } from '@/contexts/mute-list-context'
import { muteSetHas } from '@/lib/mute-set'
import { useNostr } from '@/providers/NostrProvider'
import { useUserTrust } from '@/contexts/user-trust-context'
import client from '@/services/client.service'
import noteStatsService from '@/services/note-stats.service'
import indexedDb from '@/services/indexed-db.service'
@ -658,7 +657,6 @@ const NoteList = forwardRef( @@ -658,7 +657,6 @@ const NoteList = forwardRef(
allowKindlessRelayExplore = false,
filterMutedNotes = true,
hideReplies = false,
hideUntrustedNotes = false,
areAlgoRelays = false,
relayCapabilityReady = true,
pinnedEventIds = [],
@ -796,7 +794,6 @@ const NoteList = forwardRef( @@ -796,7 +794,6 @@ const NoteList = forwardRef(
allowKindlessRelayExplore?: boolean
filterMutedNotes?: boolean
hideReplies?: boolean
hideUntrustedNotes?: boolean
areAlgoRelays?: boolean
/**
* When false (e.g. home relay feed waiting on `getRelayInfos`), skip timeline subscribe so
@ -851,7 +848,6 @@ const NoteList = forwardRef( @@ -851,7 +848,6 @@ const NoteList = forwardRef(
) => {
const { t } = useTranslation()
const { startLogin, pubkey } = useNostr()
const { isUserTrusted } = useUserTrust()
const { mutePubkeySet } = useMuteList()
const contentPolicy = useContentPolicyOptional()
const hideContentMentioningMutedUsers = contentPolicy?.hideContentMentioningMutedUsers ?? false
@ -1309,7 +1305,6 @@ const NoteList = forwardRef( @@ -1309,7 +1305,6 @@ const NoteList = forwardRef(
if (pinnedEventHexIdSet.has(evt.id)) return true
if (isEventDeleted(evt)) return true
if (hideReplies && isReplyNoteEvent(evt)) return true
if (hideUntrustedNotes && !isUserTrusted(evt.pubkey)) return true
if (filterMutedNotes && muteSetHas(mutePubkeySet, evt.pubkey)) return true
if (
filterMutedNotes &&
@ -1345,7 +1340,6 @@ const NoteList = forwardRef( @@ -1345,7 +1340,6 @@ const NoteList = forwardRef(
[
filterMutedNotes,
hideReplies,
hideUntrustedNotes,
hideContentMentioningMutedUsers,
mutePubkeySet,
pinnedEventIds,

12
src/components/NoteStats/LikeButton.tsx

@ -9,7 +9,6 @@ import { Skeleton } from '@/components/ui/skeleton' @@ -9,7 +9,6 @@ import { Skeleton } from '@/components/ui/skeleton'
import { ExtendedKind } from '@/constants'
import { useNoteStatsById } from '@/hooks/useNoteStatsById'
import { useReplyUnderDiscussionRoot } from '@/hooks/useReplyUnderDiscussionRoot'
import { shouldHideInteractions } from '@/lib/event-filtering'
import { createDeletionRequestDraftEvent, createReactionDraftEvent } from '@/lib/draft-event'
import {
DISCUSSION_DOWNVOTE_DISPLAY,
@ -23,7 +22,6 @@ import { @@ -23,7 +22,6 @@ import {
import { useNoteStatsRelayHints } from '@/hooks/useNoteStatsRelayHints'
import { useNostr } from '@/providers/NostrProvider'
import { useScreenSize } from '@/providers/ScreenSizeProvider'
import { useUserTrust } from '@/contexts/user-trust-context'
import { eventService } from '@/services/client.service'
import noteStatsService from '@/services/note-stats.service'
import type { TNoteStats } from '@/services/note-stats.service'
@ -66,11 +64,9 @@ export function LikeButtonWithStats({ @@ -66,11 +64,9 @@ export function LikeButtonWithStats({
const { isSmallScreen } = useScreenSize()
const { pubkey, publish, checkLogin } = useNostr()
const { relays: statsRelays } = useNoteStatsRelayHints()
const { hideUntrustedInteractions, isUserTrusted } = useUserTrust()
const [liking, setLiking] = useState(false)
const [isEmojiReactionsOpen, setIsEmojiReactionsOpen] = useState(false)
const isDiscussion = event.kind === ExtendedKind.DISCUSSION
const inQuietMode = shouldHideInteractions(event)
const isReplyToDiscussion = isReplyToDiscussionProp ?? false
const showDiscussionVotes = isDiscussion || isReplyToDiscussion
@ -78,9 +74,7 @@ export function LikeButtonWithStats({ @@ -78,9 +74,7 @@ export function LikeButtonWithStats({
const { myLastEmoji, likeCount, upVoteCount, downVoteCount } = useMemo(() => {
const stats = noteStats || {}
const likes = hideUntrustedInteractions
? stats.likes?.filter((like) => isUserTrusted(like.pubkey))
: stats.likes
const likes = stats.likes
const myLike = likes?.find((like) => {
if (like.pubkey !== pubkey) return false
@ -101,7 +95,7 @@ export function LikeButtonWithStats({ @@ -101,7 +95,7 @@ export function LikeButtonWithStats({
upVoteCount,
downVoteCount
}
}, [noteStats, pubkey, hideUntrustedInteractions, showDiscussionVotes])
}, [noteStats, pubkey, showDiscussionVotes])
/** Same idea as {@link ReplyButton}: merged likes (thread fetch / publish) can exist before snapshot sets `updatedAt`. */
const showLikeCount = !hideCount && (statsLoaded || (likeCount ?? 0) > 0)
@ -258,7 +252,7 @@ export function LikeButtonWithStats({ @@ -258,7 +252,7 @@ export function LikeButtonWithStats({
{liking ? (
<Skeleton className="size-4 shrink-0 rounded-full" aria-hidden />
) : myLastEmoji && !useIconOnlyLikeTrigger ? (
<Emoji emoji={inQuietMode ? '+' : myLastEmoji} classNames={{ img: EMOJI_IMG_INLINE_CLASS }} />
<Emoji emoji={myLastEmoji} classNames={{ img: EMOJI_IMG_INLINE_CLASS }} />
) : (
<SmilePlus />
)}

23
src/components/NoteStats/NoteStatsCountHover.tsx

@ -14,14 +14,12 @@ import { @@ -14,14 +14,12 @@ import {
aggregateZapsByPubkey,
dedupeBoostersByPubkey,
emojiStatsKey,
filterStatsInteractors,
groupReactionsByEmoji,
MAX_NOTE_STATS_INTERACTORS_SHOWN
} from '@/lib/note-stats-interactors'
import { cn } from '@/lib/utils'
import type { TNoteStats } from '@/services/note-stats.service'
import { useNoteFeedProfileContext } from '@/providers/NoteFeedProfileContext'
import { useUserTrust } from '@/contexts/user-trust-context'
import { TEmoji } from '@/types'
import { useMemo, useState, type PointerEvent, type ReactNode } from 'react'
import { useTranslation } from 'react-i18next'
@ -240,11 +238,10 @@ export function BoostCountHover({ @@ -240,11 +238,10 @@ export function BoostCountHover({
children: ReactNode
}) {
const { t } = useTranslation()
const { hideUntrustedInteractions, isUserTrusted } = useUserTrust()
const pubkeys = useMemo(() => {
const filtered = filterStatsInteractors(noteStats?.reposts, hideUntrustedInteractions, isUserTrusted)
const filtered = noteStats?.reposts ?? []
return dedupeBoostersByPubkey(filtered).map((r) => r.pubkey)
}, [noteStats?.reposts, hideUntrustedInteractions, isUserTrusted])
}, [noteStats?.reposts])
return (
<NoteStatsCountHover
@ -269,15 +266,14 @@ export function ReactionCountHover({ @@ -269,15 +266,14 @@ export function ReactionCountHover({
children: ReactNode
}) {
const { t } = useTranslation()
const { hideUntrustedInteractions, isUserTrusted } = useUserTrust()
const { groups, title } = useMemo(() => {
let likes = filterStatsInteractors(noteStats?.likes, hideUntrustedInteractions, isUserTrusted)
let likes = noteStats?.likes ?? []
if (emojiFilter) likes = likes.filter((l) => emojiFilter(l.emoji))
return {
groups: groupReactionsByEmoji(likes),
title: titleProp ?? t('Liked by:')
}
}, [noteStats?.likes, hideUntrustedInteractions, isUserTrusted, emojiFilter, titleProp, t])
}, [noteStats?.likes, emojiFilter, titleProp, t])
const total = groups.reduce((n, g) => n + g.pubkeys.length, 0)
@ -303,10 +299,8 @@ export function DiscussionVoteCountHover({ @@ -303,10 +299,8 @@ export function DiscussionVoteCountHover({
const { t } = useTranslation()
const emojiFilter = vote === 'up' ? isDiscussionUpvoteEmoji : isDiscussionDownvoteEmoji
const { hideUntrustedInteractions, isUserTrusted } = useUserTrust()
const pubkeys = useMemo(() => {
const likes = filterStatsInteractors(noteStats?.likes, hideUntrustedInteractions, isUserTrusted)
.filter((l) => emojiFilter(l.emoji))
const likes = (noteStats?.likes ?? []).filter((l) => emojiFilter(l.emoji))
const byPk = new Map<string, number>()
for (const l of likes) {
const pk = l.pubkey.toLowerCase()
@ -316,7 +310,7 @@ export function DiscussionVoteCountHover({ @@ -316,7 +310,7 @@ export function DiscussionVoteCountHover({
return [...byPk.entries()]
.sort((a, b) => b[1] - a[1])
.map(([pk]) => pk)
}, [noteStats?.likes, hideUntrustedInteractions, isUserTrusted, emojiFilter])
}, [noteStats?.likes, emojiFilter])
const title = (
<span className="inline-flex items-center gap-1">
@ -345,11 +339,10 @@ export function ZapCountHover({ @@ -345,11 +339,10 @@ export function ZapCountHover({
children: ReactNode
}) {
const { t } = useTranslation()
const { hideUntrustedInteractions, isUserTrusted } = useUserTrust()
const zappers = useMemo(() => {
const filtered = filterStatsInteractors(noteStats?.zaps, hideUntrustedInteractions, isUserTrusted)
const filtered = noteStats?.zaps ?? []
return aggregateZapsByPubkey(filtered)
}, [noteStats?.zaps, hideUntrustedInteractions, isUserTrusted])
}, [noteStats?.zaps])
return (
<NoteStatsCountHover

8
src/components/NoteStats/ReplyButton.tsx

@ -1,7 +1,6 @@ @@ -1,7 +1,6 @@
import { useNoteStatsById } from '@/hooks/useNoteStatsById'
import { cn } from '@/lib/utils'
import { useNostr } from '@/providers/NostrProvider'
import { useUserTrust } from '@/contexts/user-trust-context'
import type { TNoteStats } from '@/services/note-stats.service'
import { MessageCircle } from 'lucide-react'
import { Event } from 'nostr-tools'
@ -19,19 +18,16 @@ type ReplyButtonProps = { @@ -19,19 +18,16 @@ type ReplyButtonProps = {
export function ReplyButtonWithStats({ event, hideCount = false, noteStats }: ReplyButtonProps) {
const { t } = useTranslation()
const { pubkey, checkLogin } = useNostr()
const { hideUntrustedInteractions, isUserTrusted } = useUserTrust()
const { replyCount, hasReplied } = useMemo(() => {
const hasReplied = pubkey
? noteStats?.replies?.some((reply) => reply.pubkey === pubkey)
: false
return {
replyCount: hideUntrustedInteractions
? noteStats?.replies?.filter((reply) => isUserTrusted(reply.pubkey)).length ?? 0
: noteStats?.replies?.length ?? 0,
replyCount: noteStats?.replies?.length ?? 0,
hasReplied
}
}, [noteStats, event.id, hideUntrustedInteractions, isUserTrusted, pubkey])
}, [noteStats, event.id, pubkey])
const statsLoaded = noteStats?.updatedAt != null
const replyCountLabel = statsLoaded
? replyCount >= 100

8
src/components/NoteStats/RepostButton.tsx

@ -15,7 +15,6 @@ import { cn } from '@/lib/utils' @@ -15,7 +15,6 @@ import { cn } from '@/lib/utils'
import { useNoteStatsRelayHints } from '@/hooks/useNoteStatsRelayHints'
import { useNostr } from '@/providers/NostrProvider'
import { useScreenSize } from '@/providers/ScreenSizeProvider'
import { useUserTrust } from '@/contexts/user-trust-context'
import noteStatsService from '@/services/note-stats.service'
import type { TNoteStats } from '@/services/note-stats.service'
import { PencilLine, Repeat } from 'lucide-react'
@ -37,7 +36,6 @@ type RepostButtonProps = { @@ -37,7 +36,6 @@ type RepostButtonProps = {
export function RepostButtonWithStats({ event, hideCount = false, noteStats }: RepostButtonProps) {
const { t } = useTranslation()
const { isSmallScreen } = useScreenSize()
const { hideUntrustedInteractions, isUserTrusted } = useUserTrust()
const { publish, checkLogin, pubkey } = useNostr()
const { relays: statsRelays } = useNoteStatsRelayHints()
const [reposting, setReposting] = useState(false)
@ -46,12 +44,10 @@ export function RepostButtonWithStats({ event, hideCount = false, noteStats }: R @@ -46,12 +44,10 @@ export function RepostButtonWithStats({ event, hideCount = false, noteStats }: R
const statsLoaded = noteStats?.updatedAt != null
const { repostCount, hasReposted } = useMemo(() => {
return {
repostCount: hideUntrustedInteractions
? noteStats?.reposts?.filter((repost) => isUserTrusted(repost.pubkey)).length
: noteStats?.reposts?.length,
repostCount: noteStats?.reposts?.length,
hasReposted: pubkey ? noteStats?.repostPubkeySet?.has(pubkey) : false
}
}, [noteStats, event.id, hideUntrustedInteractions, isUserTrusted])
}, [noteStats, event.id, pubkey])
const showRepostCount = !hideCount && (statsLoaded || (repostCount ?? 0) > 0)
const canRepost = !hasReposted && !reposting

11
src/components/NoteStats/index.tsx

@ -7,7 +7,6 @@ import { useRssUrlThreadQueryRelays } from '@/hooks/useRssUrlThreadQueryRelays' @@ -7,7 +7,6 @@ import { useRssUrlThreadQueryRelays } from '@/hooks/useRssUrlThreadQueryRelays'
import noteStatsService from '@/services/note-stats.service'
import { ExtendedKind } from '@/constants'
import { useReplyUnderDiscussionRoot } from '@/hooks/useReplyUnderDiscussionRoot'
import { shouldHideInteractions } from '@/lib/event-filtering'
import { normalizeAnyRelayUrl } from '@/lib/url'
import { Event } from 'nostr-tools'
import { useEffect, useRef, useState } from 'react'
@ -59,9 +58,6 @@ export default function NoteStats({ @@ -59,9 +58,6 @@ export default function NoteStats({
const isDiscussion = event.kind === ExtendedKind.DISCUSSION
const isReplyToDiscussion = useReplyUnderDiscussionRoot(event)
// Hide interaction counts if event is in quiet mode
const hideInteractions = shouldHideInteractions(event)
/** Synthetic RSS article root: no boost/quote/zap bar entries that normal notes have. */
const isRssArticleRoot = event.kind === ExtendedKind.RSS_THREAD_ROOT
/** Match {@link RssUrlThreadStatsBar}: inbox/favorites/fast-read merge — plain hints miss many #i indexers. */
@ -119,19 +115,18 @@ export default function NoteStats({ @@ -119,19 +115,18 @@ export default function NoteStats({
const interactionButtons = (
<>
<ReplyButtonWithStats event={event} hideCount={hideInteractions} noteStats={noteStats} />
<ReplyButtonWithStats event={event} noteStats={noteStats} />
{!isDiscussion && !isReplyToDiscussion && !isRssArticleRoot && (
<RepostButtonWithStats event={event} hideCount={hideInteractions} noteStats={noteStats} />
<RepostButtonWithStats event={event} noteStats={noteStats} />
)}
<LikeButtonWithStats
event={event}
hideCount={hideInteractions}
noteStats={noteStats}
isReplyToDiscussion={isReplyToDiscussion}
useIconOnlyLikeTrigger={useIconOnlyLikeTrigger}
/>
{!isRssArticleRoot && (
<ZapButtonWithStats event={event} hideCount={hideInteractions} noteStats={noteStats} />
<ZapButtonWithStats event={event} noteStats={noteStats} />
)}
</>
)

41
src/components/PostEditor/PostContent.tsx

@ -801,7 +801,7 @@ export default function PostContent({ @@ -801,7 +801,7 @@ export default function PostContent({
const createDraftEvent = useCallback(async (cleanedText: string): Promise<any> => {
const uploadImetaTagsOpt = mediaImetaTags.length > 0 ? mediaImetaTags : undefined
// Get expiration and quiet settings
// Get expiration settings
const isChattingKind = (kind: number) =>
kind === kinds.ShortTextNote ||
kind === ExtendedKind.COMMENT ||
@ -810,9 +810,6 @@ export default function PostContent({ @@ -810,9 +810,6 @@ export default function PostContent({
const addExpirationTag = storage.getDefaultExpirationEnabled()
const expirationMonths = storage.getDefaultExpirationMonths()
const addQuietTag = storage.getDefaultQuietEnabled()
const quietDays = storage.getDefaultQuietDays()
// Determine if we should use protected event tag
let shouldUseProtectedEvent = false
if (parentEvent) {
@ -828,8 +825,6 @@ export default function PostContent({ @@ -828,8 +825,6 @@ export default function PostContent({
isNsfw,
addExpirationTag: false,
expirationMonths,
addQuietTag,
quietDays,
mediaImetaTags: uploadImetaTagsOpt
})
} else if (parentEvent && parentEvent.kind === ExtendedKind.PUBLIC_MESSAGE) {
@ -839,8 +834,6 @@ export default function PostContent({ @@ -839,8 +834,6 @@ export default function PostContent({
isNsfw,
addExpirationTag: false,
expirationMonths,
addQuietTag,
quietDays,
mediaImetaTags: uploadImetaTagsOpt
})
}
@ -885,8 +878,6 @@ export default function PostContent({ @@ -885,8 +878,6 @@ export default function PostContent({
isNsfw,
addExpirationTag: addExpirationTag && isChattingKind(ExtendedKind.VOICE_COMMENT),
expirationMonths,
addQuietTag,
quietDays,
mediaImetaTags: uploadImetaTagsOpt
}
)
@ -907,8 +898,6 @@ export default function PostContent({ @@ -907,8 +898,6 @@ export default function PostContent({
isNsfw,
addExpirationTag: addExpirationTag && isChattingKind(ExtendedKind.VOICE),
expirationMonths,
addQuietTag,
quietDays,
mediaImetaTags: uploadImetaTagsOpt
}
)
@ -922,8 +911,6 @@ export default function PostContent({ @@ -922,8 +911,6 @@ export default function PostContent({
isNsfw,
addExpirationTag: false,
expirationMonths,
addQuietTag,
quietDays,
mediaImetaTags: uploadImetaTagsOpt
}
)
@ -938,8 +925,6 @@ export default function PostContent({ @@ -938,8 +925,6 @@ export default function PostContent({
isNsfw,
addExpirationTag: false,
expirationMonths,
addQuietTag,
quietDays,
mediaImetaTags: uploadImetaTagsOpt
}
)
@ -988,9 +973,7 @@ export default function PostContent({ @@ -988,9 +973,7 @@ export default function PostContent({
addClientTag,
isNsfw,
addExpirationTag: false,
expirationMonths,
addQuietTag,
quietDays
expirationMonths
})
} else if (isWikiArticle) {
return await createWikiArticleDraftEvent(cleanedText, mentions, {
@ -1002,9 +985,7 @@ export default function PostContent({ @@ -1002,9 +985,7 @@ export default function PostContent({
addClientTag,
isNsfw,
addExpirationTag: false,
expirationMonths,
addQuietTag,
quietDays
expirationMonths
})
} else if (isNostrSpecification) {
const affectedKinds = parseNostrSpecAffectedKinds(nostrSpecAffectedKindRows)
@ -1017,9 +998,7 @@ export default function PostContent({ @@ -1017,9 +998,7 @@ export default function PostContent({
addClientTag,
isNsfw,
addExpirationTag: false,
expirationMonths,
addQuietTag,
quietDays
expirationMonths
})
} else if (isPublicationContent) {
return await createPublicationContentDraftEvent(cleanedText, mentions, {
@ -1031,9 +1010,7 @@ export default function PostContent({ @@ -1031,9 +1010,7 @@ export default function PostContent({
addClientTag,
isNsfw,
addExpirationTag: false,
expirationMonths,
addQuietTag,
quietDays
expirationMonths
})
}
@ -1119,8 +1096,6 @@ export default function PostContent({ @@ -1119,8 +1096,6 @@ export default function PostContent({
isNsfw,
addExpirationTag: false,
expirationMonths,
addQuietTag,
quietDays,
mediaImetaTags: uploadImetaTagsOpt
}
)
@ -1135,8 +1110,6 @@ export default function PostContent({ @@ -1135,8 +1110,6 @@ export default function PostContent({
isNsfw,
addExpirationTag: addExpirationTag && isChattingKind(ExtendedKind.COMMENT),
expirationMonths,
addQuietTag,
quietDays,
mediaImetaTags: uploadImetaTagsOpt
})
}
@ -1148,8 +1121,6 @@ export default function PostContent({ @@ -1148,8 +1121,6 @@ export default function PostContent({
isNsfw,
addExpirationTag: false,
expirationMonths,
addQuietTag,
quietDays,
mediaImetaTags: uploadImetaTagsOpt
})
}
@ -1162,8 +1133,6 @@ export default function PostContent({ @@ -1162,8 +1133,6 @@ export default function PostContent({
isNsfw,
addExpirationTag: addExpirationTag && isChattingKind(kinds.ShortTextNote),
expirationMonths,
addQuietTag,
quietDays,
mediaImetaTags: uploadImetaTagsOpt
})
}, [

1
src/components/Profile/ProfileFeed.tsx

@ -136,7 +136,6 @@ const ProfileFeed = forwardRef< @@ -136,7 +136,6 @@ const ProfileFeed = forwardRef<
mergeTimelineWhenSubRequestFiltersMatch
pinnedEventIds={pinnedEventIds}
hideReplies={false}
hideUntrustedNotes={false}
filterMutedNotes={false}
showKind1OPs={showKind1OPs}
showKind1Replies={showKind1Replies}

6
src/components/RelayInfo/RelayReviewsPreview.tsx

@ -26,7 +26,6 @@ import { cn, isTouchDevice } from '@/lib/utils' @@ -26,7 +26,6 @@ import { cn, isTouchDevice } from '@/lib/utils'
import { useMuteList } from '@/contexts/mute-list-context'
import { muteSetHas } from '@/lib/mute-set'
import { useNostr } from '@/providers/NostrProvider'
import { useUserTrust } from '@/contexts/user-trust-context'
import { queryService } from '@/services/client.service'
import { getSessionFeedSnapshot } from '@/services/session-feed-snapshot.service'
import { WheelGesturesPlugin } from 'embla-carousel-wheel-gestures'
@ -42,7 +41,6 @@ export default function RelayReviewsPreview({ relayUrl }: { relayUrl: string }) @@ -42,7 +41,6 @@ export default function RelayReviewsPreview({ relayUrl }: { relayUrl: string })
const { push } = useSecondaryPage()
const { pubkey, checkLogin, relayList } = useNostr()
const { favoriteRelays, blockedRelays } = useFavoriteRelays()
const { hideUntrustedNotes, isUserTrusted } = useUserTrust()
const { mutePubkeySet } = useMuteList()
const [showEditor, setShowEditor] = useState(false)
const [myReview, setMyReview] = useState<NostrEvent | null>(null)
@ -68,7 +66,6 @@ export default function RelayReviewsPreview({ relayUrl }: { relayUrl: string }) @@ -68,7 +66,6 @@ export default function RelayReviewsPreview({ relayUrl }: { relayUrl: string })
const ingestReviewEvent = useCallback(
(evt: NostrEvent) => {
if (muteSetHas(mutePubkeySet, evt.pubkey)) return
if (hideUntrustedNotes && !isUserTrusted(evt.pubkey)) return
const stars = getStarsFromRelayReviewEvent(evt)
if (!stars) return
@ -84,7 +81,7 @@ export default function RelayReviewsPreview({ relayUrl }: { relayUrl: string }) @@ -84,7 +81,7 @@ export default function RelayReviewsPreview({ relayUrl }: { relayUrl: string })
return [...filtered, evt].sort((a, b) => compareEvents(b, a))
})
},
[pubkey, mutePubkeySet, hideUntrustedNotes, isUserTrusted]
[pubkey, mutePubkeySet]
)
useEffect(() => {
@ -104,7 +101,6 @@ export default function RelayReviewsPreview({ relayUrl }: { relayUrl: string }) @@ -104,7 +101,6 @@ export default function RelayReviewsPreview({ relayUrl }: { relayUrl: string })
if (evt.kind !== ExtendedKind.RELAY_REVIEW || !relayReviewEventTargetsRelay(evt, relayUrl))
continue
if (muteSetHas(mutePubkeySet, evt.pubkey)) continue
if (hideUntrustedNotes && !isUserTrusted(evt.pubkey)) continue
const st = getStarsFromRelayReviewEvent(evt)
if (!st) continue
if (pubkey && evt.pubkey === pubkey) {

16
src/components/ReplyNoteList/index.tsx

@ -30,7 +30,6 @@ import { useContentPolicy } from '@/providers/ContentPolicyProvider' @@ -30,7 +30,6 @@ import { useContentPolicy } from '@/providers/ContentPolicyProvider'
import { useMuteList } from '@/contexts/mute-list-context'
import { useNostr } from '@/providers/NostrProvider'
import { useReplyIngress } from '@/hooks/useReplyIngress'
import { useUserTrust } from '@/contexts/user-trust-context'
import { useCurrentRelays } from '@/providers/CurrentRelaysProvider'
import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider'
import {
@ -109,7 +108,6 @@ function ReplyNoteList({ @@ -109,7 +108,6 @@ function ReplyNoteList({
}) {
const { t } = useTranslation()
const { navigateToNote } = useSmartNoteNavigation()
const { hideUntrustedInteractions, isUserTrusted, isTrustLoaded } = useUserTrust()
const noteStats = useNoteStatsById(event.id)
const { mutePubkeySet } = useMuteList()
const { hideContentMentioningMutedUsers } = useContentPolicy()
@ -1138,26 +1136,12 @@ function ReplyNoteList({ @@ -1138,26 +1136,12 @@ function ReplyNoteList({
if (isSuperchatKind(item.kind)) return true
// Backlink rows (quotes, highlights, …): show even when author is not in the trust list.
if (isQuote) return true
if (isTrustLoaded && hideUntrustedInteractions && !isUserTrusted(item.pubkey)) {
if (rootInfo?.type !== 'I') {
const repliesForThisReply = repliesMap.get(item.id)
if (
!repliesForThisReply ||
repliesForThisReply.events.every((evt) => !isUserTrusted(evt.pubkey))
) {
return false
}
}
}
return true
},
[
mutePubkeySet,
hideContentMentioningMutedUsers,
quoteUiIdSet,
isTrustLoaded,
hideUntrustedInteractions,
isUserTrusted,
rootInfo?.type,
repliesMap,
event,

19
src/components/RssUrlThreadStatsBar/index.tsx

@ -1,5 +1,4 @@ @@ -1,5 +1,4 @@
import { useNoteStatsById } from '@/hooks/useNoteStatsById'
import { useUserTrust } from '@/contexts/user-trust-context'
import { cn } from '@/lib/utils'
import noteStatsService from '@/services/note-stats.service'
import { useRssUrlThreadQueryRelays } from '@/hooks/useRssUrlThreadQueryRelays'
@ -21,7 +20,6 @@ export default function RssUrlThreadStatsBar({ @@ -21,7 +20,6 @@ export default function RssUrlThreadStatsBar({
const { relayUrls: statsRelays, relayMergeTier, currentRelaysKey } = useRssUrlThreadQueryRelays()
const statsRelaysRef = useRef(statsRelays)
statsRelaysRef.current = statsRelays
const { hideUntrustedInteractions, isUserTrusted } = useUserTrust()
const noteStats = useNoteStatsById(event.id)
const [loading, setLoading] = useState(false)
@ -38,23 +36,14 @@ export default function RssUrlThreadStatsBar({ @@ -38,23 +36,14 @@ export default function RssUrlThreadStatsBar({
const replies = noteStats?.replies ?? []
const likes = noteStats?.likes ?? []
const highlights = noteStats?.highlights ?? []
const trustedReplyCount = hideUntrustedInteractions
? replies.filter((r) => isUserTrusted(r.pubkey)).length
: replies.length
const trustedReactionCount = hideUntrustedInteractions
? likes.filter((l) => isUserTrusted(l.pubkey)).length
: likes.length
const trustedHighlightCount = hideUntrustedInteractions
? highlights.filter((h) => isUserTrusted(h.pubkey)).length
: highlights.length
const bookmarkCountInner = noteStats?.bookmarkPubkeySet?.size ?? 0
return {
replyCount: trustedReplyCount,
reactionCount: trustedReactionCount,
highlightCount: trustedHighlightCount,
replyCount: replies.length,
reactionCount: likes.length,
highlightCount: highlights.length,
bookmarkCount: bookmarkCountInner
}
}, [noteStats, hideUntrustedInteractions, isUserTrusted])
}, [noteStats])
return (
<div

14
src/components/Sidebar/index.tsx

@ -22,15 +22,19 @@ export default function PrimaryPageSidebar() { @@ -22,15 +22,19 @@ export default function PrimaryPageSidebar() {
const { isSmallScreen } = useScreenSize()
if (isSmallScreen) return null
const sidebarInsetX = 'px-2 xl:pl-4 xl:pr-3'
return (
<div className="imwald-sidebar flex h-full min-h-0 w-[4.8rem] shrink-0 flex-col overflow-hidden pb-2 pt-4 px-2 xl:w-[15.6rem] xl:pl-4 xl:pr-6">
<div className="imwald-sidebar flex h-full min-h-0 w-[4.8rem] shrink-0 flex-col overflow-hidden pb-2 pt-4 xl:w-[15.6rem]">
<div className="imwald-sidebar__atmosphere" aria-hidden />
<div className="relative z-[1] flex min-h-0 flex-1 flex-col overflow-hidden">
<div className="min-h-0 flex-1 space-y-2 overflow-x-hidden overflow-y-auto overscroll-contain">
<div
className={`imwald-sidebar__scroll min-h-0 flex-1 space-y-2 overflow-x-clip overflow-y-auto overscroll-contain ${sidebarInsetX}`}
>
<div className="mb-6 w-full min-w-0 shrink-0">
<Icon className="mx-auto xl:hidden" />
{/* Full-bleed banner at xl: span entire sidebar column (undo pl-4 + pr-6) */}
<div className="max-xl:hidden -ml-4 -mr-6 w-[calc(100%+2.5rem)] min-w-0">
{/* Full-bleed banner at xl: cancel horizontal inset without widening scroll overflow */}
<div className="max-xl:hidden -mx-2 min-w-0 xl:-mx-4">
<Logo className="h-auto max-h-[5.5rem] w-full max-w-full object-contain object-center" />
</div>
</div>
@ -50,7 +54,7 @@ export default function PrimaryPageSidebar() { @@ -50,7 +54,7 @@ export default function PrimaryPageSidebar() {
<SidebarCalendarWeekWidget />
</div>
</div>
<div className="shrink-0 space-y-2 pt-2">
<div className={`shrink-0 space-y-2 pt-2 ${sidebarInsetX}`}>
<HelpAndAccountMenu variant="sidebar" />
<PaneModeToggle />
<DownloadDesktopSidebarButton />

7
src/constants.ts

@ -341,10 +341,7 @@ export const StorageKey = { @@ -341,10 +341,7 @@ export const StorageKey = {
/** Per-pubkey ms timestamps: last full network hydrate (see ACCOUNT_SESSION_NETWORK_HYDRATE_MIN_INTERVAL_MS). */
ACCOUNT_NETWORK_HYDRATE_AT_MAP: 'accountNetworkHydrateAtMap',
AUTOPLAY: 'autoplay',
HIDE_UNTRUSTED_INTERACTIONS: 'hideUntrustedInteractions',
HIDE_UNTRUSTED_NOTIFICATIONS: 'hideUntrustedNotifications',
MEDIA_UPLOAD_SERVICE_CONFIG_MAP: 'mediaUploadServiceConfigMap',
HIDE_UNTRUSTED_NOTES: 'hideUntrustedNotes',
DEFAULT_SHOW_NSFW: 'defaultShowNsfw',
DISMISSED_TOO_MANY_RELAYS_ALERT: 'dismissedTooManyRelaysAlert',
SHOW_KINDS: 'showKinds',
@ -363,10 +360,6 @@ export const StorageKey = { @@ -363,10 +360,6 @@ export const StorageKey = {
SHOW_RECOMMENDED_RELAYS_PANEL: 'showRecommendedRelaysPanel',
DEFAULT_EXPIRATION_ENABLED: 'defaultExpirationEnabled',
DEFAULT_EXPIRATION_MONTHS: 'defaultExpirationMonths',
DEFAULT_QUIET_ENABLED: 'defaultQuietEnabled',
DEFAULT_QUIET_DAYS: 'defaultQuietDays',
RESPECT_QUIET_TAGS: 'respectQuietTags',
GLOBAL_QUIET_MODE: 'globalQuietMode',
SHOW_RSS_FEED: 'showRssFeed',
PANE_MODE: 'paneMode',
ADD_RANDOM_RELAYS_TO_PUBLISH: 'addRandomRelaysToPublish',

27
src/contexts/user-trust-context.tsx

@ -1,27 +0,0 @@ @@ -1,27 +0,0 @@
import { createContext, useContext } from 'react'
export type TUserTrustContext = {
isTrustLoaded: boolean
hideUntrustedInteractions: boolean
hideUntrustedNotifications: boolean
hideUntrustedNotes: boolean
updateHideUntrustedInteractions: (hide: boolean) => void
updateHideUntrustedNotifications: (hide: boolean) => void
updateHideUntrustedNotes: (hide: boolean) => void
isUserTrusted: (pubkey: string) => boolean
}
/**
* Lives in a dedicated module so lazy chunks (e.g. NoteListPage NormalFeed) share the same
* context instance as Apps UserTrustProvider. Importing useUserTrust from UserTrustProvider into
* those chunks can duplicate the module and break Provider matching.
*/
export const UserTrustContext = createContext<TUserTrustContext | undefined>(undefined)
export function useUserTrust(): TUserTrustContext {
const context = useContext(UserTrustContext)
if (!context) {
throw new Error('useUserTrust must be used within a UserTrustProvider')
}
return context
}

19
src/hooks/useConsoleLogBuffer.ts

@ -0,0 +1,19 @@ @@ -0,0 +1,19 @@
import {
getConsoleLogBuffer,
subscribeConsoleLogBuffer,
type ConsoleLogEntry
} from '@/lib/console-log-buffer'
import { useSyncExternalStore } from 'react'
function subscribe(onStoreChange: () => void) {
return subscribeConsoleLogBuffer(onStoreChange)
}
function getSnapshot(): ConsoleLogEntry[] {
return getConsoleLogBuffer()
}
/** Live view of the global console log ring buffer (see Settings → Cache). */
export function useConsoleLogBuffer(): ConsoleLogEntry[] {
return useSyncExternalStore(subscribe, getSnapshot, getSnapshot)
}

21
src/index.css

@ -297,6 +297,27 @@ @@ -297,6 +297,27 @@
border-right: 1px solid hsl(var(--sidebar-border));
}
/* Reserve a right gutter so the track sits beside labels, not over icons/text. */
.imwald-sidebar__scroll {
direction: ltr;
scrollbar-gutter: stable;
scrollbar-width: thin;
}
.imwald-sidebar__scroll::-webkit-scrollbar {
width: 8px;
}
.imwald-sidebar__scroll::-webkit-scrollbar-thumb {
border-radius: 9999px;
background-color: hsl(var(--muted-foreground) / 0.35);
}
.imwald-sidebar__scroll::-webkit-scrollbar-thumb:hover {
background-color: hsl(var(--muted-foreground) / 0.5);
}
.imwald-sidebar__scroll::-webkit-scrollbar-track {
border-radius: 9999px;
background-color: hsl(var(--muted) / 0.35);
}
.imwald-sidebar__atmosphere {
position: absolute;
inset: 0;

131
src/lib/console-log-buffer.ts

@ -0,0 +1,131 @@ @@ -0,0 +1,131 @@
export type ConsoleLogEntry = {
type: string
message: string
formattedParts?: Array<{ text: string; style?: string }>
timestamp: number
}
const MAX_ENTRIES = 1000
const buffer: ConsoleLogEntry[] = []
const listeners = new Set<() => void>()
let initialized = false
function notifyListeners() {
for (const listener of listeners) {
listener()
}
}
function formatArgs(args: unknown[]): { message: string; formattedParts: Array<{ text: string; style?: string }> } {
if (args.length > 0 && typeof args[0] === 'string' && args[0].includes('%c')) {
const formatString = args[0]
const parts = formatString.split(/%c/g)
const formattedParts: Array<{ text: string; style?: string }> = []
for (let i = 0; i < parts.length; i++) {
const text = parts[i]
const style = i < args.length - 1 && typeof args[i + 1] === 'string' ? String(args[i + 1]) : undefined
formattedParts.push({ text, style })
}
const remainingArgs = args.slice(parts.length)
if (remainingArgs.length > 0) {
const remainingText = remainingArgs
.map((arg) => {
if (typeof arg === 'object') {
try {
return JSON.stringify(arg, null, 2)
} catch {
return String(arg)
}
}
return String(arg)
})
.join(' ')
if (formattedParts.length > 0) {
formattedParts[formattedParts.length - 1].text += ' ' + remainingText
} else {
formattedParts.push({ text: remainingText })
}
}
return { message: formattedParts.map((p) => p.text).join(''), formattedParts }
}
const message = args
.map((arg) => {
if (typeof arg === 'object') {
try {
return JSON.stringify(arg, null, 2)
} catch {
return String(arg)
}
}
return String(arg)
})
.join(' ')
return { message, formattedParts: [{ text: message }] }
}
function captureLog(type: string, ...args: unknown[]) {
const { message, formattedParts } = formatArgs(args)
buffer.push({ type, message, formattedParts, timestamp: Date.now() })
if (buffer.length > MAX_ENTRIES) {
buffer.splice(0, buffer.length - MAX_ENTRIES)
}
notifyListeners()
}
/** Ring buffer of recent console output (installed at app startup). */
export function getConsoleLogBuffer(): ConsoleLogEntry[] {
return [...buffer]
}
export function clearConsoleLogBuffer() {
buffer.length = 0
notifyListeners()
}
export function subscribeConsoleLogBuffer(listener: () => void): () => void {
listeners.add(listener)
return () => listeners.delete(listener)
}
/** Wrap console.* after other patches (e.g. error-suppression) so all output is retained. */
export function initConsoleLogCapture() {
if (initialized || typeof window === 'undefined') return
initialized = true
const originalLog = console.log.bind(console)
const originalError = console.error.bind(console)
const originalWarn = console.warn.bind(console)
const originalInfo = console.info.bind(console)
const originalDebug = console.debug.bind(console)
console.log = (...args: unknown[]) => {
captureLog('log', ...args)
originalLog(...args)
}
console.error = (...args: unknown[]) => {
captureLog('error', ...args)
originalError(...args)
}
console.warn = (...args: unknown[]) => {
captureLog('warn', ...args)
originalWarn(...args)
}
console.info = (...args: unknown[]) => {
captureLog('info', ...args)
originalInfo(...args)
}
console.debug = (...args: unknown[]) => {
captureLog('debug', ...args)
originalDebug(...args)
}
}
if (typeof window !== 'undefined') {
initConsoleLogCapture()
}

76
src/lib/draft-event.ts

@ -229,8 +229,6 @@ export async function createShortTextNoteDraftEvent( @@ -229,8 +229,6 @@ export async function createShortTextNoteDraftEvent(
isNsfw?: boolean
addExpirationTag?: boolean
expirationMonths?: number
addQuietTag?: boolean
quietDays?: number
/** NIP-94 imeta rows from uploads (audio/video/images as plain URLs in content). */
mediaImetaTags?: string[][]
} = {}
@ -280,9 +278,6 @@ export async function createShortTextNoteDraftEvent( @@ -280,9 +278,6 @@ export async function createShortTextNoteDraftEvent(
tags.push(buildExpirationTag(options.expirationMonths))
}
if (options.addQuietTag && options.quietDays) {
tags.push(buildQuietTag(options.quietDays))
}
const baseDraft = {
kind: kinds.ShortTextNote,
@ -317,8 +312,6 @@ export async function createCommentDraftEvent( @@ -317,8 +312,6 @@ export async function createCommentDraftEvent(
isNsfw?: boolean
addExpirationTag?: boolean
expirationMonths?: number
addQuietTag?: boolean
quietDays?: number
mediaImetaTags?: string[][]
} = {}
): Promise<TDraftEvent> {
@ -403,9 +396,6 @@ export async function createCommentDraftEvent( @@ -403,9 +396,6 @@ export async function createCommentDraftEvent(
tags.push(buildExpirationTag(options.expirationMonths))
}
if (options.addQuietTag && options.quietDays) {
tags.push(buildQuietTag(options.quietDays))
}
const baseDraft = {
kind: ExtendedKind.COMMENT,
@ -425,8 +415,6 @@ export async function createPublicMessageReplyDraftEvent( @@ -425,8 +415,6 @@ export async function createPublicMessageReplyDraftEvent(
isNsfw?: boolean
addExpirationTag?: boolean
expirationMonths?: number
addQuietTag?: boolean
quietDays?: number
mediaImetaTags?: string[][] // Allow media imeta tags for audio/video
} = {}
): Promise<TDraftEvent> {
@ -478,9 +466,6 @@ export async function createPublicMessageReplyDraftEvent( @@ -478,9 +466,6 @@ export async function createPublicMessageReplyDraftEvent(
tags.push(buildExpirationTag(options.expirationMonths))
}
if (options.addQuietTag && options.quietDays) {
tags.push(buildQuietTag(options.quietDays))
}
// console.log('📝 Final public message reply draft tags:', {
// pTags: tags.filter(tag => tag[0] === 'p'),
@ -505,8 +490,6 @@ export async function createPublicMessageDraftEvent( @@ -505,8 +490,6 @@ export async function createPublicMessageDraftEvent(
isNsfw?: boolean
addExpirationTag?: boolean
expirationMonths?: number
addQuietTag?: boolean
quietDays?: number
mediaImetaTags?: string[][] // Allow media imeta tags for audio/video
} = {}
): Promise<TDraftEvent> {
@ -538,9 +521,6 @@ export async function createPublicMessageDraftEvent( @@ -538,9 +521,6 @@ export async function createPublicMessageDraftEvent(
tags.push(buildExpirationTag(options.expirationMonths))
}
if (options.addQuietTag && options.quietDays) {
tags.push(buildQuietTag(options.quietDays))
}
const baseDraft = {
kind: ExtendedKind.PUBLIC_MESSAGE,
@ -1146,16 +1126,12 @@ export async function createPollDraftEvent( @@ -1146,16 +1126,12 @@ export async function createPollDraftEvent(
isNsfw,
addExpirationTag,
expirationMonths,
addQuietTag,
quietDays,
mediaImetaTags
}: {
addClientTag?: boolean // accepted for API compat; client tag is added in publish()
isNsfw?: boolean
addExpirationTag?: boolean
expirationMonths?: number
addQuietTag?: boolean
quietDays?: number
mediaImetaTags?: string[][]
} = {}
): Promise<TDraftEvent> {
@ -1210,9 +1186,6 @@ export async function createPollDraftEvent( @@ -1210,9 +1186,6 @@ export async function createPollDraftEvent(
tags.push(buildExpirationTag(expirationMonths))
}
if (addQuietTag && quietDays) {
tags.push(buildQuietTag(quietDays))
}
const baseDraft = {
content: transformedEmojisContent.trim(),
@ -1658,10 +1631,6 @@ function buildExpirationTag(months: number): string[] { @@ -1658,10 +1631,6 @@ function buildExpirationTag(months: number): string[] {
return ['expiration', expirationTime.toString()]
}
function buildQuietTag(days: number): string[] {
const quietEndTime = dayjs().add(days, 'day').unix()
return ['quiet', quietEndTime.toString()]
}
function trimTagEnd(tag: string[]) {
let endIndex = tag.length - 1
@ -1691,8 +1660,6 @@ export async function createHighlightDraftEvent( @@ -1691,8 +1660,6 @@ export async function createHighlightDraftEvent(
isNsfw?: boolean
addExpirationTag?: boolean
expirationMonths?: number
addQuietTag?: boolean
quietDays?: number
mediaImetaTags?: string[][]
}
): Promise<TDraftEvent> {
@ -1818,9 +1785,6 @@ export async function createHighlightDraftEvent( @@ -1818,9 +1785,6 @@ export async function createHighlightDraftEvent(
tags.push(buildExpirationTag(options.expirationMonths))
}
if (options?.addQuietTag && options?.quietDays) {
tags.push(buildQuietTag(options.quietDays))
}
mergeUploadImetaTagsInto(tags, options?.mediaImetaTags)
@ -1843,8 +1807,6 @@ export async function createVoiceDraftEvent( @@ -1843,8 +1807,6 @@ export async function createVoiceDraftEvent(
isNsfw?: boolean
addExpirationTag?: boolean
expirationMonths?: number
addQuietTag?: boolean
quietDays?: number
/** Extra NIP-94 rows from uploads (merged after content-derived imeta, deduped by URL). */
mediaImetaTags?: string[][]
} = {}
@ -1871,9 +1833,6 @@ export async function createVoiceDraftEvent( @@ -1871,9 +1833,6 @@ export async function createVoiceDraftEvent(
tags.push(buildExpirationTag(options.expirationMonths))
}
if (options.addQuietTag && options.quietDays) {
tags.push(buildQuietTag(options.quietDays))
}
return setDraftEventCache({
kind: ExtendedKind.VOICE,
@ -1894,8 +1853,6 @@ export async function createVoiceCommentDraftEvent( @@ -1894,8 +1853,6 @@ export async function createVoiceCommentDraftEvent(
isNsfw?: boolean
addExpirationTag?: boolean
expirationMonths?: number
addQuietTag?: boolean
quietDays?: number
/** NIP-94 rows from file upload (merged before `imetaTags`; deduped by URL). */
mediaImetaTags?: string[][]
} = {}
@ -1979,9 +1936,6 @@ export async function createVoiceCommentDraftEvent( @@ -1979,9 +1936,6 @@ export async function createVoiceCommentDraftEvent(
tags.push(buildExpirationTag(options.expirationMonths))
}
if (options.addQuietTag && options.quietDays) {
tags.push(buildQuietTag(options.quietDays))
}
return setDraftEventCache({
kind: ExtendedKind.VOICE_COMMENT,
@ -2000,8 +1954,6 @@ export async function createPictureDraftEvent( @@ -2000,8 +1954,6 @@ export async function createPictureDraftEvent(
isNsfw?: boolean
addExpirationTag?: boolean
expirationMonths?: number
addQuietTag?: boolean
quietDays?: number
mediaImetaTags?: string[][]
} = {}
): Promise<TDraftEvent> {
@ -2026,9 +1978,6 @@ export async function createPictureDraftEvent( @@ -2026,9 +1978,6 @@ export async function createPictureDraftEvent(
tags.push(buildExpirationTag(options.expirationMonths))
}
if (options.addQuietTag && options.quietDays) {
tags.push(buildQuietTag(options.quietDays))
}
// Kind 20 caption is user text only; the file URL lives in `imeta`. Many indexers and caches
// still deliver full tags, but mirroring the URL in `content` matches kind-1-style clients and
@ -2060,8 +2009,6 @@ export async function createVideoDraftEvent( @@ -2060,8 +2009,6 @@ export async function createVideoDraftEvent(
isNsfw?: boolean
addExpirationTag?: boolean
expirationMonths?: number
addQuietTag?: boolean
quietDays?: number
mediaImetaTags?: string[][]
} = {}
): Promise<TDraftEvent> {
@ -2086,9 +2033,6 @@ export async function createVideoDraftEvent( @@ -2086,9 +2033,6 @@ export async function createVideoDraftEvent(
tags.push(buildExpirationTag(options.expirationMonths))
}
if (options.addQuietTag && options.quietDays) {
tags.push(buildQuietTag(options.quietDays))
}
return setDraftEventCache({
kind: videoKind, // NIP-71: 21, 22, or 34235
@ -2113,8 +2057,6 @@ export async function createLongFormArticleDraftEvent( @@ -2113,8 +2057,6 @@ export async function createLongFormArticleDraftEvent(
isNsfw?: boolean
addExpirationTag?: boolean
expirationMonths?: number
addQuietTag?: boolean
quietDays?: number
} = {}
): Promise<TDraftEvent> {
const { content: transformedEmojisContent, emojiTags } = transformCustomEmojisInContent(content)
@ -2161,9 +2103,6 @@ export async function createLongFormArticleDraftEvent( @@ -2161,9 +2103,6 @@ export async function createLongFormArticleDraftEvent(
tags.push(buildExpirationTag(options.expirationMonths))
}
if (options.addQuietTag && options.quietDays) {
tags.push(buildQuietTag(options.quietDays))
}
return setDraftEventCache({
kind: kinds.LongFormArticle,
@ -2194,8 +2133,6 @@ export async function createWikiArticleDraftEvent( @@ -2194,8 +2133,6 @@ export async function createWikiArticleDraftEvent(
isNsfw?: boolean
addExpirationTag?: boolean
expirationMonths?: number
addQuietTag?: boolean
quietDays?: number
}
): Promise<TDraftEvent> {
const { content: transformedEmojisContent, emojiTags } = transformCustomEmojisInContent(content)
@ -2231,9 +2168,6 @@ export async function createWikiArticleDraftEvent( @@ -2231,9 +2168,6 @@ export async function createWikiArticleDraftEvent(
tags.push(buildExpirationTag(options.expirationMonths))
}
if (options.addQuietTag && options.quietDays) {
tags.push(buildQuietTag(options.quietDays))
}
return setDraftEventCache({
kind: ExtendedKind.WIKI_ARTICLE,
@ -2256,8 +2190,6 @@ export async function createNostrSpecificationDraftEvent( @@ -2256,8 +2190,6 @@ export async function createNostrSpecificationDraftEvent(
isNsfw?: boolean
addExpirationTag?: boolean
expirationMonths?: number
addQuietTag?: boolean
quietDays?: number
}
): Promise<TDraftEvent> {
const { content: transformedEmojisContent, emojiTags } = transformCustomEmojisInContent(content)
@ -2295,9 +2227,6 @@ export async function createNostrSpecificationDraftEvent( @@ -2295,9 +2227,6 @@ export async function createNostrSpecificationDraftEvent(
tags.push(buildExpirationTag(options.expirationMonths))
}
if (options.addQuietTag && options.quietDays) {
tags.push(buildQuietTag(options.quietDays))
}
return setDraftEventCache({
kind: ExtendedKind.NOSTR_SPECIFICATION,
@ -2319,8 +2248,6 @@ export async function createPublicationContentDraftEvent( @@ -2319,8 +2248,6 @@ export async function createPublicationContentDraftEvent(
isNsfw?: boolean
addExpirationTag?: boolean
expirationMonths?: number
addQuietTag?: boolean
quietDays?: number
}
): Promise<TDraftEvent> {
const { content: transformedEmojisContent, emojiTags } = transformCustomEmojisInContent(content)
@ -2356,9 +2283,6 @@ export async function createPublicationContentDraftEvent( @@ -2356,9 +2283,6 @@ export async function createPublicationContentDraftEvent(
tags.push(buildExpirationTag(options.expirationMonths))
}
if (options.addQuietTag && options.quietDays) {
tags.push(buildQuietTag(options.quietDays))
}
return setDraftEventCache({
kind: ExtendedKind.PUBLICATION_CONTENT,

36
src/lib/event-filtering.ts

@ -1,6 +1,5 @@ @@ -1,6 +1,5 @@
import { Event } from 'nostr-tools'
import dayjs from 'dayjs'
import storage from '@/services/local-storage.service'
/**
* Check if an event has expired based on its expiration tag
@ -19,41 +18,6 @@ function isEventExpired(event: Event): boolean { @@ -19,41 +18,6 @@ function isEventExpired(event: Event): boolean {
return dayjs().unix() > expirationTime
}
/**
* Check if an event is in quiet mode based on its quiet tag
*/
function isEventInQuietMode(event: Event): boolean {
const quietTag = event.tags.find(tag => tag[0] === 'quiet')
if (!quietTag || !quietTag[1]) {
return false
}
const quietEndTime = parseInt(quietTag[1])
if (isNaN(quietEndTime)) {
return false
}
return dayjs().unix() < quietEndTime
}
/**
* Check if interactions should be hidden for an event based on quiet settings
*/
export function shouldHideInteractions(event: Event): boolean {
// Check global quiet mode first
if (storage.getGlobalQuietMode()) {
return true
}
// Check if we should respect quiet tags
if (!storage.getRespectQuietTags()) {
return false
}
// Check if the event is in quiet mode
return isEventInQuietMode(event)
}
/**
* Check if an event should be filtered out completely (expired)
*/

10
src/lib/note-stats-interactors.ts

@ -3,16 +3,6 @@ import { TEmoji } from '@/types' @@ -3,16 +3,6 @@ import { TEmoji } from '@/types'
export const MAX_NOTE_STATS_INTERACTORS_SHOWN = 32
export function filterStatsInteractors<T extends { pubkey: string }>(
items: T[] | undefined,
hideUntrusted: boolean,
isUserTrusted: (pk: string) => boolean
): T[] {
if (!items?.length) return []
if (!hideUntrusted) return items
return items.filter((item) => isUserTrusted(item.pubkey))
}
export function emojiStatsKey(emoji: TEmoji | string): string {
return typeof emoji === 'string' ? emoji : emoji.shortcode
}

1
src/main.tsx

@ -1,6 +1,7 @@ @@ -1,6 +1,7 @@
import './index.css'
import './polyfill'
import './lib/error-suppression'
import './lib/console-log-buffer'
import storage from './services/local-storage.service'
import './services/lightning.service'
import './lib/debug-utils'

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

@ -1,4 +1,3 @@ @@ -1,4 +1,3 @@
import HideUntrustedContentButton from '@/components/HideUntrustedContentButton'
import NoteList, { type TNoteListRef } from '@/components/NoteList'
import StoredAccountSwitchSelect from '@/components/StoredAccountSwitchSelect'
import { RefreshButton } from '@/components/RefreshButton'
@ -29,7 +28,6 @@ import { useBookmarks } from '@/providers/bookmarks-context' @@ -29,7 +28,6 @@ import { useBookmarks } from '@/providers/bookmarks-context'
import { useNostr } from '@/providers/NostrProvider'
import { useNotificationThreadWatchOptional } from '@/providers/NotificationThreadWatchProvider'
import { useScreenSize } from '@/providers/ScreenSizeProvider'
import { useUserTrust } from '@/contexts/user-trust-context'
import { dedupeFollowSetEventsByD } from '@/lib/follow-set-spell'
import client, { queryService } from '@/services/client.service'
import indexedDb from '@/services/indexed-db.service'
@ -88,7 +86,6 @@ const SpellsPage = forwardRef<TPageRef>(function SpellsPage( @@ -88,7 +86,6 @@ const SpellsPage = forwardRef<TPageRef>(function SpellsPage(
followListEvent
} = useNostr()
const { addBookmark, removeBookmark } = useBookmarks()
const { hideUntrustedNotifications } = useUserTrust()
const notificationThreadWatch = useNotificationThreadWatchOptional()
const eventsIFollowListEvent = notificationThreadWatch?.eventsIFollowListEvent ?? null
const eventsIMutedListEvent = notificationThreadWatch?.eventsIMutedListEvent ?? null
@ -1054,7 +1051,6 @@ const SpellsPage = forwardRef<TPageRef>(function SpellsPage( @@ -1054,7 +1051,6 @@ const SpellsPage = forwardRef<TPageRef>(function SpellsPage(
{notificationsFeedPubkey ? (
<StoredAccountSwitchSelect className="min-w-0 flex-1 sm:max-w-[min(100%,20rem)]" />
) : null}
<HideUntrustedContentButton type="notifications" size="titlebar-icon" />
</div>
) : null}
<div className="min-h-0 min-w-0 flex-1">
@ -1105,9 +1101,6 @@ const SpellsPage = forwardRef<TPageRef>(function SpellsPage( @@ -1105,9 +1101,6 @@ const SpellsPage = forwardRef<TPageRef>(function SpellsPage(
? notificationsMentionExtraHide
: undefined
}
hideUntrustedNotes={
selectedFauxSpell === 'notifications' ? hideUntrustedNotifications : false
}
showPaymentAttestationAction={selectedFauxSpell === 'notifications'}
/>
</div>

29
src/pages/secondary/GeneralSettingsPage/index.tsx

@ -17,10 +17,8 @@ import { useContentPolicy } from '@/providers/ContentPolicyProvider' @@ -17,10 +17,8 @@ import { useContentPolicy } from '@/providers/ContentPolicyProvider'
import { useFontSize } from '@/providers/FontSizeProvider'
import { useTheme } from '@/providers/ThemeProvider'
import { useUserPreferences } from '@/providers/UserPreferencesProvider'
import { useUserTrust } from '@/contexts/user-trust-context'
import { TMediaAutoLoadPolicy } from '@/types'
import { SelectValue } from '@radix-ui/react-select'
import { ExternalLink } from 'lucide-react'
import { forwardRef, HTMLProps, useCallback, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
@ -42,7 +40,6 @@ const GeneralSettingsPage = forwardRef(({ index, hideTitlebar = false }: { index @@ -42,7 +40,6 @@ const GeneralSettingsPage = forwardRef(({ index, hideTitlebar = false }: { index
mediaAutoLoadPolicy,
setMediaAutoLoadPolicy
} = useContentPolicy()
const { hideUntrustedNotes, updateHideUntrustedNotes } = useUserTrust()
const {
notificationListStyle,
updateNotificationListStyle,
@ -202,16 +199,6 @@ const GeneralSettingsPage = forwardRef(({ index, hideTitlebar = false }: { index @@ -202,16 +199,6 @@ const GeneralSettingsPage = forwardRef(({ index, hideTitlebar = false }: { index
onCheckedChange={updateAddRandomRelaysToPublish}
/>
</SettingItem>
<SettingItem>
<Label htmlFor="hide-untrusted-notes" className="text-base font-normal">
{t('Hide untrusted notes')}
</Label>
<Switch
id="hide-untrusted-notes"
checked={hideUntrustedNotes}
onCheckedChange={updateHideUntrustedNotes}
/>
</SettingItem>
<SettingItem>
<Label htmlFor="hide-content-mentioning-muted-users" className="text-base font-normal">
{t('Hide content mentioning muted users')}
@ -229,22 +216,6 @@ const GeneralSettingsPage = forwardRef(({ index, hideTitlebar = false }: { index @@ -229,22 +216,6 @@ const GeneralSettingsPage = forwardRef(({ index, hideTitlebar = false }: { index
<Switch id="show-nsfw" checked={defaultShowNsfw} onCheckedChange={setDefaultShowNsfw} />
</SettingItem>
{/* DEPRECATED: Double-panel setting removed for technical debt reduction */}
<SettingItem>
<div>
<a
className="flex items-center gap-1 cursor-pointer hover:underline"
href="https://emojito.meme/browse"
target="_blank"
rel="noopener noreferrer"
>
{t('Custom emoji management')}
<ExternalLink />
</a>
<div className="text-muted-foreground">
{t('After changing emojis, you may need to refresh the page')}
</div>
</div>
</SettingItem>
</div>
</SecondaryPageLayout>
)

108
src/pages/secondary/PostSettingsPage/QuietSettings.tsx

@ -1,108 +0,0 @@ @@ -1,108 +0,0 @@
import { Label } from '@/components/ui/label'
import { Switch } from '@/components/ui/switch'
import { Input } from '@/components/ui/input'
import storage from '@/services/local-storage.service'
import { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
export default function QuietSettings() {
const { t } = useTranslation()
const [enabled, setEnabled] = useState(false)
const [days, setDays] = useState(7)
const [respectQuietTags, setRespectQuietTags] = useState(true)
const [globalQuietMode, setGlobalQuietMode] = useState(false)
useEffect(() => {
setEnabled(storage.getDefaultQuietEnabled())
setDays(storage.getDefaultQuietDays())
setRespectQuietTags(storage.getRespectQuietTags())
setGlobalQuietMode(storage.getGlobalQuietMode())
}, [])
const handleEnabledChange = (checked: boolean) => {
setEnabled(checked)
storage.setDefaultQuietEnabled(checked)
}
const handleDaysChange = (value: string) => {
const num = parseInt(value)
if (!isNaN(num) && num >= 0 && Number.isInteger(num)) {
setDays(num)
storage.setDefaultQuietDays(num)
}
}
const handleRespectQuietTagsChange = (checked: boolean) => {
setRespectQuietTags(checked)
storage.setRespectQuietTags(checked)
}
const handleGlobalQuietModeChange = (checked: boolean) => {
setGlobalQuietMode(checked)
storage.setGlobalQuietMode(checked)
}
return (
<div className="space-y-4">
<div className="space-y-2">
<div className="flex items-center space-x-2">
<Label htmlFor="quiet-enabled">{t('Add quiet tags by default')}</Label>
<Switch
id="quiet-enabled"
checked={enabled}
onCheckedChange={handleEnabledChange}
/>
</div>
<div className="text-muted-foreground text-xs">
{t('Posts will automatically include quiet tags')}
</div>
</div>
{enabled && (
<div className="space-y-2">
<Label htmlFor="quiet-days">{t('Default quiet period (days)')}</Label>
<Input
id="quiet-days"
type="number"
min="0"
step="1"
value={days}
onChange={(e) => handleDaysChange(e.target.value)}
className="w-24"
/>
<div className="text-muted-foreground text-xs">
{t('Posts will be quiet for this many days')}
</div>
</div>
)}
<div className="space-y-2">
<div className="flex items-center space-x-2">
<Label htmlFor="respect-quiet-tags">{t('Respect quiet tags')}</Label>
<Switch
id="respect-quiet-tags"
checked={respectQuietTags}
onCheckedChange={handleRespectQuietTagsChange}
/>
</div>
<div className="text-muted-foreground text-xs">
{t('Hide interactions on posts with quiet tags')}
</div>
</div>
<div className="space-y-2">
<div className="flex items-center space-x-2">
<Label htmlFor="global-quiet-mode">{t('Global quiet mode')}</Label>
<Switch
id="global-quiet-mode"
checked={globalQuietMode}
onCheckedChange={handleGlobalQuietModeChange}
/>
</div>
<div className="text-muted-foreground text-xs">
{t('Hide interactions on all posts')}
</div>
</div>
</div>
)
}

5
src/pages/secondary/PostSettingsPage/index.tsx

@ -5,7 +5,6 @@ import { forwardRef, useCallback, useEffect, useState } from 'react' @@ -5,7 +5,6 @@ import { forwardRef, useCallback, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import MediaUploadServiceSetting from './MediaUploadServiceSetting'
import ExpirationSettings from './ExpirationSettings'
import QuietSettings from './QuietSettings'
import PublishSuccessToastSetting from './PublishSuccessToastSetting'
const PostSettingsPage = forwardRef(({ index, hideTitlebar = false }: { index?: number; hideTitlebar?: boolean }, ref) => {
@ -40,10 +39,6 @@ const PostSettingsPage = forwardRef(({ index, hideTitlebar = false }: { index?: @@ -40,10 +39,6 @@ const PostSettingsPage = forwardRef(({ index, hideTitlebar = false }: { index?:
<h3 className="text-lg font-medium">{t('Expiration Tags')}</h3>
<ExpirationSettings />
</div>
<div className="space-y-4">
<h3 className="text-lg font-medium">{t('Quiet Tags')}</h3>
<QuietSettings />
</div>
</div>
</SecondaryPageLayout>
)

100
src/providers/UserTrustProvider.tsx

@ -1,100 +0,0 @@ @@ -1,100 +0,0 @@
import { getPubkeysFromPTags } from '@/lib/tag'
import storage from '@/services/local-storage.service'
import { replaceableEventService } from '@/services/client.service'
import { UserTrustContext } from '@/contexts/user-trust-context'
import { kinds } from 'nostr-tools'
import { type ReactNode, useCallback, useEffect, useState } from 'react'
import { useNostr } from './NostrProvider'
const wotSet = new Set<string>()
export function UserTrustProvider({ children }: { children: ReactNode }) {
const { pubkey: currentPubkey } = useNostr()
const [isTrustLoaded, setIsTrustLoaded] = useState(false)
const [hideUntrustedInteractions, setHideUntrustedInteractions] = useState(() =>
storage.getHideUntrustedInteractions()
)
const [hideUntrustedNotifications, setHideUntrustedNotifications] = useState(() =>
storage.getHideUntrustedNotifications()
)
const [hideUntrustedNotes, setHideUntrustedNotes] = useState(() =>
storage.getHideUntrustedNotes()
)
useEffect(() => {
if (!currentPubkey) {
setIsTrustLoaded(false)
return
}
// Clear wotSet when account changes to avoid cross-account contamination
wotSet.clear()
setIsTrustLoaded(false)
const initWoT = async () => {
try {
const followListEvent = await replaceableEventService.fetchReplaceableEvent(currentPubkey, kinds.Contacts)
const followings = followListEvent ? getPubkeysFromPTags(followListEvent.tags) : []
followings.forEach((pubkey) => wotSet.add(pubkey.toLowerCase()))
const batchSize = 20
for (let i = 0; i < followings.length; i += batchSize) {
const batch = followings.slice(i, i + batchSize)
await Promise.allSettled(
batch.map(async (pubkey) => {
const innerFollow = await replaceableEventService.fetchReplaceableEvent(pubkey, kinds.Contacts)
const _followings = innerFollow ? getPubkeysFromPTags(innerFollow.tags) : []
_followings.forEach((following) => {
wotSet.add(following.toLowerCase())
})
})
)
await new Promise((resolve) => setTimeout(resolve, 200))
}
} finally {
setIsTrustLoaded(true)
}
}
void initWoT()
}, [currentPubkey])
const isUserTrusted = useCallback(
(pubkey: string) => {
if (!currentPubkey || pubkey.toLowerCase() === currentPubkey.toLowerCase()) return true
return wotSet.has(pubkey.toLowerCase())
},
[currentPubkey]
)
const updateHideUntrustedInteractions = (hide: boolean) => {
setHideUntrustedInteractions(hide)
storage.setHideUntrustedInteractions(hide)
}
const updateHideUntrustedNotifications = (hide: boolean) => {
setHideUntrustedNotifications(hide)
storage.setHideUntrustedNotifications(hide)
}
const updateHideUntrustedNotes = (hide: boolean) => {
setHideUntrustedNotes(hide)
storage.setHideUntrustedNotes(hide)
}
return (
<UserTrustContext.Provider
value={{
isTrustLoaded,
hideUntrustedInteractions,
hideUntrustedNotifications,
hideUntrustedNotes,
updateHideUntrustedInteractions,
updateHideUntrustedNotifications,
updateHideUntrustedNotes,
isUserTrusted
}}
>
{children}
</UserTrustContext.Provider>
)
}

138
src/services/local-storage.service.ts

@ -59,9 +59,6 @@ const SETTINGS_KEYS = [ @@ -59,9 +59,6 @@ const SETTINGS_KEYS = [
StorageKey.QUICK_ZAP,
StorageKey.INCLUDE_PUBLIC_ZAP_RECEIPT,
StorageKey.AUTOPLAY,
StorageKey.HIDE_UNTRUSTED_INTERACTIONS,
StorageKey.HIDE_UNTRUSTED_NOTIFICATIONS,
StorageKey.HIDE_UNTRUSTED_NOTES,
StorageKey.MEDIA_UPLOAD_SERVICE_CONFIG_MAP,
StorageKey.DEFAULT_SHOW_NSFW,
StorageKey.DISMISSED_TOO_MANY_RELAYS_ALERT,
@ -81,10 +78,6 @@ const SETTINGS_KEYS = [ @@ -81,10 +78,6 @@ const SETTINGS_KEYS = [
StorageKey.SHOW_LIVE_ACTIVITIES_BANNER,
StorageKey.DEFAULT_EXPIRATION_ENABLED,
StorageKey.DEFAULT_EXPIRATION_MONTHS,
StorageKey.DEFAULT_QUIET_ENABLED,
StorageKey.DEFAULT_QUIET_DAYS,
StorageKey.RESPECT_QUIET_TAGS,
StorageKey.GLOBAL_QUIET_MODE,
StorageKey.SHOW_RSS_FEED,
StorageKey.PANE_MODE,
StorageKey.RESTRICT_RELAYS_TO_METADATA_LISTS
@ -108,9 +101,6 @@ class LocalStorageService { @@ -108,9 +101,6 @@ class LocalStorageService {
private includePublicZapReceipt: boolean = true
private mediaUploadService: string = DEFAULT_NIP_96_SERVICE
private autoplay: boolean = true
private hideUntrustedInteractions: boolean = false
private hideUntrustedNotifications: boolean = false
private hideUntrustedNotes: boolean = false
private mediaUploadServiceConfigMap: Record<string, TMediaUploadServiceConfig> = {}
private defaultShowNsfw: boolean = false
private dismissedTooManyRelaysAlert: boolean = false
@ -127,10 +117,6 @@ class LocalStorageService { @@ -127,10 +117,6 @@ class LocalStorageService {
private shownCreateWalletGuideToastPubkeys: Set<string> = new Set()
private defaultExpirationEnabled: boolean = false
private defaultExpirationMonths: number = 6
private defaultQuietEnabled: boolean = false
private defaultQuietDays: number = 7
private respectQuietTags: boolean = true
private globalQuietMode: boolean = false
private showRssFeed: boolean = true
private panelMode: 'single' | 'double' = 'single'
private addRandomRelaysToPublish: boolean = false
@ -219,25 +205,6 @@ class LocalStorageService { @@ -219,25 +205,6 @@ class LocalStorageService {
this.autoplay = window.localStorage.getItem(StorageKey.AUTOPLAY) !== 'false'
const hideUntrustedEvents =
window.localStorage.getItem(StorageKey.HIDE_UNTRUSTED_EVENTS) === 'true'
const storedHideUntrustedInteractions = window.localStorage.getItem(
StorageKey.HIDE_UNTRUSTED_INTERACTIONS
)
const storedHideUntrustedNotifications = window.localStorage.getItem(
StorageKey.HIDE_UNTRUSTED_NOTIFICATIONS
)
const storedHideUntrustedNotes = window.localStorage.getItem(StorageKey.HIDE_UNTRUSTED_NOTES)
this.hideUntrustedInteractions = storedHideUntrustedInteractions
? storedHideUntrustedInteractions === 'true'
: hideUntrustedEvents
this.hideUntrustedNotifications = storedHideUntrustedNotifications
? storedHideUntrustedNotifications === 'true'
: hideUntrustedEvents
this.hideUntrustedNotes = storedHideUntrustedNotes
? storedHideUntrustedNotes === 'true'
: hideUntrustedEvents
const mediaUploadServiceConfigMapStr = window.localStorage.getItem(
StorageKey.MEDIA_UPLOAD_SERVICE_CONFIG_MAP
)
@ -419,7 +386,7 @@ class LocalStorageService { @@ -419,7 +386,7 @@ class LocalStorageService {
? new Set(JSON.parse(shownCreateWalletGuideToastPubkeysStr))
: new Set()
// Initialize expiration and quiet settings
// Initialize expiration settings
const defaultExpirationEnabledStr = window.localStorage.getItem(StorageKey.DEFAULT_EXPIRATION_ENABLED)
this.defaultExpirationEnabled = defaultExpirationEnabledStr === 'true'
@ -431,23 +398,6 @@ class LocalStorageService { @@ -431,23 +398,6 @@ class LocalStorageService {
}
}
const defaultQuietEnabledStr = window.localStorage.getItem(StorageKey.DEFAULT_QUIET_ENABLED)
this.defaultQuietEnabled = defaultQuietEnabledStr === 'true'
const defaultQuietDaysStr = window.localStorage.getItem(StorageKey.DEFAULT_QUIET_DAYS)
if (defaultQuietDaysStr) {
const num = parseInt(defaultQuietDaysStr)
if (!isNaN(num) && num >= 0 && Number.isInteger(num)) {
this.defaultQuietDays = num
}
}
const respectQuietTagsStr = window.localStorage.getItem(StorageKey.RESPECT_QUIET_TAGS)
this.respectQuietTags = respectQuietTagsStr === null ? true : respectQuietTagsStr === 'true'
const globalQuietModeStr = window.localStorage.getItem(StorageKey.GLOBAL_QUIET_MODE)
this.globalQuietMode = globalQuietModeStr === 'true'
const showRssFeedStr = window.localStorage.getItem(StorageKey.SHOW_RSS_FEED)
this.showRssFeed = showRssFeedStr === null ? true : showRssFeedStr === 'true' // Default to true
@ -617,12 +567,6 @@ class LocalStorageService { @@ -617,12 +567,6 @@ class LocalStorageService {
const includeReceiptStr = get(StorageKey.INCLUDE_PUBLIC_ZAP_RECEIPT)
if (includeReceiptStr != null) this.includePublicZapReceipt = includeReceiptStr !== 'false'
this.autoplay = get(StorageKey.AUTOPLAY) !== 'false'
const hideInteractions = get(StorageKey.HIDE_UNTRUSTED_INTERACTIONS)
if (hideInteractions != null) this.hideUntrustedInteractions = hideInteractions === 'true'
const hideNotifications = get(StorageKey.HIDE_UNTRUSTED_NOTIFICATIONS)
if (hideNotifications != null) this.hideUntrustedNotifications = hideNotifications === 'true'
const hideNotes = get(StorageKey.HIDE_UNTRUSTED_NOTES)
if (hideNotes != null) this.hideUntrustedNotes = hideNotes === 'true'
const mediaConfigStr = get(StorageKey.MEDIA_UPLOAD_SERVICE_CONFIG_MAP)
if (mediaConfigStr != null) this.mediaUploadServiceConfigMap = JSON.parse(mediaConfigStr) as Record<string, TMediaUploadServiceConfig>
this.defaultShowNsfw = get(StorageKey.DEFAULT_SHOW_NSFW) === 'true'
@ -658,15 +602,6 @@ class LocalStorageService { @@ -658,15 +602,6 @@ class LocalStorageService {
const num = parseInt(defaultExpirationMonthsStr)
if (!isNaN(num) && num >= 0) this.defaultExpirationMonths = num
}
this.defaultQuietEnabled = get(StorageKey.DEFAULT_QUIET_ENABLED) === 'true'
const defaultQuietDaysStr = get(StorageKey.DEFAULT_QUIET_DAYS)
if (defaultQuietDaysStr != null) {
const num = parseInt(defaultQuietDaysStr)
if (!isNaN(num) && num >= 0) this.defaultQuietDays = num
}
const respectQuietStr = get(StorageKey.RESPECT_QUIET_TAGS)
if (respectQuietStr != null) this.respectQuietTags = respectQuietStr === 'true'
this.globalQuietMode = get(StorageKey.GLOBAL_QUIET_MODE) === 'true'
const showRssStr = get(StorageKey.SHOW_RSS_FEED)
if (showRssStr != null) this.showRssFeed = showRssStr === 'true'
const paneStr = get(StorageKey.PANE_MODE)
@ -866,38 +801,6 @@ class LocalStorageService { @@ -866,38 +801,6 @@ class LocalStorageService {
this.persistSetting(StorageKey.AUTOPLAY, autoplay.toString())
}
getHideUntrustedInteractions() {
return this.hideUntrustedInteractions
}
setHideUntrustedInteractions(hideUntrustedInteractions: boolean) {
this.hideUntrustedInteractions = hideUntrustedInteractions
this.persistSetting(
StorageKey.HIDE_UNTRUSTED_INTERACTIONS,
hideUntrustedInteractions.toString()
)
}
getHideUntrustedNotifications() {
return this.hideUntrustedNotifications
}
setHideUntrustedNotifications(hideUntrustedNotifications: boolean) {
this.hideUntrustedNotifications = hideUntrustedNotifications
this.persistSetting(
StorageKey.HIDE_UNTRUSTED_NOTIFICATIONS,
hideUntrustedNotifications.toString()
)
}
getHideUntrustedNotes() {
return this.hideUntrustedNotes
}
setHideUntrustedNotes(hideUntrustedNotes: boolean) {
this.hideUntrustedNotes = hideUntrustedNotes
this.persistSetting(StorageKey.HIDE_UNTRUSTED_NOTES, hideUntrustedNotes.toString())
}
getMediaUploadServiceConfig(pubkey?: string | null): TMediaUploadServiceConfig {
const defaultConfig = { type: 'nip96', service: this.mediaUploadService } as const
@ -1083,45 +986,6 @@ class LocalStorageService { @@ -1083,45 +986,6 @@ class LocalStorageService {
}
}
// Quiet settings
getDefaultQuietEnabled() {
return this.defaultQuietEnabled
}
setDefaultQuietEnabled(enabled: boolean) {
this.defaultQuietEnabled = enabled
this.persistSetting(StorageKey.DEFAULT_QUIET_ENABLED, enabled.toString())
}
getDefaultQuietDays() {
return this.defaultQuietDays
}
setDefaultQuietDays(days: number) {
if (Number.isInteger(days) && days >= 0) {
this.defaultQuietDays = days
this.persistSetting(StorageKey.DEFAULT_QUIET_DAYS, days.toString())
}
}
getRespectQuietTags() {
return this.respectQuietTags
}
setRespectQuietTags(respect: boolean) {
this.respectQuietTags = respect
this.persistSetting(StorageKey.RESPECT_QUIET_TAGS, respect.toString())
}
getGlobalQuietMode() {
return this.globalQuietMode
}
setGlobalQuietMode(enabled: boolean) {
this.globalQuietMode = enabled
this.persistSetting(StorageKey.GLOBAL_QUIET_MODE, enabled.toString())
}
getShowRssFeed() {
return this.showRssFeed
}

Loading…
Cancel
Save