polish
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -43,7 +43,7 @@ export const WidgetKinds = {
|
||||
|
||||
export const DefaultWidgets: Array<WidgetGroup> = [
|
||||
{
|
||||
title: 'Circles / Follows',
|
||||
title: 'Local',
|
||||
data: [
|
||||
{
|
||||
kind: WidgetKinds.tmp.xfeed,
|
||||
|
||||
Reference in New Issue
Block a user