|
|
|
@ -1,11 +1,9 @@ |
|
|
|
|
|
|
|
import NewNotesButton from '@/components/NewNotesButton' |
|
|
|
import { Button } from '@/components/ui/button' |
|
|
|
import { Button } from '@/components/ui/button' |
|
|
|
import { Skeleton } from '@/components/ui/skeleton' |
|
|
|
import { BIG_RELAY_URLS, ExtendedKind } from '@/constants' |
|
|
|
import { ExtendedKind } from '@/constants' |
|
|
|
|
|
|
|
import { isReplyNoteEvent } from '@/lib/event' |
|
|
|
import { isReplyNoteEvent } from '@/lib/event' |
|
|
|
import { checkAlgoRelay } from '@/lib/relay' |
|
|
|
import { checkAlgoRelay } from '@/lib/relay' |
|
|
|
import { cn } from '@/lib/utils' |
|
|
|
import { isSafari } from '@/lib/utils' |
|
|
|
import NewNotesButton from '@/components/NewNotesButton' |
|
|
|
|
|
|
|
import { useDeepBrowsing } from '@/providers/DeepBrowsingProvider' |
|
|
|
|
|
|
|
import { useMuteList } from '@/providers/MuteListProvider' |
|
|
|
import { useMuteList } from '@/providers/MuteListProvider' |
|
|
|
import { useNostr } from '@/providers/NostrProvider' |
|
|
|
import { useNostr } from '@/providers/NostrProvider' |
|
|
|
import { useScreenSize } from '@/providers/ScreenSizeProvider' |
|
|
|
import { useScreenSize } from '@/providers/ScreenSizeProvider' |
|
|
|
@ -15,11 +13,12 @@ import relayInfoService from '@/services/relay-info.service' |
|
|
|
import { TNoteListMode } from '@/types' |
|
|
|
import { TNoteListMode } from '@/types' |
|
|
|
import dayjs from 'dayjs' |
|
|
|
import dayjs from 'dayjs' |
|
|
|
import { Event, Filter, kinds } from 'nostr-tools' |
|
|
|
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 { useTranslation } from 'react-i18next' |
|
|
|
import PullToRefresh from 'react-simple-pull-to-refresh' |
|
|
|
import PullToRefresh from 'react-simple-pull-to-refresh' |
|
|
|
import NoteCard from '../NoteCard' |
|
|
|
import NoteCard, { NoteCardLoadingSkeleton } from '../NoteCard' |
|
|
|
import PictureNoteCard from '../PictureNoteCard' |
|
|
|
import { PictureNoteCardMasonry } from '../PictureNoteCardMasonry' |
|
|
|
|
|
|
|
import TabSwitcher from '../TabSwitch' |
|
|
|
|
|
|
|
|
|
|
|
const LIMIT = 100 |
|
|
|
const LIMIT = 100 |
|
|
|
const ALGO_LIMIT = 500 |
|
|
|
const ALGO_LIMIT = 500 |
|
|
|
@ -28,19 +27,23 @@ const SHOW_COUNT = 10 |
|
|
|
export default function NoteList({ |
|
|
|
export default function NoteList({ |
|
|
|
relayUrls = [], |
|
|
|
relayUrls = [], |
|
|
|
filter = {}, |
|
|
|
filter = {}, |
|
|
|
|
|
|
|
author, |
|
|
|
className, |
|
|
|
className, |
|
|
|
filterMutedNotes = true, |
|
|
|
filterMutedNotes = true, |
|
|
|
needCheckAlgoRelay = false |
|
|
|
needCheckAlgoRelay = false, |
|
|
|
|
|
|
|
isMainFeed = false |
|
|
|
}: { |
|
|
|
}: { |
|
|
|
relayUrls?: string[] |
|
|
|
relayUrls?: string[] |
|
|
|
filter?: Filter |
|
|
|
filter?: Filter |
|
|
|
|
|
|
|
author?: string |
|
|
|
className?: string |
|
|
|
className?: string |
|
|
|
filterMutedNotes?: boolean |
|
|
|
filterMutedNotes?: boolean |
|
|
|
needCheckAlgoRelay?: boolean |
|
|
|
needCheckAlgoRelay?: boolean |
|
|
|
|
|
|
|
isMainFeed?: boolean |
|
|
|
}) { |
|
|
|
}) { |
|
|
|
const { t } = useTranslation() |
|
|
|
const { t } = useTranslation() |
|
|
|
const { isLargeScreen } = useScreenSize() |
|
|
|
const { isLargeScreen } = useScreenSize() |
|
|
|
const { startLogin } = useNostr() |
|
|
|
const { pubkey, startLogin } = useNostr() |
|
|
|
const { mutePubkeys } = useMuteList() |
|
|
|
const { mutePubkeys } = useMuteList() |
|
|
|
const [refreshCount, setRefreshCount] = useState(0) |
|
|
|
const [refreshCount, setRefreshCount] = useState(0) |
|
|
|
const [timelineKey, setTimelineKey] = useState<string | undefined>(undefined) |
|
|
|
const [timelineKey, setTimelineKey] = useState<string | undefined>(undefined) |
|
|
|
@ -49,15 +52,11 @@ export default function NoteList({ |
|
|
|
const [showCount, setShowCount] = useState(SHOW_COUNT) |
|
|
|
const [showCount, setShowCount] = useState(SHOW_COUNT) |
|
|
|
const [hasMore, setHasMore] = useState<boolean>(true) |
|
|
|
const [hasMore, setHasMore] = useState<boolean>(true) |
|
|
|
const [loading, setLoading] = useState(true) |
|
|
|
const [loading, setLoading] = useState(true) |
|
|
|
const [listMode, setListMode] = useState<TNoteListMode>(() => storage.getNoteListMode()) |
|
|
|
const [listMode, setListMode] = useState<TNoteListMode>(() => |
|
|
|
|
|
|
|
isMainFeed ? storage.getNoteListMode() : 'posts' |
|
|
|
|
|
|
|
) |
|
|
|
|
|
|
|
const [filterType, setFilterType] = useState<Exclude<TNoteListMode, 'postsAndReplies'>>('posts') |
|
|
|
const bottomRef = useRef<HTMLDivElement | null>(null) |
|
|
|
const bottomRef = useRef<HTMLDivElement | null>(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<HTMLDivElement | null>(null) |
|
|
|
const topRef = useRef<HTMLDivElement | null>(null) |
|
|
|
const filteredNewEvents = useMemo(() => { |
|
|
|
const filteredNewEvents = useMemo(() => { |
|
|
|
return newEvents.filter((event: Event) => { |
|
|
|
return newEvents.filter((event: Event) => { |
|
|
|
@ -69,7 +68,26 @@ export default function NoteList({ |
|
|
|
}, [newEvents, listMode, filterMutedNotes, mutePubkeys]) |
|
|
|
}, [newEvents, listMode, filterMutedNotes, mutePubkeys]) |
|
|
|
|
|
|
|
|
|
|
|
useEffect(() => { |
|
|
|
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() { |
|
|
|
async function init() { |
|
|
|
setLoading(true) |
|
|
|
setLoading(true) |
|
|
|
@ -78,14 +96,105 @@ export default function NoteList({ |
|
|
|
setHasMore(true) |
|
|
|
setHasMore(true) |
|
|
|
|
|
|
|
|
|
|
|
let areAlgoRelays = false |
|
|
|
let areAlgoRelays = false |
|
|
|
if (needCheckAlgoRelay) { |
|
|
|
const subRequests: { |
|
|
|
const relayInfos = await relayInfoService.getRelayInfos(relayUrls) |
|
|
|
urls: string[] |
|
|
|
areAlgoRelays = relayInfos.every((relayInfo) => checkAlgoRelay(relayInfo)) |
|
|
|
filter: Omit<Filter, 'since' | 'until'> & { 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<string, Set<string>> = {} |
|
|
|
|
|
|
|
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<string, number>() |
|
|
|
|
|
|
|
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( |
|
|
|
const { closer, timelineKey } = await client.subscribeTimeline( |
|
|
|
[...relayUrls], |
|
|
|
subRequests, |
|
|
|
{ ...noteFilter, limit: areAlgoRelays ? ALGO_LIMIT : LIMIT }, |
|
|
|
|
|
|
|
{ |
|
|
|
{ |
|
|
|
onEvents: (events, eosed) => { |
|
|
|
onEvents: (events, eosed) => { |
|
|
|
if (events.length > 0) { |
|
|
|
if (events.length > 0) { |
|
|
|
@ -118,7 +227,7 @@ export default function NoteList({ |
|
|
|
return () => { |
|
|
|
return () => { |
|
|
|
promise.then((closer) => closer()) |
|
|
|
promise.then((closer) => closer()) |
|
|
|
} |
|
|
|
} |
|
|
|
}, [JSON.stringify(relayUrls), noteFilter, refreshCount]) |
|
|
|
}, [JSON.stringify(relayUrls), filterType, refreshCount]) |
|
|
|
|
|
|
|
|
|
|
|
useEffect(() => { |
|
|
|
useEffect(() => { |
|
|
|
const options = { |
|
|
|
const options = { |
|
|
|
@ -168,29 +277,49 @@ export default function NoteList({ |
|
|
|
observerInstance.unobserve(currentBottomRef) |
|
|
|
observerInstance.unobserve(currentBottomRef) |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
}, [timelineKey, loading, hasMore, events, noteFilter, showCount]) |
|
|
|
}, [timelineKey, loading, hasMore, events, filterType, showCount]) |
|
|
|
|
|
|
|
|
|
|
|
const showNewEvents = () => { |
|
|
|
const showNewEvents = () => { |
|
|
|
topRef.current?.scrollIntoView({ behavior: 'smooth', block: 'end' }) |
|
|
|
|
|
|
|
setEvents((oldEvents) => [...newEvents, ...oldEvents]) |
|
|
|
setEvents((oldEvents) => [...newEvents, ...oldEvents]) |
|
|
|
setNewEvents([]) |
|
|
|
setNewEvents([]) |
|
|
|
|
|
|
|
setTimeout(() => { |
|
|
|
|
|
|
|
topRef.current?.scrollIntoView({ behavior: 'smooth', block: 'start' }) |
|
|
|
|
|
|
|
}, 0) |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
return ( |
|
|
|
return ( |
|
|
|
<div className={className}> |
|
|
|
<div className={className}> |
|
|
|
<ListModeSwitch |
|
|
|
<TabSwitcher |
|
|
|
listMode={listMode} |
|
|
|
value={listMode} |
|
|
|
setListMode={(listMode) => { |
|
|
|
tabs={ |
|
|
|
setListMode(listMode) |
|
|
|
pubkey && author && pubkey !== author |
|
|
|
|
|
|
|
? [ |
|
|
|
|
|
|
|
{ value: 'posts', label: 'Notes' }, |
|
|
|
|
|
|
|
{ value: 'postsAndReplies', label: 'Replies' }, |
|
|
|
|
|
|
|
{ value: 'pictures', label: 'Pictures' }, |
|
|
|
|
|
|
|
{ value: 'you', label: 'YouTabName' } |
|
|
|
|
|
|
|
] |
|
|
|
|
|
|
|
: [ |
|
|
|
|
|
|
|
{ value: 'posts', label: 'Notes' }, |
|
|
|
|
|
|
|
{ value: 'postsAndReplies', label: 'Replies' }, |
|
|
|
|
|
|
|
{ value: 'pictures', label: 'Pictures' } |
|
|
|
|
|
|
|
] |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
onTabChange={(listMode) => { |
|
|
|
|
|
|
|
setListMode(listMode as TNoteListMode) |
|
|
|
setShowCount(SHOW_COUNT) |
|
|
|
setShowCount(SHOW_COUNT) |
|
|
|
topRef.current?.scrollIntoView({ behavior: 'instant', block: 'end' }) |
|
|
|
if (isMainFeed) { |
|
|
|
storage.setNoteListMode(listMode) |
|
|
|
storage.setNoteListMode(listMode as TNoteListMode) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
setTimeout(() => { |
|
|
|
|
|
|
|
topRef.current?.scrollIntoView({ behavior: 'smooth', block: 'start' }) |
|
|
|
|
|
|
|
}, 0) |
|
|
|
}} |
|
|
|
}} |
|
|
|
/> |
|
|
|
/> |
|
|
|
<div ref={topRef} /> |
|
|
|
|
|
|
|
{filteredNewEvents.length > 0 && ( |
|
|
|
{filteredNewEvents.length > 0 && ( |
|
|
|
<NewNotesButton newEvents={filteredNewEvents} onClick={showNewEvents} /> |
|
|
|
<NewNotesButton newEvents={filteredNewEvents} onClick={showNewEvents} /> |
|
|
|
)} |
|
|
|
)} |
|
|
|
|
|
|
|
<div ref={topRef} className="scroll-mt-24" /> |
|
|
|
<PullToRefresh |
|
|
|
<PullToRefresh |
|
|
|
onRefresh={async () => { |
|
|
|
onRefresh={async () => { |
|
|
|
setRefreshCount((count) => count + 1) |
|
|
|
setRefreshCount((count) => count + 1) |
|
|
|
@ -198,8 +327,8 @@ export default function NoteList({ |
|
|
|
}} |
|
|
|
}} |
|
|
|
pullingContent="" |
|
|
|
pullingContent="" |
|
|
|
> |
|
|
|
> |
|
|
|
<div> |
|
|
|
<div className="min-h-screen"> |
|
|
|
{isPictures ? ( |
|
|
|
{listMode === 'pictures' ? ( |
|
|
|
<PictureNoteCardMasonry |
|
|
|
<PictureNoteCardMasonry |
|
|
|
className="px-2 sm:px-4 mt-2" |
|
|
|
className="px-2 sm:px-4 mt-2" |
|
|
|
columnCount={isLargeScreen ? 3 : 2} |
|
|
|
columnCount={isLargeScreen ? 3 : 2} |
|
|
|
@ -222,7 +351,7 @@ export default function NoteList({ |
|
|
|
)} |
|
|
|
)} |
|
|
|
{hasMore || loading ? ( |
|
|
|
{hasMore || loading ? ( |
|
|
|
<div ref={bottomRef}> |
|
|
|
<div ref={bottomRef}> |
|
|
|
<LoadingSkeleton isPictures={isPictures} /> |
|
|
|
<NoteCardLoadingSkeleton isPictures={listMode === 'pictures'} /> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
) : events.length ? ( |
|
|
|
) : events.length ? ( |
|
|
|
<div className="text-center text-sm text-muted-foreground mt-2"> |
|
|
|
<div className="text-center text-sm text-muted-foreground mt-2"> |
|
|
|
@ -240,117 +369,3 @@ export default function NoteList({ |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
) |
|
|
|
) |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
function ListModeSwitch({ |
|
|
|
|
|
|
|
listMode, |
|
|
|
|
|
|
|
setListMode |
|
|
|
|
|
|
|
}: { |
|
|
|
|
|
|
|
listMode: TNoteListMode |
|
|
|
|
|
|
|
setListMode: (listMode: TNoteListMode) => void |
|
|
|
|
|
|
|
}) { |
|
|
|
|
|
|
|
const { t } = useTranslation() |
|
|
|
|
|
|
|
const { deepBrowsing, lastScrollTop } = useDeepBrowsing() |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return ( |
|
|
|
|
|
|
|
<div |
|
|
|
|
|
|
|
className={cn( |
|
|
|
|
|
|
|
'sticky top-12 bg-background z-30 duration-700 transition-transform select-none', |
|
|
|
|
|
|
|
deepBrowsing && lastScrollTop > 800 ? '-translate-y-[calc(100%+12rem)]' : '' |
|
|
|
|
|
|
|
)} |
|
|
|
|
|
|
|
> |
|
|
|
|
|
|
|
<div className="flex"> |
|
|
|
|
|
|
|
<div |
|
|
|
|
|
|
|
className={`w-1/3 text-center py-2 font-semibold clickable cursor-pointer rounded-lg ${listMode === 'posts' ? '' : 'text-muted-foreground'}`} |
|
|
|
|
|
|
|
onClick={() => setListMode('posts')} |
|
|
|
|
|
|
|
> |
|
|
|
|
|
|
|
{t('Notes')} |
|
|
|
|
|
|
|
</div> |
|
|
|
|
|
|
|
<div |
|
|
|
|
|
|
|
className={`w-1/3 text-center py-2 font-semibold clickable cursor-pointer rounded-lg ${listMode === 'postsAndReplies' ? '' : 'text-muted-foreground'}`} |
|
|
|
|
|
|
|
onClick={() => setListMode('postsAndReplies')} |
|
|
|
|
|
|
|
> |
|
|
|
|
|
|
|
{t('Replies')} |
|
|
|
|
|
|
|
</div> |
|
|
|
|
|
|
|
<div |
|
|
|
|
|
|
|
className={`w-1/3 text-center py-2 font-semibold clickable cursor-pointer rounded-lg ${listMode === 'pictures' ? '' : 'text-muted-foreground'}`} |
|
|
|
|
|
|
|
onClick={() => setListMode('pictures')} |
|
|
|
|
|
|
|
> |
|
|
|
|
|
|
|
{t('Pictures')} |
|
|
|
|
|
|
|
</div> |
|
|
|
|
|
|
|
</div> |
|
|
|
|
|
|
|
<div |
|
|
|
|
|
|
|
className={`w-1/3 px-4 sm:px-6 transition-transform duration-500 ${listMode === 'postsAndReplies' ? 'translate-x-full' : listMode === 'pictures' ? 'translate-x-[200%]' : ''} `} |
|
|
|
|
|
|
|
> |
|
|
|
|
|
|
|
<div className="w-full h-1 bg-primary rounded-full" /> |
|
|
|
|
|
|
|
</div> |
|
|
|
|
|
|
|
</div> |
|
|
|
|
|
|
|
) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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( |
|
|
|
|
|
|
|
<PictureNoteCard key={event.id} className="w-full" event={event} /> |
|
|
|
|
|
|
|
) |
|
|
|
|
|
|
|
}) |
|
|
|
|
|
|
|
return newColumns |
|
|
|
|
|
|
|
}, [events, columnCount]) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return ( |
|
|
|
|
|
|
|
<div |
|
|
|
|
|
|
|
className={cn( |
|
|
|
|
|
|
|
'grid', |
|
|
|
|
|
|
|
columnCount === 2 ? 'grid-cols-2 gap-2' : 'grid-cols-3 gap-4', |
|
|
|
|
|
|
|
className |
|
|
|
|
|
|
|
)} |
|
|
|
|
|
|
|
> |
|
|
|
|
|
|
|
{columns.map((column, i) => ( |
|
|
|
|
|
|
|
<div key={i} className={columnCount === 2 ? 'space-y-2' : 'space-y-4'}> |
|
|
|
|
|
|
|
{column} |
|
|
|
|
|
|
|
</div> |
|
|
|
|
|
|
|
))} |
|
|
|
|
|
|
|
</div> |
|
|
|
|
|
|
|
) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function LoadingSkeleton({ isPictures }: { isPictures: boolean }) { |
|
|
|
|
|
|
|
const { t } = useTranslation() |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (isPictures) { |
|
|
|
|
|
|
|
return <div className="text-center text-sm text-muted-foreground">{t('loading...')}</div> |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return ( |
|
|
|
|
|
|
|
<div className="px-4 py-3"> |
|
|
|
|
|
|
|
<div className="flex items-center space-x-2"> |
|
|
|
|
|
|
|
<Skeleton className="w-10 h-10 rounded-full" /> |
|
|
|
|
|
|
|
<div className={`flex-1 w-0`}> |
|
|
|
|
|
|
|
<div className="py-1"> |
|
|
|
|
|
|
|
<Skeleton className="h-4 w-16" /> |
|
|
|
|
|
|
|
</div> |
|
|
|
|
|
|
|
<div className="py-0.5"> |
|
|
|
|
|
|
|
<Skeleton className="h-3 w-12" /> |
|
|
|
|
|
|
|
</div> |
|
|
|
|
|
|
|
</div> |
|
|
|
|
|
|
|
</div> |
|
|
|
|
|
|
|
<div className="pt-2"> |
|
|
|
|
|
|
|
<div className="my-1"> |
|
|
|
|
|
|
|
<Skeleton className="w-full h-4 my-1 mt-2" /> |
|
|
|
|
|
|
|
</div> |
|
|
|
|
|
|
|
<div className="my-1"> |
|
|
|
|
|
|
|
<Skeleton className="w-2/3 h-4 my-1" /> |
|
|
|
|
|
|
|
</div> |
|
|
|
|
|
|
|
</div> |
|
|
|
|
|
|
|
</div> |
|
|
|
|
|
|
|
) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|