diff --git a/src/components/Nip22ReplyNoteList/index.tsx b/src/components/Nip22ReplyNoteList/index.tsx index ed39c132..f66a7ce9 100644 --- a/src/components/Nip22ReplyNoteList/index.tsx +++ b/src/components/Nip22ReplyNoteList/index.tsx @@ -68,12 +68,16 @@ export default function Nip22ReplyNoteList({ relayUrls.unshift(...seenOn) } const { closer, timelineKey } = await client.subscribeTimeline( - relayUrls.slice(0, 4), - { - '#E': [event.id], - kinds: [ExtendedKind.COMMENT], - limit: LIMIT - }, + [ + { + urls: relayUrls.slice(0, 4), + filter: { + '#E': [event.id], + kinds: [ExtendedKind.COMMENT], + limit: LIMIT + } + } + ], { onEvents: (evts, eosed) => { if (evts.length > 0) { diff --git a/src/components/NoteCard/index.tsx b/src/components/NoteCard/index.tsx index d20136a2..df210cac 100644 --- a/src/components/NoteCard/index.tsx +++ b/src/components/NoteCard/index.tsx @@ -1,5 +1,7 @@ +import { Skeleton } from '@/components/ui/skeleton' import { useMuteList } from '@/providers/MuteListProvider' import { Event, kinds } from 'nostr-tools' +import { useTranslation } from 'react-i18next' import GenericNoteCard from './GenericNoteCard' import RepostNoteCard from './RepostNoteCard' @@ -24,3 +26,35 @@ export default function NoteCard({ } return } + +export function NoteCardLoadingSkeleton({ isPictures }: { isPictures: boolean }) { + const { t } = useTranslation() + + if (isPictures) { + return
{t('loading...')}
+ } + + return ( +
+
+ +
+
+ +
+
+ +
+
+
+
+
+ +
+
+ +
+
+
+ ) +} diff --git a/src/components/NoteList/index.tsx b/src/components/NoteList/index.tsx index da43ee23..f96e315c 100644 --- a/src/components/NoteList/index.tsx +++ b/src/components/NoteList/index.tsx @@ -1,11 +1,9 @@ +import NewNotesButton from '@/components/NewNotesButton' import { Button } from '@/components/ui/button' -import { Skeleton } from '@/components/ui/skeleton' -import { ExtendedKind } from '@/constants' +import { BIG_RELAY_URLS, ExtendedKind } from '@/constants' import { isReplyNoteEvent } from '@/lib/event' import { checkAlgoRelay } from '@/lib/relay' -import { cn } from '@/lib/utils' -import NewNotesButton from '@/components/NewNotesButton' -import { useDeepBrowsing } from '@/providers/DeepBrowsingProvider' +import { isSafari } from '@/lib/utils' import { useMuteList } from '@/providers/MuteListProvider' import { useNostr } from '@/providers/NostrProvider' import { useScreenSize } from '@/providers/ScreenSizeProvider' @@ -15,11 +13,12 @@ import relayInfoService from '@/services/relay-info.service' import { TNoteListMode } from '@/types' import dayjs from 'dayjs' import { Event, Filter, kinds } from 'nostr-tools' -import { ReactNode, useEffect, useMemo, useRef, useState } from 'react' +import { useEffect, useMemo, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import PullToRefresh from 'react-simple-pull-to-refresh' -import NoteCard from '../NoteCard' -import PictureNoteCard from '../PictureNoteCard' +import NoteCard, { NoteCardLoadingSkeleton } from '../NoteCard' +import { PictureNoteCardMasonry } from '../PictureNoteCardMasonry' +import TabSwitcher from '../TabSwitch' const LIMIT = 100 const ALGO_LIMIT = 500 @@ -28,19 +27,23 @@ const SHOW_COUNT = 10 export default function NoteList({ relayUrls = [], filter = {}, + author, className, filterMutedNotes = true, - needCheckAlgoRelay = false + needCheckAlgoRelay = false, + isMainFeed = false }: { relayUrls?: string[] filter?: Filter + author?: string className?: string filterMutedNotes?: boolean needCheckAlgoRelay?: boolean + isMainFeed?: boolean }) { const { t } = useTranslation() const { isLargeScreen } = useScreenSize() - const { startLogin } = useNostr() + const { pubkey, startLogin } = useNostr() const { mutePubkeys } = useMuteList() const [refreshCount, setRefreshCount] = useState(0) const [timelineKey, setTimelineKey] = useState(undefined) @@ -49,15 +52,11 @@ export default function NoteList({ const [showCount, setShowCount] = useState(SHOW_COUNT) const [hasMore, setHasMore] = useState(true) const [loading, setLoading] = useState(true) - const [listMode, setListMode] = useState(() => storage.getNoteListMode()) + const [listMode, setListMode] = useState(() => + isMainFeed ? storage.getNoteListMode() : 'posts' + ) + const [filterType, setFilterType] = useState>('posts') const bottomRef = useRef(null) - const isPictures = useMemo(() => listMode === 'pictures', [listMode]) - const noteFilter = useMemo(() => { - return { - kinds: isPictures ? [ExtendedKind.PICTURE] : [kinds.ShortTextNote, kinds.Repost], - ...filter - } - }, [JSON.stringify(filter), isPictures]) const topRef = useRef(null) const filteredNewEvents = useMemo(() => { return newEvents.filter((event: Event) => { @@ -69,7 +68,26 @@ export default function NoteList({ }, [newEvents, listMode, filterMutedNotes, mutePubkeys]) useEffect(() => { - if (relayUrls.length === 0 && !noteFilter.authors?.length) return + switch (listMode) { + case 'posts': + case 'postsAndReplies': + setFilterType('posts') + break + case 'pictures': + setFilterType('pictures') + break + case 'you': + if (!pubkey || pubkey === author) { + setFilterType('posts') + } else { + setFilterType('you') + } + break + } + }, [listMode, pubkey]) + + useEffect(() => { + if (relayUrls.length === 0 && !filter.authors?.length && !author) return async function init() { setLoading(true) @@ -78,14 +96,105 @@ export default function NoteList({ setHasMore(true) let areAlgoRelays = false - if (needCheckAlgoRelay) { - const relayInfos = await relayInfoService.getRelayInfos(relayUrls) - areAlgoRelays = relayInfos.every((relayInfo) => checkAlgoRelay(relayInfo)) + const subRequests: { + urls: string[] + filter: Omit & { limit: number } + }[] = [] + if (filterType === 'you' && author && pubkey && pubkey !== author) { + const [myRelayList, targetRelayList] = await Promise.all([ + client.fetchRelayList(pubkey), + client.fetchRelayList(author) + ]) + subRequests.push({ + urls: myRelayList.write.concat(BIG_RELAY_URLS).slice(0, 5), + filter: { + kinds: [kinds.ShortTextNote, kinds.Repost], + authors: [pubkey], + '#p': [author], + limit: LIMIT + } + }) + subRequests.push({ + urls: targetRelayList.write.concat(BIG_RELAY_URLS).slice(0, 5), + filter: { + kinds: [kinds.ShortTextNote, kinds.Repost], + authors: [author], + '#p': [pubkey], + limit: LIMIT + } + }) + } else { + if (needCheckAlgoRelay) { + const relayInfos = await relayInfoService.getRelayInfos(relayUrls) + areAlgoRelays = relayInfos.every((relayInfo) => checkAlgoRelay(relayInfo)) + } + const _filter = { + ...filter, + kinds: + filterType === 'pictures' + ? [ExtendedKind.PICTURE] + : [kinds.ShortTextNote, kinds.Repost], + limit: areAlgoRelays ? ALGO_LIMIT : LIMIT + } + if (relayUrls.length === 0 && (_filter.authors?.length || author)) { + if (!_filter.authors?.length) { + _filter.authors = [author!] + } + + // If many websocket connections are initiated simultaneously, it will be + // very slow on Safari (for unknown reason) + if ((_filter.authors?.length ?? 0) > 5 && isSafari()) { + if (!pubkey) { + subRequests.push({ urls: BIG_RELAY_URLS, filter: _filter }) + } else { + const relayList = await client.fetchRelayList(pubkey) + const urls = relayList.read.concat(BIG_RELAY_URLS).slice(0, 5) + subRequests.push({ urls, filter: _filter }) + } + } else { + const relayLists = await client.fetchRelayLists(_filter.authors) + const group: Record> = {} + relayLists.forEach((relayList, index) => { + relayList.write.slice(0, 4).forEach((url) => { + if (!group[url]) { + group[url] = new Set() + } + group[url].add(_filter.authors![index]) + }) + }) + + const relayCount = Object.keys(group).length + const coveredCount = new Map() + Object.entries(group) + .sort(([, a], [, b]) => b.size - a.size) + .forEach(([url, pubkeys]) => { + if ( + relayCount > 10 && + pubkeys.size < 10 && + Array.from(pubkeys).every((pubkey) => (coveredCount.get(pubkey) ?? 0) >= 2) + ) { + delete group[url] + } else { + pubkeys.forEach((pubkey) => { + coveredCount.set(pubkey, (coveredCount.get(pubkey) ?? 0) + 1) + }) + } + }) + + subRequests.push( + ...Object.entries(group).map(([url, authors]) => ({ + urls: [url], + filter: { ..._filter, authors: Array.from(authors) } + })) + ) + } + } else { + subRequests.push({ urls: relayUrls, filter: _filter }) + } } const { closer, timelineKey } = await client.subscribeTimeline( - [...relayUrls], - { ...noteFilter, limit: areAlgoRelays ? ALGO_LIMIT : LIMIT }, + subRequests, { onEvents: (events, eosed) => { if (events.length > 0) { @@ -118,7 +227,7 @@ export default function NoteList({ return () => { promise.then((closer) => closer()) } - }, [JSON.stringify(relayUrls), noteFilter, refreshCount]) + }, [JSON.stringify(relayUrls), filterType, refreshCount]) useEffect(() => { const options = { @@ -168,29 +277,49 @@ export default function NoteList({ observerInstance.unobserve(currentBottomRef) } } - }, [timelineKey, loading, hasMore, events, noteFilter, showCount]) + }, [timelineKey, loading, hasMore, events, filterType, showCount]) const showNewEvents = () => { - topRef.current?.scrollIntoView({ behavior: 'smooth', block: 'end' }) setEvents((oldEvents) => [...newEvents, ...oldEvents]) setNewEvents([]) + setTimeout(() => { + topRef.current?.scrollIntoView({ behavior: 'smooth', block: 'start' }) + }, 0) } return (
- { - setListMode(listMode) + { + setListMode(listMode as TNoteListMode) setShowCount(SHOW_COUNT) - topRef.current?.scrollIntoView({ behavior: 'instant', block: 'end' }) - storage.setNoteListMode(listMode) + if (isMainFeed) { + storage.setNoteListMode(listMode as TNoteListMode) + } + setTimeout(() => { + topRef.current?.scrollIntoView({ behavior: 'smooth', block: 'start' }) + }, 0) }} /> -
{filteredNewEvents.length > 0 && ( )} +
{ setRefreshCount((count) => count + 1) @@ -198,8 +327,8 @@ export default function NoteList({ }} pullingContent="" > -
- {isPictures ? ( +
+ {listMode === 'pictures' ? ( - +
) : events.length ? (
@@ -240,117 +369,3 @@ export default function NoteList({
) } - -function ListModeSwitch({ - listMode, - setListMode -}: { - listMode: TNoteListMode - setListMode: (listMode: TNoteListMode) => void -}) { - const { t } = useTranslation() - const { deepBrowsing, lastScrollTop } = useDeepBrowsing() - - return ( -
800 ? '-translate-y-[calc(100%+12rem)]' : '' - )} - > -
-
setListMode('posts')} - > - {t('Notes')} -
-
setListMode('postsAndReplies')} - > - {t('Replies')} -
-
setListMode('pictures')} - > - {t('Pictures')} -
-
-
-
-
-
- ) -} - -function PictureNoteCardMasonry({ - events, - columnCount, - className -}: { - events: Event[] - columnCount: 2 | 3 - className?: string -}) { - const columns = useMemo(() => { - const newColumns: ReactNode[][] = Array.from({ length: columnCount }, () => []) - events.forEach((event, i) => { - newColumns[i % columnCount].push( - - ) - }) - return newColumns - }, [events, columnCount]) - - return ( -
- {columns.map((column, i) => ( -
- {column} -
- ))} -
- ) -} - -function LoadingSkeleton({ isPictures }: { isPictures: boolean }) { - const { t } = useTranslation() - - if (isPictures) { - return
{t('loading...')}
- } - - return ( -
-
- -
-
- -
-
- -
-
-
-
-
- -
-
- -
-
-
- ) -} diff --git a/src/components/NotificationList/index.tsx b/src/components/NotificationList/index.tsx index 04dd5ac9..605fe6ca 100644 --- a/src/components/NotificationList/index.tsx +++ b/src/components/NotificationList/index.tsx @@ -1,9 +1,7 @@ import { Separator } from '@/components/ui/separator' import { Skeleton } from '@/components/ui/skeleton' import { BIG_RELAY_URLS, ExtendedKind } from '@/constants' -import { cn } from '@/lib/utils' import { usePrimaryPage } from '@/PageManager' -import { useDeepBrowsing } from '@/providers/DeepBrowsingProvider' import { useNostr } from '@/providers/NostrProvider' import { useNoteStats } from '@/providers/NoteStatsProvider' import { useNotification } from '@/providers/NotificationProvider' @@ -14,6 +12,7 @@ import { Event, kinds } from 'nostr-tools' import { forwardRef, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import PullToRefresh from 'react-simple-pull-to-refresh' +import TabSwitcher from '../TabSwitch' import { NotificationItem } from './NotificationItem' const LIMIT = 100 @@ -76,12 +75,16 @@ const NotificationList = forwardRef((_, ref) => { const relayList = await client.fetchRelayList(pubkey) const { closer, timelineKey } = await client.subscribeTimeline( - relayList.read.length > 0 ? relayList.read.slice(0, 5) : BIG_RELAY_URLS, - { - '#p': [pubkey], - kinds: filterKinds, - limit: LIMIT - }, + [ + { + urls: relayList.read.length > 0 ? relayList.read.slice(0, 5) : BIG_RELAY_URLS, + filter: { + '#p': [pubkey], + kinds: filterKinds, + limit: LIMIT + } + } + ], { onEvents: (events, eosed) => { if (events.length > 0) { @@ -186,11 +189,17 @@ const NotificationList = forwardRef((_, ref) => { return (
- { + { setShowCount(SHOW_COUNT) - setNotificationType(type) + setNotificationType(type as TNotificationType) }} /> { }) NotificationList.displayName = 'NotificationList' export default NotificationList - -function NotificationTypeSwitch({ - type, - setType -}: { - type: TNotificationType - setType: (type: TNotificationType) => void -}) { - const { t } = useTranslation() - const { deepBrowsing, lastScrollTop } = useDeepBrowsing() - - return ( -
800 ? '-translate-y-[calc(100%+12rem)]' : '' - )} - > -
-
setType('all')} - > - {t('All')} -
-
setType('mentions')} - > - {t('Mentions')} -
-
setType('reactions')} - > - {t('Reactions')} -
-
setType('zaps')} - > - {t('Zaps')} -
-
-
-
-
-
- ) -} diff --git a/src/components/PictureNoteCardMasonry/index.tsx b/src/components/PictureNoteCardMasonry/index.tsx new file mode 100644 index 00000000..8026ef14 --- /dev/null +++ b/src/components/PictureNoteCardMasonry/index.tsx @@ -0,0 +1,40 @@ +import { cn } from '@/lib/utils' +import { Event } from 'nostr-tools' +import { useMemo } from 'react' +import PictureNoteCard from '../PictureNoteCard' + +export function PictureNoteCardMasonry({ + events, + columnCount, + className +}: { + events: Event[] + columnCount: 2 | 3 + className?: string +}) { + const columns = useMemo(() => { + const newColumns: React.ReactNode[][] = Array.from({ length: columnCount }, () => []) + events.forEach((event, i) => { + newColumns[i % columnCount].push( + + ) + }) + return newColumns + }, [events, columnCount]) + + return ( +
+ {columns.map((column, i) => ( +
+ {column} +
+ ))} +
+ ) +} diff --git a/src/components/ReplyNoteList/index.tsx b/src/components/ReplyNoteList/index.tsx index c0bd5f16..cff613fa 100644 --- a/src/components/ReplyNoteList/index.tsx +++ b/src/components/ReplyNoteList/index.tsx @@ -97,12 +97,16 @@ export default function ReplyNoteList({ const seenOn = client.getSeenEventRelayUrls(rootInfo.id) relayUrls.unshift(...seenOn) const { closer, timelineKey } = await client.subscribeTimeline( - relayUrls.slice(0, 5), - { - '#e': [rootInfo.id], - kinds: [kinds.ShortTextNote], - limit: LIMIT - }, + [ + { + urls: relayUrls.slice(0, 5), + filter: { + '#e': [rootInfo.id], + kinds: [kinds.ShortTextNote], + limit: LIMIT + } + } + ], { onEvents: (evts, eosed) => { if (evts.length > 0) { diff --git a/src/components/ShowNewButton/index.tsx b/src/components/ShowNewButton/index.tsx new file mode 100644 index 00000000..7b6aabe2 --- /dev/null +++ b/src/components/ShowNewButton/index.tsx @@ -0,0 +1,22 @@ +import { Button } from '@/components/ui/button' +import { cn } from '@/lib/utils' +import { useDeepBrowsing } from '@/providers/DeepBrowsingProvider' +import { useTranslation } from 'react-i18next' + +export function ShowNewButton({ onClick }: { onClick: () => void }) { + const { t } = useTranslation() + const { deepBrowsing, lastScrollTop } = useDeepBrowsing() + + return ( +
800 ? '-translate-y-10' : '' + )} + > + +
+ ) +} diff --git a/src/components/TabSwitch/index.tsx b/src/components/TabSwitch/index.tsx new file mode 100644 index 00000000..d09cad2c --- /dev/null +++ b/src/components/TabSwitch/index.tsx @@ -0,0 +1,64 @@ +import { cn } from '@/lib/utils' +import { useDeepBrowsing } from '@/providers/DeepBrowsingProvider' +import { useTranslation } from 'react-i18next' + +type TabDefinition = { + value: string + label: string + onClick?: () => void +} + +export default function TabSwitcher({ + tabs, + value, + className, + onTabChange +}: { + tabs: TabDefinition[] + value: string + className?: string + onTabChange?: (tab: string) => void +}) { + const { t } = useTranslation() + const { deepBrowsing, lastScrollTop } = useDeepBrowsing() + const activeIndex = tabs.findIndex((tab) => tab.value === value) + + return ( +
800 ? '-translate-y-[calc(100%+12rem)]' : '', + className + )} + > +
+ {tabs.map((tab) => ( +
{ + tab.onClick?.() + onTabChange?.(tab.value) + }} + > + {t(tab.label)} +
+ ))} +
+
+
= 0 ? activeIndex * (100 / tabs.length) : 0}%` + }} + > +
+
+
+
+ ) +} diff --git a/src/i18n/locales/ar.ts b/src/i18n/locales/ar.ts index e50e0483..94c444b5 100644 --- a/src/i18n/locales/ar.ts +++ b/src/i18n/locales/ar.ts @@ -217,6 +217,7 @@ export default { 'Choose a relay': 'اختر ريلاي', 'no relays found': 'لم يتم العثور على ريلايات', video: 'فيديو', - 'Show n new notes': 'عرض {{n}} ملاحظات جديدة' + 'Show n new notes': 'عرض {{n}} ملاحظات جديدة', + YouTabName: 'أنت' } } diff --git a/src/i18n/locales/de.ts b/src/i18n/locales/de.ts index c6e70715..c66b71fc 100644 --- a/src/i18n/locales/de.ts +++ b/src/i18n/locales/de.ts @@ -221,6 +221,7 @@ export default { 'Choose a relay': 'Wähle ein Relay', 'no relays found': 'Keine Relays gefunden', video: 'Video', - 'Show n new notes': 'Zeige {{n}} neue Notizen' + 'Show n new notes': 'Zeige {{n}} neue Notizen', + YouTabName: 'Du' } } diff --git a/src/i18n/locales/en.ts b/src/i18n/locales/en.ts index 95f7a505..c844bc01 100644 --- a/src/i18n/locales/en.ts +++ b/src/i18n/locales/en.ts @@ -217,6 +217,7 @@ export default { 'Choose a relay': 'Choose a relay', 'no relays found': 'no relays found', video: 'video', - 'Show n new notes': 'Show {{n}} new notes' + 'Show n new notes': 'Show {{n}} new notes', + YouTabName: 'You' } } diff --git a/src/i18n/locales/es.ts b/src/i18n/locales/es.ts index c3617d04..ca20d777 100644 --- a/src/i18n/locales/es.ts +++ b/src/i18n/locales/es.ts @@ -221,6 +221,7 @@ export default { 'Choose a relay': 'Selecciona un relé', 'no relays found': 'no se encontraron relés', video: 'video', - 'Show n new notes': 'Mostrar {{n}} nuevas notas' + 'Show n new notes': 'Mostrar {{n}} nuevas notas', + YouTabName: 'You' } } diff --git a/src/i18n/locales/fr.ts b/src/i18n/locales/fr.ts index 10009bf6..cc10200b 100644 --- a/src/i18n/locales/fr.ts +++ b/src/i18n/locales/fr.ts @@ -220,6 +220,7 @@ export default { 'Choose a relay': 'Choisir un relais', 'no relays found': 'aucun relais trouvé', video: 'vidéo', - 'Show n new notes': 'Afficher {{n}} nouvelles notes' + 'Show n new notes': 'Afficher {{n}} nouvelles notes', + YouTabName: 'Vous' } } diff --git a/src/i18n/locales/it.ts b/src/i18n/locales/it.ts index eb03741a..485d93db 100644 --- a/src/i18n/locales/it.ts +++ b/src/i18n/locales/it.ts @@ -220,6 +220,7 @@ export default { 'Choose a relay': 'Scegli un relay', 'no relays found': 'Nessun relay trovato', video: 'video', - 'Show n new notes': 'Mostra {{n}} nuove note' + 'Show n new notes': 'Mostra {{n}} nuove note', + YouTabName: 'Tu' } } diff --git a/src/i18n/locales/ja.ts b/src/i18n/locales/ja.ts index ee125e7d..441b93de 100644 --- a/src/i18n/locales/ja.ts +++ b/src/i18n/locales/ja.ts @@ -218,6 +218,7 @@ export default { 'Choose a relay': 'リレイを選択', 'no relays found': 'リレイが見つかりません', video: 'ビデオ', - 'Show n new notes': '新しいノートを{{n}}件表示' + 'Show n new notes': '新しいノートを{{n}}件表示', + YouTabName: 'あなた' } } diff --git a/src/i18n/locales/pl.ts b/src/i18n/locales/pl.ts index 2d7a3ae4..34e3a18c 100644 --- a/src/i18n/locales/pl.ts +++ b/src/i18n/locales/pl.ts @@ -219,6 +219,7 @@ export default { 'Choose a relay': 'Wybierz transmiter', 'no relays found': 'Nie znaleziono transmiterów', video: 'wideo', - 'Show n new notes': 'Pokaż {{n}} nowych wpisów' + 'Show n new notes': 'Pokaż {{n}} nowych wpisów', + YouTabName: 'Ty' } } diff --git a/src/i18n/locales/pt-BR.ts b/src/i18n/locales/pt-BR.ts index 7c7ba70e..cdea7a5b 100644 --- a/src/i18n/locales/pt-BR.ts +++ b/src/i18n/locales/pt-BR.ts @@ -219,6 +219,7 @@ export default { 'Choose a relay': 'Escolher um relé', 'no relays found': 'nenhum relé encontrado', video: 'vídeo', - 'Show n new notes': 'Mostrar {{n}} novas notas' + 'Show n new notes': 'Mostrar {{n}} novas notas', + YouTabName: 'Você' } } diff --git a/src/i18n/locales/pt-PT.ts b/src/i18n/locales/pt-PT.ts index 9bbb7ed8..0c35107f 100644 --- a/src/i18n/locales/pt-PT.ts +++ b/src/i18n/locales/pt-PT.ts @@ -220,6 +220,7 @@ export default { 'Choose a relay': 'Escolher um Relé', 'no relays found': 'nenhum relé encontrado', video: 'vídeo', - 'Show n new notes': 'Mostrar {{n}} novas notas' + 'Show n new notes': 'Mostrar {{n}} novas notas', + YouTabName: 'Você' } } diff --git a/src/i18n/locales/ru.ts b/src/i18n/locales/ru.ts index 12e8c2e2..043042dc 100644 --- a/src/i18n/locales/ru.ts +++ b/src/i18n/locales/ru.ts @@ -221,6 +221,7 @@ export default { 'Choose a relay': 'Выберите ретранслятор', 'no relays found': 'ретрансляторы не найдены', video: 'видео', - 'Show n new notes': 'Показать {{n}} новых заметок' + 'Show n new notes': 'Показать {{n}} новых заметок', + YouTabName: 'Вы' } } diff --git a/src/i18n/locales/zh.ts b/src/i18n/locales/zh.ts index fac4742f..1112d09c 100644 --- a/src/i18n/locales/zh.ts +++ b/src/i18n/locales/zh.ts @@ -218,6 +218,7 @@ export default { 'Choose a relay': '选择一个服务器', 'no relays found': '未找到服务器', video: '视频', - 'Show n new notes': '显示 {{n}} 条新笔记' + 'Show n new notes': '显示 {{n}} 条新笔记', + YouTabName: '与你' } } diff --git a/src/pages/primary/ExplorePage/index.tsx b/src/pages/primary/ExplorePage/index.tsx index fd0f677b..530ebc6a 100644 --- a/src/pages/primary/ExplorePage/index.tsx +++ b/src/pages/primary/ExplorePage/index.tsx @@ -1,8 +1,7 @@ import FollowingFavoriteRelayList from '@/components/FollowingFavoriteRelayList' import RelayList from '@/components/RelayList' +import TabSwitcher from '@/components/TabSwitch' import PrimaryPageLayout from '@/layouts/PrimaryPageLayout' -import { cn } from '@/lib/utils' -import { useDeepBrowsing } from '@/providers/DeepBrowsingProvider' import { Compass } from 'lucide-react' import { forwardRef, useState } from 'react' import { useTranslation } from 'react-i18next' @@ -19,7 +18,14 @@ const ExplorePage = forwardRef((_, ref) => { titlebar={} displayScrollToTopButton > - + setTab(tab as TExploreTabs)} + /> {tab === 'following' ? : } ) @@ -37,43 +43,3 @@ function ExplorePageTitlebar() {
) } - -function Tabs({ - value, - setValue -}: { - value: TExploreTabs - setValue: (value: TExploreTabs) => void -}) { - const { t } = useTranslation() - const { deepBrowsing, lastScrollTop } = useDeepBrowsing() - - return ( -
800 ? '-translate-y-[calc(100%+12rem)]' : '' - )} - > -
-
setValue('following')} - > - {t("Following's Favorites")} -
-
setValue('all')} - > - {t('All')} -
-
-
-
-
-
- ) -} diff --git a/src/pages/primary/NoteListPage/index.tsx b/src/pages/primary/NoteListPage/index.tsx index 7a8b482d..79eee4d6 100644 --- a/src/pages/primary/NoteListPage/index.tsx +++ b/src/pages/primary/NoteListPage/index.tsx @@ -41,6 +41,7 @@ const NoteListPage = forwardRef((_, ref) => { relayUrls={relayUrls} filter={filter} needCheckAlgoRelay={feedInfo.feedType !== 'following'} + isMainFeed /> ) } diff --git a/src/pages/secondary/ProfilePage/index.tsx b/src/pages/secondary/ProfilePage/index.tsx index 9e701572..4b2208a0 100644 --- a/src/pages/secondary/ProfilePage/index.tsx +++ b/src/pages/secondary/ProfilePage/index.tsx @@ -144,7 +144,7 @@ const ProfilePage = forwardRef(({ id, index }: { id?: string; index?: number },
- + ) }) diff --git a/src/services/client.service.ts b/src/services/client.service.ts index b2eac4dc..9846ddf5 100644 --- a/src/services/client.service.ts +++ b/src/services/client.service.ts @@ -3,7 +3,6 @@ import { getProfileFromProfileEvent, getRelayListFromRelayListEvent } from '@/li import { formatPubkey, userIdToPubkey } from '@/lib/pubkey' import { extractPubkeysFromEventTags } from '@/lib/tag' import { isLocalNetworkUrl, isWebsocketUrl, normalizeUrl } from '@/lib/url' -import { isSafari } from '@/lib/utils' import { ISigner, TProfile, TRelayList } from '@/types' import { sha256 } from '@noble/hashes/sha2' import DataLoader from 'dataloader' @@ -157,9 +156,17 @@ class ClientService extends EventTarget { return hashArray.map((b) => b.toString(16).padStart(2, '0')).join('') } + private generateMultipleTimelinesKey(subRequests: { urls: string[]; filter: Filter }[]) { + const keys = subRequests.map(({ urls, filter }) => this.generateTimelineKey(urls, filter)) + const encoder = new TextEncoder() + const data = encoder.encode(JSON.stringify(keys.sort())) + const hashBuffer = sha256(data) + const hashArray = Array.from(new Uint8Array(hashBuffer)) + return hashArray.map((b) => b.toString(16).padStart(2, '0')).join('') + } + async subscribeTimeline( - urls: string[], - { authors, ...filter }: Omit & { limit: number }, + subRequests: { urls: string[]; filter: Omit & { limit: number } }[], { onEvents, onNew @@ -175,65 +182,6 @@ class ClientService extends EventTarget { needSort?: boolean } = {} ) { - if (urls.length || !authors?.length) { - return this._subscribeTimeline( - urls.length ? urls : BIG_RELAY_URLS, - filter, - { onEvents, onNew }, - { startLogin, needSort } - ) - } - - const subRequests: { urls: string[]; authors: string[] }[] = [] - // If many websocket connections are initiated simultaneously, it will be - // very slow on Safari (for unknown reason) - if (authors.length > 5 && isSafari()) { - const pubkey = await this.signer?.getPublicKey() - if (!pubkey) { - subRequests.push({ urls: BIG_RELAY_URLS, authors }) - } else { - const relayList = await this.fetchRelayList(pubkey) - const urls = relayList.read.concat(BIG_RELAY_URLS).slice(0, 5) - subRequests.push({ urls, authors }) - } - } else { - const relayLists = await this.fetchRelayLists(authors) - const group: Record> = {} - relayLists.forEach((relayList, index) => { - relayList.write.slice(0, 4).forEach((url) => { - if (!group[url]) { - group[url] = new Set() - } - group[url].add(authors[index]) - }) - }) - - const relayCount = Object.keys(group).length - const coveredCount = new Map() - Object.entries(group) - .sort(([, a], [, b]) => b.size - a.size) - .forEach(([url, pubkeys]) => { - if ( - relayCount > 10 && - pubkeys.size < 10 && - Array.from(pubkeys).every((pubkey) => (coveredCount.get(pubkey) ?? 0) >= 2) - ) { - delete group[url] - } else { - pubkeys.forEach((pubkey) => { - coveredCount.set(pubkey, (coveredCount.get(pubkey) ?? 0) + 1) - }) - } - }) - - subRequests.push( - ...Object.entries(group).map(([url, authors]) => ({ - urls: [url], - authors: Array.from(authors) - })) - ) - } - const newEventIdSet = new Set() const requestCount = subRequests.length let eventIdSet = new Set() @@ -241,10 +189,10 @@ class ClientService extends EventTarget { let eosedCount = 0 const subs = await Promise.all( - subRequests.map(({ urls, authors }) => { + subRequests.map(({ urls, filter }) => { return this._subscribeTimeline( urls, - { ...filter, authors }, + filter, { onEvents: (_events, _eosed) => { if (_eosed) { @@ -265,12 +213,12 @@ class ClientService extends EventTarget { onNew(evt) } }, - { startLogin } + { startLogin, needSort } ) }) ) - const key = this.generateTimelineKey([], { ...filter, authors }) + const key = this.generateMultipleTimelinesKey(subRequests) this.timelines[key] = subs.map((sub) => sub.timelineKey) return { diff --git a/src/types.ts b/src/types.ts index 9d436df9..fdc8b9ae 100644 --- a/src/types.ts +++ b/src/types.ts @@ -101,7 +101,7 @@ export type TLanguage = 'en' | 'zh' | 'pl' export type TImageInfo = { url: string; blurHash?: string; dim?: { width: number; height: number } } -export type TNoteListMode = 'posts' | 'postsAndReplies' | 'pictures' +export type TNoteListMode = 'posts' | 'postsAndReplies' | 'pictures' | 'you' export type TNotificationType = 'all' | 'mentions' | 'reactions' | 'zaps'