feat: auto-show new notes at top

This commit is contained in:
codytseng 2026-01-05 13:13:17 +08:00
parent 53a67d8233
commit d1b3a8c4c7
10 changed files with 32 additions and 82 deletions

View file

@ -84,7 +84,7 @@ export default function KindFilter({
variant="ghost" variant="ghost"
size="titlebar-icon" size="titlebar-icon"
className={cn( className={cn(
'relative w-fit px-3 hover:text-foreground', 'relative hover:text-foreground',
!isDifferentFromSaved && 'text-muted-foreground' !isDifferentFromSaved && 'text-muted-foreground'
)} )}
onClick={() => { onClick={() => {

View file

@ -1,28 +0,0 @@
import { Button } from '@/components/ui/button'
import { cn } from '@/lib/utils'
import { useUserPreferences } from '@/providers/UserPreferencesProvider'
import { Radio } from 'lucide-react'
import { useTranslation } from 'react-i18next'
export function LiveFeedToggle() {
const { t } = useTranslation()
const { enableLiveFeed, updateEnableLiveFeed } = useUserPreferences()
return (
<Button
variant="ghost"
size="titlebar-icon"
title={t(enableLiveFeed ? 'Disable live feed' : 'Enable live feed')}
onClick={() => updateEnableLiveFeed(!enableLiveFeed)}
>
<Radio
className={cn(
'size-4',
enableLiveFeed
? 'text-green-400 focus:text-green-300 animate-pulse'
: 'text-muted-foreground focus:text-foreground'
)}
/>
</Button>
)
}

View file

@ -9,8 +9,6 @@ import { TFeedSubRequest, TNoteListMode } from '@/types'
import { useMemo, useRef, useState } from 'react' import { useMemo, useRef, useState } from 'react'
import KindFilter from '../KindFilter' import KindFilter from '../KindFilter'
import { RefreshButton } from '../RefreshButton' import { RefreshButton } from '../RefreshButton'
import { LiveFeedToggle } from '../LiveFeedToggle'
import { useUserPreferences } from '@/providers/UserPreferencesProvider'
export default function NormalFeed({ export default function NormalFeed({
subRequests, subRequests,
@ -30,7 +28,6 @@ export default function NormalFeed({
isPubkeyFeed?: boolean isPubkeyFeed?: boolean
}) { }) {
const { showKinds } = useKindFilter() const { showKinds } = useKindFilter()
const { enableLiveFeed } = useUserPreferences()
const [temporaryShowKinds, setTemporaryShowKinds] = useState(showKinds) const [temporaryShowKinds, setTemporaryShowKinds] = useState(showKinds)
const [listMode, setListMode] = useState<TNoteListMode>(() => storage.getNoteListMode()) const [listMode, setListMode] = useState<TNoteListMode>(() => storage.getNoteListMode())
const supportTouch = useMemo(() => isTouchDevice(), []) const supportTouch = useMemo(() => isTouchDevice(), [])
@ -88,7 +85,6 @@ export default function NormalFeed({
}} }}
/> />
)} )}
<LiveFeedToggle />
{!isPubkeyFeed && <TrustScoreFilter onOpenChange={handleTrustFilterOpenChange} />} {!isPubkeyFeed && <TrustScoreFilter onOpenChange={handleTrustFilterOpenChange} />}
{showKindsFilter && ( {showKindsFilter && (
<KindFilter <KindFilter
@ -109,7 +105,6 @@ export default function NormalFeed({
areAlgoRelays={areAlgoRelays} areAlgoRelays={areAlgoRelays}
showRelayCloseReason={showRelayCloseReason} showRelayCloseReason={showRelayCloseReason}
isPubkeyFeed={isPubkeyFeed} isPubkeyFeed={isPubkeyFeed}
showNewNotesDirectly={enableLiveFeed}
/> />
) : ( ) : (
<NoteList <NoteList
@ -120,7 +115,6 @@ export default function NormalFeed({
areAlgoRelays={areAlgoRelays} areAlgoRelays={areAlgoRelays}
showRelayCloseReason={showRelayCloseReason} showRelayCloseReason={showRelayCloseReason}
isPubkeyFeed={isPubkeyFeed} isPubkeyFeed={isPubkeyFeed}
showNewNotesDirectly={enableLiveFeed}
/> />
)} )}
</> </>

View file

@ -338,9 +338,21 @@ const NoteList = forwardRef<
oldEvents.some((e) => e.id === event.id) ? oldEvents : [event, ...oldEvents] oldEvents.some((e) => e.id === event.id) ? oldEvents : [event, ...oldEvents]
) )
} else { } else {
setNewEvents((oldEvents) => const isAtTop = (() => {
[event, ...oldEvents].sort((a, b) => b.created_at - a.created_at) if (!topRef.current) return true
) const rect = topRef.current.getBoundingClientRect()
return rect.top >= 50
})()
if (isAtTop) {
setEvents((oldEvents) =>
oldEvents.some((e) => e.id === event.id) ? oldEvents : [event, ...oldEvents]
)
} else {
setNewEvents((oldEvents) =>
[event, ...oldEvents].sort((a, b) => b.created_at - a.created_at)
)
}
} }
threadService.addRepliesToThread([event]) threadService.addRepliesToThread([event])
}, },

View file

@ -7,14 +7,12 @@ import { generateBech32IdFromETag } from '@/lib/tag'
import { isTouchDevice } from '@/lib/utils' import { isTouchDevice } from '@/lib/utils'
import { useKindFilter } from '@/providers/KindFilterProvider' import { useKindFilter } from '@/providers/KindFilterProvider'
import { useNostr } from '@/providers/NostrProvider' import { useNostr } from '@/providers/NostrProvider'
import { useUserPreferences } from '@/providers/UserPreferencesProvider'
import client from '@/services/client.service' import client from '@/services/client.service'
import storage from '@/services/local-storage.service' import storage from '@/services/local-storage.service'
import relayInfoService from '@/services/relay-info.service' import relayInfoService from '@/services/relay-info.service'
import { TFeedSubRequest, TNoteListMode } from '@/types' import { TFeedSubRequest, TNoteListMode } from '@/types'
import { NostrEvent } from 'nostr-tools' import { NostrEvent } from 'nostr-tools'
import { useEffect, useMemo, useRef, useState } from 'react' import { useEffect, useMemo, useRef, useState } from 'react'
import { LiveFeedToggle } from '../LiveFeedToggle'
import { RefreshButton } from '../RefreshButton' import { RefreshButton } from '../RefreshButton'
export default function ProfileFeed({ export default function ProfileFeed({
@ -27,7 +25,6 @@ export default function ProfileFeed({
search?: string search?: string
}) { }) {
const { pubkey: myPubkey, pinListEvent: myPinListEvent } = useNostr() const { pubkey: myPubkey, pinListEvent: myPinListEvent } = useNostr()
const { enableLiveFeed } = useUserPreferences()
const { showKinds } = useKindFilter() const { showKinds } = useKindFilter()
const [temporaryShowKinds, setTemporaryShowKinds] = useState(showKinds) const [temporaryShowKinds, setTemporaryShowKinds] = useState(showKinds)
const [listMode, setListMode] = useState<TNoteListMode>(() => { const [listMode, setListMode] = useState<TNoteListMode>(() => {
@ -168,7 +165,6 @@ export default function ProfileFeed({
options={ options={
<> <>
{!supportTouch && <RefreshButton onClick={() => noteListRef.current?.refresh()} />} {!supportTouch && <RefreshButton onClick={() => noteListRef.current?.refresh()} />}
<LiveFeedToggle />
<KindFilter showKinds={temporaryShowKinds} onShowKindsChange={handleShowKindsChange} /> <KindFilter showKinds={temporaryShowKinds} onShowKindsChange={handleShowKindsChange} />
</> </>
} }
@ -180,7 +176,7 @@ export default function ProfileFeed({
hideReplies={listMode === 'posts'} hideReplies={listMode === 'posts'}
filterMutedNotes={false} filterMutedNotes={false}
pinnedEventIds={listMode === 'you' || !!search ? [] : pinnedEventIds} pinnedEventIds={listMode === 'you' || !!search ? [] : pinnedEventIds}
showNewNotesDirectly={myPubkey === pubkey || enableLiveFeed} showNewNotesDirectly={myPubkey === pubkey}
/> />
</> </>
) )

View file

@ -1,8 +1,9 @@
import { ScrollArea, ScrollBar } from '@/components/ui/scroll-area'
import { Separator } from '@/components/ui/separator'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
import { useDeepBrowsing } from '@/providers/DeepBrowsingProvider' import { useDeepBrowsing } from '@/providers/DeepBrowsingProvider'
import { ReactNode, useEffect, useRef, useState } from 'react' import { ReactNode, useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { ScrollArea, ScrollBar } from '../ui/scroll-area'
type TabDefinition = { type TabDefinition = {
value: string value: string
@ -124,7 +125,12 @@ export default function Tabs({
</div> </div>
<ScrollBar orientation="horizontal" className="opacity-0 pointer-events-none" /> <ScrollBar orientation="horizontal" className="opacity-0 pointer-events-none" />
</ScrollArea> </ScrollArea>
{options && <div className="py-1 flex items-center">{options}</div>} {options && (
<div className="py-1 flex items-center gap-1">
<Separator orientation="vertical" className="h-8" />
{options}
</div>
)}
</div> </div>
) )
} }

View file

@ -54,7 +54,6 @@ const UserAggregationList = forwardRef<
areAlgoRelays?: boolean areAlgoRelays?: boolean
showRelayCloseReason?: boolean showRelayCloseReason?: boolean
isPubkeyFeed?: boolean isPubkeyFeed?: boolean
showNewNotesDirectly?: boolean
} }
>( >(
( (
@ -64,8 +63,7 @@ const UserAggregationList = forwardRef<
filterMutedNotes = true, filterMutedNotes = true,
areAlgoRelays = false, areAlgoRelays = false,
showRelayCloseReason = false, showRelayCloseReason = false,
isPubkeyFeed = false, isPubkeyFeed = false
showNewNotesDirectly = false
}, },
ref ref
) => { ) => {
@ -97,8 +95,6 @@ const UserAggregationList = forwardRef<
const bottomRef = useRef<HTMLDivElement | null>(null) const bottomRef = useRef<HTMLDivElement | null>(null)
const topRef = useRef<HTMLDivElement | null>(null) const topRef = useRef<HTMLDivElement | null>(null)
const nonPinnedTopRef = useRef<HTMLDivElement | null>(null) const nonPinnedTopRef = useRef<HTMLDivElement | null>(null)
const showNewNotesDirectlyRef = useRef(showNewNotesDirectly)
showNewNotesDirectlyRef.current = showNewNotesDirectly
const scrollToTop = (behavior: ScrollBehavior = 'instant') => { const scrollToTop = (behavior: ScrollBehavior = 'instant') => {
setTimeout(() => { setTimeout(() => {
@ -180,13 +176,9 @@ const UserAggregationList = forwardRef<
} }
}, },
onNew: (event) => { onNew: (event) => {
if (showNewNotesDirectlyRef.current) { setNewEvents((oldEvents) =>
setEvents((oldEvents) => [event, ...oldEvents]) [event, ...oldEvents].sort((a, b) => b.created_at - a.created_at)
} else { )
setNewEvents((oldEvents) =>
[event, ...oldEvents].sort((a, b) => b.created_at - a.created_at)
)
}
threadService.addRepliesToThread([event]) threadService.addRepliesToThread([event])
}, },
onClose: (url, reason) => { onClose: (url, reason) => {

View file

@ -42,8 +42,8 @@ export const StorageKey = {
QUICK_REACTION_EMOJI: 'quickReactionEmoji', QUICK_REACTION_EMOJI: 'quickReactionEmoji',
NSFW_DISPLAY_POLICY: 'nsfwDisplayPolicy', NSFW_DISPLAY_POLICY: 'nsfwDisplayPolicy',
MIN_TRUST_SCORE: 'minTrustScore', MIN_TRUST_SCORE: 'minTrustScore',
ENABLE_LIVE_FEED: 'enableLiveFeed',
DEFAULT_RELAY_URLS: 'defaultRelayUrls', DEFAULT_RELAY_URLS: 'defaultRelayUrls',
ENABLE_LIVE_FEED: 'enableLiveFeed', // deprecated
HIDE_UNTRUSTED_NOTES: 'hideUntrustedNotes', // deprecated HIDE_UNTRUSTED_NOTES: 'hideUntrustedNotes', // deprecated
HIDE_UNTRUSTED_INTERACTIONS: 'hideUntrustedInteractions', // deprecated HIDE_UNTRUSTED_INTERACTIONS: 'hideUntrustedInteractions', // deprecated
HIDE_UNTRUSTED_NOTIFICATIONS: 'hideUntrustedNotifications', // deprecated HIDE_UNTRUSTED_NOTIFICATIONS: 'hideUntrustedNotifications', // deprecated

View file

@ -21,9 +21,6 @@ type TUserPreferencesContext = {
quickReactionEmoji: string | TEmoji quickReactionEmoji: string | TEmoji
updateQuickReactionEmoji: (emoji: string | TEmoji) => void updateQuickReactionEmoji: (emoji: string | TEmoji) => void
enableLiveFeed: boolean
updateEnableLiveFeed: (enable: boolean) => void
} }
const UserPreferencesContext = createContext<TUserPreferencesContext | undefined>(undefined) const UserPreferencesContext = createContext<TUserPreferencesContext | undefined>(undefined)
@ -48,7 +45,6 @@ export function UserPreferencesProvider({ children }: { children: React.ReactNod
) )
const [quickReaction, setQuickReaction] = useState(storage.getQuickReaction()) const [quickReaction, setQuickReaction] = useState(storage.getQuickReaction())
const [quickReactionEmoji, setQuickReactionEmoji] = useState(storage.getQuickReactionEmoji()) const [quickReactionEmoji, setQuickReactionEmoji] = useState(storage.getQuickReactionEmoji())
const [enableLiveFeed, setEnableLiveFeed] = useState(storage.getEnableLiveFeed())
useEffect(() => { useEffect(() => {
if (!isSmallScreen && enableSingleColumnLayout) { if (!isSmallScreen && enableSingleColumnLayout) {
@ -83,11 +79,6 @@ export function UserPreferencesProvider({ children }: { children: React.ReactNod
storage.setQuickReactionEmoji(emoji) storage.setQuickReactionEmoji(emoji)
} }
const updateEnableLiveFeed = (enable: boolean) => {
setEnableLiveFeed(enable)
storage.setEnableLiveFeed(enable)
}
return ( return (
<UserPreferencesContext.Provider <UserPreferencesContext.Provider
value={{ value={{
@ -102,9 +93,7 @@ export function UserPreferencesProvider({ children }: { children: React.ReactNod
quickReaction, quickReaction,
updateQuickReaction, updateQuickReaction,
quickReactionEmoji, quickReactionEmoji,
updateQuickReactionEmoji, updateQuickReactionEmoji
enableLiveFeed,
updateEnableLiveFeed
}} }}
> >
{children} {children}

View file

@ -65,7 +65,6 @@ class LocalStorageService {
private quickReactionEmoji: string | TEmoji = '+' private quickReactionEmoji: string | TEmoji = '+'
private nsfwDisplayPolicy: TNsfwDisplayPolicy = NSFW_DISPLAY_POLICY.HIDE_CONTENT private nsfwDisplayPolicy: TNsfwDisplayPolicy = NSFW_DISPLAY_POLICY.HIDE_CONTENT
private minTrustScore: number = 40 private minTrustScore: number = 40
private enableLiveFeed: boolean = false
private defaultRelayUrls: string[] = BIG_RELAY_URLS private defaultRelayUrls: string[] = BIG_RELAY_URLS
constructor() { constructor() {
@ -278,8 +277,6 @@ class LocalStorageService {
} }
} }
this.enableLiveFeed = window.localStorage.getItem(StorageKey.ENABLE_LIVE_FEED) === 'true'
const defaultRelayUrlsStr = window.localStorage.getItem(StorageKey.DEFAULT_RELAY_URLS) const defaultRelayUrlsStr = window.localStorage.getItem(StorageKey.DEFAULT_RELAY_URLS)
if (defaultRelayUrlsStr) { if (defaultRelayUrlsStr) {
try { try {
@ -305,6 +302,7 @@ class LocalStorageService {
window.localStorage.removeItem(StorageKey.ACCOUNT_MUTE_DECRYPTED_TAGS_MAP) window.localStorage.removeItem(StorageKey.ACCOUNT_MUTE_DECRYPTED_TAGS_MAP)
window.localStorage.removeItem(StorageKey.ACTIVE_RELAY_SET_ID) window.localStorage.removeItem(StorageKey.ACTIVE_RELAY_SET_ID)
window.localStorage.removeItem(StorageKey.FEED_TYPE) window.localStorage.removeItem(StorageKey.FEED_TYPE)
window.localStorage.removeItem(StorageKey.ENABLE_LIVE_FEED)
} }
getRelaySets() { getRelaySets() {
@ -634,15 +632,6 @@ class LocalStorageService {
} }
} }
getEnableLiveFeed() {
return this.enableLiveFeed
}
setEnableLiveFeed(enable: boolean) {
this.enableLiveFeed = enable
window.localStorage.setItem(StorageKey.ENABLE_LIVE_FEED, enable.toString())
}
getDefaultRelayUrls() { getDefaultRelayUrls() {
return this.defaultRelayUrls return this.defaultRelayUrls
} }