feat: auto-show new notes at top
This commit is contained in:
parent
53a67d8233
commit
d1b3a8c4c7
10 changed files with 32 additions and 82 deletions
|
|
@ -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={() => {
|
||||||
|
|
|
||||||
|
|
@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
@ -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}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|
|
||||||
|
|
@ -337,11 +337,23 @@ const NoteList = forwardRef<
|
||||||
setEvents((oldEvents) =>
|
setEvents((oldEvents) =>
|
||||||
oldEvents.some((e) => e.id === event.id) ? oldEvents : [event, ...oldEvents]
|
oldEvents.some((e) => e.id === event.id) ? oldEvents : [event, ...oldEvents]
|
||||||
)
|
)
|
||||||
|
} else {
|
||||||
|
const isAtTop = (() => {
|
||||||
|
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 {
|
} else {
|
||||||
setNewEvents((oldEvents) =>
|
setNewEvents((oldEvents) =>
|
||||||
[event, ...oldEvents].sort((a, b) => b.created_at - a.created_at)
|
[event, ...oldEvents].sort((a, b) => b.created_at - a.created_at)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
threadService.addRepliesToThread([event])
|
threadService.addRepliesToThread([event])
|
||||||
},
|
},
|
||||||
onClose: (url, reason) => {
|
onClose: (url, reason) => {
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
|
||||||
setEvents((oldEvents) => [event, ...oldEvents])
|
|
||||||
} else {
|
|
||||||
setNewEvents((oldEvents) =>
|
setNewEvents((oldEvents) =>
|
||||||
[event, ...oldEvents].sort((a, b) => b.created_at - a.created_at)
|
[event, ...oldEvents].sort((a, b) => b.created_at - a.created_at)
|
||||||
)
|
)
|
||||||
}
|
|
||||||
threadService.addRepliesToThread([event])
|
threadService.addRepliesToThread([event])
|
||||||
},
|
},
|
||||||
onClose: (url, reason) => {
|
onClose: (url, reason) => {
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue