mirror of
https://github.com/luminous-devs/lume.git
synced 2024-09-29 16:30:55 +00:00
feat: migrate settings screen to i18n
This commit is contained in:
parent
cfda9ba899
commit
23482531c5
@ -59,15 +59,6 @@ export default function Router() {
|
||||
return { Component: ProfileSettingScreen };
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "edit-contact",
|
||||
async lazy() {
|
||||
const { EditContactScreen } = await import(
|
||||
"./routes/settings/editContact"
|
||||
);
|
||||
return { Component: EditContactScreen };
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "backup",
|
||||
async lazy() {
|
||||
|
@ -2,10 +2,12 @@ import { getVersion } from "@tauri-apps/api/app";
|
||||
import { relaunch } from "@tauri-apps/plugin-process";
|
||||
import { Update, check } from "@tauri-apps/plugin-updater";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Link } from "react-router-dom";
|
||||
import { toast } from "sonner";
|
||||
|
||||
export function AboutScreen() {
|
||||
const [t] = useTranslation();
|
||||
const [version, setVersion] = useState("");
|
||||
const [newUpdate, setNewUpdate] = useState<Update>(null);
|
||||
|
||||
@ -34,7 +36,7 @@ export function AboutScreen() {
|
||||
<div className="flex flex-col items-center">
|
||||
<h1 className="leading-tight text-xl font-semibold">Lume</h1>
|
||||
<p className="text-sm font-medium text-neutral-700 dark:text-neutral-300">
|
||||
Version {version}
|
||||
{t("settings.about.version")} {version}
|
||||
</p>
|
||||
</div>
|
||||
<div className="mx-auto mt-4 flex w-full max-w-xs flex-col gap-2">
|
||||
@ -44,7 +46,7 @@ export function AboutScreen() {
|
||||
onClick={() => checkUpdate()}
|
||||
className="inline-flex h-9 w-full items-center justify-center rounded-lg bg-blue-500 text-sm font-medium text-white hover:bg-blue-600"
|
||||
>
|
||||
Check for update
|
||||
{t("settings.about.checkUpdate")}
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
@ -52,7 +54,7 @@ export function AboutScreen() {
|
||||
onClick={() => installUpdate()}
|
||||
className="inline-flex h-9 w-full items-center justify-center rounded-lg bg-blue-500 text-sm font-medium text-white hover:bg-blue-600"
|
||||
>
|
||||
Install {newUpdate.version}
|
||||
{t("settings.about.installUpdate")} {newUpdate.version}
|
||||
</button>
|
||||
)}
|
||||
<Link
|
||||
|
@ -1,7 +1,9 @@
|
||||
import { useStorage } from "@lume/storage";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export function AdvancedSettingScreen() {
|
||||
const storage = useStorage();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const clearCache = async () => {
|
||||
await storage.clearCache();
|
||||
@ -13,16 +15,18 @@ export function AdvancedSettingScreen() {
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<div className="flex items-center gap-8">
|
||||
<div className="w-24 shrink-0 text-end text-sm font-semibold">
|
||||
Cache
|
||||
{t("settings.advanced.cache.title")}
|
||||
</div>
|
||||
<div className="text-sm">
|
||||
{t("settings.advanced.cache.subtitle")}
|
||||
</div>
|
||||
<div className="text-sm">Use for boost up nostr connection</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => clearCache()}
|
||||
className="h-8 w-max rounded-lg px-3 text-sm font-semibold text-blue-500 bg-blue-100 hover:bg-blue-200"
|
||||
>
|
||||
Clear
|
||||
{t("settings.advanced.cache.button")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -3,11 +3,13 @@ import { EyeOffIcon } from "@lume/icons";
|
||||
import { useStorage } from "@lume/storage";
|
||||
import { nip19 } from "nostr-tools";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export function BackupSettingScreen() {
|
||||
const ark = useArk();
|
||||
const storage = useStorage();
|
||||
|
||||
const [t] = useTranslation();
|
||||
const [privkey, setPrivkey] = useState(null);
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
|
||||
@ -29,7 +31,9 @@ export function BackupSettingScreen() {
|
||||
<div>
|
||||
{privkey ? (
|
||||
<div>
|
||||
<div className="mb-2 text-sm font-semibold">Private key</div>
|
||||
<div className="mb-2 text-sm font-semibold">
|
||||
{t("settings.backup.privkey.title")}
|
||||
</div>
|
||||
<div className="relative">
|
||||
<input
|
||||
readOnly
|
||||
@ -50,7 +54,7 @@ export function BackupSettingScreen() {
|
||||
onClick={() => removePrivkey()}
|
||||
className="mt-2 inline-flex h-11 w-full items-center justify-center gap-2 rounded-lg bg-red-200 dark:bg-red-800 px-6 font-medium text-red-500 hover:bg-red-500 hover:text-white focus:outline-none dark:hover:text-white"
|
||||
>
|
||||
Remove private key
|
||||
{t("settings.backup.privkey.button")}
|
||||
</button>
|
||||
</div>
|
||||
) : null}
|
||||
|
@ -1,10 +1,13 @@
|
||||
import { useArk } from "@lume/ark";
|
||||
import { LoaderIcon } from "@lume/icons";
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { toast } from "sonner";
|
||||
|
||||
export function AvatarUpload({ setPicture }) {
|
||||
const ark = useArk();
|
||||
|
||||
const [t] = useTranslation();
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const upload = async () => {
|
||||
@ -36,7 +39,7 @@ export function AvatarUpload({ setPicture }) {
|
||||
{loading ? (
|
||||
<LoaderIcon className="size-4 animate-spin" />
|
||||
) : (
|
||||
"Change avatar"
|
||||
t("user.avatarButton")
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
|
@ -1,45 +0,0 @@
|
||||
import { useArk } from "@lume/ark";
|
||||
import { EditIcon, LoaderIcon } from "@lume/icons";
|
||||
import { compactNumber } from "@lume/utils";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
export function ContactCard() {
|
||||
const ark = useArk();
|
||||
const { status, data } = useQuery({
|
||||
queryKey: ["contacts"],
|
||||
queryFn: async () => {
|
||||
const contacts = await ark.getUserContacts();
|
||||
return contacts;
|
||||
},
|
||||
refetchOnWindowFocus: false,
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="col-span-1 h-44 rounded-2xl bg-neutral-100 transition-all duration-150 ease-smooth hover:scale-105 dark:bg-neutral-900">
|
||||
{status === "pending" ? (
|
||||
<div className="flex h-full w-full items-center justify-center">
|
||||
<LoaderIcon className="h-4 w-4 animate-spin" />
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex h-full w-full flex-col justify-between p-4">
|
||||
<h3 className="pt-1 text-5xl font-semibold tabular-nums text-neutral-900 dark:text-neutral-100">
|
||||
{compactNumber.format(data.length)}
|
||||
</h3>
|
||||
<div className="mt-auto flex h-6 w-full items-center justify-between">
|
||||
<p className="text-xl font-medium leading-none text-neutral-600 dark:text-neutral-400">
|
||||
Contacts
|
||||
</p>
|
||||
<Link
|
||||
to="/settings/edit-contact"
|
||||
className="inline-flex h-6 w-max items-center gap-1 rounded-full bg-neutral-200 px-2.5 text-sm font-medium hover:bg-neutral-300 dark:bg-neutral-800 dark:hover:bg-neutral-700"
|
||||
>
|
||||
<EditIcon className="h-3 w-3" />
|
||||
Edit
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
@ -1,10 +1,13 @@
|
||||
import { useArk } from "@lume/ark";
|
||||
import { LoaderIcon } from "@lume/icons";
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { toast } from "sonner";
|
||||
|
||||
export function CoverUpload({ setBanner }) {
|
||||
const ark = useArk();
|
||||
|
||||
const [t] = useTranslation();
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const upload = async () => {
|
||||
@ -36,7 +39,7 @@ export function CoverUpload({ setBanner }) {
|
||||
{loading ? (
|
||||
<LoaderIcon className="size-4 animate-spin" />
|
||||
) : (
|
||||
"Change cover"
|
||||
t("user.coverButton")
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
|
@ -1,60 +0,0 @@
|
||||
import { useArk } from "@lume/ark";
|
||||
import { LoaderIcon } from "@lume/icons";
|
||||
import { compactNumber } from "@lume/utils";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { fetch } from "@tauri-apps/plugin-http";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
export function PostCard() {
|
||||
const ark = useArk();
|
||||
const { status, data } = useQuery({
|
||||
queryKey: ["user-stats", ark.account.pubkey],
|
||||
queryFn: async ({ signal }: { signal: AbortSignal }) => {
|
||||
const res = await fetch(
|
||||
`https://api.nostr.band/v0/stats/profile/${ark.account.pubkey}`,
|
||||
{
|
||||
signal,
|
||||
},
|
||||
);
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error("Error");
|
||||
}
|
||||
|
||||
return await res.json();
|
||||
},
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnMount: false,
|
||||
refetchOnReconnect: false,
|
||||
staleTime: Infinity,
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="col-span-1 h-44 rounded-2xl bg-neutral-100 transition-all duration-150 ease-smooth hover:scale-105 dark:bg-neutral-900">
|
||||
{status === "pending" ? (
|
||||
<div className="flex h-full w-full items-center justify-center">
|
||||
<LoaderIcon className="h-4 w-4 animate-spin" />
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex h-full w-full flex-col justify-between p-4">
|
||||
<h3 className="pt-1 text-5xl font-semibold tabular-nums text-neutral-900 dark:text-neutral-100">
|
||||
{compactNumber.format(
|
||||
data.stats[ark.account.pubkey].pub_note_count,
|
||||
)}
|
||||
</h3>
|
||||
<div className="mt-auto flex h-6 w-full items-center justify-between">
|
||||
<p className="text-xl font-medium leading-none text-neutral-600 dark:text-neutral-400">
|
||||
Posts
|
||||
</p>
|
||||
<Link
|
||||
to={`/users/${ark.account.pubkey}`}
|
||||
className="inline-flex h-6 w-max items-center gap-1 rounded-full bg-neutral-200 px-2.5 text-sm font-medium hover:bg-neutral-300 dark:bg-neutral-800 dark:hover:bg-neutral-700"
|
||||
>
|
||||
View
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
@ -1,77 +0,0 @@
|
||||
import { useArk, useProfile } from "@lume/ark";
|
||||
import { EditIcon, LoaderIcon } from "@lume/icons";
|
||||
import { displayNpub } from "@lume/utils";
|
||||
import * as Avatar from "@radix-ui/react-avatar";
|
||||
import { writeText } from "@tauri-apps/plugin-clipboard-manager";
|
||||
import { minidenticon } from "minidenticons";
|
||||
import { nip19 } from "nostr-tools";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
export function ProfileCard() {
|
||||
const ark = useArk();
|
||||
const svgURI = `data:image/svg+xml;utf8,${encodeURIComponent(
|
||||
minidenticon(ark.account.pubkey, 90, 50),
|
||||
)}`;
|
||||
|
||||
const { isLoading, user } = useProfile(ark.account.pubkey);
|
||||
|
||||
const copyNpub = async () => {
|
||||
return await writeText(nip19.npubEncode(ark.account.pubkey));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mb-4 h-56 w-full rounded-2xl bg-neutral-100 transition-all duration-150 ease-smooth hover:scale-105 dark:bg-neutral-900">
|
||||
{isLoading ? (
|
||||
<div className="flex h-full w-full items-center justify-center">
|
||||
<LoaderIcon className="h-4 w-4 animate-spin" />
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex h-full w-full flex-col justify-between p-4">
|
||||
<div className="flex h-10 w-full justify-end gap-3">
|
||||
<button
|
||||
type="button"
|
||||
onClick={copyNpub}
|
||||
className="inline-flex h-8 w-28 transform items-center justify-center gap-1.5 rounded-full bg-neutral-200 text-sm font-medium hover:bg-neutral-400 active:translate-y-1 dark:bg-neutral-800 dark:hover:bg-neutral-600"
|
||||
>
|
||||
Copy NPUB
|
||||
</button>
|
||||
<Link
|
||||
to="/settings/edit-profile"
|
||||
className="inline-flex h-8 w-20 items-center justify-center gap-1.5 rounded-full bg-neutral-200 text-sm font-medium hover:bg-neutral-400 dark:bg-neutral-800 dark:hover:bg-neutral-600"
|
||||
>
|
||||
<EditIcon className="h-4 w-4" />
|
||||
Edit
|
||||
</Link>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2.5">
|
||||
<Avatar.Root className="shrink-0">
|
||||
<Avatar.Image
|
||||
src={user?.picture || user?.image}
|
||||
alt={ark.account.pubkey}
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
style={{ contentVisibility: "auto" }}
|
||||
className="h-16 w-16 rounded-xl border border-neutral-200/50 shadow-[rgba(17,_17,_26,_0.1)_0px_0px_16px] dark:border-neutral-800/50"
|
||||
/>
|
||||
<Avatar.Fallback delayMs={300}>
|
||||
<img
|
||||
src={svgURI}
|
||||
alt={ark.account.pubkey}
|
||||
className="h-16 w-16 rounded-xl bg-black dark:bg-white"
|
||||
/>
|
||||
</Avatar.Fallback>
|
||||
</Avatar.Root>
|
||||
<div>
|
||||
<h3 className="text-3xl font-semibold leading-8 text-neutral-900 dark:text-neutral-100">
|
||||
{user?.display_name || user?.name}
|
||||
</h3>
|
||||
<p className="text-lg text-neutral-700 dark:text-neutral-300">
|
||||
{user?.nip05 || displayNpub(ark.account.pubkey, 16)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
@ -1,46 +0,0 @@
|
||||
import { useArk } from "@lume/ark";
|
||||
import { EditIcon, LoaderIcon } from "@lume/icons";
|
||||
import { compactNumber } from "@lume/utils";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
export function RelayCard() {
|
||||
const ark = useArk();
|
||||
|
||||
const { status, data } = useQuery({
|
||||
queryKey: ["relays", ark.account.pubkey],
|
||||
queryFn: async () => {
|
||||
const relays = await ark.getUserRelays({});
|
||||
return relays;
|
||||
},
|
||||
refetchOnWindowFocus: false,
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="col-span-1 h-44 rounded-2xl bg-neutral-100 transition-all duration-150 ease-smooth hover:scale-105 dark:bg-neutral-900">
|
||||
{status === "pending" ? (
|
||||
<div className="flex h-full w-full items-center justify-center">
|
||||
<LoaderIcon className="h-4 w-4 animate-spin" />
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex h-full w-full flex-col justify-between p-4">
|
||||
<h3 className="pt-1 text-5xl font-semibold tabular-nums text-neutral-900 dark:text-neutral-100">
|
||||
{compactNumber.format(data?.relays?.length || 0)}
|
||||
</h3>
|
||||
<div className="mt-auto flex h-6 w-full items-center justify-between">
|
||||
<p className="text-xl font-medium leading-none text-neutral-600 dark:text-neutral-400">
|
||||
Relays
|
||||
</p>
|
||||
<Link
|
||||
to="/relays"
|
||||
className="inline-flex h-6 w-max items-center gap-1 rounded-full bg-neutral-200 px-2.5 text-sm font-medium hover:bg-neutral-300 dark:bg-neutral-800 dark:hover:bg-neutral-700"
|
||||
>
|
||||
<EditIcon className="h-3 w-3" />
|
||||
Edit
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
@ -1,62 +0,0 @@
|
||||
import { useArk } from "@lume/ark";
|
||||
import { LoaderIcon } from "@lume/icons";
|
||||
import { useStorage } from "@lume/storage";
|
||||
import { useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
export function NWCForm({ setWalletConnectURL }) {
|
||||
const ark = useArk();
|
||||
const storage = useStorage();
|
||||
|
||||
const [uri, setUri] = useState("");
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const submit = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
|
||||
if (!uri.startsWith("nostr+walletconnect:")) {
|
||||
toast.error(
|
||||
"Connect URI is required and must start with format nostr+walletconnect:, please check again",
|
||||
);
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const uriObj = new URL(uri);
|
||||
const params = new URLSearchParams(uriObj.search);
|
||||
|
||||
if (params.has("relay") && params.has("secret")) {
|
||||
await storage.createPrivkey(`${ark.account.pubkey}-nwc`, uri);
|
||||
setWalletConnectURL(uri);
|
||||
setLoading(false);
|
||||
} else {
|
||||
setLoading(false);
|
||||
toast.error("Connect URI is not valid, please check again");
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
setLoading(false);
|
||||
toast.error(String(e));
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-3 rounded-xl bg-neutral-100 p-3 dark:bg-neutral-900">
|
||||
<textarea
|
||||
name="walletConnectURL"
|
||||
value={uri}
|
||||
onChange={(e) => setUri(e.target.value)}
|
||||
placeholder="nostr+walletconnect://"
|
||||
className="h-40 w-full resize-none rounded-lg border-transparent bg-neutral-200 px-3 py-3 text-neutral-900 !outline-none placeholder:text-neutral-600 focus:border-blue-500 focus:ring focus:ring-blue-200 dark:focus:ring-blue-800 dark:bg-neutral-800 dark:text-neutral-100 dark:placeholder:text-neutral-400"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={submit}
|
||||
className="inline-flex h-11 w-full items-center justify-center rounded-lg bg-blue-500 font-medium text-white hover:bg-blue-600"
|
||||
>
|
||||
{loading ? <LoaderIcon className="h-4 w-4 animate-spin" /> : "Connect"}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -1,51 +0,0 @@
|
||||
import { useArk } from "@lume/ark";
|
||||
import { LoaderIcon } from "@lume/icons";
|
||||
import { compactNumber } from "@lume/utils";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { fetch } from "@tauri-apps/plugin-http";
|
||||
|
||||
export function ZapCard() {
|
||||
const ark = useArk();
|
||||
const { status, data } = useQuery({
|
||||
queryKey: ["user-stats", ark.account.pubkey],
|
||||
queryFn: async ({ signal }: { signal: AbortSignal }) => {
|
||||
const res = await fetch(
|
||||
`https://api.nostr.band/v0/stats/profile/${ark.account.pubkey}`,
|
||||
{
|
||||
signal,
|
||||
},
|
||||
);
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error("Error");
|
||||
}
|
||||
|
||||
return await res.json();
|
||||
},
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnMount: false,
|
||||
refetchOnReconnect: false,
|
||||
staleTime: Infinity,
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="col-span-1 h-44 rounded-2xl bg-neutral-100 transition-all duration-150 ease-smooth hover:scale-105 dark:bg-neutral-900">
|
||||
{status === "pending" ? (
|
||||
<div className="flex h-full w-full items-center justify-center">
|
||||
<LoaderIcon className="h-4 w-4 animate-spin" />
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex h-full w-full flex-col justify-between p-4">
|
||||
<h3 className="pt-1 text-5xl font-semibold tabular-nums text-neutral-900 dark:text-neutral-100">
|
||||
{compactNumber.format(
|
||||
data?.stats[ark.account.pubkey]?.zaps_received?.msats / 1000 || 0,
|
||||
)}
|
||||
</h3>
|
||||
<div className="mt-auto flex h-6 items-center text-xl font-medium leading-none text-neutral-600 dark:text-neutral-400">
|
||||
Sats received
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
@ -1,34 +0,0 @@
|
||||
import { useArk } from "@lume/ark";
|
||||
import { LoaderIcon } from "@lume/icons";
|
||||
import { User } from "@lume/ui";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
|
||||
export function EditContactScreen() {
|
||||
const ark = useArk();
|
||||
const { status, data } = useQuery({
|
||||
queryKey: ["contacts"],
|
||||
queryFn: async () => {
|
||||
return await ark.getUserContacts();
|
||||
},
|
||||
refetchOnWindowFocus: false,
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="mx-auto flex w-full max-w-xl flex-col gap-3">
|
||||
{status === "pending" ? (
|
||||
<div className="flex h-10 w-full items-center justify-center">
|
||||
<LoaderIcon className="h-4 w-4 animate-spin" />
|
||||
</div>
|
||||
) : (
|
||||
data.map((item) => (
|
||||
<div
|
||||
key={item}
|
||||
className="flex h-16 w-full items-center justify-between rounded-xl bg-neutral-100 px-2.5 dark:bg-neutral-900"
|
||||
>
|
||||
<User pubkey={item} variant="simple" />
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
@ -10,20 +10,17 @@ import {
|
||||
requestPermission,
|
||||
} from "@tauri-apps/plugin-notification";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export function GeneralSettingScreen() {
|
||||
const storage = useStorage();
|
||||
|
||||
const [t] = useTranslation();
|
||||
const [apiKey, setAPIKey] = useState("");
|
||||
const [settings, setSettings] = useState({
|
||||
lowPower: false,
|
||||
autoupdate: false,
|
||||
...storage.settings,
|
||||
notification: false,
|
||||
autolaunch: false,
|
||||
outbox: false,
|
||||
media: true,
|
||||
hashtag: true,
|
||||
notification: true,
|
||||
translation: false,
|
||||
appearance: "system",
|
||||
});
|
||||
|
||||
@ -100,47 +97,6 @@ export function GeneralSettingScreen() {
|
||||
|
||||
const permissionGranted = await isPermissionGranted();
|
||||
setSettings((prev) => ({ ...prev, notification: permissionGranted }));
|
||||
|
||||
const data = await storage.getAllSettings();
|
||||
if (!data) return;
|
||||
|
||||
for (const item of data) {
|
||||
if (item.key === "autoupdate")
|
||||
setSettings((prev) => ({
|
||||
...prev,
|
||||
autoupdate: !!parseInt(item.value),
|
||||
}));
|
||||
|
||||
if (item.key === "lowPower")
|
||||
setSettings((prev) => ({
|
||||
...prev,
|
||||
lowPower: !!parseInt(item.value),
|
||||
}));
|
||||
|
||||
if (item.key === "outbox")
|
||||
setSettings((prev) => ({
|
||||
...prev,
|
||||
outbox: !!parseInt(item.value),
|
||||
}));
|
||||
|
||||
if (item.key === "media")
|
||||
setSettings((prev) => ({
|
||||
...prev,
|
||||
media: !!parseInt(item.value),
|
||||
}));
|
||||
|
||||
if (item.key === "hashtag")
|
||||
setSettings((prev) => ({
|
||||
...prev,
|
||||
hashtag: !!parseInt(item.value),
|
||||
}));
|
||||
|
||||
if (item.key === "translation")
|
||||
setSettings((prev) => ({
|
||||
...prev,
|
||||
translation: !!parseInt(item.value),
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
loadSettings();
|
||||
@ -152,9 +108,11 @@ export function GeneralSettingScreen() {
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<div className="flex items-center gap-8">
|
||||
<div className="w-36 shrink-0 text-end text-sm font-semibold">
|
||||
Update
|
||||
{t("settings.general.update.title")}
|
||||
</div>
|
||||
<div className="text-sm">
|
||||
{t("settings.general.update.subtitle")}
|
||||
</div>
|
||||
<div className="text-sm">Automatically download new update</div>
|
||||
</div>
|
||||
<Switch.Root
|
||||
checked={settings.autoupdate}
|
||||
@ -167,10 +125,10 @@ export function GeneralSettingScreen() {
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<div className="flex items-center gap-8">
|
||||
<div className="w-36 shrink-0 text-end text-sm font-semibold">
|
||||
Low Power
|
||||
{t("settings.general.lowPower.title")}
|
||||
</div>
|
||||
<div className="text-sm">
|
||||
Sustainable for low network environment.
|
||||
{t("settings.general.lowPower.subtitle")}
|
||||
</div>
|
||||
</div>
|
||||
<Switch.Root
|
||||
@ -184,9 +142,11 @@ export function GeneralSettingScreen() {
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<div className="flex items-center gap-8">
|
||||
<div className="w-36 shrink-0 text-end text-sm font-semibold">
|
||||
Startup
|
||||
{t("settings.general.startup.title")}
|
||||
</div>
|
||||
<div className="text-sm">
|
||||
{t("settings.general.startup.subtitle")}
|
||||
</div>
|
||||
<div className="text-sm">Launch Lume at Login</div>
|
||||
</div>
|
||||
<Switch.Root
|
||||
checked={settings.autolaunch}
|
||||
@ -199,9 +159,11 @@ export function GeneralSettingScreen() {
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<div className="flex items-center gap-8">
|
||||
<div className="w-36 shrink-0 text-end text-sm font-semibold">
|
||||
Media
|
||||
{t("settings.general.media.title")}
|
||||
</div>
|
||||
<div className="text-sm">
|
||||
{t("settings.general.media.subtitle")}
|
||||
</div>
|
||||
<div className="text-sm">Automatically load media</div>
|
||||
</div>
|
||||
<Switch.Root
|
||||
checked={settings.media}
|
||||
@ -214,9 +176,11 @@ export function GeneralSettingScreen() {
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<div className="flex items-center gap-8">
|
||||
<div className="w-36 shrink-0 text-end text-sm font-semibold">
|
||||
Hashtag
|
||||
{t("settings.general.hashtag.title")}
|
||||
</div>
|
||||
<div className="text-sm">
|
||||
{t("settings.general.hashtag.subtitle")}
|
||||
</div>
|
||||
<div className="text-sm">Show all hashtags in content</div>
|
||||
</div>
|
||||
<Switch.Root
|
||||
checked={settings.hashtag}
|
||||
@ -229,9 +193,11 @@ export function GeneralSettingScreen() {
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<div className="flex items-center gap-8">
|
||||
<div className="w-36 shrink-0 text-end text-sm font-semibold">
|
||||
Notification
|
||||
{t("settings.general.notification.title")}
|
||||
</div>
|
||||
<div className="text-sm">
|
||||
{t("settings.general.notification.subtitle")}
|
||||
</div>
|
||||
<div className="text-sm">Automatically send notification</div>
|
||||
</div>
|
||||
<Switch.Root
|
||||
checked={settings.notification}
|
||||
@ -245,9 +211,11 @@ export function GeneralSettingScreen() {
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<div className="flex items-center gap-8">
|
||||
<div className="w-36 shrink-0 text-end text-sm font-semibold">
|
||||
Translation
|
||||
{t("settings.general.translation.title")}
|
||||
</div>
|
||||
<div className="text-sm">
|
||||
{t("settings.general.translation.subtitle")}
|
||||
</div>
|
||||
<div className="text-sm">Translate text to your language</div>
|
||||
</div>
|
||||
<Switch.Root
|
||||
checked={settings.translation}
|
||||
@ -260,7 +228,7 @@ export function GeneralSettingScreen() {
|
||||
{settings.translation ? (
|
||||
<div className="flex w-full items-center gap-8">
|
||||
<div className="w-36 shrink-0 text-end text-sm font-semibold">
|
||||
API Key
|
||||
{t("global.apiKey")}
|
||||
</div>
|
||||
<div className="relative w-full">
|
||||
<input
|
||||
@ -276,7 +244,7 @@ export function GeneralSettingScreen() {
|
||||
onClick={saveApi}
|
||||
className="mr-1 h-7 w-16 text-sm font-medium shrink-0 inline-flex items-center justify-center rounded-md bg-neutral-200 dark:bg-neutral-800 hover:bg-neutral-300 dark:hover:bg-neutral-700"
|
||||
>
|
||||
Save
|
||||
{t("global.save")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@ -284,7 +252,7 @@ export function GeneralSettingScreen() {
|
||||
) : null}
|
||||
<div className="flex w-full items-start gap-8">
|
||||
<div className="w-36 shrink-0 text-end text-sm font-semibold">
|
||||
Appearance
|
||||
{t("settings.general.appearance.title")}
|
||||
</div>
|
||||
<div className="flex flex-1 gap-6">
|
||||
<button
|
||||
@ -303,7 +271,7 @@ export function GeneralSettingScreen() {
|
||||
<LightIcon className="h-5 w-5" />
|
||||
</div>
|
||||
<p className="text-sm font-medium text-neutral-700 dark:text-neutral-300">
|
||||
Light
|
||||
{t("settings.general.appearance.light")}
|
||||
</p>
|
||||
</button>
|
||||
<button
|
||||
@ -322,7 +290,7 @@ export function GeneralSettingScreen() {
|
||||
<DarkIcon className="h-5 w-5" />
|
||||
</div>
|
||||
<p className="text-sm font-medium text-neutral-700 dark:text-neutral-300">
|
||||
Dark
|
||||
{t("settings.general.appearance.dark")}
|
||||
</p>
|
||||
</button>
|
||||
<button
|
||||
@ -341,7 +309,7 @@ export function GeneralSettingScreen() {
|
||||
<SystemModeIcon className="h-5 w-5" />
|
||||
</div>
|
||||
<p className="text-sm font-medium text-neutral-700 dark:text-neutral-300">
|
||||
System
|
||||
{t("settings.general.appearance.system")}
|
||||
</p>
|
||||
</button>
|
||||
</div>
|
||||
|
@ -2,12 +2,14 @@ import { useArk } from "@lume/ark";
|
||||
import { useStorage } from "@lume/storage";
|
||||
import * as Switch from "@radix-ui/react-switch";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { toast } from "sonner";
|
||||
|
||||
export function NWCScreen() {
|
||||
const ark = useArk();
|
||||
const storage = useStorage();
|
||||
|
||||
const [t] = useTranslation();
|
||||
const [settings, setSettings] = useState({
|
||||
nwc: false,
|
||||
instantZap: storage.settings.instantZap,
|
||||
@ -74,7 +76,7 @@ export function NWCScreen() {
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<div className="flex w-full items-start gap-8">
|
||||
<div className="w-36 shrink-0 text-end text-sm font-semibold">
|
||||
Connection String
|
||||
{t("settings.zap.nwc")}
|
||||
</div>
|
||||
<div className="flex flex-col items-end gap-2 w-full">
|
||||
<textarea
|
||||
@ -89,7 +91,7 @@ export function NWCScreen() {
|
||||
onClick={saveNWC}
|
||||
className="h-8 w-16 text-sm font-medium shrink-0 inline-flex items-center justify-center rounded-md bg-neutral-200 dark:bg-neutral-800 hover:bg-neutral-300 dark:hover:bg-neutral-700"
|
||||
>
|
||||
Save
|
||||
{t("global.save")}
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
@ -97,7 +99,7 @@ export function NWCScreen() {
|
||||
onClick={remove}
|
||||
className="h-8 w-16 text-sm font-medium shrink-0 inline-flex items-center justify-center rounded-md bg-neutral-200 dark:bg-neutral-800 hover:bg-neutral-300 dark:hover:bg-neutral-700"
|
||||
>
|
||||
Remove
|
||||
{t("global.delete")}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
@ -108,10 +110,10 @@ export function NWCScreen() {
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<div className="flex items-center gap-8">
|
||||
<div className="w-36 shrink-0 text-end text-sm font-semibold">
|
||||
Instant Zap
|
||||
{t("settings.zap.instant.title")}
|
||||
</div>
|
||||
<div className="text-sm">
|
||||
Zap with default amount, no confirmation
|
||||
{t("settings.zap.instant.subtitle")}
|
||||
</div>
|
||||
</div>
|
||||
<Switch.Root
|
||||
@ -125,7 +127,7 @@ export function NWCScreen() {
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<div className="flex w-full items-center gap-8">
|
||||
<div className="w-36 shrink-0 text-end text-sm font-semibold">
|
||||
Default amount
|
||||
{t("settings.zap.defaultAmount")}
|
||||
</div>
|
||||
<div className="relative w-full">
|
||||
<input
|
||||
@ -141,7 +143,7 @@ export function NWCScreen() {
|
||||
onClick={saveAmount}
|
||||
className="mr-1 h-7 w-16 text-sm font-medium shrink-0 inline-flex items-center justify-center rounded-md bg-neutral-200 dark:bg-neutral-800 hover:bg-neutral-300 dark:hover:bg-neutral-700"
|
||||
>
|
||||
Save
|
||||
{t("global.save")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,15 +1,11 @@
|
||||
import { useArk } from "@lume/ark";
|
||||
import {
|
||||
CheckCircleIcon,
|
||||
LoaderIcon,
|
||||
PlusIcon,
|
||||
UnverifiedIcon,
|
||||
} from "@lume/icons";
|
||||
import { CheckCircleIcon, LoaderIcon, UnverifiedIcon } from "@lume/icons";
|
||||
import { useStorage } from "@lume/storage";
|
||||
import { NDKKind, NDKUserProfile } from "@nostr-dev-kit/ndk";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { toast } from "sonner";
|
||||
import { AvatarUpload } from "./components/avatarUpload";
|
||||
import { CoverUpload } from "./components/coverUpload";
|
||||
@ -24,6 +20,7 @@ export function ProfileSettingScreen() {
|
||||
const [banner, setBanner] = useState("");
|
||||
const [nip05, setNIP05] = useState({ verified: true, text: "" });
|
||||
|
||||
const { t } = useTranslation();
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
@ -139,7 +136,7 @@ export function ProfileSettingScreen() {
|
||||
htmlFor="displayName"
|
||||
className="text-sm font-semibold uppercase tracking-wider"
|
||||
>
|
||||
Display Name
|
||||
{t("user.displayName")}
|
||||
</label>
|
||||
<input
|
||||
type={"text"}
|
||||
@ -153,7 +150,7 @@ export function ProfileSettingScreen() {
|
||||
htmlFor="name"
|
||||
className="text-sm font-semibold uppercase tracking-wider"
|
||||
>
|
||||
Name
|
||||
{t("user.name")}
|
||||
</label>
|
||||
<input
|
||||
type={"text"}
|
||||
@ -179,12 +176,12 @@ export function ProfileSettingScreen() {
|
||||
{nip05.verified ? (
|
||||
<span className="inline-flex h-6 items-center gap-1 rounded-full bg-teal-500 px-1 pr-1.5 text-xs font-medium text-white">
|
||||
<CheckCircleIcon className="h-4 w-4" />
|
||||
Verified
|
||||
{t("user.verified")}
|
||||
</span>
|
||||
) : (
|
||||
<span className="inline-flex h-6 items-center gap-1 rounded bg-red-500 pl-1 pr-1.5 text-xs font-medium text-white">
|
||||
<UnverifiedIcon className="h-4 w-4" />
|
||||
Unverified
|
||||
{t("user.unverified")}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
@ -200,7 +197,7 @@ export function ProfileSettingScreen() {
|
||||
htmlFor="website"
|
||||
className="text-sm font-semibold uppercase tracking-wider"
|
||||
>
|
||||
Website
|
||||
{t("user.website")}
|
||||
</label>
|
||||
<input
|
||||
type={"text"}
|
||||
@ -214,7 +211,7 @@ export function ProfileSettingScreen() {
|
||||
htmlFor="website"
|
||||
className="text-sm font-semibold uppercase tracking-wider"
|
||||
>
|
||||
Lightning address
|
||||
{t("user.lna")}
|
||||
</label>
|
||||
<input
|
||||
type={"text"}
|
||||
@ -228,7 +225,7 @@ export function ProfileSettingScreen() {
|
||||
htmlFor="about"
|
||||
className="text-sm font-semibold uppercase tracking-wider"
|
||||
>
|
||||
Bio
|
||||
{t("user.bio")}
|
||||
</label>
|
||||
<textarea
|
||||
{...register("about")}
|
||||
@ -245,7 +242,7 @@ export function ProfileSettingScreen() {
|
||||
{loading ? (
|
||||
<LoaderIcon className="size-4 animate-spin" />
|
||||
) : (
|
||||
"Update"
|
||||
t("global.update")
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
|
@ -7,9 +7,12 @@ import {
|
||||
ZapIcon,
|
||||
} from "@lume/icons";
|
||||
import { cn } from "@lume/utils";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { NavLink, Outlet } from "react-router-dom";
|
||||
|
||||
export function SettingsLayout() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div className="flex h-full min-h-0 w-full flex-col rounded-xl overflow-y-auto">
|
||||
<div className="flex h-24 shrink-0 w-full items-center justify-center px-2 bg-white/50 backdrop-blur-xl dark:bg-black/50">
|
||||
@ -27,7 +30,7 @@ export function SettingsLayout() {
|
||||
}
|
||||
>
|
||||
<SettingsIcon className="size-6" />
|
||||
<p className="text-sm font-medium">General</p>
|
||||
<p className="text-sm font-medium">{t("settings.general.title")}</p>
|
||||
</NavLink>
|
||||
<NavLink
|
||||
to="/settings/profile"
|
||||
@ -42,7 +45,7 @@ export function SettingsLayout() {
|
||||
}
|
||||
>
|
||||
<UserIcon className="size-6" />
|
||||
<p className="text-sm font-medium">User</p>
|
||||
<p className="text-sm font-medium">{t("settings.general.user")}</p>
|
||||
</NavLink>
|
||||
<NavLink
|
||||
to="/settings/nwc"
|
||||
@ -56,7 +59,7 @@ export function SettingsLayout() {
|
||||
}
|
||||
>
|
||||
<ZapIcon className="size-6" />
|
||||
<p className="text-sm font-medium">Zap</p>
|
||||
<p className="text-sm font-medium">{t("settings.zap.title")}</p>
|
||||
</NavLink>
|
||||
<NavLink
|
||||
to="/settings/backup"
|
||||
@ -70,7 +73,7 @@ export function SettingsLayout() {
|
||||
}
|
||||
>
|
||||
<SecureIcon className="size-6" />
|
||||
<p className="text-sm font-medium">Backup</p>
|
||||
<p className="text-sm font-medium">{t("settings.backup.title")}</p>
|
||||
</NavLink>
|
||||
<NavLink
|
||||
to="/settings/advanced"
|
||||
@ -84,7 +87,9 @@ export function SettingsLayout() {
|
||||
}
|
||||
>
|
||||
<AdvancedSettingsIcon className="size-6" />
|
||||
<p className="text-sm font-medium">Advanced</p>
|
||||
<p className="text-sm font-medium">
|
||||
{t("settings.advanced.title")}
|
||||
</p>
|
||||
</NavLink>
|
||||
<NavLink
|
||||
to="/settings/about"
|
||||
@ -98,7 +103,7 @@ export function SettingsLayout() {
|
||||
}
|
||||
>
|
||||
<InfoIcon className="size-6" />
|
||||
<p className="text-sm font-medium">About</p>
|
||||
<p className="text-sm font-medium">{t("settings.about.title")}</p>
|
||||
</NavLink>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -14,9 +14,11 @@
|
||||
"cancel": "Cancel",
|
||||
"save": "Save",
|
||||
"post": "Post",
|
||||
"update": "Update",
|
||||
"noResult": "No results found.",
|
||||
"emptyFeedTitle": "This feed is empty",
|
||||
"emptyFeedSubtitle": "You can follow more users to build up your timeline"
|
||||
"emptyFeedSubtitle": "You can follow more users to build up your timeline",
|
||||
"apiKey": "API Key"
|
||||
},
|
||||
"nip89": {
|
||||
"unsupported": "Lume isn't support this event",
|
||||
@ -63,6 +65,13 @@
|
||||
}
|
||||
},
|
||||
"user": {
|
||||
"displayName": "Display Name",
|
||||
"name": "Name",
|
||||
"bio": "Bio",
|
||||
"lna": "Lightning address",
|
||||
"website": "Website",
|
||||
"verified": "Verified",
|
||||
"unverified": "Unverified",
|
||||
"follow": "Follow",
|
||||
"unfollow": "Unfollow",
|
||||
"latestPosts": "Latest posts",
|
||||
@ -177,5 +186,77 @@
|
||||
"edit": "Edit Interest",
|
||||
"followAll": "Follow All",
|
||||
"unfollowAll": "Unfollow All"
|
||||
},
|
||||
"settings": {
|
||||
"general": {
|
||||
"title": "General",
|
||||
"update": {
|
||||
"title": "Update",
|
||||
"subtitle": "Automatically download new update"
|
||||
},
|
||||
"lowPower": {
|
||||
"title": "Low Power",
|
||||
"subtitle": "Sustainable for low network environment"
|
||||
},
|
||||
"startup": {
|
||||
"title": "Startup",
|
||||
"subtitle": "Launch Lume at Login"
|
||||
},
|
||||
"media": {
|
||||
"title": "Media",
|
||||
"subtitle": "Automatically load media"
|
||||
},
|
||||
"hashtag": {
|
||||
"title": "Hashtag",
|
||||
"subtitle": "Show all hashtags in content"
|
||||
},
|
||||
"notification": {
|
||||
"title": "Notification",
|
||||
"subtitle": "Automatically send notification"
|
||||
},
|
||||
"translation": {
|
||||
"title": "Translation",
|
||||
"subtitle": "Translate text to your language"
|
||||
},
|
||||
"appearance": {
|
||||
"title": "Appearance",
|
||||
"light": "Light",
|
||||
"dark": "Dark",
|
||||
"system": "System"
|
||||
}
|
||||
},
|
||||
"user": {
|
||||
"title": "User"
|
||||
},
|
||||
"zap": {
|
||||
"title": "Zap",
|
||||
"nwc": "Connection String"
|
||||
},
|
||||
"backup": {
|
||||
"title": "Backup",
|
||||
"privkey": {
|
||||
"title": "Private key",
|
||||
"button": "Remove private key"
|
||||
}
|
||||
},
|
||||
"advanced": {
|
||||
"title": "Advanced",
|
||||
"cache": {
|
||||
"title": "Cache",
|
||||
"subtitle": "Use for boost up nostr connection",
|
||||
"button": "Clear"
|
||||
},
|
||||
"instant": {
|
||||
"title": "Instant Zap",
|
||||
"subtitle": "Zap with default amount, no confirmation"
|
||||
},
|
||||
"defaultAmount": "Default amount"
|
||||
},
|
||||
"about": {
|
||||
"title": "About",
|
||||
"version": "Version",
|
||||
"checkUpdate": "Check for update",
|
||||
"installUpdate": "Install"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user