mirror of
https://github.com/lumehq/lume.git
synced 2025-03-19 14:21:45 +01:00
refactor: app settings
This commit is contained in:
parent
11dcef4e87
commit
618a45d349
package.jsonpnpm-lock.yaml
src-tauri
src
app.tsxcommands.gen.tscommons.ts
components
routes.gen.tsroutes
system
@ -24,8 +24,6 @@
|
||||
"@tanstack/query-persist-client-core": "^5.59.16",
|
||||
"@tanstack/react-query": "^5.59.16",
|
||||
"@tanstack/react-router": "^1.77.5",
|
||||
"@tanstack/react-store": "^0.5.6",
|
||||
"@tanstack/store": "^0.5.5",
|
||||
"@tauri-apps/api": "^2.0.3",
|
||||
"@tauri-apps/plugin-clipboard-manager": "^2.0.0",
|
||||
"@tauri-apps/plugin-dialog": "^2.0.1",
|
||||
|
6
pnpm-lock.yaml
generated
6
pnpm-lock.yaml
generated
@ -50,12 +50,6 @@ importers:
|
||||
'@tanstack/react-router':
|
||||
specifier: ^1.77.5
|
||||
version: 1.77.5(@tanstack/router-generator@1.74.2)(react-dom@19.0.0-rc-cae764ce-20241025(react@19.0.0-rc-cae764ce-20241025))(react@19.0.0-rc-cae764ce-20241025)
|
||||
'@tanstack/react-store':
|
||||
specifier: ^0.5.6
|
||||
version: 0.5.6(react-dom@19.0.0-rc-cae764ce-20241025(react@19.0.0-rc-cae764ce-20241025))(react@19.0.0-rc-cae764ce-20241025)
|
||||
'@tanstack/store':
|
||||
specifier: ^0.5.5
|
||||
version: 0.5.5
|
||||
'@tauri-apps/api':
|
||||
specifier: ^2.0.3
|
||||
version: 2.0.3
|
||||
|
16
src-tauri/Cargo.lock
generated
16
src-tauri/Cargo.lock
generated
@ -3480,7 +3480,7 @@ checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8"
|
||||
[[package]]
|
||||
name = "nostr"
|
||||
version = "0.35.0"
|
||||
source = "git+https://github.com/rust-nostr/nostr#282fbc39373674b7394806d47311a8e483da0ef0"
|
||||
source = "git+https://github.com/rust-nostr/nostr#939ebbdbc0b7c605411676e810c775ac3d80ef94"
|
||||
dependencies = [
|
||||
"aes",
|
||||
"base64 0.22.1",
|
||||
@ -3510,7 +3510,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "nostr-database"
|
||||
version = "0.35.0"
|
||||
source = "git+https://github.com/rust-nostr/nostr#282fbc39373674b7394806d47311a8e483da0ef0"
|
||||
source = "git+https://github.com/rust-nostr/nostr#939ebbdbc0b7c605411676e810c775ac3d80ef94"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"flatbuffers",
|
||||
@ -3524,7 +3524,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "nostr-lmdb"
|
||||
version = "0.35.0"
|
||||
source = "git+https://github.com/rust-nostr/nostr#282fbc39373674b7394806d47311a8e483da0ef0"
|
||||
source = "git+https://github.com/rust-nostr/nostr#939ebbdbc0b7c605411676e810c775ac3d80ef94"
|
||||
dependencies = [
|
||||
"heed",
|
||||
"nostr",
|
||||
@ -3537,7 +3537,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "nostr-relay-pool"
|
||||
version = "0.35.0"
|
||||
source = "git+https://github.com/rust-nostr/nostr#282fbc39373674b7394806d47311a8e483da0ef0"
|
||||
source = "git+https://github.com/rust-nostr/nostr#939ebbdbc0b7c605411676e810c775ac3d80ef94"
|
||||
dependencies = [
|
||||
"async-utility",
|
||||
"async-wsocket",
|
||||
@ -3555,7 +3555,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "nostr-sdk"
|
||||
version = "0.35.0"
|
||||
source = "git+https://github.com/rust-nostr/nostr#282fbc39373674b7394806d47311a8e483da0ef0"
|
||||
source = "git+https://github.com/rust-nostr/nostr#939ebbdbc0b7c605411676e810c775ac3d80ef94"
|
||||
dependencies = [
|
||||
"async-utility",
|
||||
"atomic-destructor",
|
||||
@ -3575,7 +3575,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "nostr-signer"
|
||||
version = "0.35.0"
|
||||
source = "git+https://github.com/rust-nostr/nostr#282fbc39373674b7394806d47311a8e483da0ef0"
|
||||
source = "git+https://github.com/rust-nostr/nostr#939ebbdbc0b7c605411676e810c775ac3d80ef94"
|
||||
dependencies = [
|
||||
"async-utility",
|
||||
"nostr",
|
||||
@ -3588,7 +3588,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "nostr-zapper"
|
||||
version = "0.35.0"
|
||||
source = "git+https://github.com/rust-nostr/nostr#282fbc39373674b7394806d47311a8e483da0ef0"
|
||||
source = "git+https://github.com/rust-nostr/nostr#939ebbdbc0b7c605411676e810c775ac3d80ef94"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"nostr",
|
||||
@ -3732,7 +3732,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "nwc"
|
||||
version = "0.35.0"
|
||||
source = "git+https://github.com/rust-nostr/nostr#282fbc39373674b7394806d47311a8e483da0ef0"
|
||||
source = "git+https://github.com/rust-nostr/nostr#939ebbdbc0b7c605411676e810c775ac3d80ef94"
|
||||
dependencies = [
|
||||
"async-utility",
|
||||
"nostr",
|
||||
|
@ -5,7 +5,7 @@ use specta::Type;
|
||||
use std::{str::FromStr, time::Duration};
|
||||
use tauri::{Emitter, State};
|
||||
|
||||
use crate::{common::get_all_accounts, Nostr};
|
||||
use crate::{common::get_all_accounts, Nostr, Settings};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Type)]
|
||||
struct Account {
|
||||
@ -249,3 +249,21 @@ pub async fn set_signer(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn get_app_settings(state: State<'_, Nostr>) -> Result<Settings, String> {
|
||||
let settings = state.settings.read().await.clone();
|
||||
Ok(settings)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn set_app_settings(settings: String, state: State<'_, Nostr>) -> Result<(), String> {
|
||||
let parsed: Settings = serde_json::from_str(&settings).map_err(|e| e.to_string())?;
|
||||
let mut settings = state.settings.write().await;
|
||||
// Update state
|
||||
settings.clone_from(&parsed);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
@ -36,6 +36,7 @@ pub async fn get_event(id: String, state: State<'_, Nostr>) -> Result<RichEvent,
|
||||
Ok(RichEvent { raw, parsed })
|
||||
} else {
|
||||
let filter = Filter::new().id(event_id);
|
||||
|
||||
let mut rich_event = RichEvent {
|
||||
raw: "".to_string(),
|
||||
parsed: None,
|
||||
|
@ -7,21 +7,9 @@ use tauri::{Emitter, Manager, State};
|
||||
|
||||
use crate::{
|
||||
common::{get_latest_event, process_event},
|
||||
Nostr, RichEvent, Settings,
|
||||
Nostr, RichEvent,
|
||||
};
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, Type)]
|
||||
pub struct Profile {
|
||||
name: String,
|
||||
display_name: String,
|
||||
about: Option<String>,
|
||||
picture: String,
|
||||
banner: Option<String>,
|
||||
nip05: Option<String>,
|
||||
lud16: Option<String>,
|
||||
website: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, Type)]
|
||||
pub struct Mention {
|
||||
pubkey: String,
|
||||
@ -116,30 +104,9 @@ pub async fn get_contact_list(id: String, state: State<'_, Nostr>) -> Result<Vec
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn set_profile(profile: Profile, state: State<'_, Nostr>) -> Result<String, String> {
|
||||
pub async fn set_profile(new_profile: String, state: State<'_, Nostr>) -> Result<String, String> {
|
||||
let client = &state.client;
|
||||
let mut metadata = Metadata::new()
|
||||
.name(profile.name)
|
||||
.display_name(profile.display_name)
|
||||
.about(profile.about.unwrap_or_default())
|
||||
.nip05(profile.nip05.unwrap_or_default())
|
||||
.lud16(profile.lud16.unwrap_or_default());
|
||||
|
||||
if let Ok(url) = Url::parse(&profile.picture) {
|
||||
metadata = metadata.picture(url)
|
||||
}
|
||||
|
||||
if let Some(b) = profile.banner {
|
||||
if let Ok(url) = Url::parse(&b) {
|
||||
metadata = metadata.banner(url)
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(w) = profile.website {
|
||||
if let Ok(url) = Url::parse(&w) {
|
||||
metadata = metadata.website(url)
|
||||
}
|
||||
}
|
||||
let metadata = Metadata::from_json(new_profile).map_err(|e| e.to_string())?;
|
||||
|
||||
match client.set_metadata(&metadata).await {
|
||||
Ok(id) => Ok(id.to_string()),
|
||||
@ -612,21 +579,6 @@ pub async fn get_notifications(id: String, state: State<'_, Nostr>) -> Result<Ve
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn get_user_settings(state: State<'_, Nostr>) -> Result<Settings, String> {
|
||||
Ok(state.settings.lock().await.clone())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn set_user_settings(settings: String, state: State<'_, Nostr>) -> Result<(), String> {
|
||||
let parsed: Settings = serde_json::from_str(&settings).map_err(|e| e.to_string())?;
|
||||
state.settings.lock().await.clone_from(&parsed);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn verify_nip05(id: String, nip05: &str) -> Result<bool, String> {
|
||||
|
@ -22,7 +22,7 @@ use tauri::{path::BaseDirectory, Emitter, EventTarget, Listener, Manager};
|
||||
use tauri_plugin_decorum::WebviewWindowExt;
|
||||
use tauri_plugin_notification::{NotificationExt, PermissionState};
|
||||
use tauri_specta::{collect_commands, Builder};
|
||||
use tokio::{sync::Mutex, sync::RwLock, time::sleep};
|
||||
use tokio::{sync::RwLock, time::sleep};
|
||||
|
||||
pub mod commands;
|
||||
pub mod common;
|
||||
@ -30,7 +30,7 @@ pub mod common;
|
||||
pub struct Nostr {
|
||||
client: Client,
|
||||
queue: RwLock<HashSet<PublicKey>>,
|
||||
settings: Mutex<Settings>,
|
||||
settings: RwLock<Settings>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
@ -40,37 +40,30 @@ pub struct Payload {
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, Type)]
|
||||
pub struct Settings {
|
||||
proxy: Option<String>,
|
||||
image_resize_service: Option<String>,
|
||||
use_relay_hint: bool,
|
||||
resize_service: bool,
|
||||
content_warning: bool,
|
||||
trusted_only: bool,
|
||||
display_avatar: bool,
|
||||
display_zap_button: bool,
|
||||
display_repost_button: bool,
|
||||
display_media: bool,
|
||||
transparent: bool,
|
||||
}
|
||||
|
||||
impl Default for Settings {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
proxy: None,
|
||||
image_resize_service: Some("https://wsrv.nl".to_string()),
|
||||
use_relay_hint: true,
|
||||
content_warning: true,
|
||||
trusted_only: false,
|
||||
resize_service: true,
|
||||
display_avatar: true,
|
||||
display_zap_button: true,
|
||||
display_repost_button: true,
|
||||
display_media: true,
|
||||
transparent: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub const DEFAULT_DIFFICULTY: u8 = 0;
|
||||
pub const FETCH_LIMIT: usize = 50;
|
||||
pub const QUEUE_DELAY: u64 = 300;
|
||||
pub const NOTIFICATION_SUB_ID: &str = "lume_notification";
|
||||
|
||||
fn main() {
|
||||
@ -113,8 +106,6 @@ fn main() {
|
||||
zap_event,
|
||||
copy_friend,
|
||||
get_notifications,
|
||||
get_user_settings,
|
||||
set_user_settings,
|
||||
verify_nip05,
|
||||
get_meta_from_event,
|
||||
get_event,
|
||||
@ -138,6 +129,8 @@ fn main() {
|
||||
close_column,
|
||||
close_all_columns,
|
||||
open_window,
|
||||
get_app_settings,
|
||||
set_app_settings,
|
||||
]);
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
@ -182,7 +175,7 @@ fn main() {
|
||||
|
||||
// Config
|
||||
let opts = Options::new()
|
||||
.gossip(true)
|
||||
.gossip(false)
|
||||
.max_avg_latency(Duration::from_millis(300))
|
||||
.automatic_authentication(true)
|
||||
.connection_timeout(Some(Duration::from_secs(5)))
|
||||
@ -238,7 +231,7 @@ fn main() {
|
||||
app.manage(Nostr {
|
||||
client,
|
||||
queue: RwLock::new(HashSet::new()),
|
||||
settings: Mutex::new(Settings::default()),
|
||||
settings: RwLock::new(Settings::default()),
|
||||
});
|
||||
|
||||
// Listen for request metadata
|
||||
@ -250,22 +243,27 @@ fn main() {
|
||||
tauri::async_runtime::spawn(async move {
|
||||
let state = handle.state::<Nostr>();
|
||||
let client = &state.client;
|
||||
let mut write_queue = state.queue.write().await;
|
||||
|
||||
if let Ok(public_key) = PublicKey::parse(parsed_payload.id) {
|
||||
let mut write_queue = state.queue.write().await;
|
||||
write_queue.insert(public_key);
|
||||
}
|
||||
|
||||
sleep(Duration::from_millis(300)).await;
|
||||
sleep(Duration::from_millis(QUEUE_DELAY)).await;
|
||||
|
||||
let read_queue = state.queue.read().await;
|
||||
|
||||
let filter_opts = FilterOptions::WaitDurationAfterEOSE(Duration::from_secs(2));
|
||||
let opts = SubscribeAutoCloseOptions::default().filter(filter_opts);
|
||||
|
||||
let limit = read_queue.len() * 2;
|
||||
let authors: Vec<PublicKey> = read_queue.iter().copied().collect();
|
||||
let filter = Filter::new().authors(authors).kind(Kind::Metadata);
|
||||
let opts = SubscribeAutoCloseOptions::default()
|
||||
.filter(FilterOptions::WaitDurationAfterEOSE(Duration::from_secs(3)));
|
||||
let filter = Filter::new()
|
||||
.authors(authors)
|
||||
.kind(Kind::Metadata)
|
||||
.limit(limit);
|
||||
|
||||
if client.subscribe(vec![filter], Some(opts)).await.is_ok() {
|
||||
let mut write_queue = state.queue.write().await;
|
||||
write_queue.clear();
|
||||
}
|
||||
});
|
||||
|
@ -45,7 +45,7 @@ broadcastQueryClient({
|
||||
|
||||
const router = createRouter({
|
||||
routeTree,
|
||||
context: { queryClient, platform },
|
||||
context: { store, queryClient, platform },
|
||||
Wrap: ({ children }) => {
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
||||
|
@ -136,9 +136,9 @@ async getProfile(id: string) : Promise<Result<string, string>> {
|
||||
else return { status: "error", error: e as any };
|
||||
}
|
||||
},
|
||||
async setProfile(profile: Profile) : Promise<Result<string, string>> {
|
||||
async setProfile(newProfile: string) : Promise<Result<string, string>> {
|
||||
try {
|
||||
return { status: "ok", data: await TAURI_INVOKE("set_profile", { profile }) };
|
||||
return { status: "ok", data: await TAURI_INVOKE("set_profile", { newProfile }) };
|
||||
} catch (e) {
|
||||
if(e instanceof Error) throw e;
|
||||
else return { status: "error", error: e as any };
|
||||
@ -288,22 +288,6 @@ async getNotifications(id: string) : Promise<Result<string[], string>> {
|
||||
else return { status: "error", error: e as any };
|
||||
}
|
||||
},
|
||||
async getUserSettings() : Promise<Result<Settings, string>> {
|
||||
try {
|
||||
return { status: "ok", data: await TAURI_INVOKE("get_user_settings") };
|
||||
} catch (e) {
|
||||
if(e instanceof Error) throw e;
|
||||
else return { status: "error", error: e as any };
|
||||
}
|
||||
},
|
||||
async setUserSettings(settings: string) : Promise<Result<null, string>> {
|
||||
try {
|
||||
return { status: "ok", data: await TAURI_INVOKE("set_user_settings", { settings }) };
|
||||
} catch (e) {
|
||||
if(e instanceof Error) throw e;
|
||||
else return { status: "error", error: e as any };
|
||||
}
|
||||
},
|
||||
async verifyNip05(id: string, nip05: string) : Promise<Result<boolean, string>> {
|
||||
try {
|
||||
return { status: "ok", data: await TAURI_INVOKE("verify_nip05", { id, nip05 }) };
|
||||
@ -487,6 +471,22 @@ async openWindow(window: NewWindow) : Promise<Result<string, string>> {
|
||||
if(e instanceof Error) throw e;
|
||||
else return { status: "error", error: e as any };
|
||||
}
|
||||
},
|
||||
async getAppSettings() : Promise<Result<Settings, string>> {
|
||||
try {
|
||||
return { status: "ok", data: await TAURI_INVOKE("get_app_settings") };
|
||||
} catch (e) {
|
||||
if(e instanceof Error) throw e;
|
||||
else return { status: "error", error: e as any };
|
||||
}
|
||||
},
|
||||
async setAppSettings(settings: string) : Promise<Result<null, string>> {
|
||||
try {
|
||||
return { status: "ok", data: await TAURI_INVOKE("set_app_settings", { settings }) };
|
||||
} catch (e) {
|
||||
if(e instanceof Error) throw e;
|
||||
else return { status: "error", error: e as any };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -504,10 +504,9 @@ export type Column = { label: string; url: string; x: number; y: number; width:
|
||||
export type Mention = { pubkey: string; avatar: string; display_name: string; name: string }
|
||||
export type Meta = { content: string; images: string[]; events: string[]; mentions: string[]; hashtags: string[] }
|
||||
export type NewWindow = { label: string; title: string; url: string; width: number; height: number; maximizable: boolean; minimizable: boolean; hidden_title: boolean; closable: boolean }
|
||||
export type Profile = { name: string; display_name: string; about: string | null; picture: string; banner: string | null; nip05: string | null; lud16: string | null; website: string | null }
|
||||
export type Relays = { connected: string[]; read: string[] | null; write: string[] | null; both: string[] | null }
|
||||
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 Settings = { resize_service: boolean; content_warning: boolean; display_avatar: boolean; display_zap_button: boolean; display_repost_button: boolean; display_media: boolean }
|
||||
export type TAURI_CHANNEL<TSend> = null
|
||||
|
||||
/** tauri-specta globals **/
|
||||
|
136
src/commons.ts
136
src/commons.ts
@ -3,7 +3,6 @@ import type {
|
||||
MaybePromise,
|
||||
PersistedQuery,
|
||||
} from "@tanstack/query-persist-client-core";
|
||||
import { Store } from "@tanstack/react-store";
|
||||
import { ask, message, open } from "@tauri-apps/plugin-dialog";
|
||||
import { readFile } from "@tauri-apps/plugin-fs";
|
||||
import { relaunch } from "@tauri-apps/plugin-process";
|
||||
@ -16,76 +15,79 @@ import relativeTime from "dayjs/plugin/relativeTime";
|
||||
import updateLocale from "dayjs/plugin/updateLocale";
|
||||
import { decode } from "light-bolt11-decoder";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import type { RichEvent, Settings } from "./commands.gen";
|
||||
import type { RichEvent } from "./commands.gen";
|
||||
import { LumeEvent } from "./system";
|
||||
import type { LumeColumn, NostrEvent } from "./types";
|
||||
|
||||
dayjs.extend(relativeTime);
|
||||
dayjs.extend(updateLocale);
|
||||
|
||||
dayjs.updateLocale("en", {
|
||||
relativeTime: {
|
||||
past: "%s ago",
|
||||
s: "just now",
|
||||
m: "1m",
|
||||
mm: "%dm",
|
||||
h: "1h",
|
||||
hh: "%dh",
|
||||
d: "1d",
|
||||
dd: "%dd",
|
||||
},
|
||||
});
|
||||
import type { NostrEvent } from "./types";
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
|
||||
export const isImagePath = (path: string) => {
|
||||
for (const suffix of ["jpg", "jpeg", "gif", "png", "webp", "avif", "tiff"]) {
|
||||
if (path.endsWith(suffix)) return true;
|
||||
const exts = ["jpg", "jpeg", "gif", "png", "webp", "avif", "tiff"];
|
||||
|
||||
for (const suffix of exts) {
|
||||
if (path.endsWith(suffix)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
export const isImageUrl = (url: string) => {
|
||||
try {
|
||||
if (!url) return false;
|
||||
const ext = new URL(url).pathname.split(".").pop();
|
||||
return ["jpg", "jpeg", "gif", "png", "webp", "avif", "tiff"].includes(ext);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
export function createdAt(time: number) {
|
||||
// Config for dayjs
|
||||
dayjs.extend(relativeTime);
|
||||
dayjs.extend(updateLocale);
|
||||
|
||||
export function formatCreatedAt(time: number, message = false) {
|
||||
let formated: string;
|
||||
// Config locale text
|
||||
dayjs.updateLocale("en", {
|
||||
relativeTime: {
|
||||
past: "%s ago",
|
||||
s: "just now",
|
||||
m: "1m",
|
||||
mm: "%dm",
|
||||
h: "1h",
|
||||
hh: "%dh",
|
||||
d: "1d",
|
||||
dd: "%dd",
|
||||
},
|
||||
});
|
||||
|
||||
const now = dayjs();
|
||||
const inputTime = dayjs.unix(time);
|
||||
const diff = now.diff(inputTime, "hour");
|
||||
|
||||
if (message) {
|
||||
if (diff < 12) {
|
||||
formated = inputTime.format("HH:mm A");
|
||||
} else {
|
||||
formated = inputTime.format("MMM DD");
|
||||
}
|
||||
if (diff < 24) {
|
||||
return inputTime.from(now, true);
|
||||
} else {
|
||||
if (diff < 24) {
|
||||
formated = inputTime.from(now, true);
|
||||
} else {
|
||||
formated = inputTime.format("MMM DD");
|
||||
}
|
||||
return inputTime.format("MMM DD");
|
||||
}
|
||||
|
||||
return formated;
|
||||
}
|
||||
|
||||
export function replyTime(time: number) {
|
||||
const inputTime = dayjs.unix(time);
|
||||
const formated = inputTime.format("MM-DD-YY HH:mm");
|
||||
export function replyAt(time: number) {
|
||||
// Config for dayjs
|
||||
dayjs.extend(relativeTime);
|
||||
dayjs.extend(updateLocale);
|
||||
|
||||
return formated;
|
||||
// Config locale text
|
||||
dayjs.updateLocale("en", {
|
||||
relativeTime: {
|
||||
past: "%s ago",
|
||||
s: "just now",
|
||||
m: "1m",
|
||||
mm: "%dm",
|
||||
h: "1h",
|
||||
hh: "%dh",
|
||||
d: "1d",
|
||||
dd: "%dd",
|
||||
},
|
||||
});
|
||||
|
||||
const inputTime = dayjs.unix(time);
|
||||
const format = inputTime.format("MM-DD-YY HH:mm");
|
||||
|
||||
return format;
|
||||
}
|
||||
|
||||
export function displayNpub(pubkey: string, len: number) {
|
||||
@ -114,7 +116,7 @@ export function displayLongHandle(str: string) {
|
||||
return `${handle.substring(0, 16)}...@${service}`;
|
||||
}
|
||||
|
||||
// source: https://github.com/synonymdev/bitkit/blob/master/src/utils/displayValues/index.ts
|
||||
// Source: https://github.com/synonymdev/bitkit/blob/master/src/utils/displayValues/index.ts
|
||||
export function getBitcoinDisplayValues(satoshis: number) {
|
||||
let bitcoinFormatted = new BitcoinUnit(satoshis, "satoshis")
|
||||
.getValue()
|
||||
@ -145,20 +147,27 @@ export function getBitcoinDisplayValues(satoshis: number) {
|
||||
};
|
||||
}
|
||||
|
||||
export function decodeZapInvoice(tags?: string[][]) {
|
||||
export function decodeZapInvoice(tags: string[][]) {
|
||||
const invoice = tags.find((tag) => tag[0] === "bolt11")?.[1];
|
||||
if (!invoice) return;
|
||||
|
||||
const decodedInvoice = decode(invoice);
|
||||
const amountSection = decodedInvoice.sections.find(
|
||||
const section = decodedInvoice.sections.find(
|
||||
(s: { name: string }) => s.name === "amount",
|
||||
);
|
||||
|
||||
// @ts-ignore, its fine.
|
||||
const amount = Number.parseInt(amountSection.value) / 1000;
|
||||
const displayValue = getBitcoinDisplayValues(amount);
|
||||
if (!section) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return displayValue;
|
||||
if (section.name === "amount") {
|
||||
const amount = Number.parseInt(section.value) / 1000;
|
||||
const displayValue = getBitcoinDisplayValues(amount);
|
||||
|
||||
return displayValue;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function checkForAppUpdates(silent: boolean) {
|
||||
@ -277,18 +286,3 @@ export function newQueryStorage(
|
||||
(await store.delete(key)) as unknown as MaybePromise<void>,
|
||||
};
|
||||
}
|
||||
|
||||
export const appSettings = new Store<Settings>({
|
||||
proxy: null,
|
||||
image_resize_service: "https://wsrv.nl",
|
||||
use_relay_hint: true,
|
||||
content_warning: true,
|
||||
trusted_only: true,
|
||||
display_avatar: true,
|
||||
display_zap_button: true,
|
||||
display_repost_button: true,
|
||||
display_media: true,
|
||||
transparent: true,
|
||||
});
|
||||
|
||||
export const appColumns = new Store<LumeColumn[]>([]);
|
||||
|
@ -1,10 +1,15 @@
|
||||
import { commands } from "@/commands.gen";
|
||||
import { appSettings, cn, displayNpub } from "@/commons";
|
||||
import { cn, displayNpub } from "@/commons";
|
||||
import { RepostIcon, Spinner } from "@/components";
|
||||
import { settingsQueryOptions } from "@/routes/__root";
|
||||
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 {
|
||||
useMutation,
|
||||
useQuery,
|
||||
useQueryClient,
|
||||
useSuspenseQuery,
|
||||
} from "@tanstack/react-query";
|
||||
import { Menu, MenuItem } from "@tauri-apps/api/menu";
|
||||
import { message } from "@tauri-apps/plugin-dialog";
|
||||
import { useCallback, useTransition } from "react";
|
||||
@ -15,7 +20,7 @@ export function NoteRepost({
|
||||
smol = false,
|
||||
}: { label?: boolean; smol?: boolean }) {
|
||||
const event = useNoteContext();
|
||||
const visible = useStore(appSettings, (state) => state.display_repost_button);
|
||||
const settings = useSuspenseQuery(settingsQueryOptions);
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { isLoading, data: status } = useQuery({
|
||||
@ -28,7 +33,7 @@ export function NoteRepost({
|
||||
return false;
|
||||
}
|
||||
},
|
||||
enabled: visible,
|
||||
enabled: settings.data.display_repost_button,
|
||||
refetchOnMount: false,
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnReconnect: false,
|
||||
@ -120,7 +125,7 @@ export function NoteRepost({
|
||||
});
|
||||
};
|
||||
|
||||
if (!visible) return null;
|
||||
if (!settings.data.display_repost_button) return null;
|
||||
|
||||
return (
|
||||
<Tooltip.Provider>
|
||||
|
@ -1,8 +1,9 @@
|
||||
import { appSettings, cn } from "@/commons";
|
||||
import { cn } from "@/commons";
|
||||
import { ZapIcon } from "@/components";
|
||||
import { settingsQueryOptions } from "@/routes/__root";
|
||||
import { LumeWindow } from "@/system";
|
||||
import { useSuspenseQuery } from "@tanstack/react-query";
|
||||
import { useSearch } from "@tanstack/react-router";
|
||||
import { useStore } from "@tanstack/react-store";
|
||||
import { useNoteContext } from "../provider";
|
||||
|
||||
export function NoteZap({
|
||||
@ -10,10 +11,10 @@ export function NoteZap({
|
||||
smol = false,
|
||||
}: { label?: boolean; smol?: boolean }) {
|
||||
const search = useSearch({ strict: false });
|
||||
const visible = useStore(appSettings, (state) => state.display_zap_button);
|
||||
const settings = useSuspenseQuery(settingsQueryOptions);
|
||||
const event = useNoteContext();
|
||||
|
||||
if (!visible) return null;
|
||||
if (!settings.data.display_zap_button) return null;
|
||||
|
||||
return (
|
||||
<button
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { appSettings, cn } from "@/commons";
|
||||
import { useStore } from "@tanstack/react-store";
|
||||
import { cn } from "@/commons";
|
||||
import { settingsQueryOptions } from "@/routes/__root";
|
||||
import { useSuspenseQuery } from "@tanstack/react-query";
|
||||
import { nanoid } from "nanoid";
|
||||
import { type ReactNode, useMemo, useState } from "react";
|
||||
import reactStringReplace from "react-string-replace";
|
||||
@ -21,14 +22,17 @@ export function NoteContent({
|
||||
className?: string;
|
||||
}) {
|
||||
const event = useNoteContext();
|
||||
const visible = useStore(appSettings, (state) => state.display_media);
|
||||
const settings = useSuspenseQuery(settingsQueryOptions);
|
||||
|
||||
const content = useMemo(() => {
|
||||
try {
|
||||
// Get parsed meta
|
||||
const { content, hashtags, events, mentions } = event.meta;
|
||||
|
||||
// Define rich content
|
||||
let richContent: ReactNode[] | string = visible ? content : event.content;
|
||||
let richContent: ReactNode[] | string = settings.data.display_media
|
||||
? content
|
||||
: event.content;
|
||||
|
||||
for (const hashtag of hashtags) {
|
||||
const regex = new RegExp(`(|^)${hashtag}\\b`, "g");
|
||||
@ -103,7 +107,7 @@ export function NoteContent({
|
||||
>
|
||||
{content}
|
||||
</div>
|
||||
{visible ? (
|
||||
{settings.data.display_media ? (
|
||||
event.meta?.images.length ? (
|
||||
<Images urls={event.meta.images} />
|
||||
) : null
|
||||
|
@ -1,6 +1,5 @@
|
||||
import { replyTime } from "@/commons";
|
||||
import { Note, Spinner } from "@/components";
|
||||
import { User } from "@/components/user";
|
||||
import { replyAt } from "@/commons";
|
||||
import { Note, Spinner, User } from "@/components";
|
||||
import { LumeWindow, useEvent } from "@/system";
|
||||
import { nip19 } from "nostr-tools";
|
||||
import { type ReactNode, memo, useMemo } from "react";
|
||||
@ -52,7 +51,7 @@ export const MentionNote = memo(function MentionNote({
|
||||
</div>
|
||||
<div className="flex-1 flex items-center justify-between">
|
||||
<span className="text-sm text-neutral-500">
|
||||
{replyTime(event.created_at)}
|
||||
{replyAt(event.created_at)}
|
||||
</span>
|
||||
<div className="invisible group-hover:visible flex items-center justify-end">
|
||||
<button
|
||||
|
@ -1,23 +1,20 @@
|
||||
import { appSettings } from "@/commons";
|
||||
import { useStore } from "@tanstack/react-store";
|
||||
import { settingsQueryOptions } from "@/routes/__root";
|
||||
import { useSuspenseQuery } from "@tanstack/react-query";
|
||||
import { useMemo } from "react";
|
||||
|
||||
export function ImagePreview({ url }: { url: string }) {
|
||||
const [service, visible] = useStore(appSettings, (state) => [
|
||||
state.image_resize_service,
|
||||
state.display_media,
|
||||
]);
|
||||
const settings = useSuspenseQuery(settingsQueryOptions);
|
||||
|
||||
const imageUrl = useMemo(() => {
|
||||
if (service?.length) {
|
||||
const newUrl = `${service}?url=${url}&ll&af&default=1&n=-1`;
|
||||
if (settings.data.resize_service) {
|
||||
const newUrl = `https://wsrv.nl?url=${url}&ll&af&default=1&n=-1`;
|
||||
return newUrl;
|
||||
} else {
|
||||
return url;
|
||||
}
|
||||
}, [service]);
|
||||
}, [settings.data.resize_service]);
|
||||
|
||||
if (!visible) {
|
||||
if (!settings.data.display_media) {
|
||||
return (
|
||||
<a
|
||||
href={url}
|
||||
|
@ -1,23 +1,24 @@
|
||||
import { appSettings, cn } from "@/commons";
|
||||
import { cn } from "@/commons";
|
||||
import { Spinner } from "@/components";
|
||||
import { settingsQueryOptions } from "@/routes/__root";
|
||||
import { ArrowLeft, ArrowRight } from "@phosphor-icons/react";
|
||||
import { useStore } from "@tanstack/react-store";
|
||||
import { useSuspenseQuery } from "@tanstack/react-query";
|
||||
import { open } from "@tauri-apps/plugin-shell";
|
||||
import useEmblaCarousel from "embla-carousel-react";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
|
||||
export function Images({ urls }: { urls: string[] }) {
|
||||
const [slidesInView, setSlidesInView] = useState([]);
|
||||
const [slidesInView, setSlidesInView] = useState<number[]>([]);
|
||||
const [emblaRef, emblaApi] = useEmblaCarousel({
|
||||
dragFree: true,
|
||||
align: "start",
|
||||
watchSlides: false,
|
||||
});
|
||||
|
||||
const service = useStore(appSettings, (state) => state.image_resize_service);
|
||||
const settings = useSuspenseQuery(settingsQueryOptions);
|
||||
|
||||
const imageUrls = useMemo(() => {
|
||||
if (service?.length) {
|
||||
if (settings.data.resize_service) {
|
||||
let newUrls: string[];
|
||||
|
||||
if (urls.length === 1) {
|
||||
@ -28,11 +29,12 @@ export function Images({ urls }: { urls: string[] }) {
|
||||
if (url.includes("bsky.network")) {
|
||||
return url;
|
||||
}
|
||||
return `${service}?url=${url}&ll&af&default=1&n=-1`;
|
||||
return `https://wsrv.nl?url=${url}&ll&af&default=1&n=-1`;
|
||||
});
|
||||
} else {
|
||||
newUrls = urls.map(
|
||||
(url) => `${service}?url=${url}&w=480&h=640&ll&af&default=1&n=-1`,
|
||||
(url) =>
|
||||
`https://wsrv.nl?url=${url}&w=480&h=640&ll&af&default=1&n=-1`,
|
||||
);
|
||||
}
|
||||
|
||||
@ -40,7 +42,7 @@ export function Images({ urls }: { urls: string[] }) {
|
||||
} else {
|
||||
return urls;
|
||||
}
|
||||
}, [service]);
|
||||
}, [settings.data.resize_service]);
|
||||
|
||||
const scrollPrev = useCallback(() => {
|
||||
if (emblaApi) emblaApi.scrollPrev();
|
||||
@ -50,23 +52,27 @@ export function Images({ urls }: { urls: string[] }) {
|
||||
if (emblaApi) emblaApi.scrollNext();
|
||||
}, [emblaApi]);
|
||||
|
||||
const updateSlidesInView = useCallback((emblaApi) => {
|
||||
const updateSlidesInView = useCallback(() => {
|
||||
setSlidesInView((slidesInView) => {
|
||||
if (slidesInView.length === emblaApi.slideNodes().length) {
|
||||
emblaApi.off("slidesInView", updateSlidesInView);
|
||||
if (slidesInView.length === emblaApi?.slideNodes().length) {
|
||||
emblaApi?.off("slidesInView", updateSlidesInView);
|
||||
}
|
||||
|
||||
const inView = emblaApi
|
||||
.slidesInView()
|
||||
?.slidesInView()
|
||||
.filter((index) => !slidesInView.includes(index));
|
||||
|
||||
return slidesInView.concat(inView);
|
||||
if (inView) {
|
||||
return slidesInView.concat(inView);
|
||||
} else {
|
||||
return slidesInView;
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
}, [emblaApi]);
|
||||
|
||||
useEffect(() => {
|
||||
if (emblaApi && urls.length > 1) {
|
||||
updateSlidesInView(emblaApi);
|
||||
updateSlidesInView();
|
||||
|
||||
emblaApi.on("slidesInView", updateSlidesInView);
|
||||
emblaApi.on("reInit", updateSlidesInView);
|
||||
|
@ -1,10 +1,10 @@
|
||||
import { appSettings } from "@/commons";
|
||||
import { useStore } from "@tanstack/react-store";
|
||||
import { settingsQueryOptions } from "@/routes/__root";
|
||||
import { useSuspenseQuery } from "@tanstack/react-query";
|
||||
|
||||
export function VideoPreview({ url }: { url: string }) {
|
||||
const visible = useStore(appSettings, (state) => state.display_media);
|
||||
const settings = useSuspenseQuery(settingsQueryOptions);
|
||||
|
||||
if (!visible) {
|
||||
if (!settings.data.display_zap_button) {
|
||||
return (
|
||||
<a
|
||||
href={url}
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { cn, replyTime } from "@/commons";
|
||||
import { Note } from "@/components/note";
|
||||
import { cn, replyAt } from "@/commons";
|
||||
import { Note, User } from "@/components";
|
||||
import { type LumeEvent, LumeWindow } from "@/system";
|
||||
import { CaretDown } from "@phosphor-icons/react";
|
||||
import { Link, useSearch } from "@tanstack/react-router";
|
||||
@ -10,7 +10,6 @@ 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";
|
||||
import { User } from "./user";
|
||||
|
||||
export const ReplyNote = memo(function ReplyNote({
|
||||
event,
|
||||
@ -66,7 +65,7 @@ export const ReplyNote = memo(function ReplyNote({
|
||||
</div>
|
||||
<div className="flex-1 flex items-center justify-between">
|
||||
<span className="text-sm text-neutral-500">
|
||||
{replyTime(event.created_at)}
|
||||
{replyAt(event.created_at)}
|
||||
</span>
|
||||
<div className="flex items-center justify-end">
|
||||
<Note.Reply smol />
|
||||
@ -147,7 +146,7 @@ function ChildReply({ event }: { event: LumeEvent }) {
|
||||
</div>
|
||||
<div className="flex-1 flex items-center justify-between">
|
||||
<span className="text-sm text-neutral-500">
|
||||
{replyTime(event.created_at)}
|
||||
{replyAt(event.created_at)}
|
||||
</span>
|
||||
<div className="invisible group-hover:visible flex items-center justify-end">
|
||||
<Note.Reply smol />
|
||||
|
@ -1,38 +1,36 @@
|
||||
import { appSettings, cn } from "@/commons";
|
||||
import { cn } from "@/commons";
|
||||
import { settingsQueryOptions } from "@/routes/__root";
|
||||
import * as Avatar from "@radix-ui/react-avatar";
|
||||
import { useStore } from "@tanstack/react-store";
|
||||
import { useSuspenseQuery } from "@tanstack/react-query";
|
||||
import { minidenticon } from "minidenticons";
|
||||
import { useMemo } from "react";
|
||||
import { useUserContext } from "./provider";
|
||||
|
||||
export function UserAvatar({ className }: { className?: string }) {
|
||||
const [service, visible] = useStore(appSettings, (state) => [
|
||||
state.image_resize_service,
|
||||
state.display_avatar,
|
||||
]);
|
||||
const settings = useSuspenseQuery(settingsQueryOptions);
|
||||
|
||||
const user = useUserContext();
|
||||
|
||||
const picture = useMemo(() => {
|
||||
if (service?.length && user.profile?.picture?.length) {
|
||||
if (settings.data.resize_service && user?.profile?.picture?.length) {
|
||||
if (user.profile?.picture.includes("_next/")) {
|
||||
return user.profile?.picture;
|
||||
}
|
||||
if (user.profile?.picture.includes("bsky.network")) {
|
||||
return user.profile?.picture;
|
||||
}
|
||||
return `${service}?url=${user.profile?.picture}&w=100&h=100&n=-1&default=${user.profile?.picture}`;
|
||||
return `https://wsrv.nl?url=${user.profile?.picture}&w=100&h=100&n=-1&default=${user.profile?.picture}`;
|
||||
} else {
|
||||
return user.profile?.picture;
|
||||
return user?.profile?.picture;
|
||||
}
|
||||
}, [user.profile?.picture]);
|
||||
}, [user]);
|
||||
|
||||
const fallback = useMemo(
|
||||
() =>
|
||||
`data:image/svg+xml;utf8,${encodeURIComponent(
|
||||
minidenticon(user.pubkey, 60, 50),
|
||||
minidenticon(user ? user.pubkey : "lume", 60, 50),
|
||||
)}`,
|
||||
[user.pubkey],
|
||||
[user],
|
||||
);
|
||||
|
||||
return (
|
||||
@ -42,18 +40,19 @@ export function UserAvatar({ className }: { className?: string }) {
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{visible ? (
|
||||
{settings.data.display_avatar ? (
|
||||
<Avatar.Image
|
||||
src={picture}
|
||||
alt={user.pubkey}
|
||||
alt={user?.pubkey}
|
||||
decoding="async"
|
||||
onContextMenu={(e) => e.preventDefault()}
|
||||
className="w-full aspect-square object-cover outline-[.5px] outline-black/5 content-visibility-auto contain-intrinsic-size-[auto]"
|
||||
/>
|
||||
) : null}
|
||||
<Avatar.Fallback>
|
||||
<img
|
||||
src={fallback}
|
||||
alt={user.pubkey}
|
||||
alt={user?.pubkey}
|
||||
className="size-full bg-black dark:bg-white outline-[.5px] outline-black/5 content-visibility-auto contain-intrinsic-size-[auto]"
|
||||
/>
|
||||
</Avatar.Fallback>
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { cn, formatCreatedAt } from "@/commons";
|
||||
import { cn, createdAt } from "@/commons";
|
||||
import { useMemo } from "react";
|
||||
|
||||
export function UserTime({
|
||||
@ -8,11 +8,11 @@ export function UserTime({
|
||||
time: number;
|
||||
className?: string;
|
||||
}) {
|
||||
const createdAt = useMemo(() => formatCreatedAt(time), [time]);
|
||||
const displayCreatedAt = useMemo(() => createdAt(time), [time]);
|
||||
|
||||
return (
|
||||
<div className={cn("text-neutral-600 dark:text-neutral-400", className)}>
|
||||
{createdAt}
|
||||
{displayCreatedAt}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -19,11 +19,11 @@ import { Route as NewPostIndexImport } from './routes/new-post/index'
|
||||
import { Route as AppIndexImport } from './routes/_app/index'
|
||||
import { Route as ZapIdImport } from './routes/zap.$id'
|
||||
import { Route as ColumnsLayoutImport } from './routes/columns/_layout'
|
||||
import { Route as IdSetProfileImport } from './routes/$id.set-profile'
|
||||
import { Route as IdSetInterestImport } from './routes/$id.set-interest'
|
||||
import { Route as IdSetGroupImport } from './routes/$id.set-group'
|
||||
import { Route as SettingsIdWalletImport } from './routes/settings.$id/wallet'
|
||||
import { Route as SettingsIdRelayImport } from './routes/settings.$id/relay'
|
||||
import { Route as SettingsIdProfileImport } from './routes/settings.$id/profile'
|
||||
import { Route as SettingsIdGeneralImport } from './routes/settings.$id/general'
|
||||
import { Route as ColumnsLayoutGlobalImport } from './routes/columns/_layout/global'
|
||||
import { Route as ColumnsLayoutCreateNewsfeedImport } from './routes/columns/_layout/create-newsfeed'
|
||||
@ -149,6 +149,14 @@ const ColumnsLayoutRoute = ColumnsLayoutImport.update({
|
||||
getParentRoute: () => ColumnsRoute,
|
||||
} as any)
|
||||
|
||||
const IdSetProfileRoute = IdSetProfileImport.update({
|
||||
id: '/$id/set-profile',
|
||||
path: '/$id/set-profile',
|
||||
getParentRoute: () => rootRoute,
|
||||
} as any).lazy(() =>
|
||||
import('./routes/$id.set-profile.lazy').then((d) => d.Route),
|
||||
)
|
||||
|
||||
const IdSetInterestRoute = IdSetInterestImport.update({
|
||||
id: '/$id/set-interest',
|
||||
path: '/$id/set-interest',
|
||||
@ -204,14 +212,6 @@ const SettingsIdRelayRoute = SettingsIdRelayImport.update({
|
||||
import('./routes/settings.$id/relay.lazy').then((d) => d.Route),
|
||||
)
|
||||
|
||||
const SettingsIdProfileRoute = SettingsIdProfileImport.update({
|
||||
id: '/profile',
|
||||
path: '/profile',
|
||||
getParentRoute: () => SettingsIdLazyRoute,
|
||||
} as any).lazy(() =>
|
||||
import('./routes/settings.$id/profile.lazy').then((d) => d.Route),
|
||||
)
|
||||
|
||||
const SettingsIdGeneralRoute = SettingsIdGeneralImport.update({
|
||||
id: '/general',
|
||||
path: '/general',
|
||||
@ -364,6 +364,13 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof IdSetInterestImport
|
||||
parentRoute: typeof rootRoute
|
||||
}
|
||||
'/$id/set-profile': {
|
||||
id: '/$id/set-profile'
|
||||
path: '/$id/set-profile'
|
||||
fullPath: '/$id/set-profile'
|
||||
preLoaderRoute: typeof IdSetProfileImport
|
||||
parentRoute: typeof rootRoute
|
||||
}
|
||||
'/columns': {
|
||||
id: '/columns'
|
||||
path: '/columns'
|
||||
@ -448,13 +455,6 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof SettingsIdGeneralImport
|
||||
parentRoute: typeof SettingsIdLazyImport
|
||||
}
|
||||
'/settings/$id/profile': {
|
||||
id: '/settings/$id/profile'
|
||||
path: '/profile'
|
||||
fullPath: '/settings/$id/profile'
|
||||
preLoaderRoute: typeof SettingsIdProfileImport
|
||||
parentRoute: typeof SettingsIdLazyImport
|
||||
}
|
||||
'/settings/$id/relay': {
|
||||
id: '/settings/$id/relay'
|
||||
path: '/relay'
|
||||
@ -651,14 +651,12 @@ const ColumnsRouteWithChildren =
|
||||
|
||||
interface SettingsIdLazyRouteChildren {
|
||||
SettingsIdGeneralRoute: typeof SettingsIdGeneralRoute
|
||||
SettingsIdProfileRoute: typeof SettingsIdProfileRoute
|
||||
SettingsIdRelayRoute: typeof SettingsIdRelayRoute
|
||||
SettingsIdWalletRoute: typeof SettingsIdWalletRoute
|
||||
}
|
||||
|
||||
const SettingsIdLazyRouteChildren: SettingsIdLazyRouteChildren = {
|
||||
SettingsIdGeneralRoute: SettingsIdGeneralRoute,
|
||||
SettingsIdProfileRoute: SettingsIdProfileRoute,
|
||||
SettingsIdRelayRoute: SettingsIdRelayRoute,
|
||||
SettingsIdWalletRoute: SettingsIdWalletRoute,
|
||||
}
|
||||
@ -673,6 +671,7 @@ export interface FileRoutesByFullPath {
|
||||
'/new': typeof NewLazyRoute
|
||||
'/$id/set-group': typeof IdSetGroupRoute
|
||||
'/$id/set-interest': typeof IdSetInterestRoute
|
||||
'/$id/set-profile': typeof IdSetProfileRoute
|
||||
'/columns': typeof ColumnsLayoutRouteWithChildren
|
||||
'/zap/$id': typeof ZapIdRoute
|
||||
'/new-account/connect': typeof NewAccountConnectLazyRoute
|
||||
@ -684,7 +683,6 @@ export interface FileRoutesByFullPath {
|
||||
'/columns/create-newsfeed': typeof ColumnsLayoutCreateNewsfeedRouteWithChildren
|
||||
'/columns/global': typeof ColumnsLayoutGlobalRoute
|
||||
'/settings/$id/general': typeof SettingsIdGeneralRoute
|
||||
'/settings/$id/profile': typeof SettingsIdProfileRoute
|
||||
'/settings/$id/relay': typeof SettingsIdRelayRoute
|
||||
'/settings/$id/wallet': typeof SettingsIdWalletRoute
|
||||
'/columns/onboarding': typeof ColumnsLayoutOnboardingLazyRoute
|
||||
@ -708,6 +706,7 @@ export interface FileRoutesByTo {
|
||||
'/new': typeof NewLazyRoute
|
||||
'/$id/set-group': typeof IdSetGroupRoute
|
||||
'/$id/set-interest': typeof IdSetInterestRoute
|
||||
'/$id/set-profile': typeof IdSetProfileRoute
|
||||
'/columns': typeof ColumnsLayoutRouteWithChildren
|
||||
'/zap/$id': typeof ZapIdRoute
|
||||
'/new-account/connect': typeof NewAccountConnectLazyRoute
|
||||
@ -719,7 +718,6 @@ export interface FileRoutesByTo {
|
||||
'/columns/create-newsfeed': typeof ColumnsLayoutCreateNewsfeedRouteWithChildren
|
||||
'/columns/global': typeof ColumnsLayoutGlobalRoute
|
||||
'/settings/$id/general': typeof SettingsIdGeneralRoute
|
||||
'/settings/$id/profile': typeof SettingsIdProfileRoute
|
||||
'/settings/$id/relay': typeof SettingsIdRelayRoute
|
||||
'/settings/$id/wallet': typeof SettingsIdWalletRoute
|
||||
'/columns/onboarding': typeof ColumnsLayoutOnboardingLazyRoute
|
||||
@ -745,6 +743,7 @@ export interface FileRoutesById {
|
||||
'/new': typeof NewLazyRoute
|
||||
'/$id/set-group': typeof IdSetGroupRoute
|
||||
'/$id/set-interest': typeof IdSetInterestRoute
|
||||
'/$id/set-profile': typeof IdSetProfileRoute
|
||||
'/columns': typeof ColumnsRouteWithChildren
|
||||
'/columns/_layout': typeof ColumnsLayoutRouteWithChildren
|
||||
'/zap/$id': typeof ZapIdRoute
|
||||
@ -757,7 +756,6 @@ export interface FileRoutesById {
|
||||
'/columns/_layout/create-newsfeed': typeof ColumnsLayoutCreateNewsfeedRouteWithChildren
|
||||
'/columns/_layout/global': typeof ColumnsLayoutGlobalRoute
|
||||
'/settings/$id/general': typeof SettingsIdGeneralRoute
|
||||
'/settings/$id/profile': typeof SettingsIdProfileRoute
|
||||
'/settings/$id/relay': typeof SettingsIdRelayRoute
|
||||
'/settings/$id/wallet': typeof SettingsIdWalletRoute
|
||||
'/columns/_layout/onboarding': typeof ColumnsLayoutOnboardingLazyRoute
|
||||
@ -784,6 +782,7 @@ export interface FileRouteTypes {
|
||||
| '/new'
|
||||
| '/$id/set-group'
|
||||
| '/$id/set-interest'
|
||||
| '/$id/set-profile'
|
||||
| '/columns'
|
||||
| '/zap/$id'
|
||||
| '/new-account/connect'
|
||||
@ -795,7 +794,6 @@ export interface FileRouteTypes {
|
||||
| '/columns/create-newsfeed'
|
||||
| '/columns/global'
|
||||
| '/settings/$id/general'
|
||||
| '/settings/$id/profile'
|
||||
| '/settings/$id/relay'
|
||||
| '/settings/$id/wallet'
|
||||
| '/columns/onboarding'
|
||||
@ -818,6 +816,7 @@ export interface FileRouteTypes {
|
||||
| '/new'
|
||||
| '/$id/set-group'
|
||||
| '/$id/set-interest'
|
||||
| '/$id/set-profile'
|
||||
| '/columns'
|
||||
| '/zap/$id'
|
||||
| '/new-account/connect'
|
||||
@ -829,7 +828,6 @@ export interface FileRouteTypes {
|
||||
| '/columns/create-newsfeed'
|
||||
| '/columns/global'
|
||||
| '/settings/$id/general'
|
||||
| '/settings/$id/profile'
|
||||
| '/settings/$id/relay'
|
||||
| '/settings/$id/wallet'
|
||||
| '/columns/onboarding'
|
||||
@ -853,6 +851,7 @@ export interface FileRouteTypes {
|
||||
| '/new'
|
||||
| '/$id/set-group'
|
||||
| '/$id/set-interest'
|
||||
| '/$id/set-profile'
|
||||
| '/columns'
|
||||
| '/columns/_layout'
|
||||
| '/zap/$id'
|
||||
@ -865,7 +864,6 @@ export interface FileRouteTypes {
|
||||
| '/columns/_layout/create-newsfeed'
|
||||
| '/columns/_layout/global'
|
||||
| '/settings/$id/general'
|
||||
| '/settings/$id/profile'
|
||||
| '/settings/$id/relay'
|
||||
| '/settings/$id/wallet'
|
||||
| '/columns/_layout/onboarding'
|
||||
@ -891,6 +889,7 @@ export interface RootRouteChildren {
|
||||
NewLazyRoute: typeof NewLazyRoute
|
||||
IdSetGroupRoute: typeof IdSetGroupRoute
|
||||
IdSetInterestRoute: typeof IdSetInterestRoute
|
||||
IdSetProfileRoute: typeof IdSetProfileRoute
|
||||
ColumnsRoute: typeof ColumnsRouteWithChildren
|
||||
ZapIdRoute: typeof ZapIdRoute
|
||||
NewAccountConnectLazyRoute: typeof NewAccountConnectLazyRoute
|
||||
@ -906,6 +905,7 @@ const rootRouteChildren: RootRouteChildren = {
|
||||
NewLazyRoute: NewLazyRoute,
|
||||
IdSetGroupRoute: IdSetGroupRoute,
|
||||
IdSetInterestRoute: IdSetInterestRoute,
|
||||
IdSetProfileRoute: IdSetProfileRoute,
|
||||
ColumnsRoute: ColumnsRouteWithChildren,
|
||||
ZapIdRoute: ZapIdRoute,
|
||||
NewAccountConnectLazyRoute: NewAccountConnectLazyRoute,
|
||||
@ -932,6 +932,7 @@ export const routeTree = rootRoute
|
||||
"/new",
|
||||
"/$id/set-group",
|
||||
"/$id/set-interest",
|
||||
"/$id/set-profile",
|
||||
"/columns",
|
||||
"/zap/$id",
|
||||
"/new-account/connect",
|
||||
@ -959,6 +960,9 @@ export const routeTree = rootRoute
|
||||
"/$id/set-interest": {
|
||||
"filePath": "$id.set-interest.tsx"
|
||||
},
|
||||
"/$id/set-profile": {
|
||||
"filePath": "$id.set-profile.tsx"
|
||||
},
|
||||
"/columns": {
|
||||
"filePath": "columns",
|
||||
"children": [
|
||||
@ -1001,7 +1005,6 @@ export const routeTree = rootRoute
|
||||
"filePath": "settings.$id.lazy.tsx",
|
||||
"children": [
|
||||
"/settings/$id/general",
|
||||
"/settings/$id/profile",
|
||||
"/settings/$id/relay",
|
||||
"/settings/$id/wallet"
|
||||
]
|
||||
@ -1029,10 +1032,6 @@ export const routeTree = rootRoute
|
||||
"filePath": "settings.$id/general.tsx",
|
||||
"parent": "/settings/$id"
|
||||
},
|
||||
"/settings/$id/profile": {
|
||||
"filePath": "settings.$id/profile.tsx",
|
||||
"parent": "/settings/$id"
|
||||
},
|
||||
"/settings/$id/relay": {
|
||||
"filePath": "settings.$id/relay.tsx",
|
||||
"parent": "/settings/$id"
|
||||
|
230
src/routes/$id.set-profile.lazy.tsx
Normal file
230
src/routes/$id.set-profile.lazy.tsx
Normal file
@ -0,0 +1,230 @@
|
||||
import { commands } from "@/commands.gen";
|
||||
import { cn, upload } from "@/commons";
|
||||
import { Spinner } from "@/components";
|
||||
import type { Metadata } from "@/types";
|
||||
import { Plus } from "@phosphor-icons/react";
|
||||
import * as ScrollArea from "@radix-ui/react-scroll-area";
|
||||
import { createLazyFileRoute } from "@tanstack/react-router";
|
||||
import { getCurrentWindow } from "@tauri-apps/api/window";
|
||||
import { message } from "@tauri-apps/plugin-dialog";
|
||||
import {
|
||||
type Dispatch,
|
||||
type ReactNode,
|
||||
type SetStateAction,
|
||||
useState,
|
||||
useTransition,
|
||||
} from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
|
||||
export const Route = createLazyFileRoute("/$id/set-profile")({
|
||||
component: Screen,
|
||||
});
|
||||
|
||||
function Screen() {
|
||||
const { id } = Route.useParams();
|
||||
const { profile, queryClient } = Route.useRouteContext();
|
||||
const { register, handleSubmit } = useForm({ defaultValues: profile });
|
||||
|
||||
const [picture, setPicture] = useState<string>("");
|
||||
const [isPending, startTransition] = useTransition();
|
||||
|
||||
const onSubmit = (data: Metadata) => {
|
||||
startTransition(async () => {
|
||||
const signer = await commands.hasSigner(id);
|
||||
|
||||
if (signer.status === "ok") {
|
||||
if (!signer.data) {
|
||||
const res = await commands.setSigner(id);
|
||||
|
||||
if (res.status === "error") {
|
||||
await message(res.error, { kind: "error" });
|
||||
return;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
await message(signer.error, { kind: "error" });
|
||||
return;
|
||||
}
|
||||
|
||||
const newProfile: Metadata = { ...profile, ...data, picture };
|
||||
const res = await commands.setProfile(JSON.stringify(newProfile));
|
||||
|
||||
if (res.status === "ok") {
|
||||
// Invalidate cache
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: ["profile", id],
|
||||
});
|
||||
|
||||
// Close current popup
|
||||
await getCurrentWindow().close();
|
||||
} else {
|
||||
await message(res.error, { title: "Profile", kind: "error" });
|
||||
return;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col size-full">
|
||||
<div data-tauri-drag-region className="shrink-0 h-11" />
|
||||
<form
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
className="min-h-0 flex-1 flex flex-col mb-0"
|
||||
>
|
||||
<div className="shrink-0 h-20 flex items-center gap-3 p-3 border-b border-black/5 dark:border-white/5">
|
||||
<div className="relative rounded-full size-12 bg-gradient-to-tr from-orange-100 via-red-50 to-blue-200">
|
||||
{profile.picture ? (
|
||||
<img
|
||||
src={picture || profile.picture}
|
||||
alt="avatar"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
className="absolute inset-0 z-10 object-cover size-12 rounded-full"
|
||||
/>
|
||||
) : null}
|
||||
<AvatarUploader
|
||||
setPicture={setPicture}
|
||||
className="absolute inset-0 z-20 flex items-center justify-center size-full text-white rounded-full bg-black/10 hover:bg-black/20 dark:bg-white/10 dark:hover:bg-white/20"
|
||||
>
|
||||
<Plus className="size-5" />
|
||||
</AvatarUploader>
|
||||
</div>
|
||||
<div className="flex-1 flex justify-between items-center">
|
||||
<div>
|
||||
<div className="font-semibold">{profile.display_name}</div>
|
||||
<div className="leading-tight text-sm text-neutral-700 dark:text-neutral-300">
|
||||
{profile.nip05?.startsWith("_")
|
||||
? profile.nip05.replace("_@", "")
|
||||
: profile.nip05}
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isPending}
|
||||
className="inline-flex items-center justify-center w-28 h-8 px-2 text-xs font-semibold text-white bg-blue-500 rounded-full hover:bg-blue-600 disabled:opacity-50"
|
||||
>
|
||||
{isPending ? <Spinner className="size-4" /> : "Update Profile"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<ScrollArea.Root
|
||||
type={"scroll"}
|
||||
scrollHideDelay={300}
|
||||
className="min-h-0 flex-1 overflow-hidden"
|
||||
>
|
||||
<ScrollArea.Viewport className="bg-white dark:bg-black h-full p-3">
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex flex-col w-full gap-1.5">
|
||||
<label htmlFor="display_name" className="text-sm font-medium">
|
||||
Display Name
|
||||
</label>
|
||||
<input
|
||||
{...register("display_name")}
|
||||
spellCheck={false}
|
||||
className="w-full px-3 bg-transparent rounded-lg h-9 border-neutral-300 placeholder:text-neutral-500 focus:border-blue-500 focus:ring focus:ring-blue-200 dark:border-neutral-700 dark:placeholder:text-neutral-400 dark:focus:ring-blue-800"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col w-full gap-1.5">
|
||||
<label htmlFor="name" className="text-sm font-medium">
|
||||
Name
|
||||
</label>
|
||||
<input
|
||||
{...register("name")}
|
||||
spellCheck={false}
|
||||
className="w-full px-3 bg-transparent rounded-lg h-9 border-neutral-300 placeholder:text-neutral-500 focus:border-blue-500 focus:ring focus:ring-blue-200 dark:border-neutral-700 dark:placeholder:text-neutral-400 dark:focus:ring-blue-800"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col w-full gap-1.5">
|
||||
<label htmlFor="website" className="text-sm font-medium">
|
||||
Website
|
||||
</label>
|
||||
<input
|
||||
type="url"
|
||||
{...register("website")}
|
||||
spellCheck={false}
|
||||
className="w-full px-3 bg-transparent rounded-lg h-9 border-neutral-300 placeholder:text-neutral-500 focus:border-blue-500 focus:ring focus:ring-blue-200 dark:border-neutral-700 dark:placeholder:text-neutral-400 dark:focus:ring-blue-800"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col w-full gap-1.5">
|
||||
<label htmlFor="banner" className="text-sm font-medium">
|
||||
Cover
|
||||
</label>
|
||||
<input
|
||||
type="url"
|
||||
{...register("banner")}
|
||||
spellCheck={false}
|
||||
className="w-full px-3 bg-transparent rounded-lg h-9 border-neutral-300 placeholder:text-neutral-500 focus:border-blue-500 focus:ring focus:ring-blue-200 dark:border-neutral-700 dark:placeholder:text-neutral-400 dark:focus:ring-blue-800"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col w-full gap-1.5">
|
||||
<label htmlFor="nip05" className="text-sm font-medium">
|
||||
NIP-05
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
{...register("nip05")}
|
||||
spellCheck={false}
|
||||
className="w-full px-3 bg-transparent rounded-lg h-9 border-neutral-300 placeholder:text-neutral-500 focus:border-blue-500 focus:ring focus:ring-blue-200 dark:border-neutral-700 dark:placeholder:text-neutral-400 dark:focus:ring-blue-800"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col w-full gap-1.5">
|
||||
<label htmlFor="lnaddress" className="text-sm font-medium">
|
||||
Lightning Address
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
{...register("lud16")}
|
||||
className="w-full px-3 bg-transparent rounded-lg h-9 border-neutral-300 placeholder:text-neutral-500 focus:border-blue-500 focus:ring focus:ring-blue-200 dark:border-neutral-700 dark:placeholder:text-neutral-400 dark:focus:ring-blue-800"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</ScrollArea.Viewport>
|
||||
<ScrollArea.Scrollbar
|
||||
className="flex select-none touch-none p-0.5 duration-[160ms] ease-out data-[orientation=vertical]:w-2"
|
||||
orientation="vertical"
|
||||
>
|
||||
<ScrollArea.Thumb className="flex-1 bg-black/10 dark:bg-white/10 rounded-full relative before:content-[''] before:absolute before:top-1/2 before:left-1/2 before:-translate-x-1/2 before:-translate-y-1/2 before:w-full before:h-full before:min-w-[44px] before:min-h-[44px]" />
|
||||
</ScrollArea.Scrollbar>
|
||||
<ScrollArea.Corner className="bg-transparent" />
|
||||
</ScrollArea.Root>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function AvatarUploader({
|
||||
setPicture,
|
||||
children,
|
||||
className,
|
||||
}: {
|
||||
setPicture: Dispatch<SetStateAction<string>>;
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
}) {
|
||||
const [isPending, startTransition] = useTransition();
|
||||
|
||||
const uploadAvatar = () => {
|
||||
startTransition(async () => {
|
||||
try {
|
||||
const image = await upload();
|
||||
|
||||
if (image) {
|
||||
setPicture(image);
|
||||
}
|
||||
} catch (e) {
|
||||
await message(String(e));
|
||||
return;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => uploadAvatar()}
|
||||
className={cn("size-4", className)}
|
||||
>
|
||||
{isPending ? <Spinner className="size-4" /> : children}
|
||||
</button>
|
||||
);
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
import { type Profile, commands } from "@/commands.gen";
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
|
||||
export const Route = createFileRoute("/settings/$id/profile")({
|
||||
export const Route = createFileRoute("/$id/set-profile")({
|
||||
beforeLoad: async ({ params }) => {
|
||||
const res = await commands.getProfile(params.id);
|
||||
|
@ -1,20 +1,38 @@
|
||||
import { commands } from "@/commands.gen";
|
||||
import { Spinner } from "@/components";
|
||||
import type { Metadata, NostrEvent } from "@/types";
|
||||
import type { QueryClient } from "@tanstack/react-query";
|
||||
import { type QueryClient, queryOptions } from "@tanstack/react-query";
|
||||
import { Outlet, createRootRouteWithContext } from "@tanstack/react-router";
|
||||
import { getCurrentWindow } from "@tauri-apps/api/window";
|
||||
import type { OsType } from "@tauri-apps/plugin-os";
|
||||
import type { Store } from "@tauri-apps/plugin-store";
|
||||
import { useEffect } from "react";
|
||||
|
||||
interface RouterContext {
|
||||
store: Store;
|
||||
queryClient: QueryClient;
|
||||
platform: OsType;
|
||||
accounts?: string[];
|
||||
}
|
||||
|
||||
export const settingsQueryOptions = queryOptions({
|
||||
queryKey: ["settings"],
|
||||
queryFn: async () => {
|
||||
const res = await commands.getAppSettings();
|
||||
|
||||
if (res.status === "ok") {
|
||||
return res.data;
|
||||
} else {
|
||||
throw new Error(res.error);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
export const Route = createRootRouteWithContext<RouterContext>()({
|
||||
component: Screen,
|
||||
pendingComponent: Pending,
|
||||
loader: ({ context }) =>
|
||||
context.queryClient.ensureQueryData(settingsQueryOptions),
|
||||
});
|
||||
|
||||
function Screen() {
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { commands } from "@/commands.gen";
|
||||
import { appColumns, cn } from "@/commons";
|
||||
import { cn } from "@/commons";
|
||||
import { PublishIcon } from "@/components";
|
||||
import { User } from "@/components/user";
|
||||
import { LumeWindow } from "@/system";
|
||||
@ -14,6 +14,7 @@ import {
|
||||
import { listen } from "@tauri-apps/api/event";
|
||||
import { Menu, MenuItem, PredefinedMenuItem } from "@tauri-apps/api/menu";
|
||||
import { writeText } from "@tauri-apps/plugin-clipboard-manager";
|
||||
import { message } from "@tauri-apps/plugin-dialog";
|
||||
import { useCallback, useEffect } from "react";
|
||||
|
||||
export const Route = createLazyFileRoute("/_app")({
|
||||
@ -107,7 +108,7 @@ function Account({ pubkey }: { pubkey: string }) {
|
||||
|
||||
const items = await Promise.all([
|
||||
MenuItem.new({
|
||||
text: "Activate",
|
||||
text: "Unlock",
|
||||
enabled: !isActive || true,
|
||||
action: async () => await commands.setSigner(pubkey),
|
||||
}),
|
||||
@ -116,10 +117,31 @@ function Account({ pubkey }: { pubkey: string }) {
|
||||
text: "View Profile",
|
||||
action: () => LumeWindow.openProfile(pubkey),
|
||||
}),
|
||||
MenuItem.new({
|
||||
text: "Update Profile",
|
||||
action: () =>
|
||||
LumeWindow.openPopup(
|
||||
`${pubkey}/set-profile`,
|
||||
"Update Profile",
|
||||
true,
|
||||
),
|
||||
}),
|
||||
PredefinedMenuItem.new({ item: "Separator" }),
|
||||
MenuItem.new({
|
||||
text: "Copy Public Key",
|
||||
action: async () => await writeText(pubkey),
|
||||
}),
|
||||
MenuItem.new({
|
||||
text: "Copy Private Key",
|
||||
action: async () => {
|
||||
const res = await commands.getPrivateKey(pubkey);
|
||||
if (res.status === "ok") {
|
||||
await writeText(res.data);
|
||||
} else {
|
||||
await message(res.error, { kind: "error" });
|
||||
}
|
||||
},
|
||||
}),
|
||||
PredefinedMenuItem.new({ item: "Separator" }),
|
||||
MenuItem.new({
|
||||
text: "Settings",
|
||||
@ -127,20 +149,13 @@ function Account({ pubkey }: { pubkey: string }) {
|
||||
}),
|
||||
PredefinedMenuItem.new({ item: "Separator" }),
|
||||
MenuItem.new({
|
||||
text: "Delete Account",
|
||||
text: "Logout",
|
||||
action: async () => {
|
||||
const res = await commands.deleteAccount(pubkey);
|
||||
|
||||
if (res.status === "ok") {
|
||||
router.invalidate();
|
||||
|
||||
// Delete column associate with this account
|
||||
appColumns.setState((prev) =>
|
||||
prev.filter((col) =>
|
||||
col.account ? col.account !== pubkey : col,
|
||||
),
|
||||
);
|
||||
|
||||
// Check remain account
|
||||
const newAccounts = context.accounts.filter(
|
||||
(account) => account !== pubkey,
|
||||
@ -176,6 +191,7 @@ function Account({ pubkey }: { pubkey: string }) {
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => showContextMenu(e)}
|
||||
onContextMenu={(e) => showContextMenu(e)}
|
||||
className="h-10 relative"
|
||||
>
|
||||
<User.Provider pubkey={pubkey}>
|
||||
|
@ -1,11 +1,10 @@
|
||||
import { commands } from "@/commands.gen";
|
||||
import { appColumns, displayNpub } from "@/commons";
|
||||
import { displayNpub } from "@/commons";
|
||||
import { Column, Spinner } from "@/components";
|
||||
import { LumeWindow } from "@/system";
|
||||
import type { ColumnEvent, LumeColumn, Metadata } from "@/types";
|
||||
import { ArrowLeft, ArrowRight, Plus } from "@phosphor-icons/react";
|
||||
import { createLazyFileRoute, useRouter } from "@tanstack/react-router";
|
||||
import { useStore } from "@tanstack/react-store";
|
||||
import { listen } from "@tauri-apps/api/event";
|
||||
import { Menu, MenuItem } from "@tauri-apps/api/menu";
|
||||
import { getCurrentWindow } from "@tauri-apps/api/window";
|
||||
@ -26,10 +25,11 @@ export const Route = createLazyFileRoute("/_app/")({
|
||||
});
|
||||
|
||||
function Screen() {
|
||||
const context = Route.useRouteContext();
|
||||
const initialAppColumns = Route.useLoaderData();
|
||||
const router = useRouter();
|
||||
const columns = useStore(appColumns, (state) => state);
|
||||
|
||||
const [columns, setColumns] = useState<LumeColumn[]>([]);
|
||||
const [emblaRef, emblaApi] = useEmblaCarousel({
|
||||
watchDrag: false,
|
||||
loop: false,
|
||||
@ -52,9 +52,9 @@ function Screen() {
|
||||
const res = await commands.closeColumn(label);
|
||||
|
||||
if (res.status === "ok") {
|
||||
appColumns.setState((prev) => prev.filter((t) => t.label !== label));
|
||||
setColumns((prev) => prev.filter((t) => t.label !== label));
|
||||
} else {
|
||||
await message(res.error, { kind: "errror" });
|
||||
await message(res.error, { kind: "error" });
|
||||
}
|
||||
},
|
||||
[columns],
|
||||
@ -64,7 +64,7 @@ function Screen() {
|
||||
const exist = columns.find((col) => col.label === column.label);
|
||||
|
||||
if (!exist) {
|
||||
appColumns.setState((prev) => [column, ...prev]);
|
||||
setColumns((prev) => [column, ...prev]);
|
||||
|
||||
if (emblaApi) {
|
||||
emblaApi.scrollTo(0, true);
|
||||
@ -85,7 +85,7 @@ function Screen() {
|
||||
if (direction === "left") newCols.splice(colIndex - 1, 0, existColumn);
|
||||
if (direction === "right") newCols.splice(colIndex + 1, 0, existColumn);
|
||||
|
||||
appColumns.setState(() => newCols);
|
||||
setColumns(() => newCols);
|
||||
}
|
||||
},
|
||||
150,
|
||||
@ -100,11 +100,9 @@ function Screen() {
|
||||
const newCols = columns.slice();
|
||||
newCols[currentColIndex] = updatedCol;
|
||||
|
||||
appColumns.setState(() => newCols);
|
||||
setColumns(() => newCols);
|
||||
}, 150);
|
||||
|
||||
const reset = useDebouncedCallback(() => appColumns.setState(() => []), 150);
|
||||
|
||||
useEffect(() => {
|
||||
if (emblaApi) {
|
||||
emblaApi.on("scroll", emitScrollEvent);
|
||||
@ -119,7 +117,6 @@ function Screen() {
|
||||
|
||||
useEffect(() => {
|
||||
const unlisten = listen<ColumnEvent>("columns", (data) => {
|
||||
if (data.payload.type === "reset") reset();
|
||||
if (data.payload.type === "add") add(data.payload.column);
|
||||
if (data.payload.type === "remove") remove(data.payload.label);
|
||||
if (data.payload.type === "move")
|
||||
@ -145,12 +142,14 @@ function Screen() {
|
||||
|
||||
useEffect(() => {
|
||||
if (initialAppColumns) {
|
||||
appColumns.setState(() => initialAppColumns);
|
||||
setColumns(() => initialAppColumns);
|
||||
}
|
||||
}, [initialAppColumns]);
|
||||
|
||||
useEffect(() => {
|
||||
window.localStorage.setItem("columns", JSON.stringify(columns));
|
||||
(async () => {
|
||||
await context.store.set("columns", JSON.stringify(columns));
|
||||
})();
|
||||
}, [columns]);
|
||||
|
||||
return (
|
||||
|
@ -5,11 +5,11 @@ import { nanoid } from "nanoid";
|
||||
export const Route = createFileRoute("/_app/")({
|
||||
loader: async ({ context }) => {
|
||||
const accounts = context.accounts;
|
||||
const prevColumns = window.localStorage.getItem("columns");
|
||||
const prev = await context.store.get("columns");
|
||||
|
||||
let initialAppColumns: LumeColumn[] = [];
|
||||
|
||||
if (!prevColumns || prevColumns.length < 1) {
|
||||
if (!prev) {
|
||||
initialAppColumns.push({
|
||||
label: "onboarding",
|
||||
name: "Onboarding",
|
||||
@ -25,7 +25,7 @@ export const Route = createFileRoute("/_app/")({
|
||||
});
|
||||
}
|
||||
} else {
|
||||
const parsed: LumeColumn[] = JSON.parse(prevColumns);
|
||||
const parsed: LumeColumn[] = JSON.parse(prev as string);
|
||||
|
||||
initialAppColumns = parsed.filter((item) =>
|
||||
item.account ? context.accounts.includes(item.account) : item,
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { commands } from "@/commands.gen";
|
||||
import { decodeZapInvoice, formatCreatedAt } from "@/commons";
|
||||
import { createdAt, decodeZapInvoice } from "@/commons";
|
||||
import { Note, RepostIcon, Spinner, User } from "@/components";
|
||||
import { LumeEvent, LumeWindow, useEvent } from "@/system";
|
||||
import { Kind, type NostrEvent } from "@/types";
|
||||
@ -300,7 +300,7 @@ function TextNote({ event }: { event: LumeEvent }) {
|
||||
<div className="flex items-baseline justify-between w-full">
|
||||
<User.Name className="text-sm font-semibold leading-tight" />
|
||||
<span className="text-sm leading-tight text-black/50 dark:text-white/50">
|
||||
{formatCreatedAt(event.created_at)}
|
||||
{createdAt(event.created_at)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="inline-flex items-baseline gap-1 text-xs">
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { commands } from "@/commands.gen";
|
||||
import { replyTime, toLumeEvents } from "@/commons";
|
||||
import { replyAt, toLumeEvents } from "@/commons";
|
||||
import { Note, Spinner, User } from "@/components";
|
||||
import { Hashtag } from "@/components/note/mentions/hashtag";
|
||||
import { MentionUser } from "@/components/note/mentions/user";
|
||||
@ -155,7 +155,7 @@ const StoryEvent = memo(function StoryEvent({ event }: { event: LumeEvent }) {
|
||||
</div>
|
||||
<div className="flex-1 flex items-center justify-between">
|
||||
<span className="text-sm text-neutral-500">
|
||||
{replyTime(event.created_at)}
|
||||
{replyAt(event.created_at)}
|
||||
</span>
|
||||
<div className="invisible group-hover:visible flex items-center justify-end gap-3">
|
||||
<Note.Reply />
|
||||
|
@ -1,12 +1,19 @@
|
||||
import { commands } from "@/commands.gen";
|
||||
import { appSettings } from "@/commons";
|
||||
import { type Settings, commands } from "@/commands.gen";
|
||||
import { Spinner } from "@/components";
|
||||
import * as Switch from "@radix-ui/react-switch";
|
||||
import { useSuspenseQuery } from "@tanstack/react-query";
|
||||
import { createLazyFileRoute } from "@tanstack/react-router";
|
||||
import { useStore } from "@tanstack/react-store";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { message } from "@tauri-apps/plugin-dialog";
|
||||
import { useCallback, useEffect, useState, useTransition } from "react";
|
||||
import {
|
||||
type Dispatch,
|
||||
type SetStateAction,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useState,
|
||||
useTransition,
|
||||
} from "react";
|
||||
import { settingsQueryOptions } from "../__root";
|
||||
|
||||
type Theme = "auto" | "light" | "dark";
|
||||
|
||||
@ -15,7 +22,11 @@ export const Route = createLazyFileRoute("/settings/$id/general")({
|
||||
});
|
||||
|
||||
function Screen() {
|
||||
const [theme, setTheme] = useState<Theme>(null);
|
||||
const settings = useSuspenseQuery(settingsQueryOptions);
|
||||
const { queryClient } = Route.useRouteContext();
|
||||
|
||||
const [theme, setTheme] = useState<Theme>("auto");
|
||||
const [newSettings, setNewSettings] = useState<Settings>();
|
||||
const [isPending, startTransition] = useTransition();
|
||||
|
||||
const changeTheme = useCallback(async (theme: string) => {
|
||||
@ -28,14 +39,14 @@ function Screen() {
|
||||
|
||||
const updateSettings = () => {
|
||||
startTransition(async () => {
|
||||
const newSettings = JSON.stringify(appSettings.state);
|
||||
const res = await commands.setUserSettings(newSettings);
|
||||
const res = await commands.setAppSettings(JSON.stringify(newSettings));
|
||||
|
||||
if (res.status === "error") {
|
||||
if (res.status === "ok") {
|
||||
await queryClient.invalidateQueries({ queryKey: ["settings"] });
|
||||
return;
|
||||
} else {
|
||||
await message(res.error, { kind: "error" });
|
||||
}
|
||||
|
||||
return;
|
||||
});
|
||||
};
|
||||
|
||||
@ -43,6 +54,16 @@ function Screen() {
|
||||
invoke("plugin:theme|get_theme").then((data) => setTheme(data as Theme));
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (settings.status === "success") {
|
||||
setNewSettings(settings.data);
|
||||
}
|
||||
}, [settings]);
|
||||
|
||||
if (!newSettings) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="relative w-full">
|
||||
<div className="flex flex-col gap-6 px-3 pb-3">
|
||||
@ -51,21 +72,22 @@ function Screen() {
|
||||
General
|
||||
</h2>
|
||||
<div className="flex flex-col px-3 divide-y divide-black/10 dark:divide-white/10 bg-black/5 dark:bg-white/5 rounded-xl">
|
||||
<Setting
|
||||
name="Relay Hint"
|
||||
description="Use the relay hint if necessary."
|
||||
label="use_relay_hint"
|
||||
/>
|
||||
<Setting
|
||||
name="Content Warning"
|
||||
description="Shows a warning for notes that have a content warning."
|
||||
label="content_warning"
|
||||
checked={newSettings.content_warning}
|
||||
setNewSettings={setNewSettings}
|
||||
/>
|
||||
{/*
|
||||
<Setting
|
||||
name="Trusted Only"
|
||||
description="Only shows note's replies from your inner circle."
|
||||
label="trusted_only"
|
||||
newSettings={newSettings}
|
||||
setNewSettings={setNewSettings}
|
||||
/>
|
||||
*/}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
@ -94,19 +116,25 @@ function Screen() {
|
||||
</div>
|
||||
</div>
|
||||
<Setting
|
||||
name="Transparent Effect"
|
||||
description="Use native window transparent effect."
|
||||
label="transparent"
|
||||
name="Show Avatar"
|
||||
description="Shows the user avatar."
|
||||
label="display_avatar"
|
||||
checked={newSettings.display_avatar}
|
||||
setNewSettings={setNewSettings}
|
||||
/>
|
||||
<Setting
|
||||
name="Show Zap Button"
|
||||
description="Shows the Zap button when viewing a note."
|
||||
label="display_zap_button"
|
||||
checked={newSettings.display_zap_button}
|
||||
setNewSettings={setNewSettings}
|
||||
/>
|
||||
<Setting
|
||||
name="Show Repost Button"
|
||||
description="Shows the Repost button when viewing a note."
|
||||
label="display_repost_button"
|
||||
checked={newSettings.display_repost_button}
|
||||
setNewSettings={setNewSettings}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@ -116,25 +144,23 @@ function Screen() {
|
||||
</h2>
|
||||
<div className="flex flex-col px-3 divide-y divide-black/10 dark:divide-white/10 bg-black/5 dark:bg-white/5 rounded-xl">
|
||||
<Setting
|
||||
name="Proxy"
|
||||
description="Set proxy address."
|
||||
label="proxy"
|
||||
name="Resize Service"
|
||||
description="Use weserv for resize image on-the-fly."
|
||||
label="resize_service"
|
||||
checked={newSettings.resize_service}
|
||||
setNewSettings={setNewSettings}
|
||||
/>
|
||||
<Setting
|
||||
name="Image Resize Service"
|
||||
description="Use weserv/images for resize image on-the-fly."
|
||||
label="image_resize_service"
|
||||
/>
|
||||
<Setting
|
||||
name="Load Remote Media"
|
||||
description="View the remote media directly."
|
||||
name="Show Remote Media"
|
||||
description="Automatically load remote media."
|
||||
label="display_media"
|
||||
checked={newSettings.display_media}
|
||||
setNewSettings={setNewSettings}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="sticky bottom-0 left-0 w-full h-16 flex items-center justify-end px-3">
|
||||
<div className="absolute left-0 bottom-0 w-full h-11 gradient-mask-t-0 bg-neutral-100 dark:bg-neutral-900" />
|
||||
<div className="w-full h-16 flex items-center justify-end px-3">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => updateSettings()}
|
||||
@ -151,33 +177,37 @@ function Setting({
|
||||
label,
|
||||
name,
|
||||
description,
|
||||
checked,
|
||||
setNewSettings,
|
||||
}: {
|
||||
label: string;
|
||||
name: string;
|
||||
description: string;
|
||||
checked: boolean;
|
||||
setNewSettings: Dispatch<SetStateAction<Settings | undefined>>;
|
||||
}) {
|
||||
const state = useStore(appSettings, (state) => state[label]);
|
||||
|
||||
const toggle = useCallback(() => {
|
||||
appSettings.setState((state) => {
|
||||
return {
|
||||
...state,
|
||||
[label]: !state[label],
|
||||
};
|
||||
setNewSettings((state) => {
|
||||
if (state) {
|
||||
return {
|
||||
...state,
|
||||
[label]: !state[label],
|
||||
};
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="flex items-start justify-between w-full gap-4 py-3">
|
||||
<div className="flex-1">
|
||||
<h3 className="font-medium">{name}</h3>
|
||||
<p className="text-sm text-neutral-700 dark:text-neutral-300">
|
||||
<h3 className="text-sm font-medium">{name}</h3>
|
||||
<p className="text-xs text-neutral-700 dark:text-neutral-300">
|
||||
{description}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex justify-end w-36 shrink-0">
|
||||
<Switch.Root
|
||||
checked={state}
|
||||
checked={checked}
|
||||
onClick={() => toggle()}
|
||||
className="relative h-7 w-12 shrink-0 cursor-default rounded-full bg-black/10 outline-none data-[state=checked]:bg-blue-500 dark:bg-white/10"
|
||||
>
|
||||
|
@ -1,17 +1,3 @@
|
||||
import { commands } from "@/commands.gen";
|
||||
import { appSettings } from "@/commons";
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
|
||||
export const Route = createFileRoute("/settings/$id/general")({
|
||||
beforeLoad: async () => {
|
||||
const res = await commands.getUserSettings();
|
||||
|
||||
if (res.status === "ok") {
|
||||
appSettings.setState((state) => {
|
||||
return { ...state, ...res.data };
|
||||
});
|
||||
} else {
|
||||
throw new Error(res.error);
|
||||
}
|
||||
},
|
||||
});
|
||||
export const Route = createFileRoute("/settings/$id/general")();
|
||||
|
@ -1,245 +0,0 @@
|
||||
import { type Profile, commands } from "@/commands.gen";
|
||||
import { cn, upload } from "@/commons";
|
||||
import { Spinner } from "@/components";
|
||||
import { Plus } from "@phosphor-icons/react";
|
||||
import { createLazyFileRoute } from "@tanstack/react-router";
|
||||
import { writeText } from "@tauri-apps/plugin-clipboard-manager";
|
||||
import { message } from "@tauri-apps/plugin-dialog";
|
||||
import {
|
||||
type Dispatch,
|
||||
type ReactNode,
|
||||
type SetStateAction,
|
||||
useState,
|
||||
useTransition,
|
||||
} from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
|
||||
export const Route = createLazyFileRoute("/settings/$id/profile")({
|
||||
component: Screen,
|
||||
});
|
||||
|
||||
function Screen() {
|
||||
const { profile } = Route.useRouteContext();
|
||||
const { register, handleSubmit } = useForm({ defaultValues: profile });
|
||||
|
||||
const [isPending, startTransition] = useTransition();
|
||||
const [picture, setPicture] = useState<string>("");
|
||||
|
||||
const onSubmit = (data: Profile) => {
|
||||
startTransition(async () => {
|
||||
const newProfile: Profile = { ...profile, ...data, picture };
|
||||
const res = await commands.setProfile(newProfile);
|
||||
|
||||
if (res.status === "error") {
|
||||
await message(res.error, { title: "Profile", kind: "error" });
|
||||
}
|
||||
|
||||
return;
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="relative flex flex-col gap-6 px-3 pb-3">
|
||||
<div className="flex items-center flex-1 h-full gap-3">
|
||||
<div className="relative rounded-full size-20 bg-gradient-to-tr from-orange-100 via-red-50 to-blue-200">
|
||||
{profile.picture ? (
|
||||
<img
|
||||
src={picture || profile.picture}
|
||||
alt="avatar"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
className="absolute inset-0 z-10 object-cover size-20 rounded-full"
|
||||
/>
|
||||
) : null}
|
||||
<AvatarUploader
|
||||
setPicture={setPicture}
|
||||
className="absolute inset-0 z-20 flex items-center justify-center size-full text-white rounded-full bg-black/10 hover:bg-black/20 dark:bg-white/10 dark:hover:bg-white/20"
|
||||
>
|
||||
<Plus className="size-5" />
|
||||
</AvatarUploader>
|
||||
</div>
|
||||
<div className="flex-1 flex items-center justify-between">
|
||||
<div>
|
||||
<div className="text-lg font-semibold">{profile.display_name}</div>
|
||||
<div className="text-neutral-700 dark:text-neutral-300">
|
||||
{profile.nip05}
|
||||
</div>
|
||||
</div>
|
||||
<PrivkeyButton />
|
||||
</div>
|
||||
</div>
|
||||
<form
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
className="flex flex-col gap-3 mb-0"
|
||||
>
|
||||
<div className="flex flex-col w-full gap-1">
|
||||
<label
|
||||
htmlFor="display_name"
|
||||
className="text-sm font-medium text-neutral-700 dark:text-neutral-300"
|
||||
>
|
||||
Display Name
|
||||
</label>
|
||||
<input
|
||||
name="display_name"
|
||||
{...register("display_name")}
|
||||
spellCheck={false}
|
||||
className="w-full px-3 bg-transparent rounded-lg h-9 border-neutral-300 placeholder:text-neutral-500 focus:border-blue-500 focus:ring focus:ring-blue-200 dark:border-neutral-700 dark:placeholder:text-neutral-400 dark:focus:ring-blue-800"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col w-full gap-1">
|
||||
<label
|
||||
htmlFor="name"
|
||||
className="text-sm font-medium text-neutral-700 dark:text-neutral-300"
|
||||
>
|
||||
Name
|
||||
</label>
|
||||
<input
|
||||
name="name"
|
||||
{...register("name")}
|
||||
spellCheck={false}
|
||||
className="w-full px-3 bg-transparent rounded-lg h-9 border-neutral-300 placeholder:text-neutral-500 focus:border-blue-500 focus:ring focus:ring-blue-200 dark:border-neutral-700 dark:placeholder:text-neutral-400 dark:focus:ring-blue-800"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col w-full gap-1">
|
||||
<label
|
||||
htmlFor="website"
|
||||
className="text-sm font-medium text-neutral-700 dark:text-neutral-300"
|
||||
>
|
||||
Website
|
||||
</label>
|
||||
<input
|
||||
name="website"
|
||||
type="url"
|
||||
{...register("website")}
|
||||
spellCheck={false}
|
||||
className="w-full px-3 bg-transparent rounded-lg h-9 border-neutral-300 placeholder:text-neutral-500 focus:border-blue-500 focus:ring focus:ring-blue-200 dark:border-neutral-700 dark:placeholder:text-neutral-400 dark:focus:ring-blue-800"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col w-full gap-1">
|
||||
<label
|
||||
htmlFor="banner"
|
||||
className="text-sm font-medium text-neutral-700 dark:text-neutral-300"
|
||||
>
|
||||
Cover
|
||||
</label>
|
||||
<input
|
||||
name="banner"
|
||||
type="url"
|
||||
{...register("banner")}
|
||||
spellCheck={false}
|
||||
className="w-full px-3 bg-transparent rounded-lg h-9 border-neutral-300 placeholder:text-neutral-500 focus:border-blue-500 focus:ring focus:ring-blue-200 dark:border-neutral-700 dark:placeholder:text-neutral-400 dark:focus:ring-blue-800"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col w-full gap-1">
|
||||
<label
|
||||
htmlFor="nip05"
|
||||
className="text-sm font-medium text-neutral-700 dark:text-neutral-300"
|
||||
>
|
||||
NIP-05
|
||||
</label>
|
||||
<input
|
||||
name="nip05"
|
||||
type="email"
|
||||
{...register("nip05")}
|
||||
spellCheck={false}
|
||||
className="w-full px-3 bg-transparent rounded-lg h-9 border-neutral-300 placeholder:text-neutral-500 focus:border-blue-500 focus:ring focus:ring-blue-200 dark:border-neutral-700 dark:placeholder:text-neutral-400 dark:focus:ring-blue-800"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col w-full gap-1">
|
||||
<label
|
||||
htmlFor="lnaddress"
|
||||
className="text-sm font-medium text-neutral-700 dark:text-neutral-300"
|
||||
>
|
||||
Lightning Address
|
||||
</label>
|
||||
<input
|
||||
name="lnaddress"
|
||||
type="email"
|
||||
{...register("lud16")}
|
||||
className="w-full px-3 bg-transparent rounded-lg h-9 border-neutral-300 placeholder:text-neutral-500 focus:border-blue-500 focus:ring focus:ring-blue-200 dark:border-neutral-700 dark:placeholder:text-neutral-400 dark:focus:ring-blue-800"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center justify-end">
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isPending}
|
||||
className="inline-flex items-center justify-center w-32 px-2 text-sm font-medium text-white bg-blue-500 rounded-lg h-9 hover:bg-blue-600 disabled:opacity-50"
|
||||
>
|
||||
{isPending ? <Spinner className="size-4" /> : "Update Profile"}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function PrivkeyButton() {
|
||||
const { id } = Route.useParams();
|
||||
|
||||
const [isPending, startTransition] = useTransition();
|
||||
const [isCopy, setIsCopy] = useState(false);
|
||||
|
||||
const copyPrivateKey = () => {
|
||||
startTransition(async () => {
|
||||
const res = await commands.getPrivateKey(id);
|
||||
|
||||
if (res.status === "ok") {
|
||||
await writeText(res.data);
|
||||
setIsCopy(true);
|
||||
} else {
|
||||
await message(res.error, { kind: "error" });
|
||||
return;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => copyPrivateKey()}
|
||||
className="inline-flex items-center justify-center px-3 text-sm font-medium text-blue-500 bg-blue-100 border border-blue-300 rounded-full h-7 hover:bg-blue-200 dark:bg-blue-900 dark:border-blue-800 dark:text-blue-300 dark:hover:bg-blue-800"
|
||||
>
|
||||
{isPending ? (
|
||||
<Spinner className="size-4" />
|
||||
) : isCopy ? (
|
||||
"Copied"
|
||||
) : (
|
||||
"Copy Private Key"
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
function AvatarUploader({
|
||||
setPicture,
|
||||
children,
|
||||
className,
|
||||
}: {
|
||||
setPicture: Dispatch<SetStateAction<string>>;
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
}) {
|
||||
const [isPending, startTransition] = useTransition();
|
||||
|
||||
const uploadAvatar = () => {
|
||||
startTransition(async () => {
|
||||
try {
|
||||
const image = await upload();
|
||||
setPicture(image);
|
||||
} catch (e) {
|
||||
await message(String(e));
|
||||
return;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => uploadAvatar()}
|
||||
className={cn("size-4", className)}
|
||||
>
|
||||
{isPending ? <Spinner className="size-4" /> : children}
|
||||
</button>
|
||||
);
|
||||
}
|
@ -97,21 +97,12 @@ export const LumeWindow = {
|
||||
});
|
||||
},
|
||||
openEditor: async (reply_to?: string, quote?: string) => {
|
||||
let url: string;
|
||||
|
||||
if (reply_to) {
|
||||
url = `/new-post?reply_to=${reply_to}`;
|
||||
}
|
||||
|
||||
if (quote?.length) {
|
||||
url = `/new-post?quote=${quote}`;
|
||||
}
|
||||
|
||||
if (!reply_to?.length && !quote?.length) {
|
||||
url = "/new-post";
|
||||
}
|
||||
|
||||
const label = `editor-${reply_to ? reply_to : 0}`;
|
||||
const url = reply_to
|
||||
? `/new-post?reply_to=${reply_to}`
|
||||
: quote?.length
|
||||
? `/new-post?quote=${quote}`
|
||||
: "/new-post";
|
||||
const query = await commands.openWindow({
|
||||
label,
|
||||
url,
|
||||
@ -145,7 +136,7 @@ export const LumeWindow = {
|
||||
hidden_title: true,
|
||||
closable: true,
|
||||
});
|
||||
} else {
|
||||
} else if (account) {
|
||||
await LumeWindow.openSettings(account, "wallet");
|
||||
}
|
||||
},
|
||||
|
Loading…
x
Reference in New Issue
Block a user