This commit is contained in:
Ren Amamiya
2023-10-10 15:49:23 +07:00
parent bc4c3b9803
commit 770a63de63
18 changed files with 137 additions and 201 deletions

View File

@@ -1,7 +1,4 @@
@import 'reactflow/dist/style.css';
@import '@vidstack/react/player/styles/default/theme.css';
@import '@vidstack/react/player/styles/default/layouts/audio.css';
@import '@vidstack/react/player/styles/default/layouts/video.css';
@tailwind base;
@tailwind components;
@@ -9,12 +6,6 @@
html {
font-size: 14px;
/* Smoothing */
text-rendering: optimizeLegibility;
-moz-osx-font-smoothing: grayscale;
font-smoothing: antialiased;
-webkit-font-smoothing: antialiased;
text-shadow: rgba(0, 0, 0, .01) 0 0 1px;
}
a {

View File

@@ -1,4 +1,3 @@
import '@fontsource-variable/inter/slnt.css';
import { message } from '@tauri-apps/plugin-dialog';
import { fetch } from '@tauri-apps/plugin-http';
import { RouterProvider, createBrowserRouter, defer, redirect } from 'react-router-dom';

View File

@@ -50,15 +50,15 @@ export function SplashScreen() {
}, [ndk, db.account]);
return (
<div className="relative flex h-screen w-screen items-center justify-center bg-neutral-50 dark:bg-neutral-950">
<div data-tauri-drag-region className="absolute left-0 top-0 z-10 h-16 w-full" />
<div className="flex min-h-0 w-full flex-1 items-center justify-center px-8">
<div className="flex flex-col items-center justify-center gap-6">
<LoaderIcon className="h-6 w-6 animate-spin text-neutral-950 dark:text-neutral-50" />
<h3 className="text-lg font-medium leading-none text-neutral-950 dark:text-neutral-50">
{!ndk ? 'Connecting...' : 'Syncing...'}
</h3>
</div>
<div
data-tauri-drag-region
className="flex h-screen w-screen items-center justify-center bg-neutral-50 dark:bg-neutral-950"
>
<div className="flex flex-col items-center justify-center gap-6">
<LoaderIcon className="h-6 w-6 animate-spin text-neutral-950 dark:text-neutral-50" />
<h3 className="text-lg font-medium leading-none text-neutral-950 dark:text-neutral-50">
{!ndk ? 'Connecting...' : 'Syncing...'}
</h3>
</div>
</div>
);

View File

@@ -28,41 +28,38 @@ export function NoteActions({
return (
<Tooltip.Provider>
<div className="-ml-1 mt-4 inline-flex w-full items-center">
<div className="inline-flex items-center gap-8">
<div className="-ml-1 mt-2 inline-flex w-full items-center">
<div className="inline-flex items-center gap-10">
<NoteReply id={id} pubkey={pubkey} root={root} />
<NoteReaction id={id} pubkey={pubkey} />
<NoteRepost id={id} pubkey={pubkey} />
<NoteZap id={id} pubkey={pubkey} />
</div>
{extraButtons && (
<div className="ml-auto">
<div className="inline-flex items-center gap-3">
<Tooltip.Root delayDuration={150}>
<Tooltip.Trigger asChild>
<button
type="button"
onClick={() =>
setWidget(db, {
kind: WidgetKinds.local.thread,
title: 'Thread',
content: id,
})
}
className="group inline-flex h-7 w-7 items-center justify-center text-neutral-500 dark:text-neutral-300"
>
<ThreadIcon className="h-5 w-5 group-hover:text-blue-500" />
</button>
</Tooltip.Trigger>
<Tooltip.Portal>
<Tooltip.Content className="-left-10 inline-flex h-7 select-none items-center justify-center rounded-md bg-neutral-200 px-3.5 text-sm text-neutral-900 will-change-[transform,opacity] data-[state=delayed-open]:data-[side=bottom]:animate-slideUpAndFade data-[state=delayed-open]:data-[side=left]:animate-slideRightAndFade data-[state=delayed-open]:data-[side=right]:animate-slideLeftAndFade data-[state=delayed-open]:data-[side=top]:animate-slideDownAndFade dark:bg-neutral-800 dark:text-neutral-100">
Open thread
<Tooltip.Arrow className="fill-neutral-200 dark:fill-neutral-800" />
</Tooltip.Content>
</Tooltip.Portal>
</Tooltip.Root>
<MoreActions id={id} pubkey={pubkey} />
</div>
<div className="ml-auto inline-flex items-center gap-3">
<Tooltip.Root delayDuration={150}>
<Tooltip.Trigger asChild>
<button
type="button"
onClick={() =>
setWidget(db, {
kind: WidgetKinds.local.thread,
title: 'Thread',
content: id,
})
}
className="group inline-flex h-7 w-7 items-center justify-center text-neutral-500 dark:text-neutral-300"
>
<ThreadIcon className="h-5 w-5 group-hover:text-blue-500" />
</button>
</Tooltip.Trigger>
<Tooltip.Portal>
<Tooltip.Content className="-left-10 inline-flex h-7 select-none items-center justify-center rounded-md bg-neutral-200 px-3.5 text-sm text-neutral-900 will-change-[transform,opacity] data-[state=delayed-open]:data-[side=bottom]:animate-slideUpAndFade data-[state=delayed-open]:data-[side=left]:animate-slideRightAndFade data-[state=delayed-open]:data-[side=right]:animate-slideLeftAndFade data-[state=delayed-open]:data-[side=top]:animate-slideDownAndFade dark:bg-neutral-800 dark:text-neutral-100">
Open thread
<Tooltip.Arrow className="fill-neutral-200 dark:fill-neutral-800" />
</Tooltip.Content>
</Tooltip.Portal>
</Tooltip.Root>
</div>
)}
</div>

View File

@@ -25,24 +25,9 @@ export function MoreActions({ id, pubkey }: { id: string; pubkey: string }) {
return (
<DropdownMenu.Root open={open} onOpenChange={setOpen}>
<Tooltip.Root delayDuration={150}>
<Tooltip.Trigger asChild>
<DropdownMenu.Trigger asChild>
<button
type="button"
className="group inline-flex h-7 w-7 items-center justify-center text-neutral-500 dark:text-neutral-300"
>
<HorizontalDotsIcon className="h-5 w-5 group-hover:text-blue-500" />
</button>
</DropdownMenu.Trigger>
</Tooltip.Trigger>
<Tooltip.Portal>
<Tooltip.Content className="-left-10 inline-flex h-7 select-none items-center justify-center rounded-md bg-neutral-200 px-3.5 text-sm text-neutral-900 will-change-[transform,opacity] data-[state=delayed-open]:data-[side=bottom]:animate-slideUpAndFade data-[state=delayed-open]:data-[side=left]:animate-slideRightAndFade data-[state=delayed-open]:data-[side=right]:animate-slideLeftAndFade data-[state=delayed-open]:data-[side=top]:animate-slideDownAndFade dark:bg-neutral-800 dark:text-neutral-100">
More
<Tooltip.Arrow className="fill-neutral-200 dark:fill-neutral-800" />
</Tooltip.Content>
</Tooltip.Portal>
</Tooltip.Root>
<DropdownMenu.Trigger>
<HorizontalDotsIcon className="h-5 w-5 text-neutral-800 hover:text-blue-500 dark:text-neutral-200" />
</DropdownMenu.Trigger>
<DropdownMenu.Portal>
<DropdownMenu.Content className="flex w-[200px] flex-col overflow-hidden rounded-xl border border-neutral-300 bg-neutral-200 focus:outline-none dark:border-neutral-700 dark:bg-neutral-800">
<DropdownMenu.Item asChild>

View File

@@ -79,7 +79,7 @@ export function ChildNote({ id, root }: { id: string; root?: string }) {
return (
<>
<div className="absolute bottom-0 left-[18px] h-[calc(100%-3.6rem)] w-0.5 bg-gradient-to-t from-black/20 to-black/10 dark:from-white/20 dark:to-white/10" />
<div className="mb-6 flex flex-col">
<div className="mb-5 flex flex-col">
<User pubkey={data.pubkey} time={data.created_at} />
<div className="-mt-3 flex items-start gap-3">
<div className="w-10 shrink-0" />

View File

@@ -26,3 +26,4 @@ export * from './mentions/boost';
export * from './mentions/invoice';
export * from './stats';
export * from './wrapper';
export * from './actions/more';

View File

@@ -29,7 +29,7 @@ export function ArticleNote(props: { event?: NDKEvent }) {
return (
<Link to={`/notes/article/${props.event.id}`} preventScrollReset={true}>
<div className="mb-2 mt-2 flex flex-col rounded-lg">
<div className="my-2 overflow-hidden rounded-lg">
{metadata.image && (
<Image
src={metadata.image}
@@ -37,16 +37,16 @@ export function ArticleNote(props: { event?: NDKEvent }) {
className="h-44 w-full rounded-t-lg object-cover"
/>
)}
<div className="flex flex-col gap-2 rounded-b-lg bg-white/10 px-3 py-3 backdrop-blur-xl">
<h5 className="line-clamp-1 text-base font-medium text-white">
<div className="flex flex-col gap-1 rounded-b-lg bg-neutral-200 px-3 py-3 dark:bg-neutral-800">
<h5 className="line-clamp-1 font-semibold text-neutral-900 dark:text-neutral-100">
{metadata.title}
</h5>
{metadata.summary ? (
<p className="line-clamp-3 break-all text-sm text-white/70">
<p className="line-clamp-3 break-all text-sm text-neutral-600 dark:text-neutral-400">
{metadata.summary}
</p>
) : null}
<span className="mt-2.5 text-sm text-white/70">
<span className="mt-2.5 text-sm text-neutral-500 dark:text-neutral-400">
{metadata.publishedAt.toString()}
</span>
</div>

View File

@@ -1,10 +1,15 @@
import { NDKEvent } from '@nostr-dev-kit/ndk';
import { MediaPlayer, MediaProvider, Poster } from '@vidstack/react';
import {
DefaultAudioLayout,
DefaultVideoLayout,
defaultLayoutIcons,
} from '@vidstack/react/player/layouts/default';
MediaControlBar,
MediaController,
MediaMuteButton,
MediaPlayButton,
MediaSeekBackwardButton,
MediaSeekForwardButton,
MediaTimeDisplay,
MediaTimeRange,
MediaVolumeRange,
} from 'media-chrome/dist/react';
import { memo } from 'react';
import { Link } from 'react-router-dom';
@@ -31,34 +36,25 @@ export function FileNote(props: { event?: NDKEvent }) {
if (type === 'video') {
return (
<div className="mb-2 mt-3">
<MediaPlayer
key={url}
src={url}
poster={`https://thumbnail.video/api/get?url=${url}&seconds=1`}
load="idle"
aspectRatio="16/9"
muted={true}
crossorigin=""
className="player"
>
<MediaProvider>
<Poster
className="vds-poster"
src={`https://thumbnail.video/api/get?url=${url}&seconds=1`}
alt="poster"
/>
</MediaProvider>
<DefaultAudioLayout
icons={defaultLayoutIcons}
smallLayoutWhen="(width < 500) or (height < 380)"
noModal={true}
<MediaController key={url} className="aspect-video">
<video
slot="media"
src={url}
poster={`https://thumbnail.video/api/get?url=${url}&seconds=1`}
preload="auto"
muted
crossOrigin=""
/>
<DefaultVideoLayout
icons={defaultLayoutIcons}
smallLayoutWhen="(width < 500) or (height < 380)"
noModal={true}
/>
</MediaPlayer>
<MediaControlBar>
<MediaPlayButton></MediaPlayButton>
<MediaSeekBackwardButton></MediaSeekBackwardButton>
<MediaSeekForwardButton></MediaSeekForwardButton>
<MediaTimeRange></MediaTimeRange>
<MediaTimeDisplay showDuration></MediaTimeDisplay>
<MediaMuteButton></MediaMuteButton>
<MediaVolumeRange></MediaVolumeRange>
</MediaControlBar>
</MediaController>
</div>
);
}

View File

@@ -23,7 +23,7 @@ export function TextNote(props: { content?: string }) {
return (
<div>
<ReactMarkdown
className="prose prose-neutral max-w-none select-text 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-0 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:break-words prose-pre:break-all 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"
className="prose prose-neutral max-w-none select-text 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:break-words prose-pre:break-all 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]}
disallowedElements={['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'pre', 'code']}
unwrapDisallowed={true}
@@ -38,14 +38,14 @@ export function TextNote(props: { content?: string }) {
return (
<div>
<ReactMarkdown
className="prose prose-neutral max-w-none select-text 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-0 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:break-words prose-pre:break-all 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"
className="prose prose-neutral max-w-none select-text 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:break-words prose-pre:break-all 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]}
components={{
a: ({ href }) => {
const cleanURL = new URL(href);
cleanURL.search = '';
return (
<Link to={href} target="_blank" className="line-clamp-1 w-full break-all">
<Link to={href} target="_blank" className="w-max break-all hover:underline">
{cleanURL.hostname + cleanURL.pathname}
</Link>
);
@@ -73,13 +73,11 @@ export function TextNote(props: { content?: string }) {
>
{richContent.parsed}
</ReactMarkdown>
<div>
{richContent.images.length > 0 && <ImagePreview urls={richContent.images} />}
{richContent.videos.length > 0 && <VideoPreview urls={richContent.videos} />}
{richContent.links.length > 0 && <LinkPreview urls={richContent.links} />}
{richContent.notes.length > 0 &&
richContent.notes.map((note: string) => <MentionNote key={note} id={note} />)}
</div>
{richContent.images.length > 0 && <ImagePreview urls={richContent.images} />}
{richContent.videos.length > 0 && <VideoPreview urls={richContent.videos} />}
{richContent.links.length > 0 && <LinkPreview urls={richContent.links} />}
{richContent.notes.length > 0 &&
richContent.notes.map((note: string) => <MentionNote key={note} id={note} />)}
</div>
);
}

View File

@@ -3,7 +3,7 @@ import { download } from '@tauri-apps/plugin-upload';
import { DownloadIcon } from '@shared/icons';
export function ImagePreview({ urls, truncate }: { urls: string[]; truncate?: boolean }) {
export function ImagePreview({ urls }: { urls: string[] }) {
const downloadImage = async (url: string) => {
const downloadDirPath = await downloadDir();
const filename = url.substring(url.lastIndexOf('/') + 1);
@@ -11,15 +11,13 @@ export function ImagePreview({ urls, truncate }: { urls: string[]; truncate?: bo
};
return (
<div className="flex flex-col gap-2">
<div className="my-2 flex flex-col gap-2">
{urls.map((url) => (
<div key={url} className="group relative min-w-0 shrink-0 grow-0 basis-full">
<div key={url} className="group relative">
<img
src={url}
alt="image"
className={`${
truncate ? 'h-auto max-h-[300px]' : 'h-auto'
} w-full rounded-lg border border-white/10 object-cover`}
className="h-auto w-full rounded-lg border border-neutral-200 object-cover dark:border-neutral-800"
/>
<button
type="button"

View File

@@ -7,9 +7,9 @@ export function LinkPreview({ urls }: { urls: string[] }) {
const domain = new URL(urls[0]);
return (
<div className="rounded-lg bg-neutral-200 dark:bg-neutral-800">
<div className="my-2">
{status === 'loading' ? (
<div className="flex flex-col">
<div className="flex flex-col bg-neutral-200 dark:bg-neutral-800">
<div className="h-44 w-full animate-pulse bg-white/10 backdrop-blur-xl" />
<div className="flex flex-col gap-2 px-3 py-3">
<div className="h-3 w-2/3 animate-pulse rounded bg-white/10 backdrop-blur-xl" />
@@ -24,7 +24,7 @@ export function LinkPreview({ urls }: { urls: string[] }) {
to={urls[0]}
target="_blank"
rel="noreferrer"
className="flex flex-col rounded-lg"
className="flex flex-col rounded-lg bg-neutral-200 dark:bg-neutral-800"
>
{error ? (
<div className="flex flex-col gap-2 px-3 py-3">
@@ -42,9 +42,6 @@ export function LinkPreview({ urls }: { urls: string[] }) {
src={data.image}
alt={urls[0]}
className="h-44 w-full rounded-t-lg object-cover"
loading="lazy"
decoding="async"
style={{ contentVisibility: 'auto' }}
/>
)}
<div className="flex flex-col gap-1 border-t border-white/5 px-3 py-3">

View File

@@ -1,43 +1,34 @@
import { MediaPlayer, MediaProvider, Poster } from '@vidstack/react';
import {
DefaultAudioLayout,
DefaultVideoLayout,
defaultLayoutIcons,
} from '@vidstack/react/player/layouts/default';
MediaControlBar,
MediaController,
MediaMuteButton,
MediaPlayButton,
MediaTimeDisplay,
MediaTimeRange,
MediaVolumeRange,
} from 'media-chrome/dist/react';
import { memo } from 'react';
export const VideoPreview = memo(function VideoPreview({ urls }: { urls: string[] }) {
return (
<div className="flex flex-col gap-2">
<div className="my-2 flex flex-col gap-2">
{urls.map((url) => (
<MediaPlayer
key={url}
src={url}
poster={`https://thumbnail.video/api/get?url=${url}&seconds=1`}
load="idle"
aspectRatio="16/9"
crossorigin=""
muted={true}
className="player"
>
<MediaProvider>
<Poster
className="vds-poster"
src={`https://thumbnail.video/api/get?url=${url}&seconds=1`}
alt="poster"
/>
</MediaProvider>
<DefaultAudioLayout
icons={defaultLayoutIcons}
smallLayoutWhen="(width < 500) or (height < 380)"
noModal={true}
<MediaController key={url} className="aspect-video overflow-hidden rounded-lg">
<video
slot="media"
src={url}
poster={`https://thumbnail.video/api/get?url=${url}&seconds=1`}
preload="auto"
muted
/>
<DefaultVideoLayout
icons={defaultLayoutIcons}
smallLayoutWhen="(width < 500) or (height < 380)"
noModal={true}
/>
</MediaPlayer>
<MediaControlBar>
<MediaPlayButton></MediaPlayButton>
<MediaTimeRange></MediaTimeRange>
<MediaTimeDisplay showDuration></MediaTimeDisplay>
<MediaMuteButton></MediaMuteButton>
<MediaVolumeRange></MediaVolumeRange>
</MediaControlBar>
</MediaController>
))}
</div>
);

View File

@@ -8,6 +8,7 @@ import remarkGfm from 'remark-gfm';
import { RepostIcon, WorldIcon } from '@shared/icons';
import { NIP05 } from '@shared/nip05';
import { MoreActions } from '@shared/notes';
import { formatCreatedAt } from '@utils/createdAt';
import { useProfile } from '@utils/hooks/useProfile';
@@ -15,11 +16,13 @@ import { displayNpub } from '@utils/shortenKey';
export const User = memo(function User({
pubkey,
eventId,
time,
variant = 'default',
embedProfile,
}: {
pubkey: string;
eventId?: string;
time?: number;
variant?:
| 'default'
@@ -301,33 +304,34 @@ export const User = memo(function User({
<HoverCard.Root>
<div className="relative z-10 flex items-start gap-3">
<HoverCard.Trigger asChild>
<Avatar.Root className="h-10 w-10 shrink-0">
<Avatar.Root className="relative top-1 shrink-0">
<Avatar.Image
src={user?.picture || user?.image}
alt={pubkey}
loading="lazy"
decoding="async"
style={{ contentVisibility: 'auto' }}
className="h-10 w-10 rounded-lg object-cover"
className="h-10 w-10 rounded-lg bg-white object-cover"
/>
<Avatar.Fallback delayMs={300}>
<img
src={svgURI}
alt={pubkey}
className="h-10 w-10 rounded-lg border border-white/5 bg-black dark:bg-white"
className="h-10 w-10 rounded-lg bg-black dark:bg-white"
/>
</Avatar.Fallback>
</Avatar.Root>
</HoverCard.Trigger>
<div className="flex flex-1 items-center gap-2">
<div className="flex flex-1 items-start gap-2">
<div className="max-w-[15rem] truncate font-semibold text-neutral-950 dark:text-neutral-50">
{user?.name ||
user?.display_name ||
user?.displayName ||
displayNpub(pubkey, 16)}
</div>
<div className="ml-auto text-neutral-500 dark:text-neutral-400">
{createdAt}
<div className="ml-auto inline-flex items-center gap-3">
<div className="text-neutral-500 dark:text-neutral-400">{createdAt}</div>
<MoreActions id={eventId} pubkey={pubkey} />
</div>
</div>
</div>