feat: live feed toggle

This commit is contained in:
codytseng 2026-01-04 20:42:20 +08:00
parent 89bb9ad2d0
commit 917fcd9839
26 changed files with 130 additions and 26 deletions

View file

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

View file

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

View file

@ -91,7 +91,7 @@ export default function TrustScoreFilter({
>
{minTrustScore < 100 ? <Shield size={16} /> : <ShieldCheck size={16} />}
{minTrustScore > 0 && minTrustScore < 100 && (
<div className="absolute inset-0 w-full h-full flex flex-col items-center justify-center text-[0.55rem] font-mono font-bold">
<div className="absolute inset-0 w-full h-full flex flex-col items-center justify-center text-[0.5rem] font-mono font-bold">
{minTrustScore}
</div>
)}

View file

@ -54,6 +54,7 @@ const UserAggregationList = forwardRef<
areAlgoRelays?: boolean
showRelayCloseReason?: boolean
isPubkeyFeed?: boolean
showNewNotesDirectly?: boolean
}
>(
(
@ -63,7 +64,8 @@ const UserAggregationList = forwardRef<
filterMutedNotes = true,
areAlgoRelays = false,
showRelayCloseReason = false,
isPubkeyFeed = false
isPubkeyFeed = false,
showNewNotesDirectly = false
},
ref
) => {
@ -176,9 +178,13 @@ const UserAggregationList = forwardRef<
}
},
onNew: (event) => {
setNewEvents((oldEvents) =>
[event, ...oldEvents].sort((a, b) => b.created_at - a.created_at)
)
if (showNewNotesDirectly) {
setEvents((oldEvents) => [event, ...oldEvents])
} else {
setNewEvents((oldEvents) =>
[event, ...oldEvents].sort((a, b) => b.created_at - a.created_at)
)
}
threadService.addRepliesToThread([event])
},
onClose: (url, reason) => {

View file

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

View file

@ -651,6 +651,8 @@ export default {
'trust-filter.hide-bottom-percent':
'تصفية أدنى {{score}}٪ من المستخدمين حسب تصنيف الثقة',
'trust-filter.trust-score-description': 'محسوبة بناءً على سمعة المستخدم والنسبة المئوية للشبكة الاجتماعية',
'Auto-load profile pictures': 'تحميل صور الملف الشخصي تلقائيًا'
'Auto-load profile pictures': 'تحميل صور الملف الشخصي تلقائيًا',
'Disable live feed': 'تعطيل التغذية المباشرة',
'Enable live feed': 'تفعيل التغذية المباشرة'
}
}

View file

@ -673,6 +673,8 @@ export default {
'Untere {{score}}% der Benutzer nach Vertrauensrang filtern',
'trust-filter.trust-score-description':
'Berechnet basierend auf Benutzerreputation und sozialem Netzwerk-Perzentil',
'Auto-load profile pictures': 'Profilbilder automatisch laden'
'Auto-load profile pictures': 'Profilbilder automatisch laden',
'Disable live feed': 'Live-Feed deaktivieren',
'Enable live feed': 'Live-Feed aktivieren'
}
}

View file

@ -656,6 +656,8 @@ export default {
'trust-filter.hide-bottom-percent':
'Filter out bottom {{score}}% of users by trust rank',
'trust-filter.trust-score-description': 'Calculated based on user reputation and social network percentile',
'Auto-load profile pictures': 'Auto-load profile pictures'
'Auto-load profile pictures': 'Auto-load profile pictures',
'Disable live feed': 'Disable live feed',
'Enable live feed': 'Enable live feed'
}
}

View file

@ -667,6 +667,8 @@ export default {
'Filtrar el {{score}}% inferior de usuarios por clasificación de confianza',
'trust-filter.trust-score-description':
'Calculado según la reputación del usuario y el percentil de la red social',
'Auto-load profile pictures': 'Cargar imágenes de perfil automáticamente'
'Auto-load profile pictures': 'Cargar imágenes de perfil automáticamente',
'Disable live feed': 'Desactivar feed en vivo',
'Enable live feed': 'Activar feed en vivo'
}
}

View file

@ -663,6 +663,8 @@ export default {
'فیلتر کردن {{score}}٪ پایین‌ترین کاربران بر اساس رتبه اعتماد',
'trust-filter.trust-score-description':
'بر اساس شهرت کاربر و صدک شبکه اجتماعی محاسبه می‌شود',
'Auto-load profile pictures': 'بارگذاری خودکار تصاویر پروفایل'
'Auto-load profile pictures': 'بارگذاری خودکار تصاویر پروفایل',
'Disable live feed': 'غیرفعال کردن فید زنده',
'Enable live feed': 'فعال کردن فید زنده'
}
}

View file

@ -671,6 +671,8 @@ export default {
'Filtrer les {{score}}% inférieurs des utilisateurs par classement de confiance',
'trust-filter.trust-score-description':
"Calculé en fonction de la réputation et du réseau social de l'utilisateur",
'Auto-load profile pictures': 'Charger les images de profil automatiquement'
'Auto-load profile pictures': 'Charger les images de profil automatiquement',
'Disable live feed': 'Désactiver le flux en direct',
'Enable live feed': 'Activer le flux en direct'
}
}

View file

@ -663,6 +663,8 @@ export default {
'विश्वास रैंक द्वारा निचले {{score}}% उपयोगकर्ताओं को फ़िल्टर करें',
'trust-filter.trust-score-description':
'उपयोगकर्ता की प्रतिष्ठा और सामाजिक नेटवर्क प्रतिशतक के आधार पर गणना की गई',
'Auto-load profile pictures': 'प्रोफ़ाइल चित्र स्वतः लोड करें'
'Auto-load profile pictures': 'प्रोफ़ाइल चित्र स्वतः लोड करें',
'Disable live feed': 'लाइव फ़ीड अक्षम करें',
'Enable live feed': 'लाइव फ़ीड सक्षम करें'
}
}

View file

@ -656,6 +656,8 @@ export default {
'Alsó {{score}}% felhasználók szűrése bizalmi rangsor szerint',
'trust-filter.trust-score-description':
'A felhasználó hírneve és a közösségi hálózat percentilise alapján számítva',
'Auto-load profile pictures': 'Profilképek automatikus betöltése'
'Auto-load profile pictures': 'Profilképek automatikus betöltése',
'Disable live feed': 'Élő hírfolyam letiltása',
'Enable live feed': 'Élő hírfolyam engedélyezése'
}
}

View file

@ -667,6 +667,8 @@ export default {
'Filtra il {{score}}% inferiore degli utenti per classifica di fiducia',
'trust-filter.trust-score-description':
"Calcolato in base alla reputazione dell'utente e al percentile del social network",
'Auto-load profile pictures': 'Caricamento automatico immagini di profilo'
'Auto-load profile pictures': 'Caricamento automatico immagini di profilo',
'Disable live feed': 'Disattiva feed live',
'Enable live feed': 'Attiva feed live'
}
}

View file

@ -662,6 +662,8 @@ export default {
'信頼ランク下位 {{score}}% のユーザーをフィルタリング',
'trust-filter.trust-score-description':
'ユーザーの評判とソーシャルネットワークに基づいて信頼度パーセンタイルを計算',
'Auto-load profile pictures': 'プロフィール画像を自動読み込み'
'Auto-load profile pictures': 'プロフィール画像を自動読み込み',
'Disable live feed': 'ライブフィードを無効にする',
'Enable live feed': 'ライブフィードを有効にする'
}
}

View file

@ -656,6 +656,8 @@ export default {
'trust-filter.only-show-wot': '신뢰 네트워크만 표시 (팔로우 + 팔로우의 팔로우)',
'trust-filter.hide-bottom-percent': '신뢰도 하위 {{score}}% 사용자 필터링',
'trust-filter.trust-score-description': '사용자의 평판과 소셜 네트워크를 기반으로 신뢰도 백분위수 계산',
'Auto-load profile pictures': '프로필 사진 자동 로드'
'Auto-load profile pictures': '프로필 사진 자동 로드',
'Disable live feed': '라이브 피드 비활성화',
'Enable live feed': '라이브 피드 활성화'
}
}

View file

@ -668,6 +668,8 @@ export default {
'Filtruj dolne {{score}}% użytkowników według rankingu zaufania',
'trust-filter.trust-score-description':
'Obliczany na podstawie reputacji użytkownika i percentyla sieci społecznościowej',
'Auto-load profile pictures': 'Automatyczne ładowanie zdjęć profilowych'
'Auto-load profile pictures': 'Automatyczne ładowanie zdjęć profilowych',
'Disable live feed': 'Wyłącz kanał na żywo',
'Enable live feed': 'Włącz kanał na żywo'
}
}

View file

@ -664,6 +664,8 @@ export default {
'Filtrar os {{score}}% inferiores de usuários por classificação de confiança',
'trust-filter.trust-score-description':
'Calculado com base na reputação do usuário e no percentil da rede social',
'Auto-load profile pictures': 'Carregar fotos de perfil automaticamente'
'Auto-load profile pictures': 'Carregar fotos de perfil automaticamente',
'Disable live feed': 'Desativar feed ao vivo',
'Enable live feed': 'Ativar feed ao vivo'
}
}

View file

@ -667,6 +667,8 @@ export default {
'Filtrar os {{score}}% inferiores de utilizadores por classificação de confiança',
'trust-filter.trust-score-description':
'Calculado com base na reputação do utilizador e no percentil da rede social',
'Auto-load profile pictures': 'Carregar fotos de perfil automaticamente'
'Auto-load profile pictures': 'Carregar fotos de perfil automaticamente',
'Disable live feed': 'Desativar feed ao vivo',
'Enable live feed': 'Ativar feed ao vivo'
}
}

View file

@ -667,6 +667,8 @@ export default {
'Отфильтровать нижние {{score}}% пользователей по рейтингу доверия',
'trust-filter.trust-score-description':
'Рассчитывается на основе репутации пользователя и процентиля социальной сети',
'Auto-load profile pictures': 'Автозагрузка аватаров'
'Auto-load profile pictures': 'Автозагрузка аватаров',
'Disable live feed': 'Отключить прямую трансляцию',
'Enable live feed': 'Включить прямую трансляцию'
}
}

View file

@ -653,6 +653,8 @@ export default {
'กรอง {{score}}% ล่างสุดของผู้ใช้ตามอันดับความไว้วางใจ',
'trust-filter.trust-score-description':
'คำนวณจากชื่อเสียงของผู้ใช้และเปอร์เซ็นไทล์ของเครือข่ายสังคม',
'Auto-load profile pictures': 'โหลดรูปโปรไฟล์อัตโนมัติ'
'Auto-load profile pictures': 'โหลดรูปโปรไฟล์อัตโนมัติ',
'Disable live feed': 'ปิดฟีดสด',
'Enable live feed': 'เปิดฟีดสด'
}
}

View file

@ -636,6 +636,8 @@ export default {
'trust-filter.only-show-wot': '僅顯示你的信任網路(關注的人 + 他們關注的人)',
'trust-filter.hide-bottom-percent': '過濾掉信任度排名後 {{score}}% 的使用者',
'trust-filter.trust-score-description': '基於使用者的聲譽和社交網路計算信任度百分位',
'Auto-load profile pictures': '自動載入大頭照'
'Auto-load profile pictures': '自動載入大頭照',
'Disable live feed': '停用即時推送',
'Enable live feed': '啟用即時推送'
}
}

View file

@ -641,6 +641,8 @@ export default {
'trust-filter.only-show-wot': '仅显示你的信任网络(关注的人 + 他们关注的人)',
'trust-filter.hide-bottom-percent': '过滤掉信任度排名后 {{score}}% 的用户',
'trust-filter.trust-score-description': '基于用户的声誉和社交网络计算信任度百分位',
'Auto-load profile pictures': '自动加载头像'
'Auto-load profile pictures': '自动加载头像',
'Disable live feed': '禁用实时推送',
'Enable live feed': '启用实时推送'
}
}

View file

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

View file

@ -64,6 +64,7 @@ class LocalStorageService {
private quickReactionEmoji: string | TEmoji = '+'
private nsfwDisplayPolicy: TNsfwDisplayPolicy = NSFW_DISPLAY_POLICY.HIDE_CONTENT
private minTrustScore: number = 40
private enableLiveFeed: boolean = false
constructor() {
if (!LocalStorageService.instance) {
@ -275,6 +276,8 @@ class LocalStorageService {
}
}
this.enableLiveFeed = window.localStorage.getItem(StorageKey.ENABLE_LIVE_FEED) === 'true'
// Clean up deprecated data
window.localStorage.removeItem(StorageKey.PINNED_PUBKEYS)
window.localStorage.removeItem(StorageKey.ACCOUNT_PROFILE_EVENT_MAP)
@ -612,6 +615,15 @@ class LocalStorageService {
window.localStorage.setItem(StorageKey.MIN_TRUST_SCORE, score.toString())
}
}
getEnableLiveFeed() {
return this.enableLiveFeed
}
setEnableLiveFeed(enable: boolean) {
this.enableLiveFeed = enable
window.localStorage.setItem(StorageKey.ENABLE_LIVE_FEED, enable.toString())
}
}
const instance = new LocalStorageService()