style: 🎨

This commit is contained in:
codytseng
2025-04-24 22:23:22 +08:00
parent d3d5842804
commit 51913a5163
6 changed files with 310 additions and 310 deletions

View File

@@ -1,78 +1,78 @@
import { useToast } from '@/hooks'
import { useBookmarks } from '@/providers/BookmarksProvider'
import { useNostr } from '@/providers/NostrProvider'
import { BookmarkIcon, Loader } from 'lucide-react'
import { useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { Event } from 'nostr-tools'
export default function BookmarkButton({ event }: { event: Event }) {
const { t } = useTranslation()
const { toast } = useToast()
const { pubkey: accountPubkey, bookmarkListEvent, checkLogin } = useNostr()
const { addBookmark, removeBookmark } = useBookmarks()
const [updating, setUpdating] = useState(false)
const isBookmarked = useMemo(
() => bookmarkListEvent?.tags.some((tag) => tag[0] === 'e' && tag[1] === event.id),
[bookmarkListEvent, event]
)
if (!accountPubkey) return null
const handleBookmark = async (e: React.MouseEvent) => {
e.stopPropagation()
checkLogin(async () => {
if (isBookmarked) return
setUpdating(true)
try {
await addBookmark(event)
} catch (error) {
toast({
title: t('Bookmark failed'),
description: (error as Error).message,
variant: 'destructive'
})
} finally {
setUpdating(false)
}
})
}
const handleRemoveBookmark = async (e: React.MouseEvent) => {
e.stopPropagation()
checkLogin(async () => {
if (!isBookmarked) return
setUpdating(true)
try {
await removeBookmark(event)
} catch (error) {
toast({
title: t('Remove bookmark failed'),
description: (error as Error).message,
variant: 'destructive'
})
} finally {
setUpdating(false)
}
})
}
return (
<button
className={`flex items-center gap-1 ${
isBookmarked ? 'text-rose-400' : 'text-muted-foreground'
} enabled:hover:text-rose-400 px-3 h-full`}
onClick={isBookmarked ? handleRemoveBookmark : handleBookmark}
disabled={updating}
title={isBookmarked ? t('Remove bookmark') : t('Bookmark')}
>
{updating ? (
<Loader className="animate-spin" />
) : (
<BookmarkIcon className={isBookmarked ? 'fill-rose-400' : ''} />
)}
</button>
)
}
import { useToast } from '@/hooks'
import { useBookmarks } from '@/providers/BookmarksProvider'
import { useNostr } from '@/providers/NostrProvider'
import { BookmarkIcon, Loader } from 'lucide-react'
import { useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { Event } from 'nostr-tools'
export default function BookmarkButton({ event }: { event: Event }) {
const { t } = useTranslation()
const { toast } = useToast()
const { pubkey: accountPubkey, bookmarkListEvent, checkLogin } = useNostr()
const { addBookmark, removeBookmark } = useBookmarks()
const [updating, setUpdating] = useState(false)
const isBookmarked = useMemo(
() => bookmarkListEvent?.tags.some((tag) => tag[0] === 'e' && tag[1] === event.id),
[bookmarkListEvent, event]
)
if (!accountPubkey) return null
const handleBookmark = async (e: React.MouseEvent) => {
e.stopPropagation()
checkLogin(async () => {
if (isBookmarked) return
setUpdating(true)
try {
await addBookmark(event)
} catch (error) {
toast({
title: t('Bookmark failed'),
description: (error as Error).message,
variant: 'destructive'
})
} finally {
setUpdating(false)
}
})
}
const handleRemoveBookmark = async (e: React.MouseEvent) => {
e.stopPropagation()
checkLogin(async () => {
if (!isBookmarked) return
setUpdating(true)
try {
await removeBookmark(event)
} catch (error) {
toast({
title: t('Remove bookmark failed'),
description: (error as Error).message,
variant: 'destructive'
})
} finally {
setUpdating(false)
}
})
}
return (
<button
className={`flex items-center gap-1 ${
isBookmarked ? 'text-rose-400' : 'text-muted-foreground'
} enabled:hover:text-rose-400 px-3 h-full`}
onClick={isBookmarked ? handleRemoveBookmark : handleBookmark}
disabled={updating}
title={isBookmarked ? t('Remove bookmark') : t('Bookmark')}
>
{updating ? (
<Loader className="animate-spin" />
) : (
<BookmarkIcon className={isBookmarked ? 'fill-rose-400' : ''} />
)}
</button>
)
}

View File

@@ -1,97 +1,97 @@
import { useFetchEvent } from '@/hooks'
import { generateEventIdFromETag } from '@/lib/tag'
import { useNostr } from '@/providers/NostrProvider'
import { kinds } from 'nostr-tools'
import { useEffect, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import NoteCard, { NoteCardLoadingSkeleton } from '../NoteCard'
const SHOW_COUNT = 10
export default function BookmarkList() {
const { t } = useTranslation()
const { bookmarkListEvent } = useNostr()
const eventIds = useMemo(() => {
if (!bookmarkListEvent) return []
return (
bookmarkListEvent.tags
.map((tag) => (tag[0] === 'e' ? generateEventIdFromETag(tag) : undefined))
.filter(Boolean) as `nevent1${string}`[]
).reverse()
}, [bookmarkListEvent])
const [showCount, setShowCount] = useState(SHOW_COUNT)
const bottomRef = useRef<HTMLDivElement | null>(null)
useEffect(() => {
const options = {
root: null,
rootMargin: '10px',
threshold: 0.1
}
const loadMore = () => {
if (showCount < eventIds.length) {
setShowCount((prev) => prev + SHOW_COUNT)
}
}
const observerInstance = new IntersectionObserver((entries) => {
if (entries[0].isIntersecting) {
loadMore()
}
}, options)
const currentBottomRef = bottomRef.current
if (currentBottomRef) {
observerInstance.observe(currentBottomRef)
}
return () => {
if (observerInstance && currentBottomRef) {
observerInstance.unobserve(currentBottomRef)
}
}
}, [showCount, eventIds])
if (eventIds.length === 0) {
return (
<div className="mt-2 text-sm text-center text-muted-foreground">
{t('no bookmarks found')}
</div>
)
}
return (
<div>
{eventIds.slice(0, showCount).map((eventId) => (
<BookmarkedNote key={eventId} eventId={eventId} />
))}
{showCount < eventIds.length ? (
<div ref={bottomRef}>
<NoteCardLoadingSkeleton isPictures={false} />
</div>
) : (
<div className="text-center text-sm text-muted-foreground mt-2">
{t('no more bookmarks')}
</div>
)}
</div>
)
}
function BookmarkedNote({ eventId }: { eventId: string }) {
const { event, isFetching } = useFetchEvent(eventId)
if (isFetching) {
return <NoteCardLoadingSkeleton isPictures={false} />
}
if (!event || event.kind !== kinds.ShortTextNote) {
return null
}
return <NoteCard event={event} className="w-full" />
}
import { useFetchEvent } from '@/hooks'
import { generateEventIdFromETag } from '@/lib/tag'
import { useNostr } from '@/providers/NostrProvider'
import { kinds } from 'nostr-tools'
import { useEffect, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import NoteCard, { NoteCardLoadingSkeleton } from '../NoteCard'
const SHOW_COUNT = 10
export default function BookmarkList() {
const { t } = useTranslation()
const { bookmarkListEvent } = useNostr()
const eventIds = useMemo(() => {
if (!bookmarkListEvent) return []
return (
bookmarkListEvent.tags
.map((tag) => (tag[0] === 'e' ? generateEventIdFromETag(tag) : undefined))
.filter(Boolean) as `nevent1${string}`[]
).reverse()
}, [bookmarkListEvent])
const [showCount, setShowCount] = useState(SHOW_COUNT)
const bottomRef = useRef<HTMLDivElement | null>(null)
useEffect(() => {
const options = {
root: null,
rootMargin: '10px',
threshold: 0.1
}
const loadMore = () => {
if (showCount < eventIds.length) {
setShowCount((prev) => prev + SHOW_COUNT)
}
}
const observerInstance = new IntersectionObserver((entries) => {
if (entries[0].isIntersecting) {
loadMore()
}
}, options)
const currentBottomRef = bottomRef.current
if (currentBottomRef) {
observerInstance.observe(currentBottomRef)
}
return () => {
if (observerInstance && currentBottomRef) {
observerInstance.unobserve(currentBottomRef)
}
}
}, [showCount, eventIds])
if (eventIds.length === 0) {
return (
<div className="mt-2 text-sm text-center text-muted-foreground">
{t('no bookmarks found')}
</div>
)
}
return (
<div>
{eventIds.slice(0, showCount).map((eventId) => (
<BookmarkedNote key={eventId} eventId={eventId} />
))}
{showCount < eventIds.length ? (
<div ref={bottomRef}>
<NoteCardLoadingSkeleton isPictures={false} />
</div>
) : (
<div className="text-center text-sm text-muted-foreground mt-2">
{t('no more bookmarks')}
</div>
)}
</div>
)
}
function BookmarkedNote({ eventId }: { eventId: string }) {
const { event, isFetching } = useFetchEvent(eventId)
if (isFetching) {
return <NoteCardLoadingSkeleton isPictures={false} />
}
if (!event || event.kind !== kinds.ShortTextNote) {
return null
}
return <NoteCard event={event} className="w-full" />
}

View File

@@ -1,68 +1,68 @@
import { Button } from '@/components/ui/button'
import { SimpleUserAvatar } from '@/components/UserAvatar'
import { cn } from '@/lib/utils'
import { useScreenSize } from '@/providers/ScreenSizeProvider'
import { Event } from 'nostr-tools'
import { useMemo } from 'react'
import { useTranslation } from 'react-i18next'
export default function NewNotesButton({
newEvents = [],
onClick
}: {
newEvents?: Event[]
onClick?: () => void
}) {
const { t } = useTranslation()
const { isSmallScreen } = useScreenSize()
const pubkeys = useMemo(() => {
const arr: string[] = []
for (const event of newEvents) {
if (!arr.includes(event.pubkey)) {
arr.push(event.pubkey)
}
if (arr.length >= 3) break
}
return arr
}, [newEvents])
return (
<>
{newEvents.length > 0 && (
<div
className={cn(
'w-full flex justify-center z-40 pointer-events-none',
isSmallScreen ? 'fixed' : 'absolute bottom-4'
)}
style={isSmallScreen ? { bottom: 'calc(4rem + env(safe-area-inset-bottom))' } : undefined}
>
<Button
onClick={onClick}
className="group rounded-full h-fit pl-2 pr-3 hover:bg-primary-hover pointer-events-auto"
>
{pubkeys.length > 0 && (
<div className="flex items-center">
{pubkeys.map((pubkey, index) => (
<div
key={pubkey}
className="relative -mr-2.5 last:mr-0"
style={{ zIndex: 3 - index }}
>
<SimpleUserAvatar
userId={pubkey}
size="small"
className="border-primary border-2 group-hover:border-primary-hover"
/>
</div>
))}
</div>
)}
<div className="text-md font-medium">
{t('Show n new notes', { n: newEvents.length > 99 ? '99+' : newEvents.length })}
</div>
</Button>
</div>
)}
</>
)
}
import { Button } from '@/components/ui/button'
import { SimpleUserAvatar } from '@/components/UserAvatar'
import { cn } from '@/lib/utils'
import { useScreenSize } from '@/providers/ScreenSizeProvider'
import { Event } from 'nostr-tools'
import { useMemo } from 'react'
import { useTranslation } from 'react-i18next'
export default function NewNotesButton({
newEvents = [],
onClick
}: {
newEvents?: Event[]
onClick?: () => void
}) {
const { t } = useTranslation()
const { isSmallScreen } = useScreenSize()
const pubkeys = useMemo(() => {
const arr: string[] = []
for (const event of newEvents) {
if (!arr.includes(event.pubkey)) {
arr.push(event.pubkey)
}
if (arr.length >= 3) break
}
return arr
}, [newEvents])
return (
<>
{newEvents.length > 0 && (
<div
className={cn(
'w-full flex justify-center z-40 pointer-events-none',
isSmallScreen ? 'fixed' : 'absolute bottom-4'
)}
style={isSmallScreen ? { bottom: 'calc(4rem + env(safe-area-inset-bottom))' } : undefined}
>
<Button
onClick={onClick}
className="group rounded-full h-fit pl-2 pr-3 hover:bg-primary-hover pointer-events-auto"
>
{pubkeys.length > 0 && (
<div className="flex items-center">
{pubkeys.map((pubkey, index) => (
<div
key={pubkey}
className="relative -mr-2.5 last:mr-0"
style={{ zIndex: 3 - index }}
>
<SimpleUserAvatar
userId={pubkey}
size="small"
className="border-primary border-2 group-hover:border-primary-hover"
/>
</div>
))}
</div>
)}
<div className="text-md font-medium">
{t('Show n new notes', { n: newEvents.length > 99 ? '99+' : newEvents.length })}
</div>
</Button>
</div>
)}
</>
)
}

View File

@@ -67,7 +67,7 @@ export default function Likes({ event }: { event: Event }) {
like(key, emoji)
}}
>
{liking === key ? <Loader className="animate-spin size-5" /> : <Emoji emoji={emoji} />}
{liking === key ? <Loader className="animate-spin size-4" /> : <Emoji emoji={emoji} />}
<div className="text-sm">{pubkeys.size}</div>
</div>
))}