feat: polish

This commit is contained in:
2024-01-14 09:39:56 +07:00
parent ab27bd5f44
commit f908c46a19
34 changed files with 671 additions and 421 deletions

View File

@@ -114,7 +114,7 @@ export function NoteZap() {
</button>
</Dialog.Trigger>
<Dialog.Portal>
<Dialog.Overlay className="fixed inset-0 z-50 bg-black/20 backdrop-blur-xl dark:bg-white/20" />
<Dialog.Overlay className="fixed inset-0 z-50 bg-black/10 backdrop-blur-sm dark:bg-white/10" />
<Dialog.Content className="fixed inset-0 z-50 flex items-center justify-center min-h-full">
<div className="relative w-full max-w-xl bg-white h-min rounded-xl dark:bg-black">
<div className="inline-flex items-center justify-between w-full px-5 py-3 shrink-0">

View File

@@ -5,6 +5,7 @@ import {
NOSTR_EVENTS,
NOSTR_MENTIONS,
VIDEOS,
canPreview,
cn,
regionNames,
} from "@lume/utils";
@@ -27,9 +28,11 @@ import { useNoteContext } from "./provider";
export function NoteContent({
className,
mini = false,
isTranslatable = false,
}: {
className?: string;
mini?: boolean;
isTranslatable?: boolean;
}) {
const storage = useStorage();
@@ -52,7 +55,7 @@ export function NoteContent({
const words = text.split(/( |\n)/);
const urls = [...getUrls(text)];
if (storage.settings.media && !storage.settings.lowPower) {
if (storage.settings.media && !storage.settings.lowPower && !mini) {
images = urls.filter((word) =>
IMAGES.some((el) => {
const url = new URL(word);
@@ -79,9 +82,11 @@ export function NoteContent({
);
}
events = words.filter((word) =>
NOSTR_EVENTS.some((el) => word.startsWith(el)),
);
if (!mini) {
events = words.filter((word) =>
NOSTR_EVENTS.some((el) => word.startsWith(el)),
);
}
const hashtags = words.filter((word) => word.startsWith("#"));
const mentions = words.filter((word) =>
@@ -198,7 +203,7 @@ export function NoteContent({
(match, i) => {
const url = new URL(match);
if (!linkPreview) {
if (!linkPreview && canPreview(match)) {
linkPreview = match;
return <LinkPreview key={match + i} url={url.toString()} />;
}
@@ -217,9 +222,11 @@ export function NoteContent({
},
);
parsedContent = reactStringReplace(parsedContent, "\n", () => {
return <div key={nanoid()} className="h-3" />;
});
if (!mini) {
parsedContent = reactStringReplace(parsedContent, "\n", () => {
return <div key={nanoid()} className="h-3" />;
});
}
if (typeof parsedContent[0] === "string") {
parsedContent[0] = parsedContent[0].trimStart();
@@ -259,7 +266,12 @@ export function NoteContent({
return (
<div className={cn(className)}>
<div className="break-p select-text whitespace-pre-line text-balance leading-normal">
<div
className={cn(
"break-p select-text text-balance leading-normal",
!mini ? "whitespace-pre-line" : "",
)}
>
{richContent}
</div>
{isTranslatable && storage.settings.translation ? (

View File

@@ -1,13 +1,17 @@
import { PinIcon } from "@lume/icons";
import { COL_TYPES } from "@lume/utils";
import { memo } from "react";
import { Link } from "react-router-dom";
import { Note } from "../";
import { useEvent } from "../../../hooks/useEvent";
import { useColumnContext } from "../../column/provider";
import { User } from "../../user";
export const MentionNote = memo(function MentionNote({
eventId,
openable = true,
}: { eventId: string; openable?: boolean }) {
const { addColumn } = useColumnContext();
const { isLoading, isError, data } = useEvent(eventId);
if (isLoading) {
@@ -34,11 +38,11 @@ export const MentionNote = memo(function MentionNote({
return (
<Note.Provider event={data}>
<Note.Root className="flex flex-col w-full gap-1 my-1 rounded-lg cursor-default bg-neutral-100 dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800">
<Note.Root className="flex flex-col w-full my-1 rounded-lg cursor-default bg-neutral-100 dark:bg-neutral-900 border border-neutral-100 dark:border-neutral-900">
<User.Provider pubkey={data.pubkey}>
<User.Root className="px-3 mt-3 flex h-6 items-center gap-2">
<User.Root className="flex h-10 px-3 items-center gap-2">
<User.Avatar className="size-6 shrink-0 rounded-md object-cover" />
<div className="flex flex-1 items-baseline gap-2">
<div className="flex-1 inline-flex gap-2">
<User.Name className="font-semibold text-neutral-900 dark:text-neutral-100" />
<span className="text-neutral-600 dark:text-neutral-400">·</span>
<User.Time
@@ -48,17 +52,30 @@ export const MentionNote = memo(function MentionNote({
</div>
</User.Root>
</User.Provider>
<div className="px-3 pb-3 mt-1">
<Note.Content />
{openable ? (
<Note.Content mini className="px-3" />
{openable ? (
<div className="mt-2 px-3 flex items-center justify-between">
<Link
to={`/events/${data.id}`}
className="mt-2 text-blue-500 hover:text-blue-600"
className="text-sm font-medium text-blue-500 hover:text-blue-600"
>
Show more
</Link>
) : null}
</div>
<button
type="button"
onClick={async () =>
await addColumn({
kind: COL_TYPES.thread,
title: "Thread",
content: data.id,
})
}
className="inline-flex items-center justify-center rounded-md text-neutral-600 dark:text-neutral-400 size-6 bg-neutral-200 dark:bg-neutral-800 hover:bg-neutral-300 dark:hover:bg-neutral-700"
>
<PinIcon className="size-4" />
</button>
</div>
) : null}
</Note.Root>
</Note.Provider>
);

View File

@@ -12,7 +12,7 @@ export function NoteUser({
return (
<User.Provider pubkey={event.pubkey}>
<User.Root className={cn("flex items-center gap-3", className)}>
<User.Avatar className="size-9 shrink-0 rounded-lg bg-white object-cover ring-1 ring-neutral-200/50 dark:ring-neutral-800/50" />
<User.Avatar className="size-9 shrink-0 rounded-lg object-cover ring-1 ring-neutral-200/50 dark:ring-neutral-800/50" />
<div className="flex h-6 flex-1 items-start justify-between gap-2">
<User.Name className="font-semibold text-neutral-950 dark:text-neutral-50" />
<User.Time

View File

@@ -1,3 +1,4 @@
import { useStorage } from "@lume/storage";
import { cn } from "@lume/utils";
import * as Avatar from "@radix-ui/react-avatar";
import { minidenticon } from "minidenticons";
@@ -7,6 +8,8 @@ import { useUserContext } from "./provider";
export function UserAvatar({ className }: { className?: string }) {
const user = useUserContext();
const storage = useStorage();
const fallbackAvatar = useMemo(
() =>
`data:image/svg+xml;utf8,${encodeURIComponent(
@@ -20,7 +23,7 @@ export function UserAvatar({ className }: { className?: string }) {
<div className="shrink-0">
<div
className={cn(
"bg-black/20 dark:bg-white/20 animate-pulse",
"bg-black/20 dark:bg-white/20 rounded animate-pulse",
className,
)}
/>
@@ -30,13 +33,23 @@ export function UserAvatar({ className }: { className?: string }) {
return (
<Avatar.Root className="shrink-0">
<Avatar.Image
src={user.image}
alt={user.pubkey}
loading="eager"
decoding="async"
className={className}
/>
{storage.settings.lowPower ? (
<Avatar.Image
src={fallbackAvatar}
alt={user.pubkey}
loading="eager"
decoding="async"
className={cn("bg-black dark:bg-white", className)}
/>
) : (
<Avatar.Image
src={user.image}
alt={user.pubkey}
loading="eager"
decoding="async"
className={className}
/>
)}
<Avatar.Fallback delayMs={120}>
<img
src={fallbackAvatar}

View File

@@ -8,7 +8,7 @@ export function UserName({ className }: { className?: string }) {
return (
<div
className={cn(
"h-4 w-20 bg-black/20 dark:bg-white/20 animate-pulse",
"h-4 w-20 bg-black/20 dark:bg-white/20 rounded animate-pulse",
className,
)}
/>
@@ -16,7 +16,7 @@ export function UserName({ className }: { className?: string }) {
}
return (
<div className={cn("w-full max-w-[15rem] truncate", className)}>
<div className={cn("truncate", className)}>
{user.displayName || user.name || "Anon"}
</div>
);

View File

@@ -29,7 +29,7 @@ export function UserNip05({
return (
<div
className={cn(
"h-4 w-20 bg-black/20 dark:bg-white/20 animate-pulse",
"h-4 w-20 bg-black/20 dark:bg-white/20 rounded animate-pulse",
className,
)}
/>

View File

@@ -9,6 +9,7 @@ import NDK, {
NDKPrivateKeySigner,
NDKRelay,
NDKRelayAuthPolicies,
NDKUser,
} from "@nostr-dev-kit/ndk";
import { fetch } from "@tauri-apps/plugin-http";
import Linkify from "linkify-react";
@@ -19,7 +20,9 @@ import { LumeContext } from "./context";
export const LumeProvider = ({ children }: PropsWithChildren<object>) => {
const storage = useStorage();
const [context, setContext] = useState<Ark>(undefined);
const [ark, setArk] = useState<Ark>(undefined);
const [ndk, setNDK] = useState<NDK>(undefined);
async function initNostrSigner({
nsecbunker,
@@ -67,7 +70,7 @@ export const LumeProvider = ({ children }: PropsWithChildren<object>) => {
}
}
async function init() {
async function initNDK() {
const explicitRelayUrls = normalizeRelayUrlSet([
"wss://bostr.nokotaro.com/",
"wss://nostr.mutinywallet.com/",
@@ -77,14 +80,12 @@ export const LumeProvider = ({ children }: PropsWithChildren<object>) => {
const outboxRelayUrls = normalizeRelayUrlSet(["wss://purplepag.es/"]);
// #TODO: user should config blacklist relays
// No need to connect depot tunnel url
const blacklistRelayUrls = storage.settings.tunnelUrl.length
? [
storage.settings.tunnelUrl,
`${storage.settings.tunnelUrl}/`,
"wss://brb.io/",
]
: ["wss://brb.io/"];
// Skip connect depot tunnel url
const blacklistRelayUrls = normalizeRelayUrlSet(
storage.settings.tunnelUrl.length
? [storage.settings.tunnelUrl, "wss://brb.io/"]
: ["wss://brb.io/"],
);
const cacheAdapter = new NDKCacheAdapterTauri(storage);
const ndk = new NDK({
@@ -115,29 +116,37 @@ export const LumeProvider = ({ children }: PropsWithChildren<object>) => {
// auth
ndk.relayAuthDefaultPolicy = async (relay: NDKRelay, challenge: string) => {
const signIn = NDKRelayAuthPolicies.signIn({ ndk });
const event = await signIn(relay, challenge).catch((e) => console.log(e));
const event = await signIn(relay, challenge).catch((e) =>
console.error(e),
);
if (event) {
sendNativeNotification(
await sendNativeNotification(
`You've sign in sucessfully to relay: ${relay.url}`,
);
return event;
}
};
// update account's metadata
if (signer) {
const user = ndk.getUser({ pubkey: storage.currentUser.pubkey });
setNDK(ndk);
}
async function initArk() {
// ark utils
const ark = new Ark({ ndk, account: storage.currentUser });
if (ndk && storage.currentUser) {
const user = new NDKUser({ pubkey: storage.currentUser.pubkey });
ndk.activeUser = user;
const contacts = await user.follows();
storage.currentUser.contacts = [...contacts].map((user) => user.pubkey);
// update contacts
await ark.getUserContacts();
// subscribe for new activity
const sub = ndk.subscribe(
{
kinds: [NDKKind.Text, NDKKind.Repost, NDKKind.Zap],
since: Math.floor(Date.now() / 1000),
"#p": [storage.currentUser.pubkey],
"#p": [ark.account.pubkey],
},
{ closeOnEose: false, groupable: false },
);
@@ -169,18 +178,18 @@ export const LumeProvider = ({ children }: PropsWithChildren<object>) => {
});
}
// ark utils
const ark = new Ark({ ndk, account: storage.currentUser });
// update context
setContext(ark);
setArk(ark);
}
useEffect(() => {
if (!context) init();
if (ndk) initArk();
}, [ndk]);
useEffect(() => {
if (!ark && !ndk) initNDK();
}, []);
if (!context) {
if (!ark) {
return (
<div
data-tauri-drag-region
@@ -207,7 +216,5 @@ export const LumeProvider = ({ children }: PropsWithChildren<object>) => {
);
}
return (
<LumeContext.Provider value={context}>{children}</LumeContext.Provider>
);
return <LumeContext.Provider value={ark}>{children}</LumeContext.Provider>;
};

View File

@@ -279,9 +279,6 @@ export class LumeStorage {
}
const account = await this.getActiveAccount();
this.currentUser = account;
this.currentUser.contacts = [];
return account;
}

View File

@@ -1,33 +0,0 @@
import { activityAtom } from "@lume/utils";
import { AnimatePresence, motion } from "framer-motion";
import { useAtomValue } from "jotai";
import { ActivityContent } from "./content";
export function Activity() {
const isActivityOpen = useAtomValue(activityAtom);
return (
<AnimatePresence initial={false} mode="wait">
{isActivityOpen ? (
<motion.div
key={isActivityOpen ? "activity-open" : "activity-close"}
layout
initial={{ scale: 0.9, opacity: 0, translateX: -20 }}
animate={{
scale: [0.95, 1],
opacity: [0.5, 1],
translateX: [-10, 0],
}}
exit={{
scale: [0.95, 0.9],
opacity: [0.5, 0],
translateX: [-10, -20],
}}
className="h-full w-[350px] px-1 pb-1 shrink-0"
>
<ActivityContent />
</motion.div>
) : null}
</AnimatePresence>
);
}

View File

@@ -1,100 +0,0 @@
import { useArk } from "@lume/ark";
import { LoaderIcon } from "@lume/icons";
import { useStorage } from "@lume/storage";
import { NDKEvent, NDKKind } from "@nostr-dev-kit/ndk";
import { useInfiniteQuery } from "@tanstack/react-query";
import { useCallback, useMemo } from "react";
import { ReplyActivity } from "./reply";
import { RepostActivity } from "./repost";
import { ZapActivity } from "./zap";
export function ActivityContent() {
const ark = useArk();
const storage = useStorage();
const { isLoading, data, hasNextPage, isFetchingNextPage, fetchNextPage } =
useInfiniteQuery({
queryKey: ["activity"],
initialPageParam: 0,
queryFn: async ({
signal,
pageParam,
}: {
signal: AbortSignal;
pageParam: number;
}) => {
const events = await ark.getInfiniteEvents({
filter: {
kinds: [NDKKind.Zap],
"#p": [ark.account.pubkey],
},
limit: 100,
pageParam,
signal,
});
return events;
},
getNextPageParam: (lastPage) => {
const lastEvent = lastPage.at(-1);
if (!lastEvent) return;
return lastEvent.created_at - 1;
},
refetchOnWindowFocus: false,
refetchOnMount: false,
refetchOnReconnect: false,
});
const allEvents = useMemo(
() => (data ? data.pages.flatMap((page) => page) : []),
[data],
);
const renderEvent = useCallback((event: NDKEvent) => {
if (event.pubkey === ark.account.pubkey) return null;
switch (event.kind) {
case NDKKind.Text:
return <ReplyActivity key={event.id} event={event} />;
case NDKKind.Repost:
return <RepostActivity key={event.id} event={event} />;
case NDKKind.Zap:
return <ZapActivity key={event.id} event={event} />;
default:
return <ReplyActivity key={event.id} event={event} />;
}
}, []);
return (
<div className="w-full h-full flex flex-col justify-between rounded-xl overflow-hidden bg-white shadow-[rgba(50,_50,_105,_0.15)_0px_2px_5px_0px,_rgba(0,_0,_0,_0.05)_0px_1px_1px_0px] dark:bg-black dark:shadow-none dark:ring-1 dark:ring-white/10">
<div className="h-full w-full min-h-0">
{isLoading ? (
<div className="w-[350px] h-full flex items-center justify-center">
<LoaderIcon className="size-5 animate-spin" />
</div>
) : allEvents.length < 1 ? (
<div className="w-full h-full flex flex-col items-center justify-center">
<p className="mb-2 text-2xl">🎉</p>
<p className="text-center font-medium">Yo! Nothing new yet.</p>
</div>
) : (
renderEvent(allEvents[0])
)}
</div>
<div className="h-16 shrink-0 px-3 flex items-center gap-3 bg-neutral-100 dark:bg-neutral-900">
<button
type="button"
className="h-11 flex-1 inline-flex items-center justify-center rounded-xl font-medium bg-neutral-200 dark:bg-neutral-800 hover:bg-neutral-300 dark:hover:bg-neutral-700"
>
Previous
</button>
<button
type="button"
className="h-11 flex-1 inline-flex items-center justify-center rounded-xl font-medium bg-blue-500 text-white hover:bg-blue-600"
>
Next
</button>
</div>
</div>
);
}

View File

@@ -1,51 +0,0 @@
import { Note, useArk } from "@lume/ark";
import { NDKEvent } from "@nostr-dev-kit/ndk";
import { ActivityRootNote } from "./rootNote";
export function ReplyActivity({ event }: { event: NDKEvent }) {
const ark = useArk();
const thread = ark.getEventThread({
content: event.content,
tags: event.tags,
});
return (
<div className="h-full pb-3 flex flex-col justify-between">
<div className="h-14 border-b border-neutral-100 dark:border-neutral-900 flex flex-col items-center justify-center px-3">
<h3 className="text-center font-semibold leading-tight">
Conversation
</h3>
<p className="text-sm text-blue-500 font-medium leading-tight">
@ Someone has replied to your note
</p>
</div>
<div className="px-3">
{thread ? (
<div className="flex flex-col gap-3 mb-1">
<ActivityRootNote eventId={thread.rootEventId} />
<ActivityRootNote eventId={thread.replyEventId} />
</div>
) : null}
<div className="mt-3 flex flex-col gap-3">
<div className="flex items-center gap-3">
<p className="text-teal-500 font-medium">New reply</p>
<div className="flex-1 h-px bg-teal-300" />
<div className="w-4 shrink-0 h-px bg-teal-300" />
</div>
<Note.Provider event={event}>
<Note.Root>
<div className="flex items-center justify-between px-3 h-14">
<Note.User className="flex-1 pr-1" />
</div>
<Note.Content className="min-w-0 px-3" />
<div className="flex items-center justify-between px-3 h-14">
<Note.Pin />
<div className="inline-flex items-center gap-10" />
</div>
</Note.Root>
</Note.Provider>
</div>
</div>
</div>
);
}

View File

@@ -1,33 +0,0 @@
import { formatCreatedAt } from "@lume/utils";
import { NDKEvent } from "@nostr-dev-kit/ndk";
import { User } from "../user";
import { ActivityRootNote } from "./rootNote";
export function RepostActivity({ event }: { event: NDKEvent }) {
const repostId = event.tags.find((el) => el[0] === "e")[1];
const createdAt = formatCreatedAt(event.created_at);
return (
<div className="h-full pb-3 flex flex-col justify-between">
<div className="h-14 border-b border-neutral-100 dark:border-neutral-900 flex flex-col items-center justify-center px-3">
<h3 className="text-center font-semibold leading-tight">Boost</h3>
<p className="text-sm text-blue-500 font-medium leading-tight">
@ Someone has reposted to your note
</p>
</div>
<div className="px-3">
<div className="flex flex-col gap-3">
<User pubkey={event.pubkey} variant="notify2" />
<div className="flex items-center gap-3">
<p className="text-teal-500 font-medium">Reposted</p>
<div className="flex-1 h-px bg-teal-300" />
<div className="w-4 shrink-0 h-px bg-teal-300" />
</div>
</div>
<div className="mt-3 flex flex-col gap-3">
<ActivityRootNote eventId={repostId} />
</div>
</div>
</div>
);
}

View File

@@ -1,40 +0,0 @@
import { Note, useEvent } from "@lume/ark";
export function ActivityRootNote({ eventId }: { eventId: string }) {
const { isLoading, isError, data } = useEvent(eventId);
if (isLoading) {
return (
<div className="relative flex gap-3">
<div className="relative flex-1 rounded-md bg-neutral-200 px-2 py-2 dark:bg-neutral-800">
<div className="h-4 w-full animate-pulse bg-neutral-300 dark:bg-neutral-700" />
</div>
</div>
);
}
if (isError) {
return (
<div className="relative flex gap-3">
<div className="relative flex-1 rounded-md bg-neutral-200 px-2 py-2 dark:bg-neutral-800">
Failed to fetch event
</div>
</div>
);
}
return (
<Note.Provider event={data}>
<Note.Root>
<div className="flex items-center justify-between px-3 h-14">
<Note.User className="flex-1 pr-1" />
</div>
<Note.Content className="min-w-0 px-3" />
<div className="flex items-center justify-between px-3 h-14">
<Note.Pin />
<div className="inline-flex items-center gap-10" />
</div>
</Note.Root>
</Note.Provider>
);
}

View File

@@ -1,37 +0,0 @@
import { compactNumber, formatCreatedAt } from "@lume/utils";
import { NDKEvent, zapInvoiceFromEvent } from "@nostr-dev-kit/ndk";
import { User } from "../user";
import { ActivityRootNote } from "./rootNote";
export function ZapActivity({ event }: { event: NDKEvent }) {
const zapEventId = event.tags.find((tag) => tag[0] === "e")[1];
const invoice = zapInvoiceFromEvent(event);
return (
<div className="h-full pb-3 flex flex-col justify-between">
<div className="h-14 border-b border-neutral-100 dark:border-neutral-900 flex flex-col items-center justify-center px-3">
<h3 className="text-center font-semibold leading-tight">Zap</h3>
<p className="text-sm text-blue-500 font-medium leading-tight">
@ Someone love your note
</p>
</div>
<div className="px-3">
<div className="flex flex-col gap-3">
<User
pubkey={event.pubkey}
variant="notify2"
subtext={`${compactNumber.format(invoice.amount)} sats`}
/>
<div className="flex items-center gap-3">
<p className="text-teal-500 font-medium">Zapped</p>
<div className="flex-1 h-px bg-teal-300" />
<div className="w-4 shrink-0 h-px bg-teal-300" />
</div>
</div>
<div className="mt-3 flex flex-col gap-3">
<ActivityRootNote eventId={zapEventId} />
</div>
</div>
</div>
);
}

View File

@@ -20,7 +20,7 @@ export function SettingsLayout() {
twMerge(
"flex w-20 shrink-0 flex-col items-center justify-center rounded-lg px-2 py-2 text-neutral-700 hover:bg-neutral-100 dark:text-neutral-300 dark:hover:bg-neutral-900",
isActive
? "bg-neutral-100 text-blue-500 hover:bg-neutral-100 dark:bg-neutral-900 dark:hover:bg-neutral-900"
? "bg-neutral-100 text-blue-500 hover:bg-neutral-100 dark:bg-neutral-950 dark:hover:bg-neutral-900"
: "",
)
}
@@ -34,7 +34,7 @@ export function SettingsLayout() {
twMerge(
"flex w-20 shrink-0 flex-col items-center justify-center rounded-lg px-2 py-2 text-neutral-700 hover:bg-neutral-100 dark:text-neutral-300 dark:hover:bg-neutral-900",
isActive
? "bg-neutral-100 text-blue-500 hover:bg-neutral-100 dark:bg-neutral-900 dark:hover:bg-neutral-900"
? "bg-neutral-100 text-blue-500 hover:bg-neutral-100 dark:bg-neutral-950 dark:hover:bg-neutral-900"
: "",
)
}
@@ -48,7 +48,7 @@ export function SettingsLayout() {
twMerge(
"flex w-20 shrink-0 flex-col items-center justify-center rounded-lg px-2 py-2 text-neutral-700 hover:bg-neutral-100 dark:text-neutral-300 dark:hover:bg-neutral-900",
isActive
? "bg-neutral-100 text-blue-500 hover:bg-neutral-100 dark:bg-neutral-900 dark:hover:bg-neutral-900"
? "bg-neutral-100 text-blue-500 hover:bg-neutral-100 dark:bg-neutral-950 dark:hover:bg-neutral-900"
: "",
)
}
@@ -62,7 +62,7 @@ export function SettingsLayout() {
twMerge(
"flex w-20 shrink-0 flex-col items-center justify-center rounded-lg px-2 py-2 text-neutral-700 hover:bg-neutral-100 dark:text-neutral-300 dark:hover:bg-neutral-900",
isActive
? "bg-neutral-100 text-blue-500 hover:bg-neutral-100 dark:bg-neutral-900 dark:hover:bg-neutral-900"
? "bg-neutral-100 text-blue-500 hover:bg-neutral-100 dark:bg-neutral-950 dark:hover:bg-neutral-900"
: "",
)
}
@@ -76,7 +76,7 @@ export function SettingsLayout() {
twMerge(
"flex w-20 shrink-0 flex-col items-center justify-center rounded-lg px-2 py-2 text-neutral-700 hover:bg-neutral-100 dark:text-neutral-300 dark:hover:bg-neutral-900",
isActive
? "bg-neutral-100 text-blue-500 hover:bg-neutral-100 dark:bg-neutral-900 dark:hover:bg-neutral-900"
? "bg-neutral-100 text-blue-500 hover:bg-neutral-100 dark:bg-neutral-950 dark:hover:bg-neutral-900"
: "",
)
}

View File

@@ -9,7 +9,7 @@ export function OnboardingModal() {
return (
<Dialog.Root open={onboarding}>
<Dialog.Portal>
<Dialog.Overlay className="fixed inset-0 z-50 bg-black/20 backdrop-blur-xl dark:bg-white/20" />
<Dialog.Overlay className="fixed inset-0 z-50 bg-black/10 backdrop-blur-sm dark:bg-white/10" />
<Dialog.Content className="fixed inset-0 z-50 flex items-center justify-center min-h-full">
<div className="relative w-full max-w-lg bg-white h-[500px] rounded-xl dark:bg-black overflow-hidden">
<OnboardingRouter />

View File

@@ -2,6 +2,7 @@ import dayjs from "dayjs";
import relativeTime from "dayjs/plugin/relativeTime";
import updateLocale from "dayjs/plugin/updateLocale";
import { nip19 } from "nostr-tools";
import { AUDIOS, IMAGES, VIDEOS } from "./constants";
dayjs.extend(relativeTime);
dayjs.extend(updateLocale);
@@ -68,3 +69,23 @@ export const compactNumber = Intl.NumberFormat("en", { notation: "compact" });
// country name
export const regionNames = new Intl.DisplayNames(["en"], { type: "language" });
// verify link can be preview
export function canPreview(text: string) {
const url = new URL(text);
const ext = url.pathname.split(".").pop();
const hostname = url.hostname;
if (VIDEOS.includes(ext)) return false;
if (IMAGES.includes(ext)) return false;
if (AUDIOS.includes(ext)) return false;
if (hostname === "youtube.com") return false;
if (hostname === "youtu.be") return false;
if (hostname === "x.com") return false;
if (hostname === "twitter.com") return false;
if (hostname === "facebook.com") return false;
if (hostname === "vimeo.com") return false;
return true;
}