Browse Source

bug-fixes

imwald
Silberengel 1 month ago
parent
commit
8332a4616d
  1. 51
      src/components/KindFilter/index.tsx
  2. 34
      src/components/MemePicker/index.tsx
  3. 6
      src/components/NormalFeed/index.tsx
  4. 100
      src/components/NoteList/index.tsx
  5. 2
      src/constants.ts
  6. 5
      src/i18n/locales/de.ts
  7. 7
      src/i18n/locales/en.ts
  8. 28
      src/providers/KindFilterProvider.tsx
  9. 17
      src/services/local-storage.service.ts
  10. 65
      src/services/meme.service.ts

51
src/components/KindFilter/index.tsx

@ -1,5 +1,6 @@ @@ -1,5 +1,6 @@
import { Button } from '@/components/ui/button'
import { Checkbox } from '@/components/ui/checkbox'
import { Switch } from '@/components/ui/switch'
import { Drawer, DrawerContent, DrawerHeader, DrawerTitle, DrawerTrigger } from '@/components/ui/drawer'
import { Label } from '@/components/ui/label'
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
@ -57,13 +58,16 @@ export default function KindFilter({ @@ -57,13 +58,16 @@ export default function KindFilter({
showKind1OPs: savedShowKind1OPs,
showKind1Replies: savedShowKind1Replies,
showKind1111: savedShowKind1111,
updateShowKinds
feedKindFilterBypass,
updateShowKinds,
updateFeedKindFilterBypass
} = useKindFilter()
const [open, setOpen] = useState(false)
const [temporaryShowKinds, setTemporaryShowKinds] = useState(showKinds)
const [temporaryShowKind1OPs, setTemporaryShowKind1OPs] = useState(savedShowKind1OPs)
const [temporaryShowKind1Replies, setTemporaryShowKind1Replies] = useState(savedShowKind1Replies)
const [temporaryShowKind1111, setTemporaryShowKind1111] = useState(savedShowKind1111)
const [temporarySeeAllEvents, setTemporarySeeAllEvents] = useState(feedKindFilterBypass)
const [isPersistent, setIsPersistent] = useState(true)
const isDifferentFromSaved = useMemo(
() => !isSameKindFilter(showKinds, savedShowKinds),
@ -74,16 +78,19 @@ export default function KindFilter({ @@ -74,16 +78,19 @@ export default function KindFilter({
!isSameKindFilter(temporaryShowKinds, savedShowKinds) ||
temporaryShowKind1OPs !== savedShowKind1OPs ||
temporaryShowKind1Replies !== savedShowKind1Replies ||
temporaryShowKind1111 !== savedShowKind1111,
temporaryShowKind1111 !== savedShowKind1111 ||
temporarySeeAllEvents !== feedKindFilterBypass,
[
temporaryShowKinds,
savedShowKinds,
temporaryShowKind1OPs,
temporaryShowKind1Replies,
temporaryShowKind1111,
temporarySeeAllEvents,
savedShowKind1OPs,
savedShowKind1Replies,
savedShowKind1111
savedShowKind1111,
feedKindFilterBypass
]
)
@ -93,9 +100,17 @@ export default function KindFilter({ @@ -93,9 +100,17 @@ export default function KindFilter({
setTemporaryShowKind1OPs(savedShowKind1OPs)
setTemporaryShowKind1Replies(savedShowKind1Replies)
setTemporaryShowKind1111(savedShowKind1111)
setTemporarySeeAllEvents(feedKindFilterBypass)
setIsPersistent(true)
}
}, [open, showKinds, savedShowKind1OPs, savedShowKind1Replies, savedShowKind1111])
}, [
open,
showKinds,
savedShowKind1OPs,
savedShowKind1Replies,
savedShowKind1111,
feedKindFilterBypass
])
const appliedShowKinds = useMemo(
() =>
@ -107,11 +122,19 @@ export default function KindFilter({ @@ -107,11 +122,19 @@ export default function KindFilter({
),
[temporaryShowKinds, temporaryShowKind1OPs, temporaryShowKind1Replies, temporaryShowKind1111]
)
const canApply = appliedShowKinds.length > 0
const canApply = temporarySeeAllEvents || appliedShowKinds.length > 0
const handleApply = () => {
if (!canApply) return
updateFeedKindFilterBypass(temporarySeeAllEvents, { persist: isPersistent })
if (temporarySeeAllEvents) {
setOpen(false)
onShowKindsChange(showKinds)
return
}
const newShowKinds = appliedShowKinds
if (!isSameKindFilter(newShowKinds, showKinds)) {
onShowKindsChange(newShowKinds)
@ -131,7 +154,8 @@ export default function KindFilter({ @@ -131,7 +154,8 @@ export default function KindFilter({
size="titlebar-icon"
className={cn(
'relative w-fit px-2 h-8 text-xs focus:text-foreground',
!isDifferentFromSaved && 'text-muted-foreground'
!isDifferentFromSaved && !feedKindFilterBypass && 'text-muted-foreground',
feedKindFilterBypass && 'text-amber-600 dark:text-amber-400'
)}
onClick={() => {
if (isSmallScreen) {
@ -149,7 +173,19 @@ export default function KindFilter({ @@ -149,7 +173,19 @@ export default function KindFilter({
const content = (
<div>
<div className="grid grid-cols-2 gap-2">
<div className="flex items-center justify-between gap-3 rounded-lg border px-3 py-2.5 mb-3">
<span className="text-sm shrink-0">{t('Use filter')}</span>
<Switch
checked={temporarySeeAllEvents}
onCheckedChange={setTemporarySeeAllEvents}
aria-label={temporarySeeAllEvents ? t('See all events') : t('Use filter')}
/>
<span className="text-sm shrink-0 text-right">{t('See all events')}</span>
</div>
<p className="text-xs text-muted-foreground mb-3">
{temporarySeeAllEvents ? t('See all events hint') : t('Use filter hint')}
</p>
<div className={cn('grid grid-cols-2 gap-2', temporarySeeAllEvents && 'opacity-50')}>
{/* Posts (OPs) - kind 1 top-level only */}
<div
className={cn(
@ -239,6 +275,7 @@ export default function KindFilter({ @@ -239,6 +275,7 @@ export default function KindFilter({
setTemporaryShowKind1OPs(savedShowKind1OPs)
setTemporaryShowKind1Replies(savedShowKind1Replies)
setTemporaryShowKind1111(savedShowKind1111)
setTemporarySeeAllEvents(feedKindFilterBypass)
}}
disabled={!isTemporaryDifferentFromSaved}
>

34
src/components/MemePicker/index.tsx

@ -13,11 +13,18 @@ import { useScreenSize } from '@/providers/ScreenSizeProvider' @@ -13,11 +13,18 @@ import { useScreenSize } from '@/providers/ScreenSizeProvider'
import { useNostr } from '@/providers/NostrProvider'
import { ExtendedKind, GIF_RELAY_URLS } from '@/constants'
import { normalizeUrl } from '@/lib/url'
import { fetchMemes, searchMemes, type MemeMetadata } from '@/services/meme.service'
import {
fetchMemes,
mergeMemesIntoIdbCache,
memeMetadataFrom1063Event,
searchMemes,
type MemeMetadata
} from '@/services/meme.service'
import mediaUpload from '@/services/media-upload.service'
import { ExternalLink, X } from 'lucide-react'
import { useCallback, useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { toast } from 'sonner'
const MEMEAMIGO_URL = 'https://www.memeamigo.lol/'
const MEMEAMIGO_SEARCH_URL = (q: string) =>
@ -154,10 +161,18 @@ export default function MemePicker({ @@ -154,10 +161,18 @@ export default function MemePicker({
seen.add(n)
return true
})
await publish(draft, { specifiedRelayUrls })
const published = await publish(draft, { specifiedRelayUrls })
const meta = memeMetadataFrom1063Event(published)
if (meta) {
await mergeMemesIntoIdbCache([meta])
setMemes((prev) => {
const next = [meta, ...prev.filter((m) => m.eventId !== meta.eventId)]
return next.slice(0, 50)
})
}
setPublishDescription('')
setQuery('')
await loadMemes('', true)
await loadMemes('', false)
} catch (err) {
setUploadError(err instanceof Error ? err.message : 'Upload failed')
} finally {
@ -221,6 +236,7 @@ export default function MemePicker({ @@ -221,6 +236,7 @@ export default function MemePicker({
kind: ExtendedKind.FILE_METADATA,
content: descriptionForPublish,
tags: [
['file', url, mime, 'size 0'],
['url', url],
['m', mime],
['t', 'memeamigo']
@ -235,10 +251,16 @@ export default function MemePicker({ @@ -235,10 +251,16 @@ export default function MemePicker({
seen.add(n)
return true
})
await publish(draft, { specifiedRelayUrls })
const published = await publish(draft, { specifiedRelayUrls })
const meta = memeMetadataFrom1063Event(published)
if (meta) {
await mergeMemesIntoIdbCache([meta])
}
setPublishDescription('')
} catch {
// ignore; URL was still inserted
} catch (err) {
toast.error(
err instanceof Error ? err.message : t('Failed to publish meme template for the picker')
)
} finally {
setPublishingPaste(false)
}

6
src/components/NormalFeed/index.tsx

@ -41,7 +41,8 @@ const NormalFeed = forwardRef<TNoteListRef, { @@ -41,7 +41,8 @@ const NormalFeed = forwardRef<TNoteListRef, {
ref
) {
const { hideUntrustedNotes } = useUserTrust()
const { showKinds, showKind1OPs, showKind1Replies, showKind1111 } = useKindFilter()
const { showKinds, showKind1OPs, showKind1Replies, showKind1111, feedKindFilterBypass } =
useKindFilter()
const [listMode, setListMode] = useState<TNoteListMode>(() => {
const storedMode = storage.getNoteListMode()
if (isMainFeed) {
@ -101,7 +102,7 @@ const NormalFeed = forwardRef<TNoteListRef, { @@ -101,7 +102,7 @@ const NormalFeed = forwardRef<TNoteListRef, {
if (!isMainFeed || !setSubHeader) return
setSubHeader(tabsElement)
return () => setSubHeader(null)
}, [isMainFeed, setSubHeader, listMode, showKindsKey, onSubHeaderRefresh])
}, [isMainFeed, setSubHeader, listMode, showKindsKey, feedKindFilterBypass, onSubHeaderRefresh])
const renderTabsInFeed = !(isMainFeed && setSubHeader)
@ -115,6 +116,7 @@ const NormalFeed = forwardRef<TNoteListRef, { @@ -115,6 +116,7 @@ const NormalFeed = forwardRef<TNoteListRef, {
showKind1OPs={showKind1OPs}
showKind1Replies={showKind1Replies}
showKind1111={showKind1111}
seeAllFeedEvents={feedKindFilterBypass}
subRequests={subRequests}
hideReplies={listMode === 'posts'}
hideUntrustedNotes={hideUntrustedNotes}

100
src/components/NoteList/index.tsx

@ -113,6 +113,7 @@ const NoteList = forwardRef( @@ -113,6 +113,7 @@ const NoteList = forwardRef(
showKind1OPs = true,
showKind1Replies = true,
showKind1111 = true,
seeAllFeedEvents = false,
filterMutedNotes = true,
hideReplies = false,
hideUntrustedNotes = false,
@ -181,6 +182,8 @@ const NoteList = forwardRef( @@ -181,6 +182,8 @@ const NoteList = forwardRef(
showKind1OPs?: boolean
showKind1Replies?: boolean
showKind1111?: boolean
/** Omit REQ kinds and skip client-side kind filtering (main feed testing). Ignored when useFilterAsIs. */
seeAllFeedEvents?: boolean
filterMutedNotes?: boolean
hideReplies?: boolean
hideUntrustedNotes?: boolean
@ -376,13 +379,23 @@ const NoteList = forwardRef( @@ -376,13 +379,23 @@ const NoteList = forwardRef(
kinds: showKindsKey,
op: showKind1OPs,
rep: showKind1Replies,
c1111: showKind1111
c1111: showKind1111,
seeAll: seeAllFeedEvents
}),
[timelineSubscriptionKey, showKindsKey, showKind1OPs, showKind1Replies, showKind1111]
[
timelineSubscriptionKey,
showKindsKey,
showKind1OPs,
showKind1Replies,
showKind1111,
seeAllFeedEvents
]
)
const showKindsRef = useRef(showKinds)
showKindsRef.current = showKinds
const seeAllFeedEventsRef = useRef(seeAllFeedEvents)
seeAllFeedEventsRef.current = seeAllFeedEvents
const useFilterAsIsRef = useRef(useFilterAsIs)
useFilterAsIsRef.current = useFilterAsIs
const clientSideKindFilterRef = useRef(clientSideKindFilter)
@ -451,15 +464,17 @@ const NoteList = forwardRef( @@ -451,15 +464,17 @@ const NoteList = forwardRef(
const idSet = new Set<string>()
return events.slice(0, showCount).filter((evt) => {
if (!showKinds.includes(evt.kind)) return false
// Kind 1: show only OPs if showKind1OPs, only replies if showKind1Replies
if (evt.kind === kinds.ShortTextNote) {
const isReply = isReplyNoteEvent(evt)
if (isReply && !showKind1Replies) return false
if (!isReply && !showKind1OPs) return false
if (!seeAllFeedEvents) {
if (!showKinds.includes(evt.kind)) return false
// Kind 1: show only OPs if showKind1OPs, only replies if showKind1Replies
if (evt.kind === kinds.ShortTextNote) {
const isReply = isReplyNoteEvent(evt)
if (isReply && !showKind1Replies) return false
if (!isReply && !showKind1OPs) return false
}
// Kind 1111 (comments): show only if showKind1111
if (evt.kind === ExtendedKind.COMMENT && !showKind1111) return false
}
// Kind 1111 (comments): show only if showKind1111
if (evt.kind === ExtendedKind.COMMENT && !showKind1111) return false
if (shouldHideEvent(evt)) return false
const id = isReplaceableEvent(evt.kind) ? getReplaceableCoordinateFromEvent(evt) : evt.id
@ -469,7 +484,16 @@ const NoteList = forwardRef( @@ -469,7 +484,16 @@ const NoteList = forwardRef(
idSet.add(id)
return true
})
}, [events, showCount, shouldHideEvent, showKinds, showKind1OPs, showKind1Replies, showKind1111])
}, [
events,
showCount,
shouldHideEvent,
showKinds,
showKind1OPs,
showKind1Replies,
showKind1111,
seeAllFeedEvents
])
useLayoutEffect(() => {
if (!feedPaintSessionPendingRef.current && !feedPaintRelayPendingRef.current) return
@ -514,13 +538,15 @@ const NoteList = forwardRef( @@ -514,13 +538,15 @@ const NoteList = forwardRef(
const idSet = new Set<string>()
return newEvents.filter((event: Event) => {
if (!showKinds.includes(event.kind)) return false
if (event.kind === kinds.ShortTextNote) {
const isReply = isReplyNoteEvent(event)
if (isReply && !showKind1Replies) return false
if (!isReply && !showKind1OPs) return false
if (!seeAllFeedEvents) {
if (!showKinds.includes(event.kind)) return false
if (event.kind === kinds.ShortTextNote) {
const isReply = isReplyNoteEvent(event)
if (isReply && !showKind1Replies) return false
if (!isReply && !showKind1OPs) return false
}
if (event.kind === ExtendedKind.COMMENT && !showKind1111) return false
}
if (event.kind === ExtendedKind.COMMENT && !showKind1111) return false
if (shouldHideEvent(event)) return false
const id = isReplaceableEvent(event.kind)
@ -532,7 +558,15 @@ const NoteList = forwardRef( @@ -532,7 +558,15 @@ const NoteList = forwardRef(
idSet.add(id)
return true
})
}, [newEvents, shouldHideEvent, showKinds, showKind1OPs, showKind1Replies, showKind1111])
}, [
newEvents,
shouldHideEvent,
showKinds,
showKind1OPs,
showKind1Replies,
showKind1111,
seeAllFeedEvents
])
useLayoutEffect(() => {
if (!onSpellFeedFirstPaint || spellFeedInstrumentToken === undefined) return
@ -753,6 +787,8 @@ const NoteList = forwardRef( @@ -753,6 +787,8 @@ const NoteList = forwardRef(
const defaultKinds = showKinds.length > 0 ? showKinds : [kinds.ShortTextNote]
const seeAllNoSpell = seeAllFeedEventsRef.current && !useFilterAsIsRef.current
const mappedSubRequests = subRequestsRef.current.map(({ urls, filter }) => {
const baseLimit = filter.limit ?? (areAlgoRelays ? ALGO_LIMIT : LIMIT)
if (useFilterAsIs) {
@ -771,6 +807,16 @@ const NoteList = forwardRef( @@ -771,6 +807,16 @@ const NoteList = forwardRef(
}
return { urls, filter: finalFilter }
}
if (seeAllNoSpell) {
const { kinds: _omitKinds, ...rest } = filter
return {
urls,
filter: {
...rest,
limit: areAlgoRelays ? ALGO_LIMIT : LIMIT
}
}
}
return {
urls,
filter: {
@ -783,6 +829,7 @@ const NoteList = forwardRef( @@ -783,6 +829,7 @@ const NoteList = forwardRef(
const filterMissingKinds = (f: Filter) => !f.kinds || f.kinds.length === 0
const invalidFilters = mappedSubRequests.filter(({ filter: f }) => {
if (seeAllNoSpell) return false
if (!filterMissingKinds(f)) return false
if (useFilterAsIs && clientSideKindFilter && timelineFilterHasNonKindScope(f)) return false
return true
@ -802,6 +849,7 @@ const NoteList = forwardRef( @@ -802,6 +849,7 @@ const NoteList = forwardRef(
}
const narrowLiveBatch = (evs: Event[]) => {
if (seeAllNoSpell) return evs
if (!useFilterAsIs || !clientSideKindFilter) return evs
return evs.filter((e) => showKinds.includes(e.kind))
}
@ -1036,14 +1084,17 @@ const NoteList = forwardRef( @@ -1036,14 +1084,17 @@ const NoteList = forwardRef(
onNew: (event: Event) => {
if (!effectActive) return
feedRelayReturnedAnyEventRef.current = true
if (!useFilterAsIs && !showKinds.includes(event.kind)) return
const seeAll = seeAllFeedEventsRef.current && !useFilterAsIs
if (!seeAll && !useFilterAsIs && !showKinds.includes(event.kind)) return
if (clientSideKindFilter && useFilterAsIs && !showKinds.includes(event.kind)) return
if (event.kind === kinds.ShortTextNote) {
const isReply = isReplyNoteEvent(event)
if (isReply && !showKind1Replies) return
if (!isReply && !showKind1OPs) return
if (!seeAll) {
if (event.kind === kinds.ShortTextNote) {
const isReply = isReplyNoteEvent(event)
if (isReply && !showKind1Replies) return
if (!isReply && !showKind1OPs) return
}
if (event.kind === ExtendedKind.COMMENT && !showKind1111) return
}
if (event.kind === ExtendedKind.COMMENT && !showKind1111) return
if (shouldHideEventRef.current(event)) return
if (pubkey && event.pubkey === pubkey) {
// If the new event is from the current user, insert it directly into the feed
@ -1132,6 +1183,7 @@ const NoteList = forwardRef( @@ -1132,6 +1183,7 @@ const NoteList = forwardRef(
showKind1OPs,
showKind1Replies,
showKind1111,
seeAllFeedEvents,
useFilterAsIs,
areAlgoRelays,
relayCapabilityReady,

2
src/constants.ts

@ -124,6 +124,8 @@ export const StorageKey = { @@ -124,6 +124,8 @@ export const StorageKey = {
SHOW_KIND_1_OPs: 'showKind1OPs',
SHOW_KIND_1_REPLIES: 'showKind1Replies',
SHOW_KIND_1111: 'showKind1111',
/** When true, main feed REQs omit `kinds` and the client does not filter by kind (testing). */
FEED_KIND_FILTER_BYPASS: 'feedKindFilterBypass',
/** @deprecated use SHOW_KIND_1_REPLIES + SHOW_KIND_1111 */
SHOW_REPLIES_AND_COMMENTS: 'showRepliesAndComments',
HIDE_CONTENT_MENTIONING_MUTED_USERS: 'hideContentMentioningMutedUsers',

5
src/i18n/locales/de.ts

@ -727,6 +727,11 @@ export default { @@ -727,6 +727,11 @@ export default {
'Select All': 'Alle auswählen',
'Clear All': 'Alle löschen',
'Set as default filter': 'Als Standardfilter festlegen',
'Use filter': 'Filter nutzen',
'See all events': 'Alle Ereignisse',
'See all events hint':
'Feed-Anfragen ohne Kind-Filter; alle Event-Arten werden angezeigt (Relay-Limits und andere Regeln gelten weiter). Zum Testen neuer Event-Kinds.',
'Use filter hint': 'Nur unten ausgewählte Kinds werden angefragt und angezeigt.',
Apply: 'Anwenden',
Reset: 'Zurücksetzen',
'Share something on this Relay': 'Teile etwas auf diesem Relay',

7
src/i18n/locales/en.ts

@ -308,6 +308,8 @@ export default { @@ -308,6 +308,8 @@ export default {
'No GIFs found. Try searching or add your own. GIFs come from Nostr kind 1063 (NIP-94) events on GIF relays.',
'No meme templates found. Try searching or open Meme Amigo. The grid only lists kind 1063 (NIP-94) files tagged memeamigo (not random photos from notes).':
'No meme templates found. Try searching or open Meme Amigo. The grid only lists kind 1063 (NIP-94) files tagged memeamigo (not random photos from notes).',
'Failed to publish meme template for the picker':
'Failed to publish meme template for the picker',
'{{name}} is not a GIF file': '{{name}} is not a GIF file',
'{{name}} is not a JPEG, PNG, or WebP file': '{{name}} is not a JPEG, PNG, or WebP file',
'R & W': 'R & W',
@ -766,6 +768,11 @@ export default { @@ -766,6 +768,11 @@ export default {
'Select All': 'Select All',
'Clear All': 'Clear All',
'Set as default filter': 'Set as default filter',
'Use filter': 'Use filter',
'See all events': 'See all events',
'See all events hint':
'Feed requests omit kind filters and every kind is shown (still subject to relay limits and other feed rules). For testing new event kinds.',
'Use filter hint': 'Only the kinds you select below are requested and shown.',
Apply: 'Apply',
Reset: 'Reset',
'Share something on this Relay': 'Share something on this Relay',

28
src/providers/KindFilterProvider.tsx

@ -25,6 +25,8 @@ type TKindFilterContext = { @@ -25,6 +25,8 @@ type TKindFilterContext = {
showKind1OPs: boolean
showKind1Replies: boolean
showKind1111: boolean
/** When true, main feed omits REQ `kinds` and skips client-side kind filtering (testing). */
feedKindFilterBypass: boolean
updateShowKinds: (
kinds: number[],
options?: {
@ -38,6 +40,7 @@ type TKindFilterContext = { @@ -38,6 +40,7 @@ type TKindFilterContext = {
updateShowKind1OPs: (value: boolean) => void
updateShowKind1Replies: (value: boolean) => void
updateShowKind1111: (value: boolean) => void
updateFeedKindFilterBypass: (value: boolean, options?: { persist?: boolean }) => void
}
const KindFilterContext = createContext<TKindFilterContext | undefined>(undefined)
@ -56,6 +59,7 @@ export function KindFilterProvider({ children }: { children: React.ReactNode }) @@ -56,6 +59,7 @@ export function KindFilterProvider({ children }: { children: React.ReactNode })
const storedShowKind1OPs = storage.getShowKind1OPs()
const storedShowKind1Replies = storage.getShowKind1Replies()
const storedShowKind1111 = storage.getShowKind1111()
const storedFeedKindFilterBypass = storage.getFeedKindFilterBypass()
const [showKinds, setShowKindsState] = useState<number[]>(
storedShowKinds.length > 0 ? storedShowKinds : defaultShowKinds
@ -63,6 +67,7 @@ export function KindFilterProvider({ children }: { children: React.ReactNode }) @@ -63,6 +67,7 @@ export function KindFilterProvider({ children }: { children: React.ReactNode })
const [showKind1OPs, setShowKind1OPsState] = useState(storedShowKind1OPs)
const [showKind1Replies, setShowKind1RepliesState] = useState(storedShowKind1Replies)
const [showKind1111, setShowKind1111State] = useState(storedShowKind1111)
const [feedKindFilterBypass, setFeedKindFilterBypassState] = useState(storedFeedKindFilterBypass)
const updateShowKinds = useCallback(
(
@ -116,18 +121,37 @@ export function KindFilterProvider({ children }: { children: React.ReactNode }) @@ -116,18 +121,37 @@ export function KindFilterProvider({ children }: { children: React.ReactNode })
setShowKindsState(next)
}, [showKinds, showKind1OPs, showKind1Replies])
const updateFeedKindFilterBypass = useCallback((value: boolean, options?: { persist?: boolean }) => {
const persist = options?.persist !== false
if (persist) storage.setFeedKindFilterBypass(value)
setFeedKindFilterBypassState(value)
}, [])
const value = useMemo(
() => ({
showKinds,
showKind1OPs,
showKind1Replies,
showKind1111,
feedKindFilterBypass,
updateShowKinds,
updateShowKind1OPs,
updateShowKind1Replies,
updateShowKind1111
updateShowKind1111,
updateFeedKindFilterBypass
}),
[showKinds, showKind1OPs, showKind1Replies, showKind1111, updateShowKinds, updateShowKind1OPs, updateShowKind1Replies, updateShowKind1111]
[
showKinds,
showKind1OPs,
showKind1Replies,
showKind1111,
feedKindFilterBypass,
updateShowKinds,
updateShowKind1OPs,
updateShowKind1Replies,
updateShowKind1111,
updateFeedKindFilterBypass
]
)
return <KindFilterContext.Provider value={value}>{children}</KindFilterContext.Provider>

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

@ -51,6 +51,7 @@ const SETTINGS_KEYS = [ @@ -51,6 +51,7 @@ const SETTINGS_KEYS = [
StorageKey.SHOW_KIND_1_OPs,
StorageKey.SHOW_KIND_1_REPLIES,
StorageKey.SHOW_KIND_1111,
StorageKey.FEED_KIND_FILTER_BYPASS,
StorageKey.HIDE_CONTENT_MENTIONING_MUTED_USERS,
StorageKey.NOTIFICATION_LIST_STYLE,
StorageKey.MEDIA_AUTO_LOAD_POLICY,
@ -96,6 +97,8 @@ class LocalStorageService { @@ -96,6 +97,8 @@ class LocalStorageService {
private showKind1OPs: boolean = true
private showKind1Replies: boolean = true
private showKind1111: boolean = true
/** Omit kinds in feed REQ + skip client kind filtering (testing). */
private feedKindFilterBypass: boolean = false
private hideContentMentioningMutedUsers: boolean = false
private notificationListStyle: TNotificationStyle = NOTIFICATION_LIST_STYLE.DETAILED
private mediaAutoLoadPolicy: TMediaAutoLoadPolicy = MEDIA_AUTO_LOAD_POLICY.ALWAYS
@ -332,6 +335,9 @@ class LocalStorageService { @@ -332,6 +335,9 @@ class LocalStorageService {
this.showKind1111 = this.showKinds.includes(ExtendedKind.COMMENT)
}
const feedKindFilterBypassStr = window.localStorage.getItem(StorageKey.FEED_KIND_FILTER_BYPASS)
this.feedKindFilterBypass = feedKindFilterBypassStr === 'true'
this.hideContentMentioningMutedUsers =
window.localStorage.getItem(StorageKey.HIDE_CONTENT_MENTIONING_MUTED_USERS) === 'true'
@ -512,6 +518,8 @@ class LocalStorageService { @@ -512,6 +518,8 @@ class LocalStorageService {
if (showKind1RepliesStr != null) this.showKind1Replies = showKind1RepliesStr === 'true'
const showKind1111Str = get(StorageKey.SHOW_KIND_1111)
if (showKind1111Str != null) this.showKind1111 = showKind1111Str === 'true'
const feedKindFilterBypassStr = get(StorageKey.FEED_KIND_FILTER_BYPASS)
if (feedKindFilterBypassStr != null) this.feedKindFilterBypass = feedKindFilterBypassStr === 'true'
this.hideContentMentioningMutedUsers = get(StorageKey.HIDE_CONTENT_MENTIONING_MUTED_USERS) === 'true'
const notifStyle = get(StorageKey.NOTIFICATION_LIST_STYLE)
if (notifStyle != null) this.notificationListStyle = notifStyle === NOTIFICATION_LIST_STYLE.COMPACT ? NOTIFICATION_LIST_STYLE.COMPACT : NOTIFICATION_LIST_STYLE.DETAILED
@ -831,6 +839,15 @@ class LocalStorageService { @@ -831,6 +839,15 @@ class LocalStorageService {
this.persistSetting(StorageKey.SHOW_KIND_1111, value.toString())
}
getFeedKindFilterBypass(): boolean {
return this.feedKindFilterBypass
}
setFeedKindFilterBypass(value: boolean) {
this.feedKindFilterBypass = value
this.persistSetting(StorageKey.FEED_KIND_FILTER_BYPASS, value.toString())
}
getHideContentMentioningMutedUsers() {
return this.hideContentMentioningMutedUsers
}

65
src/services/meme.service.ts

@ -216,13 +216,34 @@ function parseMemeFrom1063(event: NEvent): MemeMetadata | null { @@ -216,13 +216,34 @@ function parseMemeFrom1063(event: NEvent): MemeMetadata | null {
}
}
function parseMemeFromEvent(event: NEvent): MemeMetadata | null {
export function memeMetadataFrom1063Event(event: NEvent): MemeMetadata | null {
if (event.kind !== ExtendedKind.FILE_METADATA) return null
return parseMemeFrom1063(event)
}
const CACHE_MAX_AGE_MS = 5 * 60 * 1000
const MIN_MEME_CACHE_ENTRIES = 6
/** Keep offline / flaky-relay grids usable: cache is valid for days, not minutes. */
const CACHE_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000
/** Single-item lists (e.g. only your template) still cache and hydrate the picker. */
const MIN_MEME_CACHE_ENTRIES = 1
const MEME_CACHE_CAP = 200
/**
* Merge new memes into IndexedDB (dedupe by normalized URL, newest wins). Call after publishing 1063.
*/
export async function mergeMemesIntoIdbCache(incoming: MemeMetadata[]): Promise<void> {
if (incoming.length === 0) return
const row = await indexedDb.getMemeCache()
const byKey = new Map<string, MemeMetadata>()
for (const m of row?.memes ?? []) {
const meta = m as MemeMetadata
if (meta?.url) byKey.set(normalizeMemeUrl(meta.url), meta)
}
for (const m of incoming) {
if (m.url) byKey.set(normalizeMemeUrl(m.url), m)
}
const merged = [...byKey.values()].sort((a, b) => b.createdAt - a.createdAt).slice(0, MEME_CACHE_CAP)
await indexedDb.setMemeCache(merged, Date.now())
}
const THECITADEL_FOR_FILE_METADATA =
normalizeUrl('wss://thecitadel.nostr1.com') || 'wss://thecitadel.nostr1.com'
@ -245,6 +266,14 @@ export async function fetchMemes( @@ -245,6 +266,14 @@ export async function fetchMemes(
}
}
let staleFallback: MemeMetadata[] | null = null
if (!searchQuery) {
const row = await indexedDb.getMemeCache()
if (row?.memes?.length) {
staleFallback = row.memes as MemeMetadata[]
}
}
const readUrls = [
...GIF_RELAY_URLS,
...FAST_READ_RELAY_URLS,
@ -270,15 +299,23 @@ export async function fetchMemes( @@ -270,15 +299,23 @@ export async function fetchMemes(
? dedupedUrls
: [...dedupedUrls, THECITADEL_FOR_FILE_METADATA]
const events = await queryService.fetchEvents(
relays1063,
{ kinds: [ExtendedKind.FILE_METADATA], limit: limit1063 },
fetchOpts
)
let events: NEvent[] = []
try {
events = await queryService.fetchEvents(
relays1063,
{ kinds: [ExtendedKind.FILE_METADATA], limit: limit1063 },
fetchOpts
)
} catch (err) {
if (!searchQuery && staleFallback?.length) {
return staleFallback.slice(0, limit) as MemeMetadata[]
}
throw err
}
const byUrl = new Map<string, { meme: MemeMetadata; priority: number }>()
for (const event of events) {
const meme = parseMemeFromEvent(event)
const meme = memeMetadataFrom1063Event(event)
if (!meme) continue
if (searchQuery) {
@ -299,10 +336,14 @@ export async function fetchMemes( @@ -299,10 +336,14 @@ export async function fetchMemes(
const memes = Array.from(byUrl.values()).map((v) => v.meme)
memes.sort((a, b) => b.createdAt - a.createdAt)
const result = memes.slice(0, limit)
let result = memes.slice(0, limit)
if (!searchQuery && result.length === 0 && staleFallback?.length) {
result = staleFallback.slice(0, limit)
}
if (result.length >= MIN_MEME_CACHE_ENTRIES && !searchQuery) {
await indexedDb.setMemeCache(result, Date.now())
if (result.length > 0 && !searchQuery) {
await mergeMemesIntoIdbCache(result)
}
return result

Loading…
Cancel
Save