mirror of
https://github.com/lumehq/lume.git
synced 2025-03-17 21:32:32 +01:00
feat: repost with multi-account
This commit is contained in:
parent
033272fd6d
commit
b1efc33401
@ -157,6 +157,26 @@ pub fn delete_account(id: String) -> Result<(), String> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn has_signer(id: String, state: State<'_, Nostr>) -> Result<bool, String> {
|
||||
let client = &state.client;
|
||||
let public_key = PublicKey::from_str(&id).map_err(|e| e.to_string())?;
|
||||
|
||||
match client.signer().await {
|
||||
Ok(signer) => {
|
||||
let signer_key = signer.public_key().await.unwrap();
|
||||
|
||||
if signer_key == public_key {
|
||||
Ok(true)
|
||||
} else {
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
Err(_) => Ok(false),
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn set_signer(
|
||||
|
@ -410,16 +410,58 @@ pub async fn repost(raw: String, state: State<'_, Nostr>) -> Result<String, Stri
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn delete(id: String, state: State<'_, Nostr>) -> Result<String, String> {
|
||||
pub async fn is_reposted(id: String, state: State<'_, Nostr>) -> Result<bool, String> {
|
||||
let client = &state.client;
|
||||
let accounts = state.accounts.lock().unwrap().clone();
|
||||
|
||||
let event_id = EventId::parse(&id).map_err(|err| err.to_string())?;
|
||||
|
||||
match client.delete_event(event_id).await {
|
||||
Ok(event_id) => Ok(event_id.to_string()),
|
||||
let authors: Vec<PublicKey> = accounts
|
||||
.iter()
|
||||
.map(|acc| PublicKey::from_str(acc).unwrap())
|
||||
.collect();
|
||||
|
||||
let filter = Filter::new()
|
||||
.event(event_id)
|
||||
.kind(Kind::Repost)
|
||||
.authors(authors);
|
||||
|
||||
match client.database().query(vec![filter]).await {
|
||||
Ok(events) => Ok(!events.is_empty()),
|
||||
Err(err) => Err(err.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn request_delete(id: String, state: State<'_, Nostr>) -> Result<(), String> {
|
||||
let client = &state.client;
|
||||
let event_id = EventId::from_str(&id).map_err(|err| err.to_string())?;
|
||||
|
||||
match client.delete_event(event_id).await {
|
||||
Ok(_) => Ok(()),
|
||||
Err(e) => Err(e.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn is_deleted_event(id: String, state: State<'_, Nostr>) -> Result<bool, String> {
|
||||
let client = &state.client;
|
||||
let signer = client.signer().await.map_err(|err| err.to_string())?;
|
||||
let public_key = signer.public_key().await.map_err(|err| err.to_string())?;
|
||||
let event_id = EventId::from_str(&id).map_err(|err| err.to_string())?;
|
||||
let filter = Filter::new()
|
||||
.author(public_key)
|
||||
.event(event_id)
|
||||
.kind(Kind::EventDeletion);
|
||||
|
||||
match client.database().query(vec![filter]).await {
|
||||
Ok(events) => Ok(!events.is_empty()),
|
||||
Err(e) => Err(e.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn event_to_bech32(id: String, state: State<'_, Nostr>) -> Result<String, String> {
|
||||
@ -498,34 +540,3 @@ pub async fn search(query: String, state: State<'_, Nostr>) -> Result<Vec<RichEv
|
||||
Err(e) => Err(e.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn is_deleted_event(id: String, state: State<'_, Nostr>) -> Result<bool, String> {
|
||||
let client = &state.client;
|
||||
let signer = client.signer().await.map_err(|err| err.to_string())?;
|
||||
let public_key = signer.public_key().await.map_err(|err| err.to_string())?;
|
||||
let event_id = EventId::from_str(&id).map_err(|err| err.to_string())?;
|
||||
let filter = Filter::new()
|
||||
.author(public_key)
|
||||
.event(event_id)
|
||||
.kind(Kind::EventDeletion);
|
||||
|
||||
match client.database().query(vec![filter]).await {
|
||||
Ok(events) => Ok(!events.is_empty()),
|
||||
Err(e) => Err(e.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn request_delete(id: String, state: State<'_, Nostr>) -> Result<(), String> {
|
||||
let client = &state.client;
|
||||
let event_id = EventId::from_str(&id).map_err(|err| err.to_string())?;
|
||||
let builder = EventBuilder::delete(vec![event_id]);
|
||||
|
||||
match client.send_event_builder(builder).await {
|
||||
Ok(_) => Ok(()),
|
||||
Err(e) => Err(e.to_string()),
|
||||
}
|
||||
}
|
||||
|
@ -22,6 +22,7 @@ pub struct Window {
|
||||
maximizable: bool,
|
||||
minimizable: bool,
|
||||
hidden_title: bool,
|
||||
closable: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Type)]
|
||||
@ -109,7 +110,7 @@ pub fn reload_column(label: String, app_handle: tauri::AppHandle) -> Result<bool
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
#[cfg(target_os = "macos")]
|
||||
pub fn open_window(window: Window, app_handle: tauri::AppHandle) -> Result<(), String> {
|
||||
pub fn open_window(window: Window, app_handle: tauri::AppHandle) -> Result<String, String> {
|
||||
if let Some(current_window) = app_handle.get_window(&window.label) {
|
||||
if current_window.is_visible().unwrap_or_default() {
|
||||
let _ = current_window.set_focus();
|
||||
@ -117,6 +118,8 @@ pub fn open_window(window: Window, app_handle: tauri::AppHandle) -> Result<(), S
|
||||
let _ = current_window.show();
|
||||
let _ = current_window.set_focus();
|
||||
};
|
||||
|
||||
Ok(current_window.label().to_string())
|
||||
} else {
|
||||
let new_window = WebviewWindowBuilder::new(
|
||||
&app_handle,
|
||||
@ -131,6 +134,7 @@ pub fn open_window(window: Window, app_handle: tauri::AppHandle) -> Result<(), S
|
||||
.minimizable(window.minimizable)
|
||||
.maximizable(window.maximizable)
|
||||
.transparent(true)
|
||||
.closable(window.closable)
|
||||
.effects(WindowEffectsConfig {
|
||||
state: None,
|
||||
effects: vec![Effect::UnderWindowBackground],
|
||||
@ -142,24 +146,26 @@ pub fn open_window(window: Window, app_handle: tauri::AppHandle) -> Result<(), S
|
||||
|
||||
// Restore native border
|
||||
new_window.add_border(None);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
Ok(new_window.label().to_string())
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command(async)]
|
||||
#[specta::specta]
|
||||
#[cfg(target_os = "windows")]
|
||||
pub fn open_window(window: Window, app_handle: tauri::AppHandle) -> Result<(), String> {
|
||||
if let Some(window) = app_handle.get_window(&window.label) {
|
||||
if window.is_visible().unwrap_or_default() {
|
||||
let _ = window.set_focus();
|
||||
pub fn open_window(window: Window, app_handle: tauri::AppHandle) -> Result<String, String> {
|
||||
if let Some(current_window) = app_handle.get_window(&window.label) {
|
||||
if current_window.is_visible().unwrap_or_default() {
|
||||
let _ = current_window.set_focus();
|
||||
} else {
|
||||
let _ = window.show();
|
||||
let _ = window.set_focus();
|
||||
let _ = current_window.show();
|
||||
let _ = current_window.set_focus();
|
||||
};
|
||||
|
||||
Ok(current_window.label().to_string())
|
||||
} else {
|
||||
let window = WebviewWindowBuilder::new(
|
||||
let new_window = WebviewWindowBuilder::new(
|
||||
&app_handle,
|
||||
&window.label,
|
||||
WebviewUrl::App(PathBuf::from(window.url)),
|
||||
@ -171,6 +177,7 @@ pub fn open_window(window: Window, app_handle: tauri::AppHandle) -> Result<(), S
|
||||
.maximizable(window.maximizable)
|
||||
.transparent(true)
|
||||
.decorations(false)
|
||||
.closable(window.closable)
|
||||
.effects(WindowEffectsConfig {
|
||||
state: None,
|
||||
effects: vec![Effect::Mica],
|
||||
@ -181,7 +188,9 @@ pub fn open_window(window: Window, app_handle: tauri::AppHandle) -> Result<(), S
|
||||
.unwrap();
|
||||
|
||||
// Set decoration
|
||||
window.create_overlay_titlebar().unwrap();
|
||||
new_window.create_overlay_titlebar().unwrap();
|
||||
|
||||
Ok(new_window.label().to_string())
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
@ -105,6 +105,7 @@ fn main() {
|
||||
get_private_key,
|
||||
delete_account,
|
||||
reset_password,
|
||||
has_signer,
|
||||
set_signer,
|
||||
get_profile,
|
||||
set_profile,
|
||||
@ -139,12 +140,13 @@ fn main() {
|
||||
get_all_events_by_hashtags,
|
||||
get_local_events,
|
||||
get_global_events,
|
||||
is_deleted_event,
|
||||
request_delete,
|
||||
search,
|
||||
publish,
|
||||
reply,
|
||||
repost,
|
||||
is_reposted,
|
||||
request_delete,
|
||||
is_deleted_event,
|
||||
event_to_bech32,
|
||||
user_to_bech32,
|
||||
create_column,
|
||||
|
@ -99,6 +99,14 @@ async resetPassword(key: string, password: string) : Promise<Result<null, string
|
||||
else return { status: "error", error: e as any };
|
||||
}
|
||||
},
|
||||
async hasSigner(id: string) : Promise<Result<boolean, string>> {
|
||||
try {
|
||||
return { status: "ok", data: await TAURI_INVOKE("has_signer", { id }) };
|
||||
} catch (e) {
|
||||
if(e instanceof Error) throw e;
|
||||
else return { status: "error", error: e as any };
|
||||
}
|
||||
},
|
||||
async setSigner(account: string, password: string) : Promise<Result<null, string>> {
|
||||
try {
|
||||
return { status: "ok", data: await TAURI_INVOKE("set_signer", { account, password }) };
|
||||
@ -371,22 +379,6 @@ async getGlobalEvents(until: string | null) : Promise<Result<RichEvent[], string
|
||||
else return { status: "error", error: e as any };
|
||||
}
|
||||
},
|
||||
async isDeletedEvent(id: string) : Promise<Result<boolean, string>> {
|
||||
try {
|
||||
return { status: "ok", data: await TAURI_INVOKE("is_deleted_event", { id }) };
|
||||
} catch (e) {
|
||||
if(e instanceof Error) throw e;
|
||||
else return { status: "error", error: e as any };
|
||||
}
|
||||
},
|
||||
async requestDelete(id: string) : Promise<Result<null, string>> {
|
||||
try {
|
||||
return { status: "ok", data: await TAURI_INVOKE("request_delete", { id }) };
|
||||
} catch (e) {
|
||||
if(e instanceof Error) throw e;
|
||||
else return { status: "error", error: e as any };
|
||||
}
|
||||
},
|
||||
async search(query: string) : Promise<Result<RichEvent[], string>> {
|
||||
try {
|
||||
return { status: "ok", data: await TAURI_INVOKE("search", { query }) };
|
||||
@ -419,6 +411,30 @@ async repost(raw: string) : Promise<Result<string, string>> {
|
||||
else return { status: "error", error: e as any };
|
||||
}
|
||||
},
|
||||
async isReposted(id: string) : Promise<Result<boolean, string>> {
|
||||
try {
|
||||
return { status: "ok", data: await TAURI_INVOKE("is_reposted", { id }) };
|
||||
} catch (e) {
|
||||
if(e instanceof Error) throw e;
|
||||
else return { status: "error", error: e as any };
|
||||
}
|
||||
},
|
||||
async requestDelete(id: string) : Promise<Result<null, string>> {
|
||||
try {
|
||||
return { status: "ok", data: await TAURI_INVOKE("request_delete", { id }) };
|
||||
} catch (e) {
|
||||
if(e instanceof Error) throw e;
|
||||
else return { status: "error", error: e as any };
|
||||
}
|
||||
},
|
||||
async isDeletedEvent(id: string) : Promise<Result<boolean, string>> {
|
||||
try {
|
||||
return { status: "ok", data: await TAURI_INVOKE("is_deleted_event", { id }) };
|
||||
} catch (e) {
|
||||
if(e instanceof Error) throw e;
|
||||
else return { status: "error", error: e as any };
|
||||
}
|
||||
},
|
||||
async eventToBech32(id: string) : Promise<Result<string, string>> {
|
||||
try {
|
||||
return { status: "ok", data: await TAURI_INVOKE("event_to_bech32", { id }) };
|
||||
@ -467,7 +483,7 @@ async closeColumn(label: string) : Promise<Result<boolean, string>> {
|
||||
else return { status: "error", error: e as any };
|
||||
}
|
||||
},
|
||||
async openWindow(window: Window) : Promise<Result<null, string>> {
|
||||
async openWindow(window: Window) : Promise<Result<string, string>> {
|
||||
try {
|
||||
return { status: "ok", data: await TAURI_INVOKE("open_window", { window }) };
|
||||
} catch (e) {
|
||||
@ -511,7 +527,7 @@ export type RichEvent = { raw: string; parsed: Meta | null }
|
||||
export type Settings = { proxy: string | null; image_resize_service: string | null; use_relay_hint: boolean; content_warning: boolean; trusted_only: boolean; display_avatar: boolean; display_zap_button: boolean; display_repost_button: boolean; display_media: boolean; transparent: boolean }
|
||||
export type SubKind = "Subscribe" | "Unsubscribe"
|
||||
export type Subscription = { label: string; kind: SubKind; event_id: string | null; contacts: string[] | null }
|
||||
export type Window = { label: string; title: string; url: string; width: number; height: number; maximizable: boolean; minimizable: boolean; hidden_title: boolean }
|
||||
export type Window = { label: string; title: string; url: string; width: number; height: number; maximizable: boolean; minimizable: boolean; hidden_title: boolean; closable: boolean }
|
||||
|
||||
/** tauri-specta globals **/
|
||||
|
||||
|
@ -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,8 +6,6 @@ export function Frame({
|
||||
shadow,
|
||||
className,
|
||||
}: { children: ReactNode; shadow?: boolean; className?: string }) {
|
||||
const { platform } = useRouteContext({ strict: false });
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
|
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,4 @@ export * from "./user";
|
||||
export * from "./icons/reply";
|
||||
export * from "./icons/repost";
|
||||
export * from "./icons/zap";
|
||||
export * from "./icons/quote";
|
||||
|
@ -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=${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 />
|
||||
|
@ -13,6 +13,7 @@ import { createFileRoute } from '@tanstack/react-router'
|
||||
// Import Routes
|
||||
|
||||
import { Route as rootRoute } from './routes/__root'
|
||||
import { Route as SetSignerImport } from './routes/set-signer'
|
||||
import { Route as SetInterestImport } from './routes/set-interest'
|
||||
import { Route as SetGroupImport } from './routes/set-group'
|
||||
import { Route as BootstrapRelaysImport } from './routes/bootstrap-relays'
|
||||
@ -91,6 +92,11 @@ const SettingsLazyRoute = SettingsLazyImport.update({
|
||||
getParentRoute: () => rootRoute,
|
||||
} as any).lazy(() => import('./routes/_settings.lazy').then((d) => d.Route))
|
||||
|
||||
const SetSignerRoute = SetSignerImport.update({
|
||||
path: '/set-signer',
|
||||
getParentRoute: () => rootRoute,
|
||||
} as any).lazy(() => import('./routes/set-signer.lazy').then((d) => d.Route))
|
||||
|
||||
const SetInterestRoute = SetInterestImport.update({
|
||||
path: '/set-interest',
|
||||
getParentRoute: () => rootRoute,
|
||||
@ -330,6 +336,13 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof SetInterestImport
|
||||
parentRoute: typeof rootRoute
|
||||
}
|
||||
'/set-signer': {
|
||||
id: '/set-signer'
|
||||
path: '/set-signer'
|
||||
fullPath: '/set-signer'
|
||||
preLoaderRoute: typeof SetSignerImport
|
||||
parentRoute: typeof rootRoute
|
||||
}
|
||||
'/_settings': {
|
||||
id: '/_settings'
|
||||
path: ''
|
||||
@ -662,6 +675,7 @@ export interface FileRoutesByFullPath {
|
||||
'/bootstrap-relays': typeof BootstrapRelaysRoute
|
||||
'/set-group': typeof SetGroupRoute
|
||||
'/set-interest': typeof SetInterestRoute
|
||||
'/set-signer': typeof SetSignerRoute
|
||||
'/new': typeof NewLazyRoute
|
||||
'/reset': typeof ResetLazyRoute
|
||||
'/bitcoin-connect': typeof SettingsBitcoinConnectRoute
|
||||
@ -698,6 +712,7 @@ export interface FileRoutesByTo {
|
||||
'/bootstrap-relays': typeof BootstrapRelaysRoute
|
||||
'/set-group': typeof SetGroupRoute
|
||||
'/set-interest': typeof SetInterestRoute
|
||||
'/set-signer': typeof SetSignerRoute
|
||||
'': typeof SettingsLazyRouteWithChildren
|
||||
'/new': typeof NewLazyRoute
|
||||
'/reset': typeof ResetLazyRoute
|
||||
@ -737,6 +752,7 @@ export interface FileRoutesById {
|
||||
'/bootstrap-relays': typeof BootstrapRelaysRoute
|
||||
'/set-group': typeof SetGroupRoute
|
||||
'/set-interest': typeof SetInterestRoute
|
||||
'/set-signer': typeof SetSignerRoute
|
||||
'/_settings': typeof SettingsLazyRouteWithChildren
|
||||
'/new': typeof NewLazyRoute
|
||||
'/reset': typeof ResetLazyRoute
|
||||
@ -778,6 +794,7 @@ export interface FileRouteTypes {
|
||||
| '/bootstrap-relays'
|
||||
| '/set-group'
|
||||
| '/set-interest'
|
||||
| '/set-signer'
|
||||
| '/new'
|
||||
| '/reset'
|
||||
| '/bitcoin-connect'
|
||||
@ -813,6 +830,7 @@ export interface FileRouteTypes {
|
||||
| '/bootstrap-relays'
|
||||
| '/set-group'
|
||||
| '/set-interest'
|
||||
| '/set-signer'
|
||||
| ''
|
||||
| '/new'
|
||||
| '/reset'
|
||||
@ -850,6 +868,7 @@ export interface FileRouteTypes {
|
||||
| '/bootstrap-relays'
|
||||
| '/set-group'
|
||||
| '/set-interest'
|
||||
| '/set-signer'
|
||||
| '/_settings'
|
||||
| '/new'
|
||||
| '/reset'
|
||||
@ -890,6 +909,7 @@ export interface RootRouteChildren {
|
||||
BootstrapRelaysRoute: typeof BootstrapRelaysRoute
|
||||
SetGroupRoute: typeof SetGroupRoute
|
||||
SetInterestRoute: typeof SetInterestRoute
|
||||
SetSignerRoute: typeof SetSignerRoute
|
||||
SettingsLazyRoute: typeof SettingsLazyRouteWithChildren
|
||||
NewLazyRoute: typeof NewLazyRoute
|
||||
ResetLazyRoute: typeof ResetLazyRoute
|
||||
@ -906,6 +926,7 @@ const rootRouteChildren: RootRouteChildren = {
|
||||
BootstrapRelaysRoute: BootstrapRelaysRoute,
|
||||
SetGroupRoute: SetGroupRoute,
|
||||
SetInterestRoute: SetInterestRoute,
|
||||
SetSignerRoute: SetSignerRoute,
|
||||
SettingsLazyRoute: SettingsLazyRouteWithChildren,
|
||||
NewLazyRoute: NewLazyRoute,
|
||||
ResetLazyRoute: ResetLazyRoute,
|
||||
@ -933,6 +954,7 @@ export const routeTree = rootRoute
|
||||
"/bootstrap-relays",
|
||||
"/set-group",
|
||||
"/set-interest",
|
||||
"/set-signer",
|
||||
"/_settings",
|
||||
"/new",
|
||||
"/reset",
|
||||
@ -959,6 +981,9 @@ export const routeTree = rootRoute
|
||||
"/set-interest": {
|
||||
"filePath": "set-interest.tsx"
|
||||
},
|
||||
"/set-signer": {
|
||||
"filePath": "set-signer.tsx"
|
||||
},
|
||||
"/_settings": {
|
||||
"filePath": "_settings.lazy.tsx",
|
||||
"children": [
|
||||
|
@ -10,6 +10,7 @@ import { useEffect } from "react";
|
||||
interface RouterContext {
|
||||
queryClient: QueryClient;
|
||||
platform: OsType;
|
||||
account: string[];
|
||||
}
|
||||
|
||||
export const Route = createRootRouteWithContext<RouterContext>()({
|
||||
|
@ -126,24 +126,20 @@ const Account = memo(function Account({ pubkey }: { pubkey: string }) {
|
||||
async (e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
const menuItems = await Promise.all([
|
||||
const items = await Promise.all([
|
||||
MenuItem.new({
|
||||
text: "New Post",
|
||||
action: () => LumeWindow.openEditor(),
|
||||
text: "View Profile",
|
||||
action: () => LumeWindow.openProfile(pubkey),
|
||||
}),
|
||||
MenuItem.new({
|
||||
text: "Profile",
|
||||
action: () => LumeWindow.openProfile(pubkey),
|
||||
text: "Copy Public Key",
|
||||
action: async () => await writeText(pubkey),
|
||||
}),
|
||||
MenuItem.new({
|
||||
text: "Settings",
|
||||
action: () => LumeWindow.openSettings(pubkey),
|
||||
}),
|
||||
PredefinedMenuItem.new({ item: "Separator" }),
|
||||
MenuItem.new({
|
||||
text: "Copy Public Key",
|
||||
action: async () => await writeText(pubkey),
|
||||
}),
|
||||
MenuItem.new({
|
||||
text: "Logout",
|
||||
action: async () => {
|
||||
@ -162,9 +158,7 @@ const Account = memo(function Account({ pubkey }: { pubkey: string }) {
|
||||
}),
|
||||
]);
|
||||
|
||||
const menu = await Menu.new({
|
||||
items: menuItems,
|
||||
});
|
||||
const menu = await Menu.new({ items });
|
||||
|
||||
await menu.popup().catch((e) => console.error(e));
|
||||
},
|
||||
|
@ -134,7 +134,7 @@ function Groups() {
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => LumeWindow.openPopup("New group", "/set-group")}
|
||||
onClick={() => LumeWindow.openPopup("/set-group", "New group")}
|
||||
className="h-7 w-max px-2 inline-flex items-center justify-center gap-1 text-sm font-medium rounded-full bg-neutral-300 dark:bg-neutral-700 hover:bg-blue-500 hover:text-white"
|
||||
>
|
||||
<Plus className="size-3" weight="bold" />
|
||||
@ -252,7 +252,7 @@ function Interests() {
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
LumeWindow.openPopup("New interest", "/set-interest")
|
||||
LumeWindow.openPopup("/set-interest", "New interest")
|
||||
}
|
||||
className="h-7 w-max px-2 inline-flex items-center justify-center gap-1 text-sm font-medium rounded-full bg-neutral-300 dark:bg-neutral-700 hover:bg-blue-500 hover:text-white"
|
||||
>
|
||||
|
95
src/routes/set-signer.lazy.tsx
Normal file
95
src/routes/set-signer.lazy.tsx
Normal file
@ -0,0 +1,95 @@
|
||||
import { commands } from "@/commands.gen";
|
||||
import { Spinner, User } from "@/components";
|
||||
import { ArrowRight } from "@phosphor-icons/react";
|
||||
import { createLazyFileRoute } from "@tanstack/react-router";
|
||||
import { getCurrentWindow } from "@tauri-apps/api/window";
|
||||
import { message } from "@tauri-apps/plugin-dialog";
|
||||
import { useState, useTransition } from "react";
|
||||
|
||||
export const Route = createLazyFileRoute("/set-signer")({
|
||||
component: Screen,
|
||||
});
|
||||
|
||||
function Screen() {
|
||||
const { account } = Route.useSearch();
|
||||
|
||||
const [password, setPassword] = useState("");
|
||||
const [isPending, startTransition] = useTransition();
|
||||
|
||||
const unlock = () => {
|
||||
startTransition(async () => {
|
||||
if (!password.length) {
|
||||
await message("Password is required", { kind: "info" });
|
||||
return;
|
||||
}
|
||||
|
||||
const window = getCurrentWindow();
|
||||
const res = await commands.setSigner(account, password);
|
||||
|
||||
if (res.status === "ok") {
|
||||
await window.emit("signer-updated", {});
|
||||
await window.close();
|
||||
} else {
|
||||
await message(res.error, { kind: "error" });
|
||||
return;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
if (!account) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
data-tauri-drag-region
|
||||
className="size-full flex flex-col items-center justify-between gap-6 p-3"
|
||||
>
|
||||
<div
|
||||
data-tauri-drag-region
|
||||
className="flex-1 w-full px-10 flex flex-col gap-6 items-center justify-center"
|
||||
>
|
||||
<User.Provider pubkey={account}>
|
||||
<User.Root>
|
||||
<User.Avatar className="size-12 rounded-full" />
|
||||
</User.Root>
|
||||
</User.Provider>
|
||||
<div className="w-full flex gap-2 items-center justify-center">
|
||||
<input
|
||||
name="password"
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") unlock();
|
||||
}}
|
||||
disabled={isPending}
|
||||
placeholder="Enter password to unlock"
|
||||
className="px-3 flex-1 rounded-full h-10 bg-transparent border border-black/10 dark:border-white/10 focus:border-blue-500 focus:outline-none placeholder:text-neutral-400"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => unlock()}
|
||||
disabled={isPending}
|
||||
className="shrink-0 size-10 inline-flex items-center justify-center rounded-full bg-blue-500 text-white"
|
||||
>
|
||||
{isPending ? (
|
||||
<Spinner className="size-4" />
|
||||
) : (
|
||||
<ArrowRight className="size-4" weight="bold" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-auto">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => getCurrentWindow().close()}
|
||||
className="text-sm font-medium text-neutral-500"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
13
src/routes/set-signer.tsx
Normal file
13
src/routes/set-signer.tsx
Normal file
@ -0,0 +1,13 @@
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
|
||||
export interface RouteSearch {
|
||||
account?: string;
|
||||
}
|
||||
|
||||
export const Route = createFileRoute("/set-signer")({
|
||||
validateSearch: (search: Record<string, string>): RouteSearch => {
|
||||
return {
|
||||
account: search.account,
|
||||
};
|
||||
},
|
||||
});
|
@ -12,10 +12,10 @@ export class LumeEvent {
|
||||
public meta: Meta;
|
||||
public relay?: string;
|
||||
public replies?: LumeEvent[];
|
||||
#raw: NostrEvent;
|
||||
public raw: NostrEvent;
|
||||
|
||||
constructor(event: NostrEvent) {
|
||||
this.#raw = event;
|
||||
this.raw = event;
|
||||
Object.assign(this, event);
|
||||
}
|
||||
|
||||
@ -134,16 +134,6 @@ export class LumeEvent {
|
||||
}
|
||||
}
|
||||
|
||||
public async repost() {
|
||||
const query = await commands.repost(JSON.stringify(this.#raw));
|
||||
|
||||
if (query.status === "ok") {
|
||||
return query.data;
|
||||
} else {
|
||||
throw new Error(query.error);
|
||||
}
|
||||
}
|
||||
|
||||
static async publish(
|
||||
content: string,
|
||||
warning?: string,
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { commands } from "@/commands.gen";
|
||||
import type { LumeColumn, NostrEvent } from "@/types";
|
||||
import { getCurrentWindow } from "@tauri-apps/api/window";
|
||||
import { Window, getCurrentWindow } from "@tauri-apps/api/window";
|
||||
import { nanoid } from "nanoid";
|
||||
import type { LumeEvent } from "./event";
|
||||
|
||||
@ -120,6 +120,7 @@ export const LumeWindow = {
|
||||
maximizable: false,
|
||||
minimizable: false,
|
||||
hidden_title: true,
|
||||
closable: true,
|
||||
});
|
||||
|
||||
if (query.status === "ok") {
|
||||
@ -141,6 +142,7 @@ export const LumeWindow = {
|
||||
maximizable: false,
|
||||
minimizable: false,
|
||||
hidden_title: true,
|
||||
closable: true,
|
||||
});
|
||||
} else {
|
||||
await LumeWindow.openSettings(account, "bitcoin-connect");
|
||||
@ -156,6 +158,7 @@ export const LumeWindow = {
|
||||
maximizable: false,
|
||||
minimizable: false,
|
||||
hidden_title: true,
|
||||
closable: true,
|
||||
});
|
||||
|
||||
if (query.status === "ok") {
|
||||
@ -164,20 +167,21 @@ export const LumeWindow = {
|
||||
throw new Error(query.error);
|
||||
}
|
||||
},
|
||||
openPopup: async (title: string, url: string) => {
|
||||
openPopup: async (url: string, title?: string, closable = true) => {
|
||||
const query = await commands.openWindow({
|
||||
label: `popup-${nanoid()}`,
|
||||
url,
|
||||
title,
|
||||
title: title ?? "",
|
||||
width: 400,
|
||||
height: 500,
|
||||
maximizable: false,
|
||||
minimizable: false,
|
||||
hidden_title: false,
|
||||
hidden_title: !!title,
|
||||
closable,
|
||||
});
|
||||
|
||||
if (query.status === "ok") {
|
||||
return query.data;
|
||||
return await Window.getByLabel(query.data);
|
||||
} else {
|
||||
throw new Error(query.error);
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user