update space screen
This commit is contained in:
163
src/app/space/components/button.tsx
Normal file
163
src/app/space/components/button.tsx
Normal file
@@ -0,0 +1,163 @@
|
||||
import * as DropdownMenu from '@radix-ui/react-dropdown-menu';
|
||||
|
||||
import { useStorage } from '@libs/storage/provider';
|
||||
|
||||
import {
|
||||
FeedIcon,
|
||||
FileIcon,
|
||||
HashtagIcon,
|
||||
PlusIcon,
|
||||
ThreadsIcon,
|
||||
TrendingIcon,
|
||||
} from '@shared/icons';
|
||||
|
||||
import { WidgetKinds, useWidgets } from '@stores/widgets';
|
||||
|
||||
export function AddWidgetButton() {
|
||||
const { db } = useStorage();
|
||||
const setWidget = useWidgets((state) => state.setWidget);
|
||||
|
||||
const setTrendingProfilesWidget = () => {
|
||||
setWidget(db, {
|
||||
kind: WidgetKinds.trendingProfiles,
|
||||
title: 'Trending Profiles',
|
||||
content: 'https://api.nostr.band/v0/trending/profiles',
|
||||
});
|
||||
};
|
||||
|
||||
const setTrendingNotesWidget = () => {
|
||||
setWidget(db, {
|
||||
kind: WidgetKinds.trendingNotes,
|
||||
title: 'Trending Notes',
|
||||
content: 'https://api.nostr.band/v0/trending/notes',
|
||||
});
|
||||
};
|
||||
|
||||
const setArticleWidget = () => {
|
||||
setWidget(db, {
|
||||
kind: WidgetKinds.article,
|
||||
title: 'Blogs',
|
||||
content: '',
|
||||
});
|
||||
};
|
||||
|
||||
const setFileWidget = () => {
|
||||
setWidget(db, {
|
||||
kind: WidgetKinds.file,
|
||||
title: 'Files',
|
||||
content: '',
|
||||
});
|
||||
};
|
||||
|
||||
const setHashtagWidget = () => {
|
||||
setWidget(db, {
|
||||
kind: WidgetKinds.xhashtag,
|
||||
title: 'New hashtag',
|
||||
content: '',
|
||||
});
|
||||
};
|
||||
|
||||
const setGroupFeedWidget = () => {
|
||||
setWidget(db, {
|
||||
kind: WidgetKinds.xfeed,
|
||||
title: 'New user group feed',
|
||||
content: '',
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<DropdownMenu.Root>
|
||||
<div className="flex h-full shrink-0 grow-0 basis-[400px] flex-col">
|
||||
<div className="inline-flex h-full w-full flex-col items-center justify-center">
|
||||
<DropdownMenu.Trigger asChild>
|
||||
<button type="button" className="flex flex-col items-center gap-2">
|
||||
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-white/5 hover:bg-white/10">
|
||||
<PlusIcon className="h-5 w-5 text-white" />
|
||||
</div>
|
||||
<p className="font-medium text-white/50">Add widget</p>
|
||||
</button>
|
||||
</DropdownMenu.Trigger>
|
||||
</div>
|
||||
</div>
|
||||
<DropdownMenu.Portal>
|
||||
<DropdownMenu.Content
|
||||
sideOffset={-20}
|
||||
className="flex w-[256px] flex-col overflow-hidden rounded-md bg-white/10 p-2 backdrop-blur-3xl focus:outline-none"
|
||||
>
|
||||
<DropdownMenu.Item asChild>
|
||||
<button
|
||||
type="button"
|
||||
onClick={setHashtagWidget}
|
||||
className="inline-flex h-11 items-center gap-2 rounded-md px-2 text-sm font-medium text-white hover:bg-white/10"
|
||||
>
|
||||
<div className="inline-flex h-6 w-6 shrink-0 items-center justify-center rounded bg-white/10">
|
||||
<HashtagIcon className="h-4 w-4 text-white" />
|
||||
</div>
|
||||
Add hashtag feeds
|
||||
</button>
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item asChild>
|
||||
<button
|
||||
type="button"
|
||||
onClick={setGroupFeedWidget}
|
||||
className="inline-flex h-11 items-center gap-2 rounded-md px-2 text-sm font-medium text-white hover:bg-white/10"
|
||||
>
|
||||
<div className="inline-flex h-6 w-6 shrink-0 items-center justify-center rounded bg-white/10">
|
||||
<FeedIcon className="h-4 w-4 text-white" />
|
||||
</div>
|
||||
Add user group feeds
|
||||
</button>
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item asChild>
|
||||
<button
|
||||
type="button"
|
||||
onClick={setArticleWidget}
|
||||
className="inline-flex h-11 items-center gap-2 rounded-md px-2 text-sm font-medium text-white hover:bg-white/10"
|
||||
>
|
||||
<div className="inline-flex h-6 w-6 shrink-0 items-center justify-center rounded bg-white/10">
|
||||
<ThreadsIcon className="h-4 w-4 text-white" />
|
||||
</div>
|
||||
Add article feeds
|
||||
</button>
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item asChild>
|
||||
<button
|
||||
type="button"
|
||||
onClick={setFileWidget}
|
||||
className="inline-flex h-11 items-center gap-2 rounded-md px-2 text-sm font-medium text-white hover:bg-white/10"
|
||||
>
|
||||
<div className="flex h-6 w-6 shrink-0 items-center justify-center rounded bg-white/10">
|
||||
<FileIcon className="h-4 w-4 text-white" />
|
||||
</div>
|
||||
Add file feeds
|
||||
</button>
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item asChild>
|
||||
<button
|
||||
type="button"
|
||||
onClick={setTrendingProfilesWidget}
|
||||
className="inline-flex h-11 items-center gap-2 rounded-md px-2 text-sm font-medium text-white hover:bg-white/10"
|
||||
>
|
||||
<div className="inline-flex h-6 w-6 shrink-0 items-center justify-center rounded bg-white/10">
|
||||
<TrendingIcon className="h-4 w-4 text-white" />
|
||||
</div>
|
||||
Add trending accounts
|
||||
</button>
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item asChild>
|
||||
<button
|
||||
type="button"
|
||||
onClick={setTrendingNotesWidget}
|
||||
className="inline-flex h-11 items-center gap-2 rounded-md px-2 text-sm font-medium text-white hover:bg-white/10"
|
||||
>
|
||||
<div className="inline-flex h-6 w-6 shrink-0 items-center justify-center rounded bg-white/10">
|
||||
<TrendingIcon className="h-4 w-4 text-white" />
|
||||
</div>
|
||||
Add trending notes
|
||||
</button>
|
||||
</DropdownMenu.Item>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Portal>
|
||||
</DropdownMenu.Root>
|
||||
);
|
||||
}
|
||||
89
src/app/space/components/forms/feed.tsx
Normal file
89
src/app/space/components/forms/feed.tsx
Normal file
@@ -0,0 +1,89 @@
|
||||
import { useState } from 'react';
|
||||
|
||||
import { User } from '@app/auth/components/user';
|
||||
|
||||
import { useStorage } from '@libs/storage/provider';
|
||||
|
||||
import { ArrowRightCircleIcon, CheckCircleIcon } from '@shared/icons';
|
||||
|
||||
import { WidgetKinds, useWidgets } from '@stores/widgets';
|
||||
|
||||
import { Widget } from '@utils/types';
|
||||
|
||||
export function FeedWidgetForm({ params }: { params: Widget }) {
|
||||
const { db } = useStorage();
|
||||
|
||||
const [setWidget, removeWidget] = useWidgets((state) => [
|
||||
state.setWidget,
|
||||
state.removeWidget,
|
||||
]);
|
||||
const [title, setTitle] = useState<string>('');
|
||||
const [groups, setGroups] = useState<Array<string>>([]);
|
||||
|
||||
// toggle follow state
|
||||
const toggleGroup = (pubkey: string) => {
|
||||
const arr = groups.includes(pubkey)
|
||||
? groups.filter((i) => i !== pubkey)
|
||||
: [...groups, pubkey];
|
||||
setGroups(arr);
|
||||
};
|
||||
|
||||
const submit = async () => {
|
||||
setWidget(db, {
|
||||
kind: WidgetKinds.feed,
|
||||
title: title || 'Group',
|
||||
content: JSON.stringify(groups),
|
||||
});
|
||||
// remove temp widget
|
||||
removeWidget(db, params.id);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex h-full shrink-0 grow-0 basis-[400px] flex-col items-center justify-center">
|
||||
<div className="w-full px-5">
|
||||
<h3 className="mb-4 text-center text-lg font-semibold">
|
||||
Choose account you want to add to group feeds
|
||||
</h3>
|
||||
<div className="mb-0 flex flex-col gap-2">
|
||||
<div>
|
||||
<input
|
||||
value={title}
|
||||
onChange={(e) => setTitle(e.target.value)}
|
||||
placeholder="Title"
|
||||
className="relative h-11 w-full rounded-lg bg-white/10 px-3 py-1 text-white !outline-none placeholder:text-white/50"
|
||||
/>
|
||||
</div>
|
||||
<div className="scrollbar-hide flex h-[500px] w-full flex-col overflow-y-auto rounded-lg bg-white/10 py-2">
|
||||
{db.account.network.map((item: string) => (
|
||||
<button
|
||||
key={item}
|
||||
type="button"
|
||||
onClick={() => toggleGroup(item)}
|
||||
className="inline-flex transform items-center justify-between px-4 py-2 hover:bg-white/20"
|
||||
>
|
||||
<User pubkey={item} />
|
||||
{groups.includes(item) && (
|
||||
<div>
|
||||
<CheckCircleIcon className="h-4 w-4 text-green-400" />
|
||||
</div>
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex items-center justify-center">
|
||||
<button
|
||||
type="submit"
|
||||
disabled={groups.length < 1}
|
||||
onClick={submit}
|
||||
className="inline-flex h-11 w-full items-center justify-between gap-2 rounded-lg bg-fuchsia-500 px-6 font-medium leading-none text-white hover:bg-fuchsia-600 focus:outline-none disabled:opacity-50"
|
||||
>
|
||||
<span className="w-5" />
|
||||
<span>Add {groups.length} account to group feed</span>
|
||||
<ArrowRightCircleIcon className="h-5 w-5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
92
src/app/space/components/forms/hashtag.tsx
Normal file
92
src/app/space/components/forms/hashtag.tsx
Normal file
@@ -0,0 +1,92 @@
|
||||
import { Resolver, useForm } from 'react-hook-form';
|
||||
|
||||
import { useStorage } from '@libs/storage/provider';
|
||||
|
||||
import { ArrowRightCircleIcon } from '@shared/icons';
|
||||
|
||||
import { WidgetKinds, useWidgets } from '@stores/widgets';
|
||||
|
||||
import { Widget } from '@utils/types';
|
||||
|
||||
type FormValues = {
|
||||
hashtag: string;
|
||||
};
|
||||
|
||||
const resolver: Resolver<FormValues> = async (values) => {
|
||||
return {
|
||||
values: values.hashtag ? values : {},
|
||||
errors: !values.hashtag
|
||||
? {
|
||||
hashtag: {
|
||||
type: 'required',
|
||||
message: 'This is required.',
|
||||
},
|
||||
}
|
||||
: {},
|
||||
};
|
||||
};
|
||||
|
||||
export function HashTagWidgetForm({ params }: { params: Widget }) {
|
||||
const [setWidget, removeWidget] = useWidgets((state) => [
|
||||
state.setWidget,
|
||||
state.removeWidget,
|
||||
]);
|
||||
|
||||
const { db } = useStorage();
|
||||
const {
|
||||
register,
|
||||
setError,
|
||||
handleSubmit,
|
||||
formState: { errors, isDirty, isValid },
|
||||
} = useForm<FormValues>({ resolver });
|
||||
|
||||
const onSubmit = async (data: FormValues) => {
|
||||
try {
|
||||
setWidget(db, {
|
||||
kind: WidgetKinds.hashtag,
|
||||
title: data.hashtag + ' in 24 hours ago',
|
||||
content: data.hashtag.replace('#', ''),
|
||||
});
|
||||
// remove temp widget
|
||||
removeWidget(db, params.id);
|
||||
} catch (e) {
|
||||
setError('hashtag', {
|
||||
type: 'custom',
|
||||
message: e,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex h-full shrink-0 grow-0 basis-[400px] flex-col items-center justify-center">
|
||||
<div className="w-full px-5">
|
||||
<h3 className="mb-4 text-center text-lg font-semibold">
|
||||
Enter hashtag you want to follow
|
||||
</h3>
|
||||
<form onSubmit={handleSubmit(onSubmit)} className="mb-0 flex flex-col gap-2">
|
||||
<div className="flex flex-col gap-1">
|
||||
<input
|
||||
{...register('hashtag', { required: true, minLength: 1 })}
|
||||
placeholder="#bitcoin"
|
||||
className="relative h-11 w-full rounded-lg bg-white/10 px-3 py-1 text-white !outline-none placeholder:text-white/50"
|
||||
/>
|
||||
<span className="text-sm text-red-400">
|
||||
{errors.hashtag && <p>{errors.hashtag.message}</p>}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-center">
|
||||
<button
|
||||
type="submit"
|
||||
disabled={!isDirty || !isValid}
|
||||
className="inline-flex h-11 w-full items-center justify-between gap-2 rounded-lg bg-fuchsia-500 px-6 font-medium leading-none text-white hover:bg-fuchsia-600 focus:outline-none disabled:opacity-50"
|
||||
>
|
||||
<span className="w-5" />
|
||||
<span>Create</span>
|
||||
<ArrowRightCircleIcon className="h-5 w-5" />
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -10,8 +10,7 @@ import { useStorage } from '@libs/storage/provider';
|
||||
|
||||
import { CancelIcon, CheckCircleIcon, CommandIcon, LoaderIcon } from '@shared/icons';
|
||||
|
||||
import { widgetKinds } from '@stores/constants';
|
||||
import { useWidgets } from '@stores/widgets';
|
||||
import { WidgetKinds, useWidgets } from '@stores/widgets';
|
||||
|
||||
export function FeedModal() {
|
||||
const setWidget = useWidgets((state) => state.setWidget);
|
||||
@@ -40,7 +39,7 @@ export function FeedModal() {
|
||||
|
||||
// update state
|
||||
setWidget(db, {
|
||||
kind: widgetKinds.feed,
|
||||
kind: WidgetKinds.feed,
|
||||
title: data.title,
|
||||
content: JSON.stringify(selected),
|
||||
});
|
||||
|
||||
@@ -6,8 +6,7 @@ import { useStorage } from '@libs/storage/provider';
|
||||
|
||||
import { CancelIcon, CommandIcon, LoaderIcon } from '@shared/icons';
|
||||
|
||||
import { widgetKinds } from '@stores/constants';
|
||||
import { useWidgets } from '@stores/widgets';
|
||||
import { WidgetKinds, useWidgets } from '@stores/widgets';
|
||||
|
||||
export function HashtagModal() {
|
||||
const setWidget = useWidgets((state) => state.setWidget);
|
||||
@@ -28,7 +27,7 @@ export function HashtagModal() {
|
||||
|
||||
// update state
|
||||
setWidget(db, {
|
||||
kind: widgetKinds.hashtag,
|
||||
kind: WidgetKinds.hashtag,
|
||||
title: data.hashtag,
|
||||
content: data.hashtag.replace('#', ''),
|
||||
});
|
||||
|
||||
@@ -1,158 +0,0 @@
|
||||
import * as Dialog from '@radix-ui/react-dialog';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
|
||||
import { useStorage } from '@libs/storage/provider';
|
||||
|
||||
import { CancelIcon, CommandIcon, LoaderIcon } from '@shared/icons';
|
||||
import { Image } from '@shared/image';
|
||||
|
||||
import { widgetKinds } from '@stores/constants';
|
||||
import { useWidgets } from '@stores/widgets';
|
||||
|
||||
import { useImageUploader } from '@utils/hooks/useUploader';
|
||||
|
||||
export function ImageModal() {
|
||||
const upload = useImageUploader();
|
||||
const setWidget = useWidgets((state) => state.setWidget);
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [image, setImage] = useState('');
|
||||
|
||||
const { db } = useStorage();
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
reset,
|
||||
setValue,
|
||||
formState: { isDirty, isValid },
|
||||
} = useForm();
|
||||
|
||||
const uploadImage = async () => {
|
||||
const image = await upload(null, true);
|
||||
if (image.url) {
|
||||
setImage(image.url);
|
||||
}
|
||||
};
|
||||
|
||||
const onSubmit = async (data: { kind: number; title: string; content: string }) => {
|
||||
setLoading(true);
|
||||
|
||||
// mutate
|
||||
setWidget(db, { kind: widgetKinds.image, title: data.title, content: data.content });
|
||||
|
||||
setLoading(false);
|
||||
// reset form
|
||||
reset();
|
||||
// close modal
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setValue('content', image);
|
||||
}, [setValue, image]);
|
||||
|
||||
return (
|
||||
<Dialog.Root open={open} onOpenChange={setOpen}>
|
||||
<Dialog.Trigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex h-9 w-72 items-center justify-start gap-2.5 rounded-md px-2.5"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="inline-flex h-6 w-6 shrink-0 items-center justify-center rounded bg-white/10">
|
||||
<CommandIcon width={12} height={12} className="text-white" />
|
||||
</div>
|
||||
<div className="inline-flex h-6 w-6 shrink-0 items-center justify-center rounded bg-white/10">
|
||||
<span className="text-sm leading-none text-white">I</span>
|
||||
</div>
|
||||
</div>
|
||||
<h5 className="font-medium text-white/50">Add image widget</h5>
|
||||
</button>
|
||||
</Dialog.Trigger>
|
||||
<Dialog.Portal className="relative z-10">
|
||||
<Dialog.Overlay className="fixed inset-0 z-50 bg-black/80 backdrop-blur-xl" />
|
||||
<Dialog.Content className="fixed inset-0 z-50 flex min-h-full items-center justify-center">
|
||||
<div className="relative h-min w-full max-w-xl rounded-xl bg-white/10">
|
||||
<div className="h-min w-full shrink-0 border-b border-white/10 bg-white/5 px-5 py-5">
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="flex items-center justify-between">
|
||||
<Dialog.Title className="text-lg font-semibold leading-none text-white">
|
||||
Create image block
|
||||
</Dialog.Title>
|
||||
<Dialog.Close className="inline-flex h-6 w-6 items-center justify-center rounded-md hover:bg-white/10">
|
||||
<CancelIcon className="h-4 w-4 text-white/50" />
|
||||
</Dialog.Close>
|
||||
</div>
|
||||
<Dialog.Description className="text-sm leading-tight text-white/50">
|
||||
Pin your favorite image to Space then you can view every time that you
|
||||
use Lume, your image will be broadcast to Nostr Relay as well
|
||||
</Dialog.Description>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex h-full w-full flex-col overflow-y-auto px-5 pb-5 pt-3">
|
||||
<form
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
className="mb-0 flex h-full w-full flex-col gap-3"
|
||||
>
|
||||
<input type={'hidden'} {...register('content')} value={image} />
|
||||
<div className="flex flex-col gap-1">
|
||||
<label
|
||||
htmlFor="title"
|
||||
className="text-sm font-medium uppercase tracking-wider text-white/50"
|
||||
>
|
||||
Title *
|
||||
</label>
|
||||
<input
|
||||
type={'text'}
|
||||
{...register('title', {
|
||||
required: true,
|
||||
})}
|
||||
spellCheck={false}
|
||||
className="relative h-11 w-full rounded-lg bg-white/10 px-3 py-2 text-white !outline-none placeholder:text-white/50"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<label
|
||||
htmlFor="picture"
|
||||
className="text-sm font-medium uppercase tracking-wider text-white/50"
|
||||
>
|
||||
Picture
|
||||
</label>
|
||||
<div className="relative inline-flex h-56 w-full items-center justify-center overflow-hidden rounded-lg bg-white/10">
|
||||
<Image
|
||||
src={image}
|
||||
alt="content"
|
||||
className="relative z-10 h-auto max-h-[156px] w-[150px] rounded-md object-cover"
|
||||
/>
|
||||
<div className="absolute bottom-3 right-3 z-10">
|
||||
<button
|
||||
onClick={() => uploadImage()}
|
||||
type="button"
|
||||
className="inline-flex h-6 items-center justify-center rounded bg-white/10 px-3 text-sm font-medium text-white hover:bg-fuchsia-500"
|
||||
>
|
||||
Upload
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={!isDirty || !isValid}
|
||||
className="inline-flex h-11 w-full transform items-center justify-center rounded-lg bg-fuchsia-500 font-medium text-white active:translate-y-1 disabled:cursor-not-allowed disabled:opacity-30"
|
||||
>
|
||||
{loading ? (
|
||||
<LoaderIcon className="h-4 w-4 animate-spin text-white" />
|
||||
) : (
|
||||
'Confirm'
|
||||
)}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog.Content>
|
||||
</Dialog.Portal>
|
||||
</Dialog.Root>
|
||||
);
|
||||
}
|
||||
114
src/app/space/components/widgets/article.tsx
Normal file
114
src/app/space/components/widgets/article.tsx
Normal file
@@ -0,0 +1,114 @@
|
||||
import { NDKEvent } from '@nostr-dev-kit/ndk';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useVirtualizer } from '@tanstack/react-virtual';
|
||||
import { useCallback, useRef } from 'react';
|
||||
|
||||
import { useNDK } from '@libs/ndk/provider';
|
||||
|
||||
import { NoteKind_1, NoteSkeleton, Repost } from '@shared/notes';
|
||||
import { NoteKindUnsupport } from '@shared/notes/kinds/unsupport';
|
||||
import { TitleBar } from '@shared/titleBar';
|
||||
|
||||
import { nHoursAgo } from '@utils/date';
|
||||
import { Widget } from '@utils/types';
|
||||
|
||||
export function ArticleWidget({ params }: { params: Widget }) {
|
||||
const { ndk } = useNDK();
|
||||
const { status, data } = useQuery(['article-widget', params.content], async () => {
|
||||
const events = await ndk.fetchEvents({
|
||||
kinds: [30023],
|
||||
'#t': [params.content],
|
||||
since: nHoursAgo(48),
|
||||
});
|
||||
return [...events] as unknown as NDKEvent[];
|
||||
});
|
||||
|
||||
const parentRef = useRef<HTMLDivElement>(null);
|
||||
const virtualizer = useVirtualizer({
|
||||
count: data ? data.length : 0,
|
||||
getScrollElement: () => parentRef.current,
|
||||
estimateSize: () => 650,
|
||||
overscan: 4,
|
||||
});
|
||||
const items = virtualizer.getVirtualItems();
|
||||
|
||||
// render event match event kind
|
||||
const renderItem = useCallback(
|
||||
(index: string | number) => {
|
||||
const event: NDKEvent = data[index];
|
||||
if (!event) return;
|
||||
|
||||
switch (event.kind) {
|
||||
case 1:
|
||||
return (
|
||||
<div key={event.id} data-index={index} ref={virtualizer.measureElement}>
|
||||
<NoteKind_1 event={event} skipMetadata={false} />
|
||||
</div>
|
||||
);
|
||||
case 6:
|
||||
return (
|
||||
<div
|
||||
key={event.id + index}
|
||||
data-index={index}
|
||||
ref={virtualizer.measureElement}
|
||||
>
|
||||
<Repost key={event.id} event={event} />
|
||||
</div>
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<div
|
||||
key={event.id + index}
|
||||
data-index={index}
|
||||
ref={virtualizer.measureElement}
|
||||
>
|
||||
<NoteKindUnsupport key={event.id} event={event} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
},
|
||||
[data]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="relative w-[400px] shrink-0 bg-white/10">
|
||||
<TitleBar id={params.id} title={params.title + ' in 24 hours ago'} />
|
||||
<div ref={parentRef} className="scrollbar-hide h-full overflow-y-auto pb-20">
|
||||
{status === 'loading' ? (
|
||||
<div className="px-3 py-1.5">
|
||||
<div className="rounded-xl bg-white/10 px-3 py-3">
|
||||
<NoteSkeleton />
|
||||
</div>
|
||||
</div>
|
||||
) : items.length === 0 ? (
|
||||
<div className="px-3 py-1.5">
|
||||
<div className="rounded-xl bg-white/10 px-3 py-6">
|
||||
<div className="flex flex-col items-center gap-4">
|
||||
<p className="text-center text-sm font-medium text-white">
|
||||
No new postrs about this hashtag in 24 hours ago
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
style={{
|
||||
position: 'relative',
|
||||
width: '100%',
|
||||
height: `${virtualizer.getTotalSize()}px`,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="absolute left-0 top-0 w-full"
|
||||
style={{
|
||||
transform: `translateY(${items[0].start}px)`,
|
||||
}}
|
||||
>
|
||||
{items.map((item) => renderItem(item.index))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -17,7 +17,7 @@ export function FeedWidget({ params }: { params: Widget }) {
|
||||
const { db } = useStorage();
|
||||
const { status, data, hasNextPage, isFetchingNextPage, fetchNextPage } =
|
||||
useInfiniteQuery({
|
||||
queryKey: ['newsfeed', params.content],
|
||||
queryKey: ['groupfeed-widget', params.content],
|
||||
queryFn: async ({ pageParam = 0 }) => {
|
||||
const authors = JSON.parse(params.content);
|
||||
return await db.getAllEventsByAuthors(authors, 20, pageParam);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { NDKEvent, NDKFilter } from '@nostr-dev-kit/ndk';
|
||||
import { useInfiniteQuery } from '@tanstack/react-query';
|
||||
import { useVirtualizer } from '@tanstack/react-virtual';
|
||||
import { destr } from 'destr';
|
||||
import { useCallback, useEffect, useMemo, useRef } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
@@ -137,7 +138,7 @@ export function NetworkWidget() {
|
||||
}
|
||||
await db.createEvent(
|
||||
event.id,
|
||||
JSON.stringify(event),
|
||||
destr(event),
|
||||
event.pubkey,
|
||||
event.kind,
|
||||
root,
|
||||
|
||||
@@ -13,7 +13,7 @@ interface Response {
|
||||
|
||||
export function TrendingNotesWidget({ params }: { params: Widget }) {
|
||||
const { status, data } = useQuery(
|
||||
['trending-notes'],
|
||||
['trending-notes-widget'],
|
||||
async () => {
|
||||
const res = await fetch(params.content);
|
||||
if (!res.ok) {
|
||||
@@ -33,7 +33,7 @@ export function TrendingNotesWidget({ params }: { params: Widget }) {
|
||||
|
||||
return (
|
||||
<div className="scrollbar-hide relative h-full w-[400px] shrink-0 overflow-y-auto bg-white/10 pb-20">
|
||||
<TitleBar title={params.title} />
|
||||
<TitleBar id={params.id} title={params.title} />
|
||||
<div className="h-full">
|
||||
{status === 'loading' ? (
|
||||
<div className="px-3 py-1.5">
|
||||
|
||||
@@ -13,7 +13,7 @@ interface Response {
|
||||
|
||||
export function TrendingProfilesWidget({ params }: { params: Widget }) {
|
||||
const { status, data } = useQuery(
|
||||
['trending-profiles'],
|
||||
['trending-profiles-widget'],
|
||||
async () => {
|
||||
const res = await fetch(params.content);
|
||||
if (!res.ok) {
|
||||
@@ -33,7 +33,7 @@ export function TrendingProfilesWidget({ params }: { params: Widget }) {
|
||||
|
||||
return (
|
||||
<div className="scrollbar-hide relative h-full w-[400px] shrink-0 overflow-y-auto bg-white/10 pb-20">
|
||||
<TitleBar title={params.title} />
|
||||
<TitleBar id={params.id} title={params.title} />
|
||||
<div className="h-full">
|
||||
{status === 'loading' ? (
|
||||
<div className="px-3 py-1.5">
|
||||
|
||||
@@ -1,16 +1,21 @@
|
||||
import { useCallback, useEffect } from 'react';
|
||||
|
||||
import { AddWidgetButton } from '@app/space/components/button';
|
||||
import { FeedWidgetForm } from '@app/space/components/forms/feed';
|
||||
import { HashTagWidgetForm } from '@app/space/components/forms/hashtag';
|
||||
import { FeedWidget } from '@app/space/components/widgets/feed';
|
||||
import { HashtagWidget } from '@app/space/components/widgets/hashtag';
|
||||
import { NetworkWidget } from '@app/space/components/widgets/network';
|
||||
import { ThreadBlock } from '@app/space/components/widgets/thread';
|
||||
import { TrendingNotesWidget } from '@app/space/components/widgets/trendingNotes';
|
||||
import { TrendingProfilesWidget } from '@app/space/components/widgets/trendingProfile';
|
||||
import { UserWidget } from '@app/space/components/widgets/user';
|
||||
|
||||
import { useStorage } from '@libs/storage/provider';
|
||||
|
||||
import { LoaderIcon, PlusIcon } from '@shared/icons';
|
||||
import { LoaderIcon } from '@shared/icons';
|
||||
|
||||
import { useWidgets } from '@stores/widgets';
|
||||
import { WidgetKinds, useWidgets } from '@stores/widgets';
|
||||
|
||||
import { Widget } from '@utils/types';
|
||||
|
||||
@@ -26,16 +31,24 @@ export function SpaceScreen() {
|
||||
(widget: Widget) => {
|
||||
if (!widget) return;
|
||||
switch (widget.kind) {
|
||||
case 1:
|
||||
case WidgetKinds.feed:
|
||||
return <FeedWidget key={widget.id} params={widget} />;
|
||||
case 2:
|
||||
case WidgetKinds.thread:
|
||||
return <ThreadBlock key={widget.id} params={widget} />;
|
||||
case 3:
|
||||
case WidgetKinds.hashtag:
|
||||
return <HashtagWidget key={widget.id} params={widget} />;
|
||||
case 5:
|
||||
case WidgetKinds.user:
|
||||
return <UserWidget key={widget.id} params={widget} />;
|
||||
case 9999:
|
||||
case WidgetKinds.trendingProfiles:
|
||||
return <TrendingProfilesWidget key={widget.id} params={widget} />;
|
||||
case WidgetKinds.trendingNotes:
|
||||
return <TrendingNotesWidget key={widget.id} params={widget} />;
|
||||
case WidgetKinds.network:
|
||||
return <NetworkWidget key={widget.id} />;
|
||||
case WidgetKinds.xhashtag:
|
||||
return <HashTagWidgetForm key={widget.id} params={widget} />;
|
||||
case WidgetKinds.xfeed:
|
||||
return <FeedWidgetForm key={widget.id} params={widget} />;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@@ -58,16 +71,7 @@ export function SpaceScreen() {
|
||||
) : (
|
||||
widgets.map((widget) => renderItem(widget))
|
||||
)}
|
||||
<div className="flex h-full shrink-0 grow-0 basis-[400px] flex-col">
|
||||
<div className="inline-flex h-full w-full flex-col items-center justify-center">
|
||||
<button type="button" className="flex flex-col items-center gap-2">
|
||||
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-white/5 hover:bg-white/10">
|
||||
<PlusIcon className="h-5 w-5 text-white" />
|
||||
</div>
|
||||
<p className="font-medium text-white/50">Add widget</p>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<AddWidgetButton />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user