feat: polish
This commit is contained in:
@@ -105,7 +105,7 @@ export class Ark {
|
||||
|
||||
public getCleanPubkey(pubkey: string) {
|
||||
try {
|
||||
let hexstring = pubkey.replace("nostr:", "").split("'")[0];
|
||||
let hexstring = pubkey.replace("nostr:", "").split("'")[0].split(".")[0];
|
||||
|
||||
if (
|
||||
hexstring.startsWith("npub1") ||
|
||||
@@ -128,24 +128,10 @@ export class Ark {
|
||||
public async getUserProfile(pubkey?: string) {
|
||||
try {
|
||||
const currentUserPubkey = this.account.pubkey;
|
||||
|
||||
// get clean pubkey without any special characters
|
||||
let hexstring = pubkey
|
||||
? pubkey.replace("nostr:", "").split("'")[0]
|
||||
const hexstring = pubkey
|
||||
? this.getCleanPubkey(pubkey)
|
||||
: currentUserPubkey;
|
||||
|
||||
if (
|
||||
hexstring.startsWith("npub1") ||
|
||||
hexstring.startsWith("nprofile1") ||
|
||||
hexstring.startsWith("naddr1")
|
||||
) {
|
||||
const decoded = nip19.decode(hexstring);
|
||||
|
||||
if (decoded.type === "nprofile") hexstring = decoded.data.pubkey;
|
||||
if (decoded.type === "npub") hexstring = decoded.data;
|
||||
if (decoded.type === "naddr") hexstring = decoded.data.pubkey;
|
||||
}
|
||||
|
||||
const user = this.ndk.getUser({
|
||||
pubkey: hexstring,
|
||||
});
|
||||
@@ -157,6 +143,7 @@ export class Ark {
|
||||
if (!profile) return null;
|
||||
return profile;
|
||||
} catch (e) {
|
||||
console.log(pubkey);
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
@@ -164,24 +151,10 @@ export class Ark {
|
||||
public async getUserContacts(pubkey?: string) {
|
||||
try {
|
||||
const currentUserPubkey = this.account.pubkey;
|
||||
|
||||
// get clean pubkey without any special characters
|
||||
let hexstring = pubkey
|
||||
? pubkey.replace("nostr:", "").split("'")[0]
|
||||
const hexstring = pubkey
|
||||
? this.getCleanPubkey(pubkey)
|
||||
: currentUserPubkey;
|
||||
|
||||
if (
|
||||
hexstring.startsWith("npub1") ||
|
||||
hexstring.startsWith("nprofile1") ||
|
||||
hexstring.startsWith("naddr1")
|
||||
) {
|
||||
const decoded = nip19.decode(hexstring);
|
||||
|
||||
if (decoded.type === "nprofile") hexstring = decoded.data.pubkey;
|
||||
if (decoded.type === "npub") hexstring = decoded.data;
|
||||
if (decoded.type === "naddr") hexstring = decoded.data.pubkey;
|
||||
}
|
||||
|
||||
const user = this.ndk.getUser({
|
||||
pubkey: hexstring,
|
||||
});
|
||||
@@ -252,8 +225,8 @@ export class Ark {
|
||||
return [...events];
|
||||
}
|
||||
|
||||
public async getEventById({ id }: { id: string }) {
|
||||
let eventId: string = id;
|
||||
public async getEventById(id: string) {
|
||||
let eventId: string = id.replace("nostr:", "").split("'")[0].split(".")[0];
|
||||
|
||||
if (
|
||||
eventId.startsWith("nevent1") ||
|
||||
@@ -264,7 +237,6 @@ export class Ark {
|
||||
|
||||
if (decode.type === "nevent") eventId = decode.data.id;
|
||||
if (decode.type === "note") eventId = decode.data;
|
||||
|
||||
if (decode.type === "naddr") {
|
||||
return await this.ndk.fetchEvent({
|
||||
kinds: [decode.data.kind],
|
||||
|
||||
@@ -60,12 +60,12 @@ export function ColumnHeader({
|
||||
</button>
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Portal>
|
||||
<DropdownMenu.Content className="flex w-[200px] p-2 flex-col overflow-hidden rounded-xl border border-neutral-100 bg-neutral-50 dark:bg-neutral-950 focus:outline-none dark:border-neutral-900">
|
||||
<DropdownMenu.Content className="flex w-[200px] p-2 flex-col overflow-hidden rounded-2xl bg-black/70 dark:bg-white/20 backdrop-blur-lg focus:outline-none">
|
||||
<DropdownMenu.Item asChild>
|
||||
<button
|
||||
type="button"
|
||||
onClick={refresh}
|
||||
className="inline-flex items-center gap-2 px-3 text-sm font-medium rounded-lg h-9 text-neutral-700 hover:bg-blue-100 hover:text-blue-500 focus:outline-none dark:text-neutral-300 dark:hover:bg-neutral-900 dark:hover:text-neutral-50"
|
||||
className="inline-flex items-center gap-2 px-3 text-sm font-medium rounded-lg h-9 text-white/50 hover:bg-black/10 hover:text-white focus:outline-none dark:text-white/50 dark:hover:bg-white/10 dark:hover:text-white"
|
||||
>
|
||||
<RefreshIcon className="size-5" />
|
||||
Refresh
|
||||
@@ -75,7 +75,7 @@ export function ColumnHeader({
|
||||
<button
|
||||
type="button"
|
||||
onClick={moveLeft}
|
||||
className="inline-flex items-center gap-2 px-3 text-sm font-medium rounded-lg h-9 text-neutral-700 hover:bg-blue-100 hover:text-blue-500 focus:outline-none dark:text-neutral-300 dark:hover:bg-neutral-900 dark:hover:text-neutral-50"
|
||||
className="inline-flex items-center gap-2 px-3 text-sm font-medium rounded-lg h-9 text-white/50 hover:bg-black/10 hover:text-white focus:outline-none dark:text-white/50 dark:hover:bg-white/10 dark:hover:text-white"
|
||||
>
|
||||
<MoveLeftIcon className="size-5" />
|
||||
Move left
|
||||
@@ -85,18 +85,18 @@ export function ColumnHeader({
|
||||
<button
|
||||
type="button"
|
||||
onClick={moveRight}
|
||||
className="inline-flex items-center gap-2 px-3 text-sm font-medium rounded-lg h-9 text-neutral-700 hover:bg-blue-100 hover:text-blue-500 focus:outline-none dark:text-neutral-300 dark:hover:bg-neutral-900 dark:hover:text-neutral-50"
|
||||
className="inline-flex items-center gap-2 px-3 text-sm font-medium rounded-lg h-9 text-white/50 hover:bg-black/10 hover:text-white focus:outline-none dark:text-white/50 dark:hover:bg-white/10 dark:hover:text-white"
|
||||
>
|
||||
<MoveRightIcon className="size-5" />
|
||||
Move right
|
||||
</button>
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Separator className="h-px my-1 bg-neutral-100 dark:bg-neutral-900" />
|
||||
<DropdownMenu.Separator className="h-px my-1 bg-white/10 dark:bg-black/10" />
|
||||
<DropdownMenu.Item asChild>
|
||||
<button
|
||||
type="button"
|
||||
onClick={deleteWidget}
|
||||
className="inline-flex items-center gap-2 px-3 text-sm font-medium text-red-500 rounded-lg h-9 hover:bg-red-500 hover:text-red-50 focus:outline-none"
|
||||
className="inline-flex items-center gap-2 px-3 text-sm font-medium text-red-300 rounded-lg h-9 hover:bg-red-500 hover:text-red-50 focus:outline-none"
|
||||
>
|
||||
<TrashIcon className="size-5" />
|
||||
Delete
|
||||
|
||||
@@ -1,15 +1,11 @@
|
||||
import { ChatsIcon, ReplyIcon } from "@lume/icons";
|
||||
import { editorAtom, editorValueAtom } from "@lume/utils";
|
||||
import { ChatsIcon } from "@lume/icons";
|
||||
import * as Tooltip from "@radix-ui/react-tooltip";
|
||||
import { useSetAtom } from "jotai";
|
||||
import { nip19 } from "nostr-tools";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useNoteContext } from "../provider";
|
||||
|
||||
export function NoteReply() {
|
||||
const event = useNoteContext();
|
||||
|
||||
const setEditorValue = useSetAtom(editorValueAtom);
|
||||
const setIsEditorOpen = useSetAtom(editorAtom);
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<Tooltip.Provider>
|
||||
@@ -17,25 +13,7 @@ export function NoteReply() {
|
||||
<Tooltip.Trigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setEditorValue([
|
||||
{
|
||||
type: "paragraph",
|
||||
children: [{ text: "" }],
|
||||
},
|
||||
{
|
||||
type: "event",
|
||||
// @ts-expect-error, useless
|
||||
eventId: `nostr:${nip19.noteEncode(event.id)}`,
|
||||
children: [{ text: "" }],
|
||||
},
|
||||
{
|
||||
type: "paragraph",
|
||||
children: [{ text: "" }],
|
||||
},
|
||||
]);
|
||||
setIsEditorOpen(true);
|
||||
}}
|
||||
onClick={() => navigate(`/events/${event.id}`)}
|
||||
className="inline-flex items-center justify-center group h-7 w-7 text-neutral-600 dark:text-neutral-400"
|
||||
>
|
||||
<ChatsIcon className="size-5 group-hover:text-blue-500" />
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import { LoaderIcon, RepostIcon } from "@lume/icons";
|
||||
import { editorAtom, editorValueAtom } from "@lume/utils";
|
||||
import * as DropdownMenu from "@radix-ui/react-dropdown-menu";
|
||||
import * as Tooltip from "@radix-ui/react-tooltip";
|
||||
import { useSetAtom } from "jotai";
|
||||
import { nip19 } from "nostr-tools";
|
||||
import { useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
@@ -7,11 +11,14 @@ import { useNoteContext } from "../provider";
|
||||
|
||||
export function NoteRepost() {
|
||||
const event = useNoteContext();
|
||||
const setEditorValue = useSetAtom(editorValueAtom);
|
||||
const setIsEditorOpen = useSetAtom(editorAtom);
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [isRepost, setIsRepost] = useState(false);
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const submit = async () => {
|
||||
const repost = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
|
||||
@@ -30,34 +37,79 @@ export function NoteRepost() {
|
||||
}
|
||||
};
|
||||
|
||||
const quote = () => {
|
||||
setEditorValue([
|
||||
{
|
||||
type: "paragraph",
|
||||
children: [{ text: "" }],
|
||||
},
|
||||
{
|
||||
type: "event",
|
||||
// @ts-expect-error, useless
|
||||
eventId: `nostr:${nip19.noteEncode(event.id)}`,
|
||||
children: [{ text: "" }],
|
||||
},
|
||||
{
|
||||
type: "paragraph",
|
||||
children: [{ text: "" }],
|
||||
},
|
||||
]);
|
||||
setIsEditorOpen(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<Tooltip.Provider>
|
||||
<Tooltip.Root delayDuration={150}>
|
||||
<Tooltip.Trigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
onClick={submit}
|
||||
className="inline-flex items-center justify-center group h-7 w-7 text-neutral-600 dark:text-neutral-400"
|
||||
>
|
||||
{loading ? (
|
||||
<LoaderIcon className="size-4 animate-spin" />
|
||||
) : (
|
||||
<RepostIcon
|
||||
className={twMerge(
|
||||
"size-5 group-hover:text-blue-600",
|
||||
isRepost ? "text-blue-500" : "",
|
||||
<DropdownMenu.Root open={open} onOpenChange={setOpen}>
|
||||
<Tooltip.Provider>
|
||||
<Tooltip.Root delayDuration={150}>
|
||||
<DropdownMenu.Trigger asChild>
|
||||
<Tooltip.Trigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex items-center justify-center group h-7 w-7 text-neutral-600 dark:text-neutral-400"
|
||||
>
|
||||
{loading ? (
|
||||
<LoaderIcon className="size-4 animate-spin" />
|
||||
) : (
|
||||
<RepostIcon
|
||||
className={twMerge(
|
||||
"size-5 group-hover:text-blue-600",
|
||||
isRepost ? "text-blue-500" : "",
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</button>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Portal>
|
||||
<Tooltip.Content className="inline-flex h-7 select-none text-neutral-50 dark:text-neutral-950 items-center justify-center rounded-md bg-neutral-950 dark:bg-neutral-50 px-3.5 text-sm 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">
|
||||
Repost
|
||||
<Tooltip.Arrow className="fill-neutral-950 dark:fill-neutral-50" />
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Portal>
|
||||
</Tooltip.Root>
|
||||
</Tooltip.Provider>
|
||||
</button>
|
||||
</Tooltip.Trigger>
|
||||
</DropdownMenu.Trigger>
|
||||
<Tooltip.Portal>
|
||||
<Tooltip.Content className="inline-flex h-7 select-none text-neutral-50 dark:text-neutral-950 items-center justify-center rounded-md bg-neutral-950 dark:bg-neutral-50 px-3.5 text-sm 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">
|
||||
Repost
|
||||
<Tooltip.Arrow className="fill-neutral-950 dark:fill-neutral-50" />
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Portal>
|
||||
</Tooltip.Root>
|
||||
</Tooltip.Provider>
|
||||
<DropdownMenu.Portal>
|
||||
<DropdownMenu.Content className="flex w-[200px] p-2 flex-col overflow-hidden rounded-2xl bg-black/70 dark:bg-white/20 backdrop-blur-lg focus:outline-none">
|
||||
<DropdownMenu.Item asChild>
|
||||
<button
|
||||
type="button"
|
||||
onClick={repost}
|
||||
className="inline-flex items-center gap-2 px-3 text-sm font-medium rounded-lg h-9 text-white/50 hover:bg-black/10 hover:text-white focus:outline-none dark:text-white/50 dark:hover:bg-white/10 dark:hover:text-white"
|
||||
>
|
||||
Repost
|
||||
</button>
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item asChild>
|
||||
<button
|
||||
type="button"
|
||||
onClick={quote}
|
||||
className="inline-flex items-center gap-2 px-3 text-sm font-medium rounded-lg h-9 text-white/50 hover:bg-black/10 hover:text-white focus:outline-none dark:text-white/50 dark:hover:bg-white/10 dark:hover:text-white"
|
||||
>
|
||||
Quote
|
||||
</button>
|
||||
</DropdownMenu.Item>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Portal>
|
||||
</DropdownMenu.Root>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { PinIcon } from "@lume/icons";
|
||||
import { PinIcon, RefreshIcon } from "@lume/icons";
|
||||
import { COL_TYPES } from "@lume/utils";
|
||||
import { memo } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
@@ -18,14 +18,15 @@ export const MentionNote = memo(function MentionNote({
|
||||
return (
|
||||
<div
|
||||
contentEditable={false}
|
||||
className="w-full p-3 my-1 rounded-lg cursor-default bg-neutral-100 dark:bg-neutral-900"
|
||||
className="flex items-center justify-between w-full p-3 my-1 rounded-lg cursor-default bg-neutral-100 dark:bg-neutral-900"
|
||||
>
|
||||
Loading
|
||||
<p>Loading</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (isError) {
|
||||
if (isError || !data) {
|
||||
console.log(eventId);
|
||||
return (
|
||||
<div
|
||||
contentEditable={false}
|
||||
|
||||
@@ -12,7 +12,7 @@ export const MentionUser = memo(function MentionUser({
|
||||
const ark = useArk();
|
||||
const cleanPubkey = ark.getCleanPubkey(pubkey);
|
||||
|
||||
const { isLoading, user } = useProfile(pubkey);
|
||||
const { isLoading, isError, user } = useProfile(pubkey);
|
||||
const { addColumn } = useColumnContext();
|
||||
|
||||
return (
|
||||
@@ -20,7 +20,9 @@ export const MentionUser = memo(function MentionUser({
|
||||
<DropdownMenu.Trigger className="text-blue-500 break-words hover:text-blue-600">
|
||||
{isLoading
|
||||
? "@anon"
|
||||
: `@${user.name || user.display_name || user.username || "anon"}`}
|
||||
: isError
|
||||
? pubkey
|
||||
: `@${user.name || user.displayName || user.username || "anon"}`}
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Content className="left-[50px] z-50 relative flex w-[200px] flex-col overflow-hidden rounded-xl border border-neutral-200 bg-neutral-950 focus:outline-none dark:border-neutral-900">
|
||||
<DropdownMenu.Item asChild>
|
||||
|
||||
@@ -5,6 +5,7 @@ import { nip19 } from "nostr-tools";
|
||||
import { type EventPointer } from "nostr-tools/lib/types/nip19";
|
||||
import { useState } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { toast } from "sonner";
|
||||
import { useNoteContext } from "./provider";
|
||||
|
||||
export function NoteMenu() {
|
||||
@@ -25,6 +26,10 @@ export function NoteMenu() {
|
||||
await writeText(JSON.stringify(await event.toNostrEvent()));
|
||||
};
|
||||
|
||||
const copyNpub = async () => {
|
||||
await writeText(nip19.npubEncode(event.pubkey));
|
||||
};
|
||||
|
||||
const copyLink = async () => {
|
||||
await writeText(
|
||||
`https://njump.me/${nip19.neventEncode({
|
||||
@@ -35,6 +40,11 @@ export function NoteMenu() {
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
const muteUser = async () => {
|
||||
event.muted();
|
||||
toast.info("You've muted this user");
|
||||
};
|
||||
|
||||
return (
|
||||
<DropdownMenu.Root open={open} onOpenChange={setOpen}>
|
||||
<DropdownMenu.Trigger asChild>
|
||||
@@ -46,12 +56,12 @@ export function NoteMenu() {
|
||||
</button>
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Portal>
|
||||
<DropdownMenu.Content className="flex w-[200px] p-2 flex-col overflow-hidden rounded-xl border border-neutral-200 bg-neutral-950 focus:outline-none dark:border-neutral-900">
|
||||
<DropdownMenu.Content className="flex w-[200px] p-2 flex-col overflow-hidden rounded-2xl bg-black/70 dark:bg-white/20 backdrop-blur-lg focus:outline-none">
|
||||
<DropdownMenu.Item asChild>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => copyLink()}
|
||||
className="inline-flex items-center h-10 px-4 text-sm text-white rounded-lg hover:bg-neutral-900 focus:outline-none"
|
||||
className="inline-flex items-center gap-2 px-3 text-sm font-medium rounded-lg h-9 text-white/50 hover:bg-black/10 hover:text-white focus:outline-none dark:text-white/50 dark:hover:bg-white/10 dark:hover:text-white"
|
||||
>
|
||||
Copy shareable link
|
||||
</button>
|
||||
@@ -60,28 +70,47 @@ export function NoteMenu() {
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => copyID()}
|
||||
className="inline-flex items-center h-10 px-4 text-sm text-white rounded-lg hover:bg-neutral-900 focus:outline-none"
|
||||
className="inline-flex items-center gap-2 px-3 text-sm font-medium rounded-lg h-9 text-white/50 hover:bg-black/10 hover:text-white focus:outline-none dark:text-white/50 dark:hover:bg-white/10 dark:hover:text-white"
|
||||
>
|
||||
Copy note ID
|
||||
Copy ID
|
||||
</button>
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item asChild>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => copyRaw()}
|
||||
className="inline-flex items-center h-10 px-4 text-sm text-white rounded-lg hover:bg-neutral-900 focus:outline-none"
|
||||
onClick={() => copyNpub()}
|
||||
className="inline-flex items-center gap-2 px-3 text-sm font-medium rounded-lg h-9 text-white/50 hover:bg-black/10 hover:text-white focus:outline-none dark:text-white/50 dark:hover:bg-white/10 dark:hover:text-white"
|
||||
>
|
||||
Copy raw event
|
||||
Copy npub
|
||||
</button>
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item asChild>
|
||||
<Link
|
||||
to={`/users/${event.pubkey}`}
|
||||
className="inline-flex items-center h-10 px-4 text-sm text-white rounded-lg hover:bg-neutral-900 focus:outline-none"
|
||||
className="inline-flex items-center gap-2 px-3 text-sm font-medium rounded-lg h-9 text-white/50 hover:bg-black/10 hover:text-white focus:outline-none dark:text-white/50 dark:hover:bg-white/10 dark:hover:text-white"
|
||||
>
|
||||
View profile
|
||||
</Link>
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Separator className="h-px my-1 bg-white/10 dark:bg-black/10" />
|
||||
<DropdownMenu.Item asChild>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => copyRaw()}
|
||||
className="inline-flex items-center gap-2 px-3 text-sm font-medium rounded-lg h-9 text-white/50 hover:bg-black/10 hover:text-white focus:outline-none dark:text-white/50 dark:hover:bg-white/10 dark:hover:text-white"
|
||||
>
|
||||
Copy raw event
|
||||
</button>
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item asChild>
|
||||
<button
|
||||
type="button"
|
||||
onClick={muteUser}
|
||||
className="inline-flex items-center gap-2 px-3 text-sm font-medium text-red-300 rounded-lg h-9 hover:bg-red-500 hover:text-red-50 focus:outline-none"
|
||||
>
|
||||
Mute
|
||||
</button>
|
||||
</DropdownMenu.Item>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Portal>
|
||||
</DropdownMenu.Root>
|
||||
|
||||
@@ -54,12 +54,12 @@ export function LinkPreview({ url }: { url: string }) {
|
||||
<div className="flex flex-col items-start px-3 py-3">
|
||||
<div className="flex flex-col items-start gap-1 text-left">
|
||||
{data.title ? (
|
||||
<div className="text-base font-semibold break-all text-neutral-900 dark:text-neutral-100">
|
||||
<div className="text-base font-semibold break-p text-neutral-900 dark:text-neutral-100">
|
||||
{data.title}
|
||||
</div>
|
||||
) : null}
|
||||
{data.description ? (
|
||||
<div className="mb-2 text-sm break-all line-clamp-3 text-neutral-700 dark:text-neutral-400">
|
||||
<div className="mb-2 text-sm break-p line-clamp-3 text-neutral-700 dark:text-neutral-400">
|
||||
{data.description}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
@@ -24,7 +24,7 @@ export function RepostNote({
|
||||
return new NDKEvent(ark.ndk, embed);
|
||||
}
|
||||
const id = event.tags.find((el) => el[0] === "e")[1];
|
||||
return await ark.getEventById({ id });
|
||||
return await ark.getEventById(id);
|
||||
} catch {
|
||||
throw new Error("Failed to get repost event");
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ export function useEvent(id: string) {
|
||||
const { status, isLoading, isError, data } = useQuery({
|
||||
queryKey: ["event", id],
|
||||
queryFn: async () => {
|
||||
const event = await ark.getEventById({ id });
|
||||
const event = await ark.getEventById(id);
|
||||
if (!event)
|
||||
throw new Error(
|
||||
`Cannot get event with ${id}, will be retry after 10 seconds`,
|
||||
|
||||
@@ -90,9 +90,9 @@ export const LumeProvider = ({ children }: PropsWithChildren<object>) => {
|
||||
: ["wss://brb.io/"],
|
||||
);
|
||||
|
||||
const cacheAdapter = new NDKCacheAdapterTauri(storage);
|
||||
const tauriCache = new NDKCacheAdapterTauri(storage);
|
||||
const ndk = new NDK({
|
||||
cacheAdapter,
|
||||
cacheAdapter: tauriCache,
|
||||
explicitRelayUrls,
|
||||
outboxRelayUrls,
|
||||
blacklistRelayUrls,
|
||||
|
||||
@@ -1,22 +1,24 @@
|
||||
import { SVGProps } from 'react';
|
||||
import { SVGProps } from "react";
|
||||
|
||||
export function LogoutIcon(props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="1.5"
|
||||
d="M20.25 12H9m11.25 0l-4.5 4.5m4.5-4.5l-4.5-4.5m-4.5 12.75h-6.5a1 1 0 01-1-1V4.75a1 1 0 011-1h6.5"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
export function LogoutIcon(
|
||||
props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>,
|
||||
) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="M18.189 9a15 15 0 012.654 2.556c.105.13.157.287.157.444m-2.811 3a14.998 14.998 0 002.654-2.556A.704.704 0 0021 12m0 0H8m5-7.472A6 6 0 003 9v6a6 6 0 0010 4.472"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -73,7 +73,9 @@ export class NDKCacheAdapterTauri implements NDKCacheAdapter {
|
||||
): Promise<void> {
|
||||
const _filter = { ...filter };
|
||||
_filter.limit = undefined;
|
||||
const filterKeys = Object.keys(_filter || {}).sort();
|
||||
const filterKeys = Object.keys(_filter || {})
|
||||
.sort()
|
||||
.filter((e) => e !== "limit" && e !== "since");
|
||||
|
||||
try {
|
||||
await Promise.allSettled([
|
||||
@@ -119,7 +121,9 @@ export class NDKCacheAdapterTauri implements NDKCacheAdapter {
|
||||
id: event.tagId(),
|
||||
pubkey: event.pubkey,
|
||||
content: event.content,
|
||||
// biome-ignore lint/style/noNonNullAssertion: <explanation>
|
||||
kind: event.kind!,
|
||||
// biome-ignore lint/style/noNonNullAssertion: <explanation>
|
||||
createdAt: event.created_at!,
|
||||
relay: relay?.url,
|
||||
event: JSON.stringify(event.rawEvent()),
|
||||
@@ -357,8 +361,8 @@ export class NDKCacheAdapterTauri implements NDKCacheAdapter {
|
||||
const kinds = filter.kinds as number[];
|
||||
|
||||
for (const event of events) {
|
||||
// biome-ignore lint/style/noNonNullAssertion: <explanation>
|
||||
if (!kinds?.includes(event.kind!)) continue;
|
||||
|
||||
subscription.eventReceived(event, undefined, true);
|
||||
}
|
||||
|
||||
|
||||
@@ -414,6 +414,12 @@ export class LumeStorage {
|
||||
await this.#db.execute("DELETE FROM ndk_users;");
|
||||
}
|
||||
|
||||
public async clearProfileCache(pubkey: string) {
|
||||
await this.#db.execute("DELETE FROM ndk_users WHERE pubkey = $1;", [
|
||||
pubkey,
|
||||
]);
|
||||
}
|
||||
|
||||
public async logout() {
|
||||
this.currentUser = null;
|
||||
return await this.#db.execute(
|
||||
|
||||
@@ -1,45 +1,73 @@
|
||||
import { useArk, useProfile } from "@lume/ark";
|
||||
import { useNetworkStatus } from "@lume/utils";
|
||||
import { SettingsIcon } from "@lume/icons";
|
||||
import { cn, useNetworkStatus } from "@lume/utils";
|
||||
import * as Avatar from "@radix-ui/react-avatar";
|
||||
import * as DropdownMenu from "@radix-ui/react-dropdown-menu";
|
||||
import { minidenticon } from "minidenticons";
|
||||
import { useMemo } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { Logout } from "./logout";
|
||||
|
||||
export function ActiveAccount() {
|
||||
const ark = useArk();
|
||||
const isOnline = useNetworkStatus();
|
||||
const svgURI = useMemo(
|
||||
() =>
|
||||
`data:image/svg+xml;utf8,${encodeURIComponent(
|
||||
minidenticon(ark.account.pubkey, 90, 50),
|
||||
)}`,
|
||||
[],
|
||||
);
|
||||
|
||||
const { user } = useProfile(ark.account.pubkey);
|
||||
|
||||
const svgURI = `data:image/svg+xml;utf8,${encodeURIComponent(
|
||||
minidenticon(ark.account.pubkey, 90, 50),
|
||||
)}`;
|
||||
|
||||
return (
|
||||
<Link to="/settings/" className="relative inline-block">
|
||||
<Avatar.Root>
|
||||
<Avatar.Image
|
||||
src={user?.picture || user?.image}
|
||||
alt={ark.account.pubkey}
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
style={{ contentVisibility: "auto" }}
|
||||
className="aspect-square h-auto w-full rounded-xl object-cover"
|
||||
/>
|
||||
<Avatar.Fallback delayMs={150}>
|
||||
<img
|
||||
src={svgURI}
|
||||
alt={ark.account.pubkey}
|
||||
className="aspect-square h-auto w-full rounded-xl bg-black dark:bg-white"
|
||||
<DropdownMenu.Root>
|
||||
<DropdownMenu.Trigger asChild>
|
||||
<div>
|
||||
<Avatar.Root>
|
||||
<Avatar.Image
|
||||
src={user?.picture || user?.image}
|
||||
alt={ark.account.pubkey}
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
style={{ contentVisibility: "auto" }}
|
||||
className="aspect-square h-auto w-full rounded-xl object-cover"
|
||||
/>
|
||||
<Avatar.Fallback delayMs={150}>
|
||||
<img
|
||||
src={svgURI}
|
||||
alt={ark.account.pubkey}
|
||||
className="aspect-square h-auto w-full rounded-xl bg-black dark:bg-white"
|
||||
/>
|
||||
</Avatar.Fallback>
|
||||
</Avatar.Root>
|
||||
<span
|
||||
className={cn(
|
||||
"absolute bottom-0 right-0 block h-2 w-2 rounded-full ring-2 ring-neutral-100 dark:ring-neutral-900",
|
||||
isOnline ? "bg-teal-500" : "bg-red-500",
|
||||
)}
|
||||
/>
|
||||
</Avatar.Fallback>
|
||||
</Avatar.Root>
|
||||
<span
|
||||
className={twMerge(
|
||||
"absolute bottom-0 right-0 block h-2 w-2 rounded-full ring-2 ring-neutral-100 dark:ring-neutral-900",
|
||||
isOnline ? "bg-teal-500" : "bg-red-500",
|
||||
)}
|
||||
/>
|
||||
</Link>
|
||||
</div>
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Portal>
|
||||
<DropdownMenu.Content
|
||||
side="right"
|
||||
sideOffset={5}
|
||||
className="flex w-[200px] p-2 flex-col overflow-hidden rounded-2xl bg-black/70 dark:bg-white/20 backdrop-blur-lg focus:outline-none"
|
||||
>
|
||||
<DropdownMenu.Item asChild>
|
||||
<Link
|
||||
to="/settings/"
|
||||
className="inline-flex items-center gap-2 px-3 text-sm font-medium rounded-lg h-9 text-white/50 hover:bg-black/10 hover:text-white focus:outline-none dark:text-white/50 dark:hover:bg-white/10 dark:hover:text-white"
|
||||
>
|
||||
<SettingsIcon className="size-5" />
|
||||
Settings
|
||||
</Link>
|
||||
</DropdownMenu.Item>
|
||||
<Logout />
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Portal>
|
||||
</DropdownMenu.Root>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { LogoutIcon } from "@lume/icons";
|
||||
import { useStorage } from "@lume/storage";
|
||||
import * as AlertDialog from "@radix-ui/react-alert-dialog";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
@@ -30,8 +31,9 @@ export function Logout() {
|
||||
<AlertDialog.Trigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex h-10 items-center rounded-lg px-2 text-sm font-medium text-white hover:bg-blue-600 focus:outline-none"
|
||||
className="inline-flex items-center gap-2 px-3 text-sm font-medium rounded-lg h-9 text-white/50 hover:bg-black/10 hover:text-white focus:outline-none dark:text-white/50 dark:hover:bg-white/10 dark:hover:text-white"
|
||||
>
|
||||
<LogoutIcon className="size-5" />
|
||||
Logout
|
||||
</button>
|
||||
</AlertDialog.Trigger>
|
||||
|
||||
@@ -1,42 +1,27 @@
|
||||
import {
|
||||
AdvancedSettingsIcon,
|
||||
InfoIcon,
|
||||
NwcFilledIcon,
|
||||
NwcIcon,
|
||||
SecureIcon,
|
||||
SettingsIcon,
|
||||
UserIcon,
|
||||
} from "@lume/icons";
|
||||
import { cn } from "@lume/utils";
|
||||
import { NavLink, Outlet } from "react-router-dom";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
export function SettingsLayout() {
|
||||
return (
|
||||
<div className="flex h-full min-h-0 w-full flex-col gap-8 overflow-y-auto">
|
||||
<div className="flex h-24 w-full items-center justify-center border-b border-neutral-200 px-2 dark:border-neutral-900">
|
||||
<div className="flex h-full min-h-0 w-full flex-col rounded-xl overflow-y-auto">
|
||||
<div className="flex h-24 shrink-0 w-full items-center justify-center px-2 bg-white/50 backdrop-blur-xl dark:bg-black/50">
|
||||
<div className="flex items-center gap-0.5">
|
||||
<NavLink
|
||||
to="/settings/"
|
||||
end
|
||||
to="/settings/"
|
||||
className={({ isActive }) =>
|
||||
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",
|
||||
cn(
|
||||
"flex w-20 shrink-0 flex-col gap-1 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-950 dark:hover:bg-neutral-900"
|
||||
: "",
|
||||
)
|
||||
}
|
||||
>
|
||||
<UserIcon className="size-6" />
|
||||
<p className="text-sm font-medium">User</p>
|
||||
</NavLink>
|
||||
<NavLink
|
||||
to="/settings/general"
|
||||
className={({ isActive }) =>
|
||||
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-950 dark:hover:bg-neutral-900"
|
||||
? "bg-black/10 dark:bg-white/10 text-blue-500 hover:bg-black/20 dark:hover:bg-white/20"
|
||||
: "",
|
||||
)
|
||||
}
|
||||
@@ -44,13 +29,28 @@ export function SettingsLayout() {
|
||||
<SettingsIcon className="size-6" />
|
||||
<p className="text-sm font-medium">General</p>
|
||||
</NavLink>
|
||||
<NavLink
|
||||
to="/settings/profile"
|
||||
end
|
||||
className={({ isActive }) =>
|
||||
cn(
|
||||
"flex w-20 shrink-0 flex-col gap-1 items-center justify-center rounded-lg px-2 py-2 text-neutral-700 hover:bg-black/10 dark:text-neutral-300 dark:hover:bg-white/10",
|
||||
isActive
|
||||
? "bg-black/10 dark:bg-white/10 text-blue-500 hover:bg-black/20 dark:hover:bg-white/20"
|
||||
: "",
|
||||
)
|
||||
}
|
||||
>
|
||||
<UserIcon className="size-6" />
|
||||
<p className="text-sm font-medium">User</p>
|
||||
</NavLink>
|
||||
<NavLink
|
||||
to="/settings/nwc"
|
||||
className={({ isActive }) =>
|
||||
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",
|
||||
cn(
|
||||
"flex w-20 shrink-0 flex-col gap-1 items-center justify-center rounded-lg px-2 py-2 text-neutral-700 hover:bg-black/10 dark:text-neutral-300 dark:hover:bg-white/10",
|
||||
isActive
|
||||
? "bg-neutral-100 text-blue-500 hover:bg-neutral-100 dark:bg-neutral-950 dark:hover:bg-neutral-900"
|
||||
? "bg-black/10 dark:bg-white/10 text-blue-500 hover:bg-black/20 dark:hover:bg-white/20"
|
||||
: "",
|
||||
)
|
||||
}
|
||||
@@ -61,10 +61,10 @@ export function SettingsLayout() {
|
||||
<NavLink
|
||||
to="/settings/backup"
|
||||
className={({ isActive }) =>
|
||||
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",
|
||||
cn(
|
||||
"flex w-20 shrink-0 flex-col gap-1 items-center justify-center rounded-lg px-2 py-2 text-neutral-700 hover:bg-black/10 dark:text-neutral-300 dark:hover:bg-white/10",
|
||||
isActive
|
||||
? "bg-neutral-100 text-blue-500 hover:bg-neutral-100 dark:bg-neutral-950 dark:hover:bg-neutral-900"
|
||||
? "bg-black/10 dark:bg-white/10 text-blue-500 hover:bg-black/20 dark:hover:bg-white/20"
|
||||
: "",
|
||||
)
|
||||
}
|
||||
@@ -75,10 +75,10 @@ export function SettingsLayout() {
|
||||
<NavLink
|
||||
to="/settings/advanced"
|
||||
className={({ isActive }) =>
|
||||
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",
|
||||
cn(
|
||||
"flex w-20 shrink-0 flex-col gap-1 items-center justify-center rounded-lg px-2 py-2 text-neutral-700 hover:bg-black/10 dark:text-neutral-300 dark:hover:bg-white/10",
|
||||
isActive
|
||||
? "bg-neutral-100 text-blue-500 hover:bg-neutral-100 dark:bg-neutral-950 dark:hover:bg-neutral-900"
|
||||
? "bg-black/10 dark:bg-white/10 text-blue-500 hover:bg-black/20 dark:hover:bg-white/20"
|
||||
: "",
|
||||
)
|
||||
}
|
||||
@@ -89,10 +89,10 @@ export function SettingsLayout() {
|
||||
<NavLink
|
||||
to="/settings/about"
|
||||
className={({ isActive }) =>
|
||||
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",
|
||||
cn(
|
||||
"flex w-20 shrink-0 flex-col gap-1 items-center justify-center rounded-lg px-2 py-2 text-neutral-700 hover:bg-black/10 dark:text-neutral-300 dark:hover:bg-white/10",
|
||||
isActive
|
||||
? "bg-neutral-100 text-blue-500 hover:bg-neutral-100 dark:bg-neutral-950 dark:hover:bg-neutral-900"
|
||||
? "bg-black/10 dark:bg-white/10 text-blue-500 hover:bg-black/20 dark:hover:bg-white/20"
|
||||
: "",
|
||||
)
|
||||
}
|
||||
@@ -102,7 +102,9 @@ export function SettingsLayout() {
|
||||
</NavLink>
|
||||
</div>
|
||||
</div>
|
||||
<Outlet />
|
||||
<div className="flex-1 py-6 min-h-0 overflow-y-auto 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">
|
||||
<Outlet />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ export function Navigation() {
|
||||
className={cn(
|
||||
"flex items-center justify-center h-auto w-full text-black aspect-square rounded-xl hover:text-white dark:text-white",
|
||||
isEditorOpen
|
||||
? "bg-blue-500"
|
||||
? "bg-blue-500 text-white"
|
||||
: "bg-black/5 hover:bg-blue-500 dark:bg-white/5 dark:hover:bg-blue-500",
|
||||
)}
|
||||
>
|
||||
@@ -114,7 +114,7 @@ export function Navigation() {
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<NavLink
|
||||
to="/settings"
|
||||
to="/settings/"
|
||||
preventScrollReset={true}
|
||||
className="inline-flex flex-col items-center justify-center"
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user