feat: 💨
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
|
import { Skeleton } from '@/components/ui/skeleton'
|
||||||
import { PICTURE_EVENT_KIND } from '@/constants'
|
import { PICTURE_EVENT_KIND } from '@/constants'
|
||||||
import { isReplyNoteEvent } from '@/lib/event'
|
import { isReplyNoteEvent } from '@/lib/event'
|
||||||
import { checkAlgoRelay } from '@/lib/relay'
|
import { checkAlgoRelay } from '@/lib/relay'
|
||||||
@@ -187,14 +188,14 @@ export default function NoteList({
|
|||||||
}}
|
}}
|
||||||
pullingContent=""
|
pullingContent=""
|
||||||
>
|
>
|
||||||
<div className="space-y-2 sm:space-y-2">
|
<div>
|
||||||
{newEvents.filter((event: Event) => {
|
{newEvents.filter((event: Event) => {
|
||||||
return (
|
return (
|
||||||
(!filterMutedNotes || !mutePubkeys.includes(event.pubkey)) &&
|
(!filterMutedNotes || !mutePubkeys.includes(event.pubkey)) &&
|
||||||
(listMode !== 'posts' || !isReplyNoteEvent(event))
|
(listMode !== 'posts' || !isReplyNoteEvent(event))
|
||||||
)
|
)
|
||||||
}).length > 0 && (
|
}).length > 0 && (
|
||||||
<div className="flex justify-center w-full mt-2">
|
<div className="flex justify-center w-full my-2">
|
||||||
<Button size="lg" onClick={showNewEvents}>
|
<Button size="lg" onClick={showNewEvents}>
|
||||||
{t('show new notes')}
|
{t('show new notes')}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -202,7 +203,7 @@ export default function NoteList({
|
|||||||
)}
|
)}
|
||||||
{isPictures ? (
|
{isPictures ? (
|
||||||
<PictureNoteCardMasonry
|
<PictureNoteCardMasonry
|
||||||
className="px-2 sm:px-4 pt-2"
|
className="px-2 sm:px-4 mt-2"
|
||||||
columnCount={isLargeScreen ? 3 : 2}
|
columnCount={isLargeScreen ? 3 : 2}
|
||||||
events={events}
|
events={events}
|
||||||
/>
|
/>
|
||||||
@@ -220,11 +221,12 @@ export default function NoteList({
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="text-center text-sm text-muted-foreground">
|
|
||||||
{hasMore || refreshing ? (
|
{hasMore || refreshing ? (
|
||||||
<div ref={bottomRef}>{t('loading...')}</div>
|
<div ref={bottomRef}>
|
||||||
|
<LoadingSkeleton isPictures={isPictures} />
|
||||||
|
</div>
|
||||||
) : events.length ? (
|
) : events.length ? (
|
||||||
t('no more notes')
|
<div className="text-center text-sm text-muted-foreground mt-2">t('no more notes')</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex justify-center w-full mt-2">
|
<div className="flex justify-center w-full mt-2">
|
||||||
<Button size="lg" onClick={() => setRefreshCount((pre) => pre + 1)}>
|
<Button size="lg" onClick={() => setRefreshCount((pre) => pre + 1)}>
|
||||||
@@ -233,7 +235,6 @@ export default function NoteList({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</PullToRefresh>
|
</PullToRefresh>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
@@ -320,3 +321,45 @@ function PictureNoteCardMasonry({
|
|||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function LoadingSkeleton({ isPictures }: { isPictures: boolean }) {
|
||||||
|
const { isLargeScreen } = useScreenSize()
|
||||||
|
|
||||||
|
if (isPictures) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'px-2 sm:px-4 grid',
|
||||||
|
isLargeScreen ? 'grid-cols-3 gap-4' : 'grid-cols-2 gap-2'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{[...Array(isLargeScreen ? 3 : 2)].map((_, i) => (
|
||||||
|
<div key={i}>
|
||||||
|
<Skeleton className="rounded-lg w-full aspect-[6/8]" />
|
||||||
|
<div className="p-2">
|
||||||
|
<Skeleton className="w-32 h-5" />
|
||||||
|
<div className="flex items-center gap-2 mt-2">
|
||||||
|
<Skeleton className="w-5 h-5 rounded-full" />
|
||||||
|
<Skeleton className="w-16 h-3" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</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="space-y-1">
|
||||||
|
<Skeleton className="w-10 h-4" />
|
||||||
|
<Skeleton className="w-20 h-3" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Skeleton className="w-full h-5 mt-2" />
|
||||||
|
<Skeleton className="w-2/3 h-5 mt-2" />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { Skeleton } from '@/components/ui/skeleton'
|
||||||
import { COMMENT_EVENT_KIND, PICTURE_EVENT_KIND } from '@/constants'
|
import { COMMENT_EVENT_KIND, PICTURE_EVENT_KIND } from '@/constants'
|
||||||
import { useFetchEvent } from '@/hooks'
|
import { useFetchEvent } from '@/hooks'
|
||||||
import { extractEmbeddedNotesFromContent, extractImagesFromContent } from '@/lib/event'
|
import { extractEmbeddedNotesFromContent, extractImagesFromContent } from '@/lib/event'
|
||||||
@@ -9,7 +10,15 @@ import client from '@/services/client.service'
|
|||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import { Heart, MessageCircle, Repeat, ThumbsUp } from 'lucide-react'
|
import { Heart, MessageCircle, Repeat, ThumbsUp } from 'lucide-react'
|
||||||
import { Event, kinds, nip19, validateEvent } from 'nostr-tools'
|
import { Event, kinds, nip19, validateEvent } from 'nostr-tools'
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
import {
|
||||||
|
forwardRef,
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useImperativeHandle,
|
||||||
|
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 { embedded, embeddedNostrNpubRenderer, embeddedNostrProfileRenderer } from '../Embedded'
|
import { embedded, embeddedNostrNpubRenderer, embeddedNostrProfileRenderer } from '../Embedded'
|
||||||
@@ -18,7 +27,7 @@ import UserAvatar from '../UserAvatar'
|
|||||||
|
|
||||||
const LIMIT = 100
|
const LIMIT = 100
|
||||||
|
|
||||||
export default function NotificationList() {
|
const NotificationList = forwardRef((_, ref) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { pubkey } = useNostr()
|
const { pubkey } = useNostr()
|
||||||
const [refreshCount, setRefreshCount] = useState(0)
|
const [refreshCount, setRefreshCount] = useState(0)
|
||||||
@@ -27,6 +36,16 @@ export default function NotificationList() {
|
|||||||
const [notifications, setNotifications] = useState<Event[]>([])
|
const [notifications, setNotifications] = useState<Event[]>([])
|
||||||
const [until, setUntil] = useState<number | undefined>(dayjs().unix())
|
const [until, setUntil] = useState<number | undefined>(dayjs().unix())
|
||||||
const bottomRef = useRef<HTMLDivElement | null>(null)
|
const bottomRef = useRef<HTMLDivElement | null>(null)
|
||||||
|
useImperativeHandle(
|
||||||
|
ref,
|
||||||
|
() => ({
|
||||||
|
refresh: () => {
|
||||||
|
if (refreshing) return
|
||||||
|
setRefreshCount((count) => count + 1)
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
[refreshing]
|
||||||
|
)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!pubkey) {
|
if (!pubkey) {
|
||||||
@@ -133,7 +152,33 @@ export default function NotificationList() {
|
|||||||
))}
|
))}
|
||||||
<div className="text-center text-sm text-muted-foreground">
|
<div className="text-center text-sm text-muted-foreground">
|
||||||
{until || refreshing ? (
|
{until || refreshing ? (
|
||||||
<div ref={bottomRef}>{t('loading...')}</div>
|
<div ref={bottomRef}>
|
||||||
|
<div className="flex gap-2 items-center h-11 py-2">
|
||||||
|
<Skeleton className="w-7 h-7 rounded-full" />
|
||||||
|
<Skeleton className="w-6 h-6 rounded-full" />
|
||||||
|
<Skeleton className="h-6 flex-1 w-0" />
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2 items-center h-11 py-2">
|
||||||
|
<Skeleton className="w-7 h-7 rounded-full" />
|
||||||
|
<Skeleton className="w-6 h-6 rounded-full" />
|
||||||
|
<Skeleton className="h-6 flex-1 w-0" />
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2 items-center h-11 py-2">
|
||||||
|
<Skeleton className="w-7 h-7 rounded-full" />
|
||||||
|
<Skeleton className="w-6 h-6 rounded-full" />
|
||||||
|
<Skeleton className="h-6 flex-1 w-0" />
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2 items-center h-11 py-2">
|
||||||
|
<Skeleton className="w-7 h-7 rounded-full" />
|
||||||
|
<Skeleton className="w-6 h-6 rounded-full" />
|
||||||
|
<Skeleton className="h-6 flex-1 w-0" />
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2 items-center h-11 py-2">
|
||||||
|
<Skeleton className="w-7 h-7 rounded-full" />
|
||||||
|
<Skeleton className="w-6 h-6 rounded-full" />
|
||||||
|
<Skeleton className="h-6 flex-1 w-0" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
) : (
|
) : (
|
||||||
t('no more notifications')
|
t('no more notifications')
|
||||||
)}
|
)}
|
||||||
@@ -141,7 +186,9 @@ export default function NotificationList() {
|
|||||||
</div>
|
</div>
|
||||||
</PullToRefresh>
|
</PullToRefresh>
|
||||||
)
|
)
|
||||||
}
|
})
|
||||||
|
NotificationList.displayName = 'NotificationList'
|
||||||
|
export default NotificationList
|
||||||
|
|
||||||
function NotificationItem({ notification }: { notification: Event }) {
|
function NotificationItem({ notification }: { notification: Event }) {
|
||||||
if (notification.kind === kinds.Reaction) {
|
if (notification.kind === kinds.Reaction) {
|
||||||
|
|||||||
@@ -1,10 +1,22 @@
|
|||||||
import NotificationList from '@/components/NotificationList'
|
import NotificationList from '@/components/NotificationList'
|
||||||
import PrimaryPageLayout from '@/layouts/PrimaryPageLayout'
|
import PrimaryPageLayout from '@/layouts/PrimaryPageLayout'
|
||||||
|
import { usePrimaryPage } from '@/PageManager'
|
||||||
import { Bell } from 'lucide-react'
|
import { Bell } from 'lucide-react'
|
||||||
import { forwardRef } from 'react'
|
import { forwardRef, useEffect, useRef } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
const NotificationListPage = forwardRef((_, ref) => {
|
const NotificationListPage = forwardRef((_, ref) => {
|
||||||
|
const { current } = usePrimaryPage()
|
||||||
|
const firstRenderRef = useRef(true)
|
||||||
|
const notificationListRef = useRef<{ refresh: () => void }>(null)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (current === 'notifications' && !firstRenderRef.current) {
|
||||||
|
notificationListRef.current?.refresh()
|
||||||
|
}
|
||||||
|
firstRenderRef.current = false
|
||||||
|
}, [current])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PrimaryPageLayout
|
<PrimaryPageLayout
|
||||||
ref={ref}
|
ref={ref}
|
||||||
@@ -13,7 +25,7 @@ const NotificationListPage = forwardRef((_, ref) => {
|
|||||||
displayScrollToTopButton
|
displayScrollToTopButton
|
||||||
>
|
>
|
||||||
<div className="px-4">
|
<div className="px-4">
|
||||||
<NotificationList />
|
<NotificationList ref={notificationListRef} />
|
||||||
</div>
|
</div>
|
||||||
</PrimaryPageLayout>
|
</PrimaryPageLayout>
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user