polish
This commit is contained in:
@@ -1,32 +0,0 @@
|
||||
import { minidenticon } from 'minidenticons';
|
||||
import { ImgHTMLAttributes, memo, useState } from 'react';
|
||||
|
||||
export const Image = memo(function Image({
|
||||
src,
|
||||
...props
|
||||
}: ImgHTMLAttributes<HTMLImageElement>) {
|
||||
const [isError, setIsError] = useState(false);
|
||||
|
||||
if (isError || !src) {
|
||||
const svgURI =
|
||||
'data:image/svg+xml;utf8,' + encodeURIComponent(minidenticon(props.alt, 90, 50));
|
||||
return (
|
||||
<img src={svgURI} alt={props.alt} {...props} style={{ backgroundColor: '#000' }} />
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<img
|
||||
{...props}
|
||||
src={src}
|
||||
onError={({ currentTarget }) => {
|
||||
currentTarget.onerror = null;
|
||||
setIsError(true);
|
||||
}}
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
alt="lume default img"
|
||||
style={{ contentVisibility: 'auto' }}
|
||||
/>
|
||||
);
|
||||
});
|
||||
@@ -35,11 +35,11 @@ export function ArticleNote({ event }: { event: NDKEvent }) {
|
||||
<div className="mb-3 h-min w-full px-3">
|
||||
<div className="relative flex flex-col gap-2 overflow-hidden rounded-xl bg-neutral-50 pt-3 dark:bg-neutral-950">
|
||||
<User pubkey={event.pubkey} time={event.created_at} eventId={event.id} />
|
||||
<div>
|
||||
<div className="px-3">
|
||||
<Link
|
||||
to={`/notes/article/${event.id}`}
|
||||
preventScrollReset={true}
|
||||
className="flex w-full flex-col rounded-lg border border-neutral-200 bg-neutral-100 dark:border-neutral-800 dark:bg-neutral-900"
|
||||
className="flex w-full flex-col rounded-lg bg-neutral-100 dark:bg-neutral-900"
|
||||
>
|
||||
{metadata.image && (
|
||||
<img
|
||||
@@ -48,7 +48,7 @@ export function ArticleNote({ event }: { event: NDKEvent }) {
|
||||
className="h-56 w-full rounded-t-lg object-cover"
|
||||
/>
|
||||
)}
|
||||
<div className="flex flex-col gap-1 rounded-b-lg bg-neutral-200 px-3 py-3 dark:bg-neutral-800">
|
||||
<div className="flex flex-col gap-1 rounded-b-lg rounded-t-lg bg-neutral-100 px-3 py-3 dark:bg-neutral-900">
|
||||
<h5 className="break-all font-semibold text-neutral-900 dark:text-neutral-100">
|
||||
{metadata.title}
|
||||
</h5>
|
||||
|
||||
@@ -6,7 +6,6 @@ export * from './child';
|
||||
export * from './notify';
|
||||
export * from './unknown';
|
||||
export * from './skeleton';
|
||||
export * from './stats';
|
||||
export * from './actions';
|
||||
export * from './actions/reaction';
|
||||
export * from './actions/reply';
|
||||
|
||||
@@ -3,7 +3,7 @@ import { download } from '@tauri-apps/plugin-upload';
|
||||
import { SyntheticEvent } from 'react';
|
||||
import Zoom from 'react-medium-image-zoom';
|
||||
|
||||
import { DownloadIcon } from '@shared/icons';
|
||||
import { CancelIcon, DownloadIcon } from '@shared/icons';
|
||||
|
||||
export function ImagePreview({ url }: { url: string }) {
|
||||
const downloadImage = async (url: string) => {
|
||||
@@ -17,7 +17,7 @@ export function ImagePreview({ url }: { url: string }) {
|
||||
};
|
||||
|
||||
return (
|
||||
<Zoom key={url} zoomMargin={50}>
|
||||
<Zoom key={url} zoomMargin={50} IconUnzoom={() => <CancelIcon className="h-4 w-4" />}>
|
||||
<div className="group relative mt-2">
|
||||
<img
|
||||
src={url}
|
||||
|
||||
@@ -49,7 +49,7 @@ export function LinkPreview({ url }: { url: string }) {
|
||||
<img
|
||||
src={data.image}
|
||||
alt={url}
|
||||
className="h-44 w-full rounded-t-lg bg-white object-cover"
|
||||
className="h-48 w-full rounded-t-lg bg-white object-cover"
|
||||
/>
|
||||
) : null}
|
||||
<div className="flex flex-col items-start px-3 py-3">
|
||||
|
||||
@@ -12,25 +12,15 @@ export function Reply({ event, root }: { event: NDKEventWithReplies; root?: stri
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<User pubkey={event.pubkey} time={event.created_at} eventId={event.id} />
|
||||
<MemoizedTextKind content={event.content} />
|
||||
<div className="-ml-1">
|
||||
<NoteActions
|
||||
id={event.id}
|
||||
pubkey={event.pubkey}
|
||||
root={root}
|
||||
extraButtons={false}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="pl-4">
|
||||
<Collapsible.Root open={open} onOpenChange={setOpen}>
|
||||
{event.replies?.length > 0 ? (
|
||||
<div>
|
||||
<Collapsible.Root open={open} onOpenChange={setOpen}>
|
||||
<div className="rounded-xl bg-neutral-50 dark:bg-neutral-950">
|
||||
<div className="flex flex-col gap-2 pt-3">
|
||||
<User pubkey={event.pubkey} time={event.created_at} eventId={event.id} />
|
||||
<MemoizedTextKind content={event.content} />
|
||||
<div className="-ml-1 flex items-center justify-between">
|
||||
{event.replies?.length > 0 ? (
|
||||
<Collapsible.Trigger asChild>
|
||||
<div className="inline-flex h-10 items-center gap-1 font-semibold text-blue-500">
|
||||
<div className="ml-4 inline-flex h-14 items-center gap-1 font-semibold text-blue-500">
|
||||
<NavArrowDownIcon
|
||||
className={twMerge('h-3 w-3', open ? 'rotate-180 transform' : '')}
|
||||
/>
|
||||
@@ -39,13 +29,23 @@ export function Reply({ event, root }: { event: NDKEventWithReplies; root?: stri
|
||||
(event.replies?.length === 1 ? 'reply' : 'replies')}
|
||||
</div>
|
||||
</Collapsible.Trigger>
|
||||
<Collapsible.Content>
|
||||
{event.replies?.map((sub) => <SubReply key={sub.id} event={sub} />)}
|
||||
</Collapsible.Content>
|
||||
</div>
|
||||
) : null}
|
||||
<NoteActions
|
||||
id={event.id}
|
||||
pubkey={event.pubkey}
|
||||
root={root}
|
||||
extraButtons={false}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className={twMerge('px-3', open ? 'pb-3' : '')}>
|
||||
{event.replies?.length > 0 ? (
|
||||
<Collapsible.Content>
|
||||
{event.replies?.map((sub) => <SubReply key={sub.id} event={sub} />)}
|
||||
</Collapsible.Content>
|
||||
) : null}
|
||||
</Collapsible.Root>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Collapsible.Root>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -48,6 +48,7 @@ export function ReplyList({ eventId }: { eventId: string }) {
|
||||
|
||||
return (
|
||||
<div className="mt-3 flex flex-col gap-5">
|
||||
<h3 className="font-semibold">Replies</h3>
|
||||
{data?.length === 0 ? (
|
||||
<div className="mt-2 flex w-full items-center justify-center">
|
||||
<div className="flex flex-col items-center justify-center gap-2 py-6">
|
||||
|
||||
@@ -5,7 +5,7 @@ import { User } from '@shared/user';
|
||||
|
||||
export function SubReply({ event }: { event: NDKEvent }) {
|
||||
return (
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex flex-col gap-2 rounded-lg bg-neutral-100 pt-3 dark:bg-neutral-900">
|
||||
<User pubkey={event.pubkey} time={event.created_at} eventId={event.id} />
|
||||
<MemoizedTextKind content={event.content} />
|
||||
<div className="-ml-1">
|
||||
|
||||
@@ -1,85 +0,0 @@
|
||||
import { NDKEvent, NDKFilter } from '@nostr-dev-kit/ndk';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { decode } from 'light-bolt11-decoder';
|
||||
|
||||
import { useNDK } from '@libs/ndk/provider';
|
||||
|
||||
import { LoaderIcon } from '@shared/icons';
|
||||
|
||||
import { compactNumber } from '@utils/number';
|
||||
|
||||
export function NoteStats({ id }: { id: string }) {
|
||||
const { ndk } = useNDK();
|
||||
const { status, data } = useQuery({
|
||||
queryKey: ['note-stats', id],
|
||||
queryFn: async () => {
|
||||
let reactions = 0;
|
||||
let reposts = 0;
|
||||
let zaps = 0;
|
||||
|
||||
const filter: NDKFilter = {
|
||||
'#e': [id],
|
||||
kinds: [6, 7, 9735],
|
||||
};
|
||||
|
||||
const events = await ndk.fetchEvents(filter);
|
||||
events.forEach((event: NDKEvent) => {
|
||||
switch (event.kind) {
|
||||
case 6:
|
||||
reposts += 1;
|
||||
break;
|
||||
case 7:
|
||||
reactions += 1;
|
||||
break;
|
||||
case 9735: {
|
||||
const bolt11 = event.tags.find((tag) => tag[0] === 'bolt11')[1];
|
||||
if (bolt11) {
|
||||
const decoded = decode(bolt11);
|
||||
const amount = decoded.sections.find((item) => item.name === 'amount');
|
||||
const sats = amount.value / 1000;
|
||||
zaps += sats;
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
return { reposts, reactions, zaps };
|
||||
},
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnReconnect: false,
|
||||
});
|
||||
|
||||
if (status === 'pending') {
|
||||
return (
|
||||
<div className="flex h-11 items-center">
|
||||
<LoaderIcon className="h-4 w-4 animate-spin text-white" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mt-3 flex w-full flex-wrap gap-2">
|
||||
<div className="flex flex-1 flex-col rounded-lg bg-neutral-100 px-3 py-2 dark:bg-neutral-900">
|
||||
<div className="text-lg font-semibold text-neutral-900 dark:text-neutral-100">
|
||||
{compactNumber.format(data.reactions)}
|
||||
</div>
|
||||
<div className="text-sm text-neutral-500 dark:text-neutral-300">Reactions</div>
|
||||
</div>
|
||||
<div className="flex flex-1 flex-col rounded-lg bg-neutral-100 px-3 py-2 dark:bg-neutral-900">
|
||||
<div className="text-lg font-semibold text-neutral-900 dark:text-neutral-100">
|
||||
{compactNumber.format(data.reposts)}
|
||||
</div>
|
||||
<div className="text-sm text-neutral-500 dark:text-neutral-300">Reposts</div>
|
||||
</div>
|
||||
<div className="flex flex-1 flex-col rounded-lg bg-neutral-100 px-3 py-2 dark:bg-neutral-900">
|
||||
<div className="text-lg font-semibold text-neutral-900 dark:text-neutral-100">
|
||||
{compactNumber.format(data.zaps)}
|
||||
</div>
|
||||
<div className="text-sm text-neutral-500 dark:text-neutral-300">Zaps</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -298,7 +298,7 @@ export const User = memo(function User({
|
||||
alt={pubkey}
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
className="h-10 w-10 rounded-lg"
|
||||
className="h-10 w-10 rounded-lg object-cover"
|
||||
/>
|
||||
<Avatar.Fallback delayMs={300}>
|
||||
<img
|
||||
|
||||
@@ -3,8 +3,6 @@ import { useEffect, useState } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
import { UserStats } from '@app/users/components/stats';
|
||||
|
||||
import { useNDK } from '@libs/ndk/provider';
|
||||
import { useStorage } from '@libs/storage/provider';
|
||||
|
||||
@@ -71,10 +69,9 @@ export function UserProfile({ pubkey }: { pubkey: string }) {
|
||||
<img
|
||||
src={user?.picture || user?.image}
|
||||
alt={pubkey}
|
||||
className="h-12 w-12 shrink-0 rounded-lg"
|
||||
className="h-12 w-12 shrink-0 rounded-lg object-cover"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
style={{ contentVisibility: 'auto' }}
|
||||
/>
|
||||
<div className="inline-flex items-center gap-2">
|
||||
{followed ? (
|
||||
@@ -102,7 +99,7 @@ export function UserProfile({ pubkey }: { pubkey: string }) {
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-2 flex flex-1 flex-col">
|
||||
<div className="mt-2 flex flex-1 flex-col gap-1.5">
|
||||
<div className="flex flex-col">
|
||||
<h5 className="text-lg font-semibold">
|
||||
{user?.name || user?.display_name || user?.displayName || 'Anon'}
|
||||
@@ -119,11 +116,8 @@ export function UserProfile({ pubkey }: { pubkey: string }) {
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-col gap-3">
|
||||
<p className="mb-3 mt-2 max-w-[500px] select-text break-words text-neutral-900 dark:text-neutral-100">
|
||||
{user?.about}
|
||||
</p>
|
||||
<UserStats pubkey={pubkey} />
|
||||
<div className="max-w-[500px] select-text break-words text-neutral-900 dark:text-neutral-100">
|
||||
{user?.about}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -17,6 +17,8 @@ import { TitleBar } from '@shared/titleBar';
|
||||
import { WidgetWrapper } from '@shared/widgets';
|
||||
import { LiveUpdater } from '@shared/widgets';
|
||||
|
||||
import { FETCH_LIMIT } from '@stores/constants';
|
||||
|
||||
export function NewsfeedWidget() {
|
||||
const { db } = useStorage();
|
||||
const { relayUrls, ndk, fetcher } = useNDK();
|
||||
@@ -40,7 +42,7 @@ export function NewsfeedWidget() {
|
||||
kinds: [NDKKind.Text, NDKKind.Repost],
|
||||
authors: db.account.circles,
|
||||
},
|
||||
20,
|
||||
FETCH_LIMIT,
|
||||
{ asOf: pageParam === 0 ? undefined : pageParam, abortSignal: signal }
|
||||
);
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@ import { useNDK } from '@libs/ndk/provider';
|
||||
import { useStorage } from '@libs/storage/provider';
|
||||
|
||||
import { FollowIcon, UnfollowIcon } from '@shared/icons';
|
||||
import { Image } from '@shared/image';
|
||||
|
||||
import { compactNumber } from '@utils/number';
|
||||
import { shortenKey } from '@utils/shortenKey';
|
||||
@@ -95,8 +94,9 @@ export function NostrBandUserProfile({ data }: { data: Profile }) {
|
||||
<div className="rounded-xl bg-neutral-100 px-5 py-5 dark:bg-neutral-900">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="inline-flex items-center gap-2">
|
||||
<Image
|
||||
<img
|
||||
src={profile.picture}
|
||||
alt={data.pubkey}
|
||||
className="h-11 w-11 shrink-0 rounded-lg object-cover"
|
||||
/>
|
||||
<div className="inline-flex flex-col">
|
||||
@@ -128,12 +128,10 @@ export function NostrBandUserProfile({ data }: { data: Profile }) {
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-2">
|
||||
<p className="whitespace-pre-line break-words text-neutral-900 dark:text-neutral-100">
|
||||
{profile.about || profile.bio}
|
||||
</p>
|
||||
<div className="mt-2 whitespace-pre-line break-words text-neutral-900 dark:text-neutral-100">
|
||||
{profile.about || profile.bio}
|
||||
</div>
|
||||
<div className="mt-8">
|
||||
<div className="mt-5">
|
||||
{status === 'pending' ? (
|
||||
<p>Loading...</p>
|
||||
) : (
|
||||
|
||||
@@ -11,6 +11,8 @@ import { MemoizedNotifyNote, NoteSkeleton } from '@shared/notes';
|
||||
import { TitleBar } from '@shared/titleBar';
|
||||
import { WidgetWrapper } from '@shared/widgets';
|
||||
|
||||
import { FETCH_LIMIT } from '@stores/constants';
|
||||
|
||||
import { useNostr } from '@utils/hooks/useNostr';
|
||||
import { sendNativeNotification } from '@utils/notification';
|
||||
|
||||
@@ -37,7 +39,7 @@ export function NotificationWidget() {
|
||||
kinds: [NDKKind.Text, NDKKind.Repost, NDKKind.Reaction, NDKKind.Zap],
|
||||
'#p': [db.account.pubkey],
|
||||
},
|
||||
20,
|
||||
FETCH_LIMIT,
|
||||
{ asOf: pageParam === 0 ? undefined : pageParam, abortSignal: signal }
|
||||
);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user