This commit is contained in:
2023-11-01 13:01:52 +07:00
parent e218ebee89
commit f2dddf97f5
11 changed files with 115 additions and 837 deletions

View File

@@ -16,10 +16,15 @@ import { WidgetWrapper } from '@shared/widgets';
import { DefaultWidgets, WidgetKinds } from '@stores/constants';
import { useWidget } from '@utils/hooks/useWidget';
import { Widget, WidgetGroup } from '@utils/types';
import { Widget, WidgetGroup, WidgetGroupItem } from '@utils/types';
export function WidgetList({ params }: { params: Widget }) {
const { addWidget } = useWidget();
const { addWidget, removeWidget } = useWidget();
const open = (item: WidgetGroupItem) => {
addWidget.mutate({ kind: item.kind, title: item.title, content: '' });
removeWidget.mutate(params.id);
};
const renderIcon = useCallback(
(kind: number) => {
@@ -65,20 +70,16 @@ export function WidgetList({ params }: { params: Widget }) {
const renderItem = useCallback((row: WidgetGroup, index: number) => {
return (
<div key={index} className="flex flex-col gap-2">
<h3 className="text-sm font-semibold text-neutral-500 dark:text-neutral-300">
{row.title}
</h3>
<div className="flex flex-col divide-y divide-neutral-100 overflow-hidden rounded-xl bg-neutral-50 dark:divide-neutral-900 dark:bg-neutral-950">
<h3 className="text-sm font-semibold">{row.title}</h3>
<div className="flex flex-col divide-y divide-neutral-200 overflow-hidden rounded-xl bg-neutral-100 dark:divide-neutral-800 dark:bg-neutral-900">
{row.data.map((item, index) => (
<button
onClick={() =>
addWidget.mutate({ kind: item.kind, title: item.title, content: '' })
}
onClick={() => open(item)}
key={index}
className="group flex items-center gap-2.5 px-4 hover:bg-neutral-200 dark:hover:bg-neutral-800"
>
{item.icon ? (
<div className="h-10 w-10 shrink-0 rounded-md">
<div className="h-10 w-10 shrink-0 rounded-lg">
<img
src={item.icon}
alt={item.title}
@@ -86,7 +87,7 @@ export function WidgetList({ params }: { params: Widget }) {
/>
</div>
) : (
<div className="inline-flex h-10 w-10 shrink-0 items-center justify-center rounded-md bg-neutral-200 group-hover:bg-neutral-300 dark:bg-neutral-800 dark:group-hover:bg-neutral-700">
<div className="inline-flex h-10 w-10 shrink-0 items-center justify-center rounded-lg bg-neutral-200 group-hover:bg-neutral-300 dark:bg-neutral-800 dark:group-hover:bg-neutral-700">
{renderIcon(item.kind)}
</div>
)}
@@ -94,7 +95,7 @@ export function WidgetList({ params }: { params: Widget }) {
<h5 className="line-clamp-1 text-sm font-semibold text-neutral-900 dark:text-neutral-100">
{item.title}
</h5>
<p className="line-clamp-1 text-sm text-neutral-500 dark:text-neutral-300">
<p className="line-clamp-1 text-sm text-neutral-600 dark:text-neutral-400">
{item.description}
</p>
</div>
@@ -108,7 +109,7 @@ export function WidgetList({ params }: { params: Widget }) {
return (
<WidgetWrapper>
<TitleBar id={params.id} title="Add widget" />
<div className="h-full overflow-y-auto pb-20 scrollbar-none">
<div className="flex-1 overflow-y-auto pb-10 scrollbar-none">
<div className="flex flex-col gap-6 px-3">
{DefaultWidgets.map((row: WidgetGroup, index: number) =>
renderItem(row, index)
@@ -117,10 +118,10 @@ export function WidgetList({ params }: { params: Widget }) {
<button
type="button"
disabled
className="inline-flex h-14 w-full items-center justify-center gap-2.5 rounded-xl bg-neutral-100 text-sm font-medium text-neutral-900 dark:bg-neutral-900 dark:text-neutral-100"
className="inline-flex h-14 w-full items-center justify-center gap-2.5 rounded-xl bg-neutral-50 text-sm font-medium text-neutral-900 dark:bg-neutral-950 dark:text-neutral-100"
>
Build your own widget{' '}
<div className="-rotate-3 transform-gpu rounded-md border border-neutral-300 bg-neutral-200 px-1.5 py-1 dark:border-neutral-700 dark:bg-neutral-800">
<div className="-rotate-3 transform-gpu rounded-md border border-neutral-200 bg-neutral-100 px-1.5 py-1 dark:border-neutral-800 dark:bg-neutral-900">
<span className="bg-gradient-to-r from-blue-400 via-red-400 to-orange-500 bg-clip-text text-xs text-transparent dark:from-blue-200 dark:via-red-200 dark:to-orange-300">
Coming soon
</span>

View File

@@ -1,10 +1,4 @@
import { NDKEvent } from '@nostr-dev-kit/ndk';
import ReactMarkdown from 'react-markdown';
import { Link } from 'react-router-dom';
import rehypeExternalLinks from 'rehype-external-links';
import remarkGfm from 'remark-gfm';
import { Boost, Hashtag, Invoice, MentionUser } from '@shared/notes';
export function ArticleDetailNote({ event }: { event: NDKEvent }) {
/*const metadata = useMemo(() => {
@@ -30,39 +24,8 @@ export function ArticleDetailNote({ event }: { event: NDKEvent }) {
}, [event.id]);*/
return (
<ReactMarkdown
className="break-p prose prose-neutral max-w-none select-text whitespace-pre-line leading-normal dark:prose-invert prose-headings:mb-1 prose-headings:mt-3 prose-p:mb-0 prose-p:mt-0 prose-p:last:mb-1 prose-a:font-normal prose-a:text-blue-500 prose-blockquote:mb-1 prose-blockquote:mt-1 prose-blockquote:border-l-[2px] prose-blockquote:border-blue-500 prose-blockquote:pl-2 prose-pre:whitespace-pre-wrap prose-pre:bg-white/10 prose-ol:m-0 prose-ol:mb-1 prose-ul:mb-1 prose-ul:mt-1 prose-img:mb-2 prose-img:mt-3 prose-hr:mx-0 prose-hr:my-2 hover:prose-a:text-blue-500"
remarkPlugins={[remarkGfm]}
rehypePlugins={[rehypeExternalLinks({ target: '_blank' })]}
components={{
a: ({ href }) => {
const cleanURL = new URL(href);
cleanURL.search = '';
return (
<Link to={href} target="_blank" className="w-max break-all hover:!underline">
{cleanURL.hostname + cleanURL.pathname}
</Link>
);
},
del: ({ children }) => {
const key = children[0] as string;
if (typeof key !== 'string') return;
if (key.startsWith('pub') && key.length > 50 && key.length < 100) {
return <MentionUser pubkey={key.replace('pub-', '')} />;
}
if (key.startsWith('tag')) {
return <Hashtag tag={key.replace('tag-', '')} />;
}
if (key.startsWith('boost')) {
return <Boost boost={key.replace('boost-', '')} />;
}
if (key.startsWith('lnbc')) {
return <Invoice invoice={key.replace('lnbc-', '')} />;
}
},
}}
>
<div className="break-p prose prose-neutral max-w-none select-text whitespace-pre-line leading-normal dark:prose-invert prose-headings:mb-1 prose-headings:mt-3 prose-p:mb-0 prose-p:mt-0 prose-p:last:mb-1 prose-a:font-normal prose-a:text-blue-500 prose-blockquote:mb-1 prose-blockquote:mt-1 prose-blockquote:border-l-[2px] prose-blockquote:border-blue-500 prose-blockquote:pl-2 prose-pre:whitespace-pre-wrap prose-pre:bg-white/10 prose-ol:m-0 prose-ol:mb-1 prose-ul:mb-1 prose-ul:mt-1 prose-img:mb-2 prose-img:mt-3 prose-hr:mx-0 prose-hr:my-2 hover:prose-a:text-blue-500">
{event.content}
</ReactMarkdown>
</div>
);
}

View File

@@ -1,4 +1,6 @@
import { NDKEvent } from '@nostr-dev-kit/ndk';
import { downloadDir } from '@tauri-apps/api/path';
import { download } from '@tauri-apps/plugin-upload';
import {
MediaControlBar,
MediaController,
@@ -9,7 +11,9 @@ import {
MediaVolumeRange,
} from 'media-chrome/dist/react';
import { memo } from 'react';
import { Link } from 'react-router-dom';
import { DownloadIcon } from '@shared/icons';
import { LinkPreview } from '@shared/notes';
import { fileType } from '@utils/nip94';
@@ -17,56 +21,58 @@ export function FileNote(props: { event?: NDKEvent }) {
const url = props.event.tags.find((el) => el[0] === 'url')[1];
const type = fileType(url);
const downloadImage = async (url: string) => {
const downloadDirPath = await downloadDir();
const filename = url.substring(url.lastIndexOf('/') + 1);
return await download(url, downloadDirPath + `/${filename}`);
};
if (type === 'image') {
return (
<div className="mb-2 mt-3">
<div key={url} className="group relative mt-2">
<img
src={url}
alt={props.event.content}
className="h-auto w-full rounded-lg object-cover"
loading="lazy"
decoding="async"
style={{ contentVisibility: 'auto' }}
alt={url}
className="h-auto w-full rounded-lg border border-neutral-300 object-cover dark:border-neutral-700"
/>
<button
type="button"
onClick={() => downloadImage(url)}
className="absolute right-2 top-2 hidden h-10 w-10 items-center justify-center rounded-lg bg-black/50 backdrop-blur-xl group-hover:inline-flex hover:bg-blue-500"
>
<DownloadIcon className="h-5 w-5 text-white" />
</button>
</div>
);
}
if (type === 'video') {
return (
<div className="mb-2 mt-3">
<MediaController
key={url}
className="aspect-video w-full overflow-hidden rounded-lg"
>
<video
slot="media"
src={url}
poster={`https://thumbnail.video/api/get?url=${url}&seconds=1`}
preload="none"
muted
/>
<MediaControlBar>
<MediaPlayButton></MediaPlayButton>
<MediaTimeRange></MediaTimeRange>
<MediaTimeDisplay showDuration></MediaTimeDisplay>
<MediaMuteButton></MediaMuteButton>
<MediaVolumeRange></MediaVolumeRange>
</MediaControlBar>
</MediaController>
</div>
<MediaController
key={url}
className="mt-2 aspect-video w-full overflow-hidden rounded-lg"
>
<video
slot="media"
src={url}
poster={`https://thumbnail.video/api/get?url=${url}&seconds=1`}
preload="none"
muted
/>
<MediaControlBar>
<MediaPlayButton></MediaPlayButton>
<MediaTimeRange></MediaTimeRange>
<MediaTimeDisplay showDuration></MediaTimeDisplay>
<MediaMuteButton></MediaMuteButton>
<MediaVolumeRange></MediaVolumeRange>
</MediaControlBar>
</MediaController>
);
}
return (
<div className="mb-2">
<Link
to={url}
target="_blank"
className="break-all font-normal text-blue-500 hover:text-blue-600"
>
{url}
</Link>
<div className="mt-2">
<LinkPreview urls={[url]} />
</div>
);
}

View File

@@ -26,7 +26,7 @@ export function TextNote(props: { content?: string; truncate?: boolean }) {
}
return (
<div>
<div className="flex w-full flex-col items-start gap-2">
<Markdown
options={{
overrides: {

View File

@@ -49,7 +49,7 @@ export const MentionNote = memo(function MentionNote({ id }: { id: string }) {
if (status === 'pending') {
return (
<div className="mt-3 cursor-default rounded-lg border border-neutral-300 bg-neutral-200 p-3 dark:border-neutral-700 dark:bg-neutral-800">
<div className="w-full cursor-default rounded-lg border border-neutral-300 bg-neutral-200 p-3 dark:border-neutral-700 dark:bg-neutral-800">
<NoteSkeleton />
</div>
);
@@ -58,7 +58,7 @@ export const MentionNote = memo(function MentionNote({ id }: { id: string }) {
if (status === 'error') {
const noteLink = `https://njump.me/${nip19.noteEncode(id)}`;
return (
<div className="mt-3 rounded-lg bg-neutral-200 px-3 py-3 dark:bg-neutral-800">
<div className="w-full rounded-lg bg-neutral-200 px-3 py-3 dark:bg-neutral-800">
<div className="flex items-center gap-2">
<div className="inline-flex h-6 w-6 items-end justify-center rounded bg-black pb-1">
<img src="/lume.png" alt="lume" className="h-auto w-1/3" />
@@ -79,7 +79,7 @@ export const MentionNote = memo(function MentionNote({ id }: { id: string }) {
<div
role="button"
onClick={(e) => openThread(e, id)}
className="mt-3 cursor-default rounded-lg border border-neutral-300 bg-neutral-200 p-3 dark:border-neutral-700 dark:bg-neutral-800"
className="w-full cursor-default rounded-lg border border-neutral-300 bg-neutral-200 p-3 dark:border-neutral-700 dark:bg-neutral-800"
>
<User pubkey={data.pubkey} time={data.created_at} variant="mention" />
<div className="mt-1 text-left">{renderKind(data)}</div>

View File

@@ -11,7 +11,7 @@ export function ImagePreview({ urls }: { urls: string[] }) {
};
return (
<div className="my-2 flex flex-col gap-2">
<div className="flex w-full flex-col gap-2">
{urls.map((url) => (
<div key={url} className="group relative">
<img

View File

@@ -7,68 +7,55 @@ function isImage(url: string) {
}
export function LinkPreview({ urls }: { urls: string[] }) {
const { status, data, error } = useOpenGraph(urls[0]);
const { status, data } = useOpenGraph(urls[0]);
const domain = new URL(urls[0]);
return (
<div className="my-2">
{status === 'pending' ? (
<div className="flex flex-col bg-neutral-200 dark:bg-neutral-800">
<div className="h-44 w-full animate-pulse bg-neutral-400 dark:bg-neutral-600" />
<div className="flex flex-col gap-2 px-3 py-3">
<div className="h-3 w-2/3 animate-pulse rounded bg-neutral-400 dark:bg-neutral-600" />
<div className="h-3 w-3/4 animate-pulse rounded bg-neutral-400 dark:bg-neutral-600" />
<span className="mt-2.5 text-sm leading-none text-neutral-600 dark:text-neutral-400">
{domain.hostname}
</span>
</div>
if (status === 'pending') {
return (
<div className="flex w-full flex-col bg-neutral-200 dark:bg-neutral-800">
<div className="h-44 w-full animate-pulse bg-neutral-400 dark:bg-neutral-600" />
<div className="flex flex-col gap-2 px-3 py-3">
<div className="h-3 w-2/3 animate-pulse rounded bg-neutral-400 dark:bg-neutral-600" />
<div className="h-3 w-3/4 animate-pulse rounded bg-neutral-400 dark:bg-neutral-600" />
<span className="mt-2.5 text-sm leading-none text-neutral-600 dark:text-neutral-400">
{domain.hostname}
</span>
</div>
) : (
<Link
to={urls[0]}
target="_blank"
rel="noreferrer"
className="flex flex-col rounded-lg border border-neutral-300 bg-neutral-200 dark:border-neutral-700 dark:bg-neutral-800"
>
{error ? (
<div className="flex flex-col gap-2 px-3 py-3">
<p className="text-sm text-neutral-600 dark:text-neutral-400">
Can&apos;t fetch open graph, click to open webpage
</p>
<span className="text-sm leading-none text-neutral-900 dark:text-neutral-100">
{domain.hostname}
</span>
</div>
) : (
<>
{isImage(data.image) ? (
<img
src={data.image}
alt={urls[0]}
className="h-44 w-full rounded-t-lg bg-white object-cover"
/>
) : null}
<div className="flex flex-col items-start px-3 py-3">
<div className="flex flex-col items-start gap-1 text-left">
{data.title && (
<h5 className="line-clamp-1 text-base font-semibold text-neutral-900 dark:text-neutral-100">
{data.title}
</h5>
)}
{data.description ? (
<p className="mb-2.5 line-clamp-3 break-all text-sm text-neutral-700 dark:text-neutral-400">
{data.description}
</p>
) : null}
</div>
<span className="break-all text-sm text-neutral-600 dark:text-neutral-400">
{domain.hostname}
</span>
</div>
</>
</div>
);
}
return (
<Link
to={urls[0]}
target="_blank"
rel="noreferrer"
className="flex w-full flex-col rounded-lg border border-neutral-300 bg-neutral-200 dark:border-neutral-700 dark:bg-neutral-800"
>
{isImage(data.image) ? (
<img
src={data.image}
alt={urls[0]}
className="h-44 w-full rounded-t-lg bg-white object-cover"
/>
) : null}
<div className="flex flex-col items-start px-3 py-3">
<div className="flex flex-col items-start gap-1 text-left">
{data.title && (
<h5 className="line-clamp-1 text-base font-semibold text-neutral-900 dark:text-neutral-100">
{data.title}
</h5>
)}
</Link>
)}
</div>
{data.description ? (
<p className="mb-2.5 line-clamp-3 break-all text-sm text-neutral-700 dark:text-neutral-400">
{data.description}
</p>
) : null}
</div>
<span className="break-all text-sm text-neutral-600 dark:text-neutral-400">
{domain.hostname}
</span>
</div>
</Link>
);
}

View File

@@ -11,7 +11,7 @@ import { memo } from 'react';
export const VideoPreview = memo(function VideoPreview({ urls }: { urls: string[] }) {
return (
<div className="my-2 flex flex-col gap-2">
<div className="flex w-full flex-col gap-2">
{urls.map((url) => (
<MediaController key={url} className="aspect-video overflow-hidden rounded-lg">
<video

View File

@@ -43,7 +43,7 @@ export const WidgetKinds = {
export const DefaultWidgets: Array<WidgetGroup> = [
{
title: 'Circles / Follows',
title: 'Local',
data: [
{
kind: WidgetKinds.tmp.xfeed,