feat: Multi Accounts (#237)
* wip: new sync * wip: restructure routes * update * feat: improve sync * feat: repost with multi-account * feat: improve sync * feat: publish with multi account * fix: settings screen * feat: add zap for multi accounts
This commit is contained in:
@@ -1,23 +1,17 @@
|
||||
import { commands } from "@/commands.gen";
|
||||
import { appColumns } from "@/commons";
|
||||
import { useRect } from "@/system";
|
||||
import type { LumeColumn } from "@/types";
|
||||
import { CaretDown, Check } from "@phosphor-icons/react";
|
||||
import { useParams } from "@tanstack/react-router";
|
||||
import { useStore } from "@tanstack/react-store";
|
||||
import { Menu, MenuItem, PredefinedMenuItem } from "@tauri-apps/api/menu";
|
||||
import { getCurrentWindow } from "@tauri-apps/api/window";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { User } from "./user";
|
||||
|
||||
export function Column({ column }: { column: LumeColumn }) {
|
||||
const params = useParams({ strict: false });
|
||||
const webviewLabel = useMemo(
|
||||
() => `column-${params.account}_${column.label}`,
|
||||
[params.account, column.label],
|
||||
);
|
||||
const webviewLabel = useMemo(() => `column-${column.label}`, [column.label]);
|
||||
|
||||
const [rect, ref] = useRect();
|
||||
const [error, setError] = useState<string>(null);
|
||||
const [_error, setError] = useState<string>(null);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
@@ -52,7 +46,7 @@ export function Column({ column }: { column: LumeColumn }) {
|
||||
y: initialRect.y,
|
||||
width: initialRect.width,
|
||||
height: initialRect.height,
|
||||
url: `${column.url}?account=${params.account}&label=${column.label}&name=${column.name}`,
|
||||
url: `${column.url}?label=${column.label}&name=${column.name}`,
|
||||
})
|
||||
.then((res) => {
|
||||
if (res.status === "ok") {
|
||||
@@ -73,42 +67,35 @@ export function Column({ column }: { column: LumeColumn }) {
|
||||
});
|
||||
};
|
||||
}
|
||||
}, [params.account]);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="h-full w-[440px] shrink-0 border-r border-black/5 dark:border-white/5">
|
||||
<div className="flex flex-col gap-px size-full">
|
||||
<Header label={column.label} />
|
||||
<Header
|
||||
label={column.label}
|
||||
name={column.name}
|
||||
account={column.account}
|
||||
/>
|
||||
<div ref={ref} className="flex-1 size-full" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Header({ label }: { label: string }) {
|
||||
function Header({
|
||||
label,
|
||||
name,
|
||||
account,
|
||||
}: { label: string; name: string; account?: string }) {
|
||||
const [title, setTitle] = useState("");
|
||||
const [isChanged, setIsChanged] = useState(false);
|
||||
|
||||
const column = useStore(appColumns, (state) =>
|
||||
state.find((col) => col.label === label),
|
||||
);
|
||||
|
||||
const saveNewTitle = async () => {
|
||||
const mainWindow = getCurrentWindow();
|
||||
await mainWindow.emit("columns", { type: "set_title", label, title });
|
||||
|
||||
// update search params
|
||||
// @ts-ignore, hahaha
|
||||
search.name = title;
|
||||
|
||||
// reset state
|
||||
setIsChanged(false);
|
||||
};
|
||||
|
||||
const showContextMenu = useCallback(async (e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
const window = getCurrentWindow();
|
||||
|
||||
const menuItems = await Promise.all([
|
||||
MenuItem.new({
|
||||
text: "Reload",
|
||||
@@ -116,10 +103,6 @@ function Header({ label }: { label: string }) {
|
||||
await commands.reloadColumn(label);
|
||||
},
|
||||
}),
|
||||
MenuItem.new({
|
||||
text: "Open in new window",
|
||||
action: () => console.log("not implemented."),
|
||||
}),
|
||||
PredefinedMenuItem.new({ item: "Separator" }),
|
||||
MenuItem.new({
|
||||
text: "Move left",
|
||||
@@ -160,6 +143,15 @@ function Header({ label }: { label: string }) {
|
||||
await menu.popup().catch((e) => console.error(e));
|
||||
}, []);
|
||||
|
||||
const saveNewTitle = async () => {
|
||||
await getCurrentWindow().emit("columns", {
|
||||
type: "set_title",
|
||||
label,
|
||||
title,
|
||||
});
|
||||
setIsChanged(false);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (title.length > 0) setIsChanged(true);
|
||||
}, [title.length]);
|
||||
@@ -168,13 +160,20 @@ function Header({ label }: { label: string }) {
|
||||
<div className="group flex items-center justify-center gap-2 w-full h-9 shrink-0">
|
||||
<div className="flex items-center justify-center shrink-0 h-7">
|
||||
<div className="relative flex items-center gap-2">
|
||||
{account?.length ? (
|
||||
<User.Provider pubkey={account}>
|
||||
<User.Root>
|
||||
<User.Avatar className="size-6 rounded-full" />
|
||||
</User.Root>
|
||||
</User.Provider>
|
||||
) : null}
|
||||
<div
|
||||
contentEditable
|
||||
suppressContentEditableWarning={true}
|
||||
onBlur={(e) => setTitle(e.currentTarget.textContent)}
|
||||
className="text-[12px] font-semibold focus:outline-none"
|
||||
>
|
||||
{column.name}
|
||||
{name}
|
||||
</div>
|
||||
{isChanged ? (
|
||||
<button
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
import { cn } from "@/commons";
|
||||
import { Note } from "@/components/note";
|
||||
import type { LumeEvent } from "@/system";
|
||||
import { ChatsTeardrop } from "@phosphor-icons/react";
|
||||
import { memo, useMemo } from "react";
|
||||
|
||||
export const Conversation = memo(function Conversation({
|
||||
event,
|
||||
className,
|
||||
}: {
|
||||
event: LumeEvent;
|
||||
className?: string;
|
||||
}) {
|
||||
const thread = useMemo(() => event.thread, [event]);
|
||||
|
||||
return (
|
||||
<Note.Provider event={event}>
|
||||
<Note.Root
|
||||
className={cn(
|
||||
"bg-white dark:bg-black/20 rounded-xl flex flex-col gap-3 shadow-primary dark:ring-1 ring-neutral-800/50",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<div className="flex flex-col gap-3">
|
||||
{thread?.root?.id ? <Note.Child event={thread?.root} isRoot /> : null}
|
||||
<div className="flex items-center gap-2 px-3">
|
||||
<div className="inline-flex items-center gap-1.5 shrink-0 text-sm font-medium text-neutral-600 dark:text-neutral-400">
|
||||
<ChatsTeardrop className="size-4" />
|
||||
Thread
|
||||
</div>
|
||||
<div className="flex-1 h-px bg-neutral-100 dark:bg-white/5" />
|
||||
</div>
|
||||
{thread?.reply?.id ? <Note.Child event={thread?.reply} /> : null}
|
||||
<div>
|
||||
<div className="flex items-center justify-between px-3 h-14">
|
||||
<Note.User />
|
||||
</div>
|
||||
<Note.Content className="px-3" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center px-3 h-14">
|
||||
<Note.Open />
|
||||
</div>
|
||||
</Note.Root>
|
||||
</Note.Provider>
|
||||
);
|
||||
});
|
||||
@@ -1,5 +1,4 @@
|
||||
import { cn } from "@/commons";
|
||||
import { useRouteContext } from "@tanstack/react-router";
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
export function Frame({
|
||||
@@ -7,17 +6,13 @@ export function Frame({
|
||||
shadow,
|
||||
className,
|
||||
}: { children: ReactNode; shadow?: boolean; className?: string }) {
|
||||
const { platform } = useRouteContext({ strict: false });
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
className,
|
||||
platform === "linux"
|
||||
? "bg-white dark:bg-neutral-950"
|
||||
: "bg-white dark:bg-white/10",
|
||||
"bg-white dark:bg-neutral-800",
|
||||
shadow
|
||||
? "shadow-lg shadow-neutral-500/10 dark:shadow-none dark:ring-1 dark:ring-white/20"
|
||||
? "shadow-primary dark:shadow-none dark:ring-1 dark:ring-neutral-700/50"
|
||||
: "",
|
||||
)}
|
||||
>
|
||||
|
||||
19
src/components/icons/publish.tsx
Normal file
19
src/components/icons/publish.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import type { SVGProps } from "react";
|
||||
|
||||
export const PublishIcon = (props: SVGProps<SVGSVGElement>) => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
fillRule="evenodd"
|
||||
d="M19.432 2.738c.505.54.728 1.327.443 2.133-.606 1.713-1.798 3.124-2.797 4.087a15.74 15.74 0 01-1.045.921l.137.1c.93.684 1.416 1.975.757 3.118-1.221 2.12-4.356 5.803-11.192 5.803a.753.753 0 01-.15-.015A32.702 32.702 0 005.5 21.25a.75.75 0 01-1.5 0c0-4.43.821-8.93 2.909-12.485 2.106-3.587 5.49-6.182 10.492-6.749a2.404 2.404 0 012.031.722z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
23
src/components/icons/quote.tsx
Normal file
23
src/components/icons/quote.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import type { SVGProps } from "react";
|
||||
|
||||
export const QuoteIcon = (props: SVGProps<SVGSVGElement>) => (
|
||||
<svg
|
||||
width={24}
|
||||
height={24}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M2.75 5.75a2 2 0 0 1 2-2h14.5a2 2 0 0 1 2 2v10.5a2 2 0 0 1-2 2h-3.874a1 1 0 0 0-.638.23l-2.098 1.738a1 1 0 0 1-1.28-.003l-2.066-1.731a1 1 0 0 0-.642-.234H4.75a2 2 0 0 1-2-2z"
|
||||
stroke="currentColor"
|
||||
strokeWidth={1.5}
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M9.523 8C8.406 8 7.5 8.91 7.5 10.033a2.028 2.028 0 0 0 2.81 1.874q-.072.132-.157.251c-.353.502-.875.885-1.554 1.34a.453.453 0 0 0-.125.626.45.45 0 0 0 .624.125c.67-.449 1.328-.913 1.79-1.569.474-.674.716-1.51.658-2.66A2.03 2.03 0 0 0 9.523 8m4.945 0c-1.117 0-2.023.91-2.023 2.033a2.028 2.028 0 0 0 2.81 1.874q-.072.132-.156.251c-.353.502-.876.885-1.554 1.34a.453.453 0 0 0-.125.626.45.45 0 0 0 .623.125c.67-.449 1.328-.913 1.79-1.569.474-.674.717-1.51.658-2.66A2.03 2.03 0 0 0 14.468 8"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
@@ -4,10 +4,8 @@ export * from "./spinner";
|
||||
export * from "./column";
|
||||
|
||||
// Newsfeed
|
||||
export * from "./repost";
|
||||
export * from "./conversation";
|
||||
export * from "./quote";
|
||||
export * from "./text";
|
||||
export * from "./repost";
|
||||
export * from "./reply";
|
||||
|
||||
// Global components
|
||||
@@ -18,3 +16,5 @@ export * from "./user";
|
||||
export * from "./icons/reply";
|
||||
export * from "./icons/repost";
|
||||
export * from "./icons/zap";
|
||||
export * from "./icons/quote";
|
||||
export * from "./icons/publish";
|
||||
|
||||
@@ -8,7 +8,7 @@ export function NoteOpenThread() {
|
||||
|
||||
return (
|
||||
<Tooltip.Provider>
|
||||
<Tooltip.Root delayDuration={150}>
|
||||
<Tooltip.Root delayDuration={300}>
|
||||
<Tooltip.Trigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
|
||||
40
src/components/note/buttons/quote.tsx
Normal file
40
src/components/note/buttons/quote.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
import { cn } from "@/commons";
|
||||
import { QuoteIcon } from "@/components";
|
||||
import { LumeWindow } from "@/system";
|
||||
import * as Tooltip from "@radix-ui/react-tooltip";
|
||||
import { useNoteContext } from "../provider";
|
||||
|
||||
export function NoteQuote({
|
||||
label = false,
|
||||
smol = false,
|
||||
}: { label?: boolean; smol?: boolean }) {
|
||||
const event = useNoteContext();
|
||||
|
||||
return (
|
||||
<Tooltip.Provider>
|
||||
<Tooltip.Root delayDuration={150}>
|
||||
<Tooltip.Trigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => LumeWindow.openEditor(null, event.id)}
|
||||
className={cn(
|
||||
"inline-flex items-center justify-center text-neutral-800 dark:text-neutral-200",
|
||||
label
|
||||
? "rounded-full h-7 gap-1.5 w-20 text-sm font-medium hover:bg-black/10 dark:hover:bg-white/10"
|
||||
: "size-7",
|
||||
)}
|
||||
>
|
||||
<QuoteIcon className={cn("shrink-0", smol ? "size-4" : "size-5")} />
|
||||
{label ? "Quote" : null}
|
||||
</button>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Portal>
|
||||
<Tooltip.Content className="inline-flex h-7 select-none items-center justify-center rounded-md bg-neutral-950 px-3.5 text-sm text-neutral-50 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-50 dark:text-neutral-950">
|
||||
Quote
|
||||
<Tooltip.Arrow className="fill-neutral-950 dark:fill-neutral-50" />
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Portal>
|
||||
</Tooltip.Root>
|
||||
</Tooltip.Provider>
|
||||
);
|
||||
}
|
||||
@@ -18,10 +18,8 @@ export function NoteReply({
|
||||
type="button"
|
||||
onClick={() => LumeWindow.openEditor(event.id)}
|
||||
className={cn(
|
||||
"inline-flex items-center justify-center text-neutral-800 dark:text-neutral-200",
|
||||
label
|
||||
? "rounded-full h-7 gap-1.5 w-20 text-sm font-medium hover:bg-black/10 dark:hover:bg-white/10"
|
||||
: "size-7",
|
||||
"h-7 rounded-full inline-flex items-center justify-center text-neutral-800 hover:bg-black/5 dark:hover:bg-white/5 dark:text-neutral-200 text-sm font-medium",
|
||||
label ? "w-24 gap-1.5" : "w-14",
|
||||
)}
|
||||
>
|
||||
<ReplyIcon className={cn("shrink-0", smol ? "size-4" : "size-5")} />
|
||||
|
||||
@@ -1,88 +1,174 @@
|
||||
import { appSettings, cn } from "@/commons";
|
||||
import { commands } from "@/commands.gen";
|
||||
import { appSettings, cn, displayNpub } from "@/commons";
|
||||
import { RepostIcon, Spinner } from "@/components";
|
||||
import { LumeWindow } from "@/system";
|
||||
import type { Metadata } from "@/types";
|
||||
import * as Tooltip from "@radix-ui/react-tooltip";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { useStore } from "@tanstack/react-store";
|
||||
import { Menu, MenuItem } from "@tauri-apps/api/menu";
|
||||
import { message } from "@tauri-apps/plugin-dialog";
|
||||
import { useCallback, useState } from "react";
|
||||
import type { Window } from "@tauri-apps/api/window";
|
||||
import { useCallback, useEffect, useState, useTransition } from "react";
|
||||
import { useNoteContext } from "../provider";
|
||||
|
||||
export function NoteRepost({
|
||||
label = false,
|
||||
smol = false,
|
||||
}: { label?: boolean; smol?: boolean }) {
|
||||
const visible = useStore(appSettings, (state) => state.display_repost_button);
|
||||
const event = useNoteContext();
|
||||
const visible = useStore(appSettings, (state) => state.display_repost_button);
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [isRepost, setIsRepost] = useState(false);
|
||||
const { isLoading, data: status } = useQuery({
|
||||
queryKey: ["is-reposted", event.id],
|
||||
queryFn: async () => {
|
||||
const res = await commands.isReposted(event.id);
|
||||
if (res.status === "ok") {
|
||||
return res.data;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
enabled: visible,
|
||||
refetchOnMount: false,
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnReconnect: false,
|
||||
staleTime: Number.POSITIVE_INFINITY,
|
||||
retry: false,
|
||||
});
|
||||
|
||||
const repost = async () => {
|
||||
if (isRepost) return;
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
|
||||
// repost
|
||||
await event.repost();
|
||||
|
||||
// update state
|
||||
setLoading(false);
|
||||
setIsRepost(true);
|
||||
} catch {
|
||||
setLoading(false);
|
||||
await message("Repost failed, try again later", {
|
||||
title: "Lume",
|
||||
kind: "info",
|
||||
});
|
||||
}
|
||||
};
|
||||
const [isPending, startTransition] = useTransition();
|
||||
const [popup, setPopup] = useState<Window>(null);
|
||||
|
||||
const showContextMenu = useCallback(async (e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
const menuItems = await Promise.all([
|
||||
MenuItem.new({
|
||||
text: "Repost",
|
||||
action: async () => repost(),
|
||||
}),
|
||||
MenuItem.new({
|
||||
text: "Quote",
|
||||
action: () => LumeWindow.openEditor(null, event.id),
|
||||
}),
|
||||
]);
|
||||
const accounts = await commands.getAccounts();
|
||||
const list = [];
|
||||
|
||||
const menu = await Menu.new({
|
||||
items: menuItems,
|
||||
});
|
||||
for (const account of accounts) {
|
||||
const res = await commands.getProfile(account);
|
||||
let name = "unknown";
|
||||
|
||||
if (res.status === "ok") {
|
||||
const profile: Metadata = JSON.parse(res.data);
|
||||
name = profile.display_name ?? profile.name;
|
||||
}
|
||||
|
||||
list.push(
|
||||
MenuItem.new({
|
||||
text: `Repost as ${name} (${displayNpub(account, 16)})`,
|
||||
action: async () => submit(account),
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
const items = await Promise.all(list);
|
||||
const menu = await Menu.new({ items });
|
||||
|
||||
await menu.popup().catch((e) => console.error(e));
|
||||
}, []);
|
||||
|
||||
const repost = useMutation({
|
||||
mutationFn: async () => {
|
||||
// Cancel any outgoing refetches
|
||||
await queryClient.cancelQueries({ queryKey: ["is-reposted", event.id] });
|
||||
|
||||
// Optimistically update to the new value
|
||||
queryClient.setQueryData(["is-reposted", event.id], true);
|
||||
|
||||
const res = await commands.repost(JSON.stringify(event.raw));
|
||||
|
||||
if (res.status === "ok") {
|
||||
return;
|
||||
} else {
|
||||
throw new Error(res.error);
|
||||
}
|
||||
},
|
||||
onError: () => {
|
||||
queryClient.setQueryData(["is-reposted", event.id], false);
|
||||
},
|
||||
onSettled: async () => {
|
||||
return await queryClient.invalidateQueries({
|
||||
queryKey: ["is-reposted", event.id],
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const submit = (account: string) => {
|
||||
startTransition(async () => {
|
||||
if (!status) {
|
||||
const signer = await commands.hasSigner(account);
|
||||
|
||||
if (signer.status === "ok") {
|
||||
if (!signer.data) {
|
||||
const newPopup = await LumeWindow.openPopup(
|
||||
`/set-signer/${account}`,
|
||||
undefined,
|
||||
false,
|
||||
);
|
||||
|
||||
setPopup(newPopup);
|
||||
return;
|
||||
}
|
||||
|
||||
repost.mutate();
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!visible) return;
|
||||
if (!popup) return;
|
||||
|
||||
const unlisten = popup.listen("signer-updated", async () => {
|
||||
repost.mutate();
|
||||
});
|
||||
|
||||
return () => {
|
||||
unlisten.then((f) => f());
|
||||
};
|
||||
}, [popup]);
|
||||
|
||||
if (!visible) return null;
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => showContextMenu(e)}
|
||||
className={cn(
|
||||
"inline-flex items-center justify-center text-neutral-800 dark:text-neutral-200",
|
||||
label
|
||||
? "rounded-full h-7 gap-1.5 w-24 text-sm font-medium hover:bg-black/10 dark:hover:bg-white/10"
|
||||
: "size-7",
|
||||
)}
|
||||
>
|
||||
{loading ? (
|
||||
<Spinner className="size-4" />
|
||||
) : (
|
||||
<RepostIcon
|
||||
className={cn(
|
||||
smol ? "size-4" : "size-5",
|
||||
isRepost ? "text-blue-500" : "",
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{label ? "Repost" : null}
|
||||
</button>
|
||||
<Tooltip.Provider>
|
||||
<Tooltip.Root delayDuration={300}>
|
||||
<Tooltip.Trigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => showContextMenu(e)}
|
||||
className={cn(
|
||||
"h-7 rounded-full inline-flex items-center justify-center text-neutral-800 hover:bg-black/5 dark:hover:bg-white/5 dark:text-neutral-200 text-sm font-medium",
|
||||
label ? "w-24 gap-1.5" : "w-14",
|
||||
)}
|
||||
>
|
||||
{isPending || isLoading ? (
|
||||
<Spinner className="size-4" />
|
||||
) : (
|
||||
<RepostIcon
|
||||
className={cn(
|
||||
smol ? "size-4" : "size-5",
|
||||
status ? "text-blue-500" : "",
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{label ? "Repost" : null}
|
||||
</button>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Portal>
|
||||
<Tooltip.Content className="inline-flex h-7 select-none items-center justify-center rounded-md bg-neutral-950 px-3.5 text-sm text-neutral-50 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-50 dark:text-neutral-950">
|
||||
Repost
|
||||
<Tooltip.Arrow className="fill-neutral-950 dark:fill-neutral-50" />
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Portal>
|
||||
</Tooltip.Root>
|
||||
</Tooltip.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -20,10 +20,8 @@ export function NoteZap({
|
||||
type="button"
|
||||
onClick={() => LumeWindow.openZap(event.id, search.account)}
|
||||
className={cn(
|
||||
"inline-flex items-center justify-center text-neutral-800 dark:text-neutral-200",
|
||||
label
|
||||
? "rounded-full h-7 gap-1.5 w-20 text-sm font-medium hover:bg-black/10 dark:hover:bg-white/10"
|
||||
: "size-7",
|
||||
"h-7 rounded-full inline-flex items-center justify-center text-neutral-800 hover:bg-black/5 dark:hover:bg-white/5 dark:text-neutral-200 text-sm font-medium",
|
||||
label ? "w-24 gap-1.5" : "w-14",
|
||||
)}
|
||||
>
|
||||
<ZapIcon className={smol ? "size-4" : "size-5"} />
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { NoteOpenThread } from "./buttons/open";
|
||||
import { NoteQuote } from "./buttons/quote";
|
||||
import { NoteReply } from "./buttons/reply";
|
||||
import { NoteRepost } from "./buttons/repost";
|
||||
import { NoteZap } from "./buttons/zap";
|
||||
@@ -16,6 +17,7 @@ export const Note = {
|
||||
User: NoteUser,
|
||||
Menu: NoteMenu,
|
||||
Reply: NoteReply,
|
||||
Quote: NoteQuote,
|
||||
Repost: NoteRepost,
|
||||
Content: NoteContent,
|
||||
ContentLarge: NoteContentLarge,
|
||||
|
||||
@@ -51,11 +51,11 @@ export const MentionNote = memo(function MentionNote({
|
||||
<span className="text-sm text-neutral-500">
|
||||
{replyTime(event.created_at)}
|
||||
</span>
|
||||
<div className="invisible group-hover:visible flex items-center justify-end gap-3">
|
||||
<div className="invisible group-hover:visible flex items-center justify-end">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => LumeWindow.openEvent(event)}
|
||||
className="text-sm font-medium text-blue-500 hover:text-blue-600"
|
||||
className="mr-3 text-sm font-medium text-blue-500 hover:text-blue-600"
|
||||
>
|
||||
Show all
|
||||
</button>
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
import { cn } from "@/commons";
|
||||
import { Note } from "@/components/note";
|
||||
import type { LumeEvent } from "@/system";
|
||||
import { Quotes } from "@phosphor-icons/react";
|
||||
import { memo } from "react";
|
||||
|
||||
export const Quote = memo(function Quote({
|
||||
event,
|
||||
className,
|
||||
}: {
|
||||
event: LumeEvent;
|
||||
className?: string;
|
||||
}) {
|
||||
return (
|
||||
<Note.Provider event={event}>
|
||||
<Note.Root className={cn("", className)}>
|
||||
<div className="flex flex-col gap-3">
|
||||
<Note.Child event={event.quote} isRoot />
|
||||
<div className="flex items-center gap-2 px-3">
|
||||
<div className="inline-flex items-center gap-1.5 shrink-0 text-sm font-medium text-neutral-600 dark:text-neutral-400">
|
||||
<Quotes className="size-4" />
|
||||
Quote
|
||||
</div>
|
||||
<div className="flex-1 h-px bg-neutral-100 dark:bg-white/5" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex items-center justify-between px-3 h-14">
|
||||
<Note.User />
|
||||
</div>
|
||||
<Note.Content className="px-3" quote={false} clean />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center px-3 h-14">
|
||||
<Note.Open />
|
||||
</div>
|
||||
</Note.Root>
|
||||
</Note.Provider>
|
||||
);
|
||||
});
|
||||
@@ -1,21 +1,12 @@
|
||||
import { commands } from "@/commands.gen";
|
||||
import { appSettings, cn, replyTime } from "@/commons";
|
||||
import { cn, replyTime } from "@/commons";
|
||||
import { Note } from "@/components/note";
|
||||
import { type LumeEvent, LumeWindow } from "@/system";
|
||||
import { CaretDown } from "@phosphor-icons/react";
|
||||
import { Link, useSearch } from "@tanstack/react-router";
|
||||
import { useStore } from "@tanstack/react-store";
|
||||
import { Menu, MenuItem } from "@tauri-apps/api/menu";
|
||||
import { writeText } from "@tauri-apps/plugin-clipboard-manager";
|
||||
import { nip19 } from "nostr-tools";
|
||||
import {
|
||||
type ReactNode,
|
||||
memo,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
} from "react";
|
||||
import { type ReactNode, memo, useCallback, useMemo } from "react";
|
||||
import reactStringReplace from "react-string-replace";
|
||||
import { Hashtag } from "./note/mentions/hashtag";
|
||||
import { MentionUser } from "./note/mentions/user";
|
||||
@@ -28,11 +19,7 @@ export const ReplyNote = memo(function ReplyNote({
|
||||
event: LumeEvent;
|
||||
className?: string;
|
||||
}) {
|
||||
const trustedOnly = useStore(appSettings, (state) => state.trusted_only);
|
||||
const search = useSearch({ strict: false });
|
||||
|
||||
const [isTrusted, setIsTrusted] = useState<boolean>(null);
|
||||
|
||||
const showContextMenu = useCallback(async (e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
@@ -57,24 +44,6 @@ export const ReplyNote = memo(function ReplyNote({
|
||||
await menu.popup().catch((e) => console.error(e));
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
async function check() {
|
||||
const res = await commands.isTrustedUser(event.pubkey);
|
||||
|
||||
if (res.status === "ok") {
|
||||
setIsTrusted(res.data);
|
||||
}
|
||||
}
|
||||
|
||||
if (trustedOnly) {
|
||||
check();
|
||||
}
|
||||
}, []);
|
||||
|
||||
if (isTrusted !== null && isTrusted === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Note.Provider event={event}>
|
||||
<User.Provider pubkey={event.pubkey}>
|
||||
@@ -99,7 +68,7 @@ export const ReplyNote = memo(function ReplyNote({
|
||||
<span className="text-sm text-neutral-500">
|
||||
{replyTime(event.created_at)}
|
||||
</span>
|
||||
<div className="flex items-center justify-end gap-5">
|
||||
<div className="flex items-center justify-end">
|
||||
<Note.Reply smol />
|
||||
<Note.Repost smol />
|
||||
<Note.Zap smol />
|
||||
@@ -180,7 +149,7 @@ function ChildReply({ event }: { event: LumeEvent }) {
|
||||
<span className="text-sm text-neutral-500">
|
||||
{replyTime(event.created_at)}
|
||||
</span>
|
||||
<div className="invisible group-hover:visible flex items-center justify-end gap-5">
|
||||
<div className="invisible group-hover:visible flex items-center justify-end">
|
||||
<Note.Reply smol />
|
||||
<Note.Repost smol />
|
||||
<Note.Zap smol />
|
||||
|
||||
@@ -36,7 +36,7 @@ export const RepostNote = memo(function RepostNote({
|
||||
</div>
|
||||
<Note.Content className="px-3" />
|
||||
<div className="flex items-center justify-between px-3 mt-3 h-14">
|
||||
<div className="inline-flex items-center gap-6">
|
||||
<div className="inline-flex items-center gap-2">
|
||||
<Note.Open />
|
||||
<Note.Reply />
|
||||
<Note.Repost />
|
||||
|
||||
@@ -18,7 +18,7 @@ export const TextNote = memo(function TextNote({
|
||||
<Note.Menu />
|
||||
</div>
|
||||
<Note.Content className="px-3" />
|
||||
<div className="flex items-center gap-6 px-3 mt-3 h-14">
|
||||
<div className="flex items-center gap-2 px-3 mt-3 h-14">
|
||||
<Note.Open />
|
||||
<Note.Reply />
|
||||
<Note.Repost />
|
||||
|
||||
@@ -7,7 +7,7 @@ import { message } from "@tauri-apps/plugin-dialog";
|
||||
import { useTransition } from "react";
|
||||
import { useUserContext } from "./provider";
|
||||
|
||||
export function UserFollowButton({ className }: { className?: string }) {
|
||||
export function UserButton({ className }: { className?: string }) {
|
||||
const user = useUserContext();
|
||||
|
||||
const { queryClient } = useRouteContext({ strict: false });
|
||||
@@ -18,7 +18,7 @@ export function UserFollowButton({ className }: { className?: string }) {
|
||||
} = useQuery({
|
||||
queryKey: ["status", user.pubkey],
|
||||
queryFn: async () => {
|
||||
const res = await commands.checkContact(user.pubkey);
|
||||
const res = await commands.isContact(user.pubkey);
|
||||
|
||||
if (res.status === "ok") {
|
||||
return res.data;
|
||||
@@ -1,7 +1,7 @@
|
||||
import { UserAbout } from "./about";
|
||||
import { UserAvatar } from "./avatar";
|
||||
import { UserButton } from "./button";
|
||||
import { UserCover } from "./cover";
|
||||
import { UserFollowButton } from "./followButton";
|
||||
import { UserName } from "./name";
|
||||
import { UserNip05 } from "./nip05";
|
||||
import { UserProvider } from "./provider";
|
||||
@@ -17,5 +17,5 @@ export const User = {
|
||||
NIP05: UserNip05,
|
||||
Time: UserTime,
|
||||
About: UserAbout,
|
||||
Button: UserFollowButton,
|
||||
Button: UserButton,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user