Merge pull request #151 from luminous-devs/feat/multi-lang

Add support for multi-languages
This commit is contained in:
Ren Amamiya 2024-01-30 09:12:06 +07:00 committed by GitHub
commit 711c1d561a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
90 changed files with 2048 additions and 1342 deletions

View File

@ -36,6 +36,8 @@
"@radix-ui/react-tooltip": "^1.0.7",
"@tanstack/react-query": "^5.17.19",
"framer-motion": "^10.18.0",
"i18next": "^23.8.0",
"i18next-resources-to-backend": "^1.2.0",
"jotai": "^2.6.3",
"minidenticons": "^4.2.0",
"nanoid": "^5.0.4",
@ -45,6 +47,7 @@
"react-currency-input-field": "^3.6.14",
"react-dom": "^18.2.0",
"react-hook-form": "^7.49.3",
"react-i18next": "^14.0.1",
"react-router-dom": "^6.21.3",
"smol-toml": "^1.1.4",
"sonner": "^1.3.1",

View File

@ -1,7 +1,9 @@
import { ColumnProvider, LumeProvider } from "@lume/ark";
import { StorageProvider } from "@lume/storage";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { I18nextProvider } from "react-i18next";
import { Toaster } from "sonner";
import i18n from "./i18n";
import Router from "./router";
const queryClient = new QueryClient({
@ -14,15 +16,17 @@ const queryClient = new QueryClient({
export default function App() {
return (
<QueryClientProvider client={queryClient}>
<Toaster position="top-center" theme="system" closeButton />
<StorageProvider>
<LumeProvider>
<ColumnProvider>
<Router />
</ColumnProvider>
</LumeProvider>
</StorageProvider>
</QueryClientProvider>
<I18nextProvider i18n={i18n} defaultNS={"translation"}>
<QueryClientProvider client={queryClient}>
<Toaster position="top-center" theme="system" closeButton />
<StorageProvider>
<LumeProvider>
<ColumnProvider>
<Router />
</ColumnProvider>
</LumeProvider>
</StorageProvider>
</QueryClientProvider>
</I18nextProvider>
);
}

26
apps/desktop/src/i18n.ts Normal file
View File

@ -0,0 +1,26 @@
import { resolveResource } from "@tauri-apps/api/path";
import { readTextFile } from "@tauri-apps/plugin-fs";
import { locale } from "@tauri-apps/plugin-os";
import i18n from "i18next";
import resourcesToBackend from "i18next-resources-to-backend";
import { initReactI18next } from "react-i18next";
const currentLocale = (await locale()).slice(0, 2);
i18n
.use(
resourcesToBackend(async (language: string) => {
const file_path = await resolveResource(`locales/${language}.json`);
return JSON.parse(await readTextFile(file_path));
}),
)
.use(initReactI18next)
.init({
lng: currentLocale,
fallbackLng: "en",
interpolation: {
escapeValue: false,
},
});
export default i18n;

View File

@ -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() {

View File

@ -1,8 +1,11 @@
import { User } from "@lume/ark";
import { NDKEvent } from "@nostr-dev-kit/ndk";
import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
export function ActivityRepost({ event }: { event: NDKEvent }) {
const { t } = useTranslation();
return (
<Link
to={`/activity/${event.id}`}
@ -14,7 +17,7 @@ export function ActivityRepost({ event }: { event: NDKEvent }) {
<User.Avatar className="size-8 rounded-lg shrink-0" />
<div className="inline-flex items-center gap-1.5">
<User.Name className="max-w-[8rem] font-semibold text-neutral-950 dark:text-neutral-50" />
<p className="shrink-0">reposted</p>
<p className="shrink-0">{t("activity.repost")}</p>
</div>
</div>
<User.Time

View File

@ -1,8 +1,11 @@
import { User } from "@lume/ark";
import { NDKEvent } from "@nostr-dev-kit/ndk";
import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
export function ActivityText({ event }: { event: NDKEvent }) {
const { t } = useTranslation();
return (
<Link
to={`/activity/${event.id}`}
@ -14,7 +17,7 @@ export function ActivityText({ event }: { event: NDKEvent }) {
<User.Avatar className="size-8 rounded-lg shrink-0" />
<div className="inline-flex items-center gap-1.5">
<User.Name className="max-w-[8rem] font-semibold text-neutral-950 dark:text-neutral-50" />
<p className="shrink-0">mention you</p>
<p className="shrink-0">{t("activity.mention")}</p>
</div>
</div>
<User.Time

View File

@ -1,9 +1,11 @@
import { User } from "@lume/ark";
import { compactNumber } from "@lume/utils";
import { NDKEvent, zapInvoiceFromEvent } from "@nostr-dev-kit/ndk";
import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
export function ActivityZap({ event }: { event: NDKEvent }) {
const { t } = useTranslation();
const invoice = zapInvoiceFromEvent(event);
return (
@ -18,7 +20,7 @@ export function ActivityZap({ event }: { event: NDKEvent }) {
<div className="inline-flex items-center gap-1.5">
<User.Name className="max-w-[8rem] font-semibold text-neutral-950 dark:text-neutral-50" />
<p className="shrink-0">
zapped {compactNumber.format(invoice.amount)} sats
{t("activity.zap")} {compactNumber.format(invoice.amount)} sats
</p>
</div>
</div>

View File

@ -4,6 +4,7 @@ import { FETCH_LIMIT } from "@lume/utils";
import { NDKEvent, NDKKind } from "@nostr-dev-kit/ndk";
import { useInfiniteQuery, useQueryClient } from "@tanstack/react-query";
import { useCallback, useMemo } from "react";
import { useTranslation } from "react-i18next";
import { ActivityRepost } from "./activityRepost";
import { ActivityText } from "./activityText";
import { ActivityZap } from "./activityZap";
@ -12,6 +13,7 @@ export function ActivityList() {
const ark = useArk();
const queryClient = useQueryClient();
const { t } = useTranslation();
const { data, hasNextPage, isLoading, isFetchingNextPage, fetchNextPage } =
useInfiniteQuery({
queryKey: ["activity"],
@ -86,7 +88,7 @@ export function ActivityList() {
) : !allEvents.length ? (
<div className="w-full h-full flex flex-col items-center justify-center">
<p className="mb-2 text-2xl">🎉</p>
<p className="text-center font-medium">Yo! Nothing new yet.</p>
<p className="text-center font-medium">{t("activity.empty")}</p>
</div>
) : (
allEvents.map((event) => renderEvenKind(event))
@ -104,7 +106,7 @@ export function ActivityList() {
) : (
<>
<ArrowRightCircleIcon className="size-5" />
Load more
{t("global.loadMore")}
</>
)}
</button>

View File

@ -1,16 +1,20 @@
import { User } from "@lume/ark";
import { NDKEvent } from "@nostr-dev-kit/ndk";
import { useTranslation } from "react-i18next";
import { ActivityRootNote } from "./rootNote";
export function ActivitySingleRepost({ event }: { event: NDKEvent }) {
const { t } = useTranslation();
const repostId = event.tags.find((el) => el[0] === "e")[1];
return (
<div className="pb-3 flex flex-col">
<div className="h-14 shrink-0 border-b border-neutral-100 dark:border-neutral-900 flex flex-col items-center justify-center px-3">
<h3 className="text-center font-semibold leading-tight">Boost</h3>
<h3 className="text-center font-semibold leading-tight">
{t("activity.boost")}
</h3>
<p className="text-sm text-blue-500 font-medium leading-tight">
@ Someone has reposted to your note
{t("activity.boostSubtitle")}
</p>
</div>
<div className="flex-1 min-h-0">
@ -22,7 +26,7 @@ export function ActivitySingleRepost({ event }: { event: NDKEvent }) {
</User.Provider>
<div className="flex flex-col items-center gap-3">
<div className="h-4 w-px bg-blue-500" />
<h3 className="font-semibold">Reposted</h3>
<h3 className="font-semibold capitalize">{t("activity.repost")}</h3>
<div className="h-4 w-px bg-blue-500" />
</div>
<ActivityRootNote eventId={repostId} />

View File

@ -1,5 +1,6 @@
import { Note, useArk } from "@lume/ark";
import { NDKEvent } from "@nostr-dev-kit/ndk";
import { useTranslation } from "react-i18next";
import { ActivityRootNote } from "./rootNote";
export function ActivitySingleText({ event }: { event: NDKEvent }) {
@ -9,14 +10,16 @@ export function ActivitySingleText({ event }: { event: NDKEvent }) {
tags: event.tags,
});
const { t } = useTranslation();
return (
<div className="h-full w-full flex flex-col justify-between">
<div className="h-14 border-b border-neutral-100 dark:border-neutral-900 flex flex-col items-center justify-center px-3">
<h3 className="text-center font-semibold leading-tight">
Conversation
{t("activity.conversation")}
</h3>
<p className="text-sm text-blue-500 font-medium leading-tight">
@ Someone has replied to your note
{t("activity.conversationSubtitle")}
</p>
</div>
<div className="overflow-y-auto">
@ -33,7 +36,9 @@ export function ActivitySingleText({ event }: { event: NDKEvent }) {
) : null}
<div className="mt-3 flex flex-col gap-3">
<div className="flex items-center gap-3">
<p className="text-teal-500 font-medium">New reply</p>
<p className="text-teal-500 font-medium">
{t("activity.newReply")}
</p>
<div className="flex-1 h-px bg-teal-300" />
<div className="w-4 shrink-0 h-px bg-teal-300" />
</div>

View File

@ -1,10 +1,12 @@
import { activityUnreadAtom } from "@lume/utils";
import { useSetAtom } from "jotai";
import { useEffect } from "react";
import { useTranslation } from "react-i18next";
import { Outlet } from "react-router-dom";
import { ActivityList } from "./components/list";
export function ActivityScreen() {
const { t } = useTranslation();
const setUnreadActivity = useSetAtom(activityUnreadAtom);
useEffect(() => {
@ -15,7 +17,7 @@ export function ActivityScreen() {
<div className="flex h-full w-full rounded-xl shadow-[rgba(50,_50,_105,_0.15)_0px_2px_5px_0px,_rgba(0,_0,_0,_0.05)_0px_1px_1px_0px] dark:shadow-none dark:ring-1 dark:ring-white/10">
<div className="h-full flex flex-col w-96 shrink-0 rounded-l-xl bg-white/50 backdrop-blur-xl dark:bg-black/50">
<div className="h-14 shrink-0 flex items-center px-5 text-lg font-semibold border-b border-black/10 dark:border-white/10">
Activity
{t("activity.title")}
</div>
<ActivityList />
</div>

View File

@ -14,6 +14,7 @@ import { Window } from "@tauri-apps/api/window";
import { useSetAtom } from "jotai";
import { useState } from "react";
import { useForm } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { useLoaderData, useNavigate } from "react-router-dom";
import { toast } from "sonner";
@ -43,6 +44,7 @@ export function CreateAccountAddress() {
const [serviceId, setServiceId] = useState(services?.[0]?.id);
const [loading, setIsLoading] = useState(false);
const { t } = useTranslation();
const {
register,
handleSubmit,
@ -156,7 +158,7 @@ export function CreateAccountAddress() {
<div className="flex flex-col w-full max-w-md gap-8 mx-auto">
<div className="flex flex-col gap-1 text-center items-center">
<h1 className="text-2xl font-semibold">
Let's set up your account on Nostr
{t("signupWithProvider.title")}
</h1>
</div>
{!services ? (
@ -174,7 +176,7 @@ export function CreateAccountAddress() {
htmlFor="username"
className="text-sm font-semibold uppercase text-neutral-600"
>
Username *
{t("signupWithProvider.username")}
</label>
<div className="flex flex-col gap-1.5">
<div className="flex items-center justify-between w-full gap-2 bg-neutral-900 rounded-xl">
@ -203,7 +205,7 @@ export function CreateAccountAddress() {
<Select.Viewport className="p-3">
<Select.Group>
<Select.Label className="mb-2 text-sm font-medium uppercase px-7 text-neutral-600">
Choose a Provider
{t("signupWithProvider.chooseProvider")}
</Select.Label>
{services.map((service) => (
<Item key={service.id} event={service} />
@ -215,8 +217,7 @@ export function CreateAccountAddress() {
</Select.Root>
</div>
<span className="text-sm text-neutral-600">
Use to login to Lume and other Nostr apps. You can choose
provider you trust to manage your account
{t("signupWithProvider.usernameFooter")}
</span>
</div>
</div>
@ -226,7 +227,7 @@ export function CreateAccountAddress() {
htmlFor="email"
className="text-sm font-semibold uppercase text-neutral-600"
>
Backup Email (optional)
{t("signupWithProvider.email")}
</label>
<input
type={"email"}
@ -238,7 +239,7 @@ export function CreateAccountAddress() {
/>
</div>
<span className="text-sm text-neutral-600">
Use for recover your account if you lose your password
{t("signupWithProvider.emailFooter")}
</span>
</div>
</div>
@ -251,7 +252,7 @@ export function CreateAccountAddress() {
{loading ? (
<LoaderIcon className="size-5 animate-spin" />
) : (
"Continue"
t("global.continue")
)}
</button>
</div>

View File

@ -11,6 +11,7 @@ import { useSetAtom } from "jotai";
import { nanoid } from "nanoid";
import { getPublicKey, nip19 } from "nostr-tools";
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { useNavigate } from "react-router-dom";
import { toast } from "sonner";
@ -20,6 +21,7 @@ export function CreateAccountKeys() {
const setOnboarding = useSetAtom(onboardingAtom);
const navigate = useNavigate();
const [t] = useTranslation();
const [key, setKey] = useState("");
const [loading, setLoading] = useState(false);
const [showKey, setShowKey] = useState(false);
@ -76,11 +78,10 @@ export function CreateAccountKeys() {
<div className="flex flex-col w-full max-w-md gap-8 mx-auto">
<div className="flex flex-col gap-1 text-center items-center">
<h1 className="text-2xl font-semibold">
This is your new Account Key
{t("signupWithSelfManage.title")}
</h1>
<p className="text-lg font-medium leading-snug text-neutral-600 dark:text-neutral-500">
Keep your key in safe place. If you lose this key, you will lose
access to your account.
{t("signupWithSelfManage.subtitle")}
</p>
</div>
<div className="flex flex-col gap-6 mb-0">
@ -122,7 +123,7 @@ export function CreateAccountKeys() {
className="text-sm leading-none text-neutral-500"
htmlFor="confirm1"
>
I understand the risk of lost private key.
{t("signupWithSelfManage.confirm1")}
</label>
</div>
<div className="flex items-center gap-2">
@ -142,7 +143,7 @@ export function CreateAccountKeys() {
className="text-sm leading-none text-neutral-500"
htmlFor="confirm2"
>
I will make sure keep it safe and not sharing with anyone.
{t("signupWithSelfManage.confirm2")}
</label>
</div>
<div className="flex items-center gap-2">
@ -162,7 +163,7 @@ export function CreateAccountKeys() {
className="text-sm leading-none text-neutral-500"
htmlFor="confirm3"
>
I understand I cannot recover private key.
{t("signupWithSelfManage.confirm3")}
</label>
</div>
</div>
@ -176,7 +177,7 @@ export function CreateAccountKeys() {
{loading ? (
<LoaderIcon className="size-5 animate-spin" />
) : (
"Save key & Continue"
t("signupWithSelfManage.button")
)}
</button>
</div>

View File

@ -1,11 +1,13 @@
import { LoaderIcon } from "@lume/icons";
import { cn } from "@lume/utils";
import { useState } from "react";
import { Link, useNavigate } from "react-router-dom";
import { useTranslation } from "react-i18next";
import { useNavigate } from "react-router-dom";
export function CreateAccountScreen() {
const navigate = useNavigate();
const [t] = useTranslation();
const [method, setMethod] = useState<"self" | "managed">("self");
const [loading, setLoading] = useState(false);
@ -23,9 +25,9 @@ export function CreateAccountScreen() {
<div className="relative flex items-center justify-center w-full h-full">
<div className="flex flex-col w-full max-w-md gap-8 mx-auto">
<div className="flex flex-col gap-1 text-center items-center">
<h1 className="text-2xl font-semibold">Let's Get Started</h1>
<h1 className="text-2xl font-semibold">{t("signup.title")}</h1>
<p className="text-lg font-medium leading-snug text-neutral-600 dark:text-neutral-500">
Choose one of methods below to create your account
{t("signup.subtitle")}
</p>
</div>
<div className="flex flex-col gap-4">
@ -37,9 +39,9 @@ export function CreateAccountScreen() {
method === "self" ? "ring-1 ring-teal-500" : "",
)}
>
<p className="font-semibold">Self-Managed</p>
<p className="font-semibold">{t("signup.selfManageMethod")}</p>
<p className="text-sm font-medium text-neutral-500">
You create your keys and keep them safe.
{t("signup.selfManageMethodDescription")}
</p>
</button>
<button
@ -50,9 +52,9 @@ export function CreateAccountScreen() {
method === "managed" ? "ring-1 ring-teal-500" : "",
)}
>
<p className="font-semibold">Managed by Provider</p>
<p className="font-semibold">{t("signup.providerMethod")}</p>
<p className="text-sm font-medium text-neutral-500">
A 3rd party provider will handle your sign in keys for you.
{t("signup.providerMethodDescription")}
</p>
</button>
<button
@ -63,7 +65,7 @@ export function CreateAccountScreen() {
{loading ? (
<LoaderIcon className="size-5 animate-spin" />
) : (
"Continue"
t("global.continue")
)}
</button>
</div>

View File

@ -4,6 +4,7 @@ import { useStorage } from "@lume/storage";
import { getPublicKey, nip19 } from "nostr-tools";
import { useState } from "react";
import { useForm } from "react-hook-form";
import { Trans, useTranslation } from "react-i18next";
import { useNavigate } from "react-router-dom";
import { toast } from "sonner";
@ -15,6 +16,7 @@ export function LoginWithKey() {
const [showKey, setShowKey] = useState(false);
const [loading, setLoading] = useState(false);
const { t } = useTranslation("loginWithPrivkey.subtitle");
const {
register,
handleSubmit,
@ -52,19 +54,21 @@ export function LoginWithKey() {
<div className="relative flex items-center justify-center w-full h-full">
<div className="flex flex-col w-full max-w-md gap-8 mx-auto">
<div className="flex flex-col gap-1 text-center items-center">
<h1 className="text-2xl font-semibold">Enter your Private Key</h1>
<p className="text-lg font-medium leading-snug text-neutral-600 dark:text-neutral-500">
Lume will put your private key to{" "}
<span className="text-teal-500">
{storage.platform === "macos"
? "Apple Keychain"
: storage.platform === "windows"
? "Credential Manager"
: "Secret Service"}
</span>
.
<br />
It will be secured by your OS.
<h1 className="text-2xl font-semibold">
{t("loginWithPrivkey.title")}
</h1>
<p className="text-lg font-medium whitespace-pre-line leading-snug text-neutral-600 dark:text-neutral-500">
<Trans t={t}>
Lume will put your private key to{" "}
<span className="text-teal-500">
{storage.platform === "macos"
? "Apple Keychain"
: storage.platform === "windows"
? "Credential Manager"
: "Secret Service"}
</span>
. It will be secured by your OS.
</Trans>
</p>
</div>
<div className="flex flex-col gap-6">
@ -107,7 +111,7 @@ export function LoginWithKey() {
{loading ? (
<LoaderIcon className="size-5 animate-spin" />
) : (
"Continue"
t("global.continue")
)}
</button>
</form>

View File

@ -5,6 +5,7 @@ import NDK, { NDKNip46Signer, NDKPrivateKeySigner } from "@nostr-dev-kit/ndk";
import { nip19 } from "nostr-tools";
import { useState } from "react";
import { useForm } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { useNavigate } from "react-router-dom";
import { toast } from "sonner";
@ -15,6 +16,7 @@ export function LoginWithNsecbunker() {
const [loading, setLoading] = useState(false);
const { t } = useTranslation();
const {
register,
handleSubmit,
@ -69,7 +71,7 @@ export function LoginWithNsecbunker() {
<div className="flex flex-col w-full max-w-md gap-8 mx-auto">
<div className="flex flex-col gap-1 text-center items-center">
<h1 className="text-2xl font-semibold">
Enter your nsecbunker token
{t("loginWithBunker.title")}
</h1>
</div>
<div className="flex flex-col gap-6">
@ -101,7 +103,7 @@ export function LoginWithNsecbunker() {
{loading ? (
<LoaderIcon className="size-5 animate-spin" />
) : (
"Continue"
t("global.continue")
)}
</button>
</form>

View File

@ -7,6 +7,7 @@ import { Window } from "@tauri-apps/api/window";
import { fetch } from "@tauri-apps/plugin-http";
import { useState } from "react";
import { useForm } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { useNavigate } from "react-router-dom";
import { toast } from "sonner";
@ -19,6 +20,7 @@ export function LoginWithOAuth() {
const [loading, setLoading] = useState(false);
const { t } = useTranslation();
const {
register,
handleSubmit,
@ -130,7 +132,9 @@ export function LoginWithOAuth() {
<div className="relative flex items-center justify-center w-full h-full">
<div className="flex flex-col w-full max-w-md gap-8 mx-auto">
<div className="flex flex-col gap-1 text-center items-center">
<h1 className="text-2xl font-semibold">Enter your Nostr Address</h1>
<h1 className="text-2xl font-semibold">
{t("loginWithAddress.title")}
</h1>
</div>
<div className="flex flex-col gap-6">
<form
@ -161,7 +165,7 @@ export function LoginWithOAuth() {
{loading ? (
<LoaderIcon className="size-5 animate-spin" />
) : (
"Continue"
t("global.continue")
)}
</button>
</form>

View File

@ -1,11 +1,14 @@
import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
export function LoginScreen() {
const { t } = useTranslation();
return (
<div className="relative flex items-center justify-center w-full h-full">
<div className="flex flex-col w-full max-w-md gap-8 mx-auto">
<div className="flex flex-col gap-1 text-center items-center">
<h1 className="text-2xl font-semibold">Welcome back, anon!</h1>
<h1 className="text-2xl font-semibold">{t("login.title")}</h1>
</div>
<div className="flex flex-col gap-6">
<div className="flex flex-col gap-2">
@ -13,13 +16,13 @@ export function LoginScreen() {
to="/auth/login-oauth"
className="inline-flex items-center justify-center w-full h-12 text-lg font-medium text-white bg-blue-500 rounded-xl hover:bg-blue-600"
>
Login with Nostr Address
{t("login.loginWithAddress")}
</Link>
<Link
to="/auth/login-nsecbunker"
className="inline-flex items-center justify-center w-full h-12 text-lg font-medium text-neutral-50 rounded-xl bg-neutral-950 hover:bg-neutral-900"
>
Login with nsecBunker
{t("login.loginWithBunker")}
</Link>
</div>
<div className="flex flex-col gap-6">
@ -29,7 +32,7 @@ export function LoginScreen() {
</div>
<div className="relative flex justify-center">
<span className="px-2 font-medium bg-black text-neutral-600">
Or continue with
{t("login.or")}
</span>
</div>
</div>
@ -38,13 +41,10 @@ export function LoginScreen() {
to="/auth/login-key"
className="mb-2 inline-flex items-center justify-center w-full h-12 text-lg font-medium text-neutral-50 rounded-xl bg-neutral-950 hover:bg-neutral-900"
>
Login with Private Key
{t("login.loginWithPrivkey")}
</Link>
<p className="text-sm text-center text-neutral-500">
Lume will put your Private Key in{" "}
<span className="text-teal-600">Secure Storage</span> depended
on your OS Platform. It will be secured by Password or Biometric
ID
{t("login.footer")}
</p>
</div>
</div>

View File

@ -8,6 +8,7 @@ import {
requestPermission,
} from "@tauri-apps/plugin-notification";
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { useNavigate } from "react-router-dom";
import { toast } from "sonner";
@ -16,6 +17,7 @@ export function OnboardingScreen() {
const storage = useStorage();
const navigate = useNavigate();
const [t] = useTranslation();
const [loading, setLoading] = useState(false);
const [apiKey, setAPIKey] = useState("");
const [settings, setSettings] = useState({
@ -91,10 +93,10 @@ export function OnboardingScreen() {
<div className="mx-auto flex w-full max-w-md flex-col gap-8">
<div className="flex flex-col gap-1 text-center items-center">
<h1 className="text-2xl font-semibold">
You&apos;re almost ready to use Lume.
{t("onboardingSettings.title")}
</h1>
<p className="text-lg font-medium leading-snug text-neutral-600 dark:text-neutral-500">
Let&apos;s start personalizing your experience.
{t("onboardingSettings.subtitle")}
</p>
</div>
<div className="flex flex-col gap-3">
@ -107,10 +109,11 @@ export function OnboardingScreen() {
<Switch.Thumb className="block h-6 w-6 translate-x-0.5 rounded-full bg-neutral-50 transition-transform duration-100 will-change-transform data-[state=checked]:translate-x-[19px]" />
</Switch.Root>
<div>
<h3 className="font-semibold text-lg">Push notification</h3>
<h3 className="font-semibold text-lg">
{t("onboardingSettings.notification.title")}
</h3>
<p className="text-neutral-500">
Enabling push notifications will allow you to receive
notifications from Lume.
{t("onboardingSettings.notification.subtitle")}
</p>
</div>
</div>
@ -123,10 +126,11 @@ export function OnboardingScreen() {
<Switch.Thumb className="block h-6 w-6 translate-x-0.5 rounded-full bg-neutral-50 transition-transform duration-100 will-change-transform data-[state=checked]:translate-x-[19px]" />
</Switch.Root>
<div>
<h3 className="font-semibold text-lg">Low Power Mode</h3>
<h3 className="font-semibold text-lg">
{t("onboardingSettings.lowPower.title")}
</h3>
<p className="text-neutral-500">
Limited relay connection and hide all media, sustainable for low
network environment.
{t("onboardingSettings.lowPower.subtitle")}
</p>
</div>
</div>
@ -140,11 +144,10 @@ export function OnboardingScreen() {
</Switch.Root>
<div>
<h3 className="font-semibold text-lg">
Translation (nostr.wine)
{t("onboardingSettings.translation.title")}
</h3>
<p className="text-neutral-500">
Translate text to your preferred language, powered by Nostr
Wine.
{t("onboardingSettings.translation.subtitle")}
</p>
</div>
</div>
@ -175,10 +178,7 @@ export function OnboardingScreen() {
) : null}
<div className="flex items-center gap-2 rounded-xl px-5 py-3 text-sm bg-blue-950 text-blue-300">
<InfoIcon className="size-8" />
<p>
There are many more settings you can configure from the
&quot;Settings&quot; screen. Be sure to visit it later.
</p>
<p>{t("onboardingSettings.footer")}</p>
</div>
<button
type="button"
@ -188,7 +188,7 @@ export function OnboardingScreen() {
{loading ? (
<LoaderIcon className="size-5 animate-spin" />
) : (
"Continue"
t("global.continue")
)}
</button>
</div>

View File

@ -1,6 +1,9 @@
import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
export function WelcomeScreen() {
const { t } = useTranslation();
return (
<div className="flex flex-col items-center justify-between w-full h-full">
<div />
@ -12,10 +15,8 @@ export function WelcomeScreen() {
alt="lume"
className="w-2/3"
/>
<p className="mt-5 text-lg font-medium leading-snug text-neutral-600 dark:text-neutral-500">
Lume is a magnificent client for Nostr to meet, explore
<br />
and freely share your thoughts with everyone.
<p className="mt-5 text-lg whitespace-pre-line font-medium leading-snug text-neutral-600 dark:text-neutral-500">
{t("welcome.title")}
</p>
</div>
<div className="flex flex-col w-full max-w-xs gap-2 mx-auto">
@ -23,19 +24,19 @@ export function WelcomeScreen() {
to="/auth/create"
className="inline-flex items-center justify-center w-full h-12 text-lg font-medium text-white bg-blue-500 rounded-xl hover:bg-blue-600"
>
Join Nostr
{t("welcome.signup")}
</Link>
<Link
to="/auth/login"
className="inline-flex items-center justify-center w-full h-12 text-lg font-medium text-neutral-50 rounded-xl bg-neutral-950 hover:bg-neutral-900"
>
Login
{t("welcome.login")}
</Link>
</div>
</div>
<div className="flex items-center justify-center h-11">
<p className="text-neutral-700">
Before joining Nostr, you can take time to learn more about Nostr{" "}
{t("welcome.footer")}{" "}
<Link
to="https://nostr.com"
target="_blank"

View File

@ -53,7 +53,7 @@ export function ErrorScreen() {
return (
<div
data-tauri-drag-region
className="relative flex h-screen w-screen items-center justify-center bg-blue-600 overflow-hidden rounded-t-xl"
className="relative flex h-screen w-screen items-center justify-center bg-blue-500 overflow-hidden rounded-xl"
>
<div className="flex w-full max-w-2xl flex-col items-start gap-8">
<div className="flex flex-col">
@ -95,7 +95,7 @@ export function ErrorScreen() {
<div className="flex w-full flex-col gap-2">
<div className="flex w-full items-center justify-between">
<div className="text-xl font-semibold text-white">
3. Report this issue to Lume&apos;s Devs
3. Report this issue to Lume
</div>
<a
href="https://github.com/luminous-devs/lume/issues/new"
@ -120,13 +120,13 @@ export function ErrorScreen() {
</div>
<div className="select-text text-lg font-medium text-blue-300">
<p>
While waiting for Lume&apos;s Devs to release the bug fixes,
you always can use other Nostr clients with your account:
While waiting for Lume release the bug fixes, you always can
use other Nostr clients with your account:
</p>
<div className="mt-2 flex flex-col gap-1 text-white">
<a
className="hover:!underline"
href="https://snort.social"
href="https://snort.social/"
target="_blank"
rel="noreferrer"
>
@ -134,15 +134,15 @@ export function ErrorScreen() {
</a>
<a
className="hover:!underline"
href="https://primal.net"
href="https://nostter.app/"
target="_blank"
rel="noreferrer"
>
primal.net
nostter
</a>
<a
className="hover:!underline"
href="https://nostrudel.ninja"
href="https://nostrudel.ninja/"
target="_blank"
rel="noreferrer"
>

View File

@ -18,10 +18,13 @@ import { TutorialModal } from "@lume/ui/src/tutorial/modal";
import { COL_TYPES } from "@lume/utils";
import * as Tooltip from "@radix-ui/react-tooltip";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { VList } from "virtua";
export function HomeScreen() {
const { t } = useTranslation();
const { columns, vlistRef, addColumn } = useColumnContext();
const [selectedIndex, setSelectedIndex] = useState(-1);
const renderItem = (column: IColumn) => {
@ -124,7 +127,7 @@ export function HomeScreen() {
</Tooltip.Trigger>
<Tooltip.Portal>
<Tooltip.Content className="inline-flex h-7 select-none text-neutral-50 dark:text-neutral-950 items-center justify-center rounded-md bg-neutral-950 dark:bg-neutral-50 px-3.5 text-sm will-change-[transform,opacity] data-[state=delayed-open]:data-[side=bottom]:animate-slideUpAndFade data-[state=delayed-open]:data-[side=left]:animate-slideRightAndFade data-[state=delayed-open]:data-[side=right]:animate-slideLeftAndFade data-[state=delayed-open]:data-[side=top]:animate-slideDownAndFade">
Move Left
{t("global.moveLeft")}
<Tooltip.Arrow className="fill-neutral-950 dark:fill-neutral-50" />
</Tooltip.Content>
</Tooltip.Portal>
@ -151,7 +154,7 @@ export function HomeScreen() {
</Tooltip.Trigger>
<Tooltip.Portal>
<Tooltip.Content className="inline-flex h-7 select-none text-neutral-50 dark:text-neutral-950 items-center justify-center rounded-md bg-neutral-950 dark:bg-neutral-50 px-3.5 text-sm will-change-[transform,opacity] data-[state=delayed-open]:data-[side=bottom]:animate-slideUpAndFade data-[state=delayed-open]:data-[side=left]:animate-slideRightAndFade data-[state=delayed-open]:data-[side=right]:animate-slideLeftAndFade data-[state=delayed-open]:data-[side=top]:animate-slideDownAndFade">
Move Right
{t("global.moveRight")}
<Tooltip.Arrow className="fill-neutral-950 dark:fill-neutral-50" />
</Tooltip.Content>
</Tooltip.Portal>
@ -174,7 +177,7 @@ export function HomeScreen() {
</Tooltip.Trigger>
<Tooltip.Portal>
<Tooltip.Content className="inline-flex h-7 select-none text-neutral-50 dark:text-neutral-950 items-center justify-center rounded-md bg-neutral-950 dark:bg-neutral-50 px-3.5 text-sm will-change-[transform,opacity] data-[state=delayed-open]:data-[side=bottom]:animate-slideUpAndFade data-[state=delayed-open]:data-[side=left]:animate-slideRightAndFade data-[state=delayed-open]:data-[side=right]:animate-slideLeftAndFade data-[state=delayed-open]:data-[side=top]:animate-slideDownAndFade">
New Column
{t("global.newColum")}
<Tooltip.Arrow className="fill-neutral-950 dark:fill-neutral-50" />
</Tooltip.Content>
</Tooltip.Portal>

View File

@ -4,10 +4,13 @@ import { FETCH_LIMIT } from "@lume/utils";
import { NDKEvent, NDKKind } from "@nostr-dev-kit/ndk";
import { useInfiniteQuery } from "@tanstack/react-query";
import { useCallback, useMemo } from "react";
import { useTranslation } from "react-i18next";
import { VList } from "virtua";
export function RelayEventList({ relayUrl }: { relayUrl: string }) {
const ark = useArk();
const { t } = useTranslation();
const { status, data, hasNextPage, isFetchingNextPage, fetchNextPage } =
useInfiniteQuery({
queryKey: ["relay-events", relayUrl],
@ -37,14 +40,10 @@ export function RelayEventList({ relayUrl }: { relayUrl: string }) {
if (!lastEvent) return;
return lastEvent.created_at - 1;
},
select: (data) => data?.pages.flatMap((page) => page),
refetchOnWindowFocus: false,
});
const allEvents = useMemo(
() => (data ? data.pages.flatMap((page) => page) : []),
[data],
);
const renderItem = useCallback(
(event: NDKEvent) => {
switch (event.kind) {
@ -64,7 +63,7 @@ export function RelayEventList({ relayUrl }: { relayUrl: string }) {
{status === "pending" ? (
<NoteSkeleton />
) : (
allEvents.map((item) => renderItem(item))
data.map((item) => renderItem(item))
)}
<div className="flex h-16 items-center justify-center px-3 pb-3">
{hasNextPage ? (
@ -79,7 +78,7 @@ export function RelayEventList({ relayUrl }: { relayUrl: string }) {
) : (
<>
<ArrowRightCircleIcon className="h-5 w-5" />
Load more
{t("global.loading")}
</>
)}
</button>

View File

@ -1,17 +1,20 @@
import { User, useRelaylist } from "@lume/ark";
import { PlusIcon, SearchIcon } from "@lume/icons";
import { normalizeRelayUrl } from "nostr-fetch";
import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
export function RelayItem({ url, users }: { url: string; users?: string[] }) {
const domain = new URL(url).hostname;
const { t } = useTranslation();
const { connectRelay } = useRelaylist();
return (
<div className="flex h-14 w-full items-center justify-between border-b border-neutral-100 px-5 dark:border-neutral-950">
<div className="inline-flex items-center gap-2">
<span className="text-sm font-semibold text-neutral-500 dark:text-neutral-400">
Relay:{" "}
{t("global.relay")}:{" "}
</span>
<span className="max-w-[200px] truncate text-sm font-medium text-neutral-900 dark:text-neutral-100">
{url}
@ -39,7 +42,7 @@ export function RelayItem({ url, users }: { url: string; users?: string[] }) {
className="inline-flex h-8 items-center justify-center gap-2 rounded-lg bg-neutral-100 px-2 text-sm font-medium hover:bg-neutral-200 dark:bg-neutral-900 dark:hover:bg-neutral-800"
>
<SearchIcon className="size-4" />
Inspect
{t("global.inspect")}
</Link>
<button
type="button"

View File

@ -1,91 +0,0 @@
import { useArk, useRelaylist } from "@lume/ark";
import { LoaderIcon, PlusIcon, ShareIcon } from "@lume/icons";
import { User } from "@lume/ui";
import { useQuery } from "@tanstack/react-query";
import { useNavigate } from "react-router-dom";
import { VList } from "virtua";
export function RelayList() {
const ark = useArk();
const { connectRelay } = useRelaylist();
const { status, data } = useQuery({
queryKey: ["relays"],
queryFn: async () => {
return await ark.getAllRelaysFromContacts();
},
refetchOnWindowFocus: false,
refetchOnMount: false,
refetchOnReconnect: false,
staleTime: Infinity,
});
const navigate = useNavigate();
const inspectRelay = (relayUrl: string) => {
const url = new URL(relayUrl);
navigate(`/relays/${url.hostname}`);
};
return (
<div className="col-span-2 bg-white">
{status === "pending" ? (
<div className="flex h-full w-full items-center justify-center pb-10">
<div className="inline-flex flex-col items-center justify-center gap-2">
<LoaderIcon className="h-5 w-5 animate-spin text-neutral-900 dark:text-neutral-100" />
<p>Loading relay...</p>
</div>
</div>
) : (
<VList className="h-full">
<div className="inline-flex h-16 w-full items-center border-b border-neutral-100 px-3 dark:border-neutral-900">
<h3 className="font-semibold">Relay discovery</h3>
</div>
{[...data].map(([key, value]) => (
<div
key={key}
className="flex h-14 w-full items-center justify-between border-b border-neutral-100 px-3 dark:border-neutral-900"
>
<div className="inline-flex items-center gap-2 divide-x divide-neutral-100 dark:divide-neutral-900">
<div className="inline-flex items-center gap-2">
<button
type="button"
onClick={() => inspectRelay(key)}
className="inline-flex h-6 items-center justify-center gap-1 rounded bg-neutral-200 px-1.5 text-sm font-medium text-neutral-900 hover:bg-neutral-300 dark:bg-neutral-800 dark:text-neutral-100 dark:hover:bg-neutral-700"
>
<ShareIcon className="h-3 w-3" />
Inspect
</button>
<button
type="button"
onClick={() => connectRelay.mutate(key)}
className="inline-flex h-6 w-6 items-center justify-center rounded text-neutral-900 hover:bg-neutral-200 dark:text-neutral-100 dark:hover:bg-neutral-800"
>
<PlusIcon className="h-3 w-3" />
</button>
</div>
<div className="inline-flex items-center gap-2 pl-3">
<span className="text-sm font-semibold text-neutral-500 dark:text-neutral-400">
Relay:{" "}
</span>
<span className="max-w-[200px] truncate text-sm font-medium text-neutral-900 dark:text-neutral-100">
{key}
</span>
</div>
</div>
<div className="isolate flex -space-x-2">
{value.slice(0, 4).map((item) => (
<User key={item} pubkey={item} variant="stacked" />
))}
{value.length > 4 ? (
<div className="inline-flex h-8 w-8 items-center justify-center rounded-full bg-neutral-200 text-neutral-900 ring-1 ring-neutral-300 dark:bg-neutral-800 dark:text-neutral-100 dark:ring-neutral-700">
<span className="text-xs font-medium">+{value.length}</span>
</div>
) : null}
</div>
</div>
))}
</VList>
)}
</div>
);
}

View File

@ -3,11 +3,13 @@ import { CancelIcon, LoaderIcon, RefreshIcon } from "@lume/icons";
import { cn } from "@lume/utils";
import { NDKKind, NDKSubscriptionCacheUsage } from "@nostr-dev-kit/ndk";
import { useQuery } from "@tanstack/react-query";
import { useTranslation } from "react-i18next";
import { RelayForm } from "./relayForm";
export function RelaySidebar({ className }: { className?: string }) {
const ark = useArk();
const { t } = useTranslation();
const { removeRelay } = useRelaylist();
const { status, data, isRefetching, refetch } = useQuery({
queryKey: ["relay-personal"],
@ -40,7 +42,7 @@ export function RelaySidebar({ className }: { className?: string }) {
)}
>
<div className="inline-flex items-center justify-between w-full h-14 px-3 border-b border-black/10 dark:border-white/10">
<h3 className="font-semibold">Connected relays</h3>
<h3 className="font-semibold">{t("relays.sidebar.title")}</h3>
<button
type="button"
onClick={() => refetch()}
@ -58,7 +60,7 @@ export function RelaySidebar({ className }: { className?: string }) {
</div>
) : !data.length ? (
<div className="flex items-center justify-center w-full h-20 rounded-lg bg-black/10 dark:bg-white/10">
<p className="text-sm font-medium">Empty.</p>
<p className="text-sm font-medium">{t("relays.sidebar.empty")}</p>
</div>
) : (
data.map((item) => (

View File

@ -1,8 +1,11 @@
import { cn } from "@lume/utils";
import { useTranslation } from "react-i18next";
import { NavLink, Outlet } from "react-router-dom";
import { RelaySidebar } from "./components/sidebar";
export function RelaysScreen() {
const { t } = useTranslation();
return (
<div className="grid h-full w-full lg:grid-cols-4 xl:grid-cols-5 rounded-xl shadow-[rgba(50,_50,_105,_0.15)_0px_2px_5px_0px,_rgba(0,_0,_0,_0.05)_0px_1px_1px_0px] dark:shadow-none dark:ring-1 dark:ring-white/10">
<RelaySidebar className="col-span-1" />
@ -20,7 +23,7 @@ export function RelaysScreen() {
)
}
>
Global
{t("relays.global")}
</NavLink>
<NavLink
to={"/relays/follows/"}
@ -33,7 +36,7 @@ export function RelaysScreen() {
)
}
>
Follows
{t("relays.follows")}
</NavLink>
</div>
<div className="flex flex-col flex-1 min-h-0 overflow-y-auto">

View File

@ -1,15 +1,16 @@
import { ArrowLeftIcon, LoaderIcon } from "@lume/icons";
import { LoaderIcon } from "@lume/icons";
import { NIP11 } from "@lume/types";
import { User } from "@lume/ui";
import { Suspense } from "react";
import { Await, useLoaderData, useNavigate, useParams } from "react-router-dom";
import { useTranslation } from "react-i18next";
import { Await, useLoaderData, useParams } from "react-router-dom";
import { RelayEventList } from "./components/relayEventList";
export function RelayUrlScreen() {
const { t } = useTranslation();
const { url } = useParams();
const data: { relay?: { [key: string]: string } } = useLoaderData();
const navigate = useNavigate();
const getSoftwareName = (url: string) => {
const filename = url.substring(url.lastIndexOf("/") + 1);
@ -32,7 +33,7 @@ export function RelayUrlScreen() {
fallback={
<div className="flex items-center gap-2 text-sm font-medium text-neutral-900 dark:text-neutral-100">
<LoaderIcon className="h-4 w-4 animate-spin" />
Loading...
{t("global.loading")}
</div>
}
>
@ -40,7 +41,7 @@ export function RelayUrlScreen() {
resolve={data.relay}
errorElement={
<div className="text-sm font-medium">
<p>Could not load relay information 😬</p>
<p>{t("relays.relayView.empty")}</p>
</div>
}
>
@ -55,7 +56,7 @@ export function RelayUrlScreen() {
{resolvedRelay.pubkey ? (
<div className="flex flex-col gap-1">
<h5 className="text-sm font-semibold text-neutral-600 dark:text-neutral-400">
Owner:
{t("relays.relayView.owner")}:
</h5>
<div className="w-full rounded-lg bg-neutral-100 px-2 py-2 dark:bg-neutral-900">
<User pubkey={resolvedRelay.pubkey} variant="simple" />
@ -65,7 +66,7 @@ export function RelayUrlScreen() {
{resolvedRelay.contact ? (
<div>
<h5 className="text-sm font-semibold text-neutral-600 dark:text-neutral-400">
Contact:
{t("relays.relayView.contact")}:
</h5>
<a
href={`mailto:${resolvedRelay.contact}`}
@ -79,7 +80,7 @@ export function RelayUrlScreen() {
) : null}
<div>
<h5 className="text-sm font-semibold text-neutral-600 dark:text-neutral-400">
Software:
{t("relays.relayView.software")}:
</h5>
<a
href={resolvedRelay.software}
@ -94,7 +95,7 @@ export function RelayUrlScreen() {
</div>
<div>
<h5 className="text-sm font-semibold text-neutral-600 dark:text-neutral-400">
Supported NIPs:
{t("relays.relayView.nips")}:
</h5>
<div className="mt-2 grid grid-cols-7 gap-2">
{resolvedRelay.supported_nips.map((item) => (
@ -113,26 +114,24 @@ export function RelayUrlScreen() {
{resolvedRelay.limitation ? (
<div>
<h5 className="text-sm font-semibold text-neutral-600 dark:text-neutral-400">
Limitation
{t("relays.relayView.limit")}
</h5>
<div className="flex flex-col gap-2 divide-y divide-white/5">
{Object.keys(resolvedRelay.limitation).map(
(key, index) => {
return (
<div
key={key + index}
className="flex items-baseline justify-between pt-2"
>
<p className="text-sm font-medium text-neutral-900 dark:text-neutral-100">
{titleCase(key)}:
</p>
<p className="text-sm font-medium text-neutral-600 dark:text-neutral-400">
{resolvedRelay.limitation[key].toString()}
</p>
</div>
);
},
)}
{Object.keys(resolvedRelay.limitation).map((key) => {
return (
<div
key={key}
className="flex items-baseline justify-between pt-2"
>
<p className="text-sm font-medium text-neutral-900 dark:text-neutral-100">
{titleCase(key)}:
</p>
<p className="text-sm font-medium text-neutral-600 dark:text-neutral-400">
{resolvedRelay.limitation[key].toString()}
</p>
</div>
);
})}
</div>
</div>
) : null}
@ -144,10 +143,10 @@ export function RelayUrlScreen() {
rel="noreferrer"
className="inline-flex h-10 w-full items-center justify-center rounded-lg bg-blue-500 text-sm font-medium hover:bg-blue-600"
>
Open payment website
{t("relays.relayView.payment")}
</a>
<span className="text-center text-xs text-neutral-600 dark:text-neutral-400">
You need to make a payment to connect this relay
{t("relays.relayView.paymentNote")}
</span>
</div>
) : null}

View File

@ -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

View File

@ -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>

View File

@ -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}

View File

@ -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>
);

View File

@ -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>
);
}

View File

@ -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>
);

View File

@ -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>
);
}

View File

@ -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>
);
}

View File

@ -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>
);
}

View File

@ -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>
);
}

View File

@ -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>
);
}

View File

@ -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>
);
}

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -32,6 +32,7 @@
"re-resizable": "^6.9.11",
"react": "^18.2.0",
"react-currency-input-field": "^3.6.14",
"react-i18next": "^14.0.1",
"react-router-dom": "^6.21.3",
"react-string-replace": "^1.1.1",
"sonner": "^1.3.1",

View File

@ -3,12 +3,11 @@ import {
MoveLeftIcon,
MoveRightIcon,
RefreshIcon,
ThreadIcon,
TrashIcon,
} from "@lume/icons";
import * as DropdownMenu from "@radix-ui/react-dropdown-menu";
import { useQueryClient } from "@tanstack/react-query";
import { ReactNode } from "react";
import { useTranslation } from "react-i18next";
import { InterestModal } from "./interestModal";
import { useColumnContext } from "./provider";
@ -16,14 +15,14 @@ export function ColumnHeader({
id,
title,
queryKey,
icon,
}: {
id: number;
title: string;
queryKey?: string[];
icon?: ReactNode;
}) {
const queryClient = useQueryClient();
const { t } = useTranslation();
const { moveColumn, removeColumn } = useColumnContext();
const refresh = async () => {
@ -63,7 +62,7 @@ export function ColumnHeader({
className="inline-flex items-center gap-3 px-3 text-sm font-medium rounded-lg h-9 text-black/70 hover:bg-black/10 hover:text-black focus:outline-none dark:text-white/70 dark:hover:bg-white/10 dark:hover:text-white"
>
<RefreshIcon className="size-4" />
Refresh
{t("global.refresh")}
</button>
</DropdownMenu.Item>
{queryKey?.[0] === "foryou-9998" ? (
@ -81,7 +80,7 @@ export function ColumnHeader({
className="inline-flex items-center gap-3 px-3 text-sm font-medium rounded-lg h-9 text-black/70 hover:bg-black/10 hover:text-black focus:outline-none dark:text-white/70 dark:hover:bg-white/10 dark:hover:text-white"
>
<MoveLeftIcon className="size-4" />
Move left
{t("global.moveLeft")}
</button>
</DropdownMenu.Item>
<DropdownMenu.Item asChild>
@ -91,7 +90,7 @@ export function ColumnHeader({
className="inline-flex items-center gap-3 px-3 text-sm font-medium rounded-lg h-9 text-black/70 hover:bg-black/10 hover:text-black focus:outline-none dark:text-white/70 dark:hover:bg-white/10 dark:hover:text-white"
>
<MoveRightIcon className="size-4" />
Move right
{t("global.moveRight")}
</button>
</DropdownMenu.Item>
<DropdownMenu.Separator className="h-px my-1 bg-black/10 dark:bg-white/10" />
@ -102,7 +101,7 @@ export function ColumnHeader({
className="inline-flex items-center gap-3 px-3 text-sm font-medium text-red-500 rounded-lg h-9 hover:bg-red-500 hover:text-red-50 focus:outline-none"
>
<TrashIcon className="size-4" />
Delete
{t("global.Delete")}
</button>
</DropdownMenu.Item>
</DropdownMenu.Content>

View File

@ -4,6 +4,7 @@ import { TOPICS, cn } from "@lume/utils";
import * as Dialog from "@radix-ui/react-dialog";
import { useQueryClient } from "@tanstack/react-query";
import { ReactNode, useState } from "react";
import { useTranslation } from "react-i18next";
import { toast } from "sonner";
export function InterestModal({
@ -14,6 +15,7 @@ export function InterestModal({
const storage = useStorage();
const queryClient = useQueryClient();
const [t] = useTranslation();
const [open, setOpen] = useState(false);
const [loading, setLoading] = useState(false);
const [hashtags, setHashtags] = useState(storage.interests?.hashtags || []);
@ -65,7 +67,7 @@ export function InterestModal({
) : (
<>
<EditInterestIcon className="size-4" />
Edit interest
{t("interests.edit")}
</>
)}
</Dialog.Trigger>
@ -80,7 +82,7 @@ export function InterestModal({
<div className="w-full h-full flex flex-col">
<div className="h-16 shrink-0 px-8 border-b border-neutral-100 dark:border-neutral-900 flex w-full items-center justify-between">
<div className="flex flex-col">
<h3 className="font-semibold">Edit Interest</h3>
<h3 className="font-semibold">{t("interests.edit")}</h3>
</div>
</div>
<div className="w-full flex-1 min-h-0 flex flex-col justify-between">
@ -104,7 +106,7 @@ export function InterestModal({
onClick={() => toggleAll(topic.content)}
className="text-sm font-medium text-blue-500"
>
Follow All
{t("interests.followAll")}
</button>
</div>
<div className="flex flex-wrap items-center gap-3">
@ -131,7 +133,7 @@ export function InterestModal({
<div className="h-16 shrink-0 w-full flex items-center px-8 justify-center gap-2 border-t border-neutral-100 dark:border-neutral-900 bg-neutral-50 dark:bg-neutral-950">
<Dialog.Close className="inline-flex h-9 flex-1 gap-2 shrink-0 items-center justify-center rounded-lg bg-neutral-100 font-medium dark:bg-neutral-900 dark:hover:bg-neutral-800 hover:bg-blue-200">
<ArrowLeftIcon className="size-4" />
Cancel
{t("global.cancel")}
</Dialog.Close>
<button
type="button"
@ -141,7 +143,7 @@ export function InterestModal({
{loading ? (
<LoaderIcon className="size-4 animate-spin" />
) : (
"Save"
t("global.save")
)}
</button>
</div>

View File

@ -1,11 +1,14 @@
import { PinIcon } from "@lume/icons";
import { COL_TYPES } from "@lume/utils";
import * as Tooltip from "@radix-ui/react-tooltip";
import { useTranslation } from "react-i18next";
import { useColumnContext } from "../../column/provider";
import { useNoteContext } from "../provider";
export function NotePin() {
const event = useNoteContext();
const { t } = useTranslation();
const { addColumn } = useColumnContext();
return (
@ -24,12 +27,12 @@ export function NotePin() {
className="inline-flex items-center justify-center gap-2 pl-2 pr-3 text-sm font-medium rounded-full h-7 w-max bg-neutral-100 hover:bg-neutral-200 dark:hover:bg-neutral-800 dark:bg-neutral-900"
>
<PinIcon className="size-4" />
Pin
{t("note.buttons.pin")}
</button>
</Tooltip.Trigger>
<Tooltip.Portal>
<Tooltip.Content className="inline-flex h-7 select-none text-neutral-50 dark:text-neutral-950 items-center justify-center rounded-md bg-neutral-950 dark:bg-neutral-50 px-3.5 text-sm will-change-[transform,opacity] data-[state=delayed-open]:data-[side=bottom]:animate-slideUpAndFade data-[state=delayed-open]:data-[side=left]:animate-slideRightAndFade data-[state=delayed-open]:data-[side=right]:animate-slideLeftAndFade data-[state=delayed-open]:data-[side=top]:animate-slideDownAndFade">
Pin note
{t("note.buttons.pinTooltip")}
<Tooltip.Arrow className="fill-neutral-950 dark:fill-neutral-50" />
</Tooltip.Content>
</Tooltip.Portal>

View File

@ -1,5 +1,6 @@
import { ReplyIcon } from "@lume/icons";
import * as Tooltip from "@radix-ui/react-tooltip";
import { useTranslation } from "react-i18next";
import { useNavigate } from "react-router-dom";
import { useNoteContext } from "../provider";
@ -7,6 +8,8 @@ export function NoteReply() {
const event = useNoteContext();
const navigate = useNavigate();
const { t } = useTranslation();
return (
<Tooltip.Provider>
<Tooltip.Root delayDuration={150}>
@ -21,7 +24,7 @@ export function NoteReply() {
</Tooltip.Trigger>
<Tooltip.Portal>
<Tooltip.Content className="inline-flex h-7 select-none text-neutral-50 dark:text-neutral-950 items-center justify-center rounded-md bg-neutral-950 dark:bg-neutral-50 px-3.5 text-sm will-change-[transform,opacity] data-[state=delayed-open]:data-[side=bottom]:animate-slideUpAndFade data-[state=delayed-open]:data-[side=left]:animate-slideRightAndFade data-[state=delayed-open]:data-[side=right]:animate-slideLeftAndFade data-[state=delayed-open]:data-[side=top]:animate-slideDownAndFade">
View thread
{t("note.menu.viewThread")}
<Tooltip.Arrow className="fill-neutral-950 dark:fill-neutral-50" />
</Tooltip.Content>
</Tooltip.Portal>

View File

@ -5,6 +5,7 @@ import * as Tooltip from "@radix-ui/react-tooltip";
import { useSetAtom } from "jotai";
import { nip19 } from "nostr-tools";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { toast } from "sonner";
import { useNoteContext } from "../provider";
@ -13,6 +14,7 @@ export function NoteRepost() {
const setEditorValue = useSetAtom(editorValueAtom);
const setIsEditorOpen = useSetAtom(editorAtom);
const [t] = useTranslation();
const [loading, setLoading] = useState(false);
const [isRepost, setIsRepost] = useState(false);
const [open, setOpen] = useState(false);
@ -81,7 +83,7 @@ export function NoteRepost() {
</DropdownMenu.Trigger>
<Tooltip.Portal>
<Tooltip.Content className="inline-flex h-7 select-none text-neutral-50 dark:text-neutral-950 items-center justify-center rounded-md bg-neutral-950 dark:bg-neutral-50 px-3.5 text-sm will-change-[transform,opacity] data-[state=delayed-open]:data-[side=bottom]:animate-slideUpAndFade data-[state=delayed-open]:data-[side=left]:animate-slideRightAndFade data-[state=delayed-open]:data-[side=right]:animate-slideLeftAndFade data-[state=delayed-open]:data-[side=top]:animate-slideDownAndFade">
Repost
{t("note.buttons.repost")}
<Tooltip.Arrow className="fill-neutral-950 dark:fill-neutral-50" />
</Tooltip.Content>
</Tooltip.Portal>
@ -96,7 +98,7 @@ export function NoteRepost() {
className="inline-flex items-center gap-3 px-3 text-sm font-medium rounded-lg h-9 text-black/70 hover:bg-black/10 hover:text-black focus:outline-none dark:text-white/70 dark:hover:bg-white/10 dark:hover:text-white"
>
<RepostIcon className="size-4" />
Repost
{t("note.buttons.repost")}
</button>
</DropdownMenu.Item>
<DropdownMenu.Item asChild>
@ -106,7 +108,7 @@ export function NoteRepost() {
className="inline-flex items-center gap-3 px-3 text-sm font-medium rounded-lg h-9 text-black/70 hover:bg-black/10 hover:text-black focus:outline-none dark:text-white/70 dark:hover:bg-white/10 dark:hover:text-white"
>
<ReplyIcon className="size-4" />
Quote
{t("note.buttons.quote")}
</button>
</DropdownMenu.Item>
</DropdownMenu.Content>

View File

@ -8,6 +8,7 @@ import * as Tooltip from "@radix-ui/react-tooltip";
import { QRCodeSVG } from "qrcode.react";
import { useState } from "react";
import CurrencyInput from "react-currency-input-field";
import { useTranslation } from "react-i18next";
import { toast } from "sonner";
import { useProfile } from "../../../hooks/useProfile";
import { useNoteContext } from "../provider";
@ -23,6 +24,7 @@ export function NoteZap() {
const [isLoading, setIsLoading] = useState(false);
const [invoice, setInvoice] = useState<string>(null);
const { t } = useTranslation();
const { user } = useProfile(event.pubkey);
const createZapRequest = async (instant?: boolean) => {
@ -99,7 +101,7 @@ export function NoteZap() {
</Tooltip.Trigger>
<Tooltip.Portal>
<Tooltip.Content className="inline-flex h-7 select-none text-neutral-50 dark:text-neutral-950 items-center justify-center rounded-md bg-neutral-950 dark:bg-neutral-50 px-3.5 text-sm will-change-[transform,opacity] data-[state=delayed-open]:data-[side=bottom]:animate-slideUpAndFade data-[state=delayed-open]:data-[side=left]:animate-slideRightAndFade data-[state=delayed-open]:data-[side=right]:animate-slideLeftAndFade data-[state=delayed-open]:data-[side=top]:animate-slideDownAndFade">
Zap
{t("note.zap.tooltip")}
<Tooltip.Arrow className="fill-neutral-950 dark:fill-neutral-50" />
</Tooltip.Content>
</Tooltip.Portal>
@ -124,7 +126,7 @@ export function NoteZap() {
</Dialog.Trigger>
<Tooltip.Portal>
<Tooltip.Content className="inline-flex h-7 select-none text-neutral-50 dark:text-neutral-950 items-center justify-center rounded-md bg-neutral-950 dark:bg-neutral-50 px-3.5 text-sm will-change-[transform,opacity] data-[state=delayed-open]:data-[side=bottom]:animate-slideUpAndFade data-[state=delayed-open]:data-[side=left]:animate-slideRightAndFade data-[state=delayed-open]:data-[side=right]:animate-slideLeftAndFade data-[state=delayed-open]:data-[side=top]:animate-slideDownAndFade">
Zap
{t("note.zap.tooltip")}
<Tooltip.Arrow className="fill-neutral-950 dark:fill-neutral-50" />
</Tooltip.Content>
</Tooltip.Portal>
@ -145,7 +147,7 @@ export function NoteZap() {
<div className="inline-flex items-center justify-center w-full px-5 py-3 shrink-0">
<div className="w-6" />
<Dialog.Title className="font-semibold text-center">
Send zap to{" "}
{t("note.zap.modalTitle")}{" "}
{user?.name ||
user?.displayName ||
displayNpub(event.pubkey, 16)}
@ -217,7 +219,7 @@ export function NoteZap() {
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
placeholder="Enter message (optional)"
placeholder={t("note.zap.messagePlaceholder")}
className="w-full resize-none rounded-lg border-transparent bg-neutral-100 px-3 py-3 !outline-none placeholder:text-neutral-600 focus:border-blue-500 focus:ring focus:ring-blue-200 dark:bg-neutral-950 dark:text-neutral-400"
/>
<div className="flex flex-col gap-2">
@ -227,10 +229,10 @@ export function NoteZap() {
className="inline-flex items-center justify-center w-full pb-[2px] font-semibold border-t rounded-lg border-neutral-900 dark:border-neutral-800 h-9 bg-neutral-950 text-neutral-50 dark:bg-neutral-900 hover:bg-neutral-900 dark:hover:bg-neutral-800"
>
{isCompleted
? "Zapped"
? t("note.zap.buttonFinish")
: isLoading
? "Processing..."
: "Zap"}
? t("note.zap.buttonLoading")
: t("note.zap.zap")}
</button>
</div>
</div>
@ -241,11 +243,11 @@ export function NoteZap() {
<QRCodeSVG value={invoice} size={256} />
</div>
<div className="flex flex-col items-center gap-1">
<h3 className="text-lg font-medium">Scan to zap</h3>
<h3 className="text-lg font-medium">
{t("note.zap.invoiceButton")}
</h3>
<span className="text-center text-sm text-neutral-600 dark:text-neutral-400">
You must use Bitcoin wallet which support Lightning
<br />
such as: Blue Wallet, Bitkit, Phoenix,...
{t("note.zap.invoiceFooter")}
</span>
</div>
</div>

View File

@ -1,7 +1,7 @@
import { NOSTR_MENTIONS } from "@lume/utils";
import { nanoid } from "nanoid";
import { nip19 } from "nostr-tools";
import { ReactNode, useMemo } from "react";
import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
import reactStringReplace from "react-string-replace";
import { useEvent } from "../../hooks/useEvent";
@ -13,6 +13,7 @@ export function NoteChild({
eventId,
isRoot,
}: { eventId: string; isRoot?: boolean }) {
const { t } = useTranslation();
const { isLoading, isError, data } = useEvent(eventId);
const richContent = useMemo(() => {
@ -91,7 +92,7 @@ export function NoteChild({
return (
<div className="relative flex gap-3">
<div className="relative flex-1 rounded-md bg-neutral-200 px-2 py-2 dark:bg-neutral-800">
Failed to fetch event
{t("note.error")}
</div>
</div>
);
@ -111,7 +112,7 @@ export function NoteChild({
<div className="absolute left-2 top-2 inline-flex items-center gap-1.5 font-semibold leading-tight">
<User.Name className="max-w-[10rem] truncate" />
<div className="font-normal text-neutral-700 dark:text-neutral-300">
{isRoot ? "posted:" : "replied:"}
{isRoot ? t("note.posted") : t("note.replied")}:
</div>
</div>
</User.Root>

View File

@ -1,6 +1,7 @@
import { PinIcon } from "@lume/icons";
import { COL_TYPES, NOSTR_MENTIONS } from "@lume/utils";
import { ReactNode, useMemo } from "react";
import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
import reactStringReplace from "react-string-replace";
import { useEvent } from "../../../hooks/useEvent";
@ -13,6 +14,7 @@ export function MentionNote({
eventId,
openable = true,
}: { eventId: string; openable?: boolean }) {
const { t } = useTranslation();
const { addColumn } = useColumnContext();
const { isLoading, isError, data } = useEvent(eventId);
@ -98,7 +100,7 @@ export function MentionNote({
contentEditable={false}
className="w-full p-3 my-1 rounded-lg cursor-default bg-neutral-100 dark:bg-neutral-900"
>
Failed to fetch event.
{t("note.error")}
</div>
);
}
@ -127,7 +129,7 @@ export function MentionNote({
to={`/events/${data.id}`}
className="text-sm text-blue-500 hover:text-blue-600"
>
Show more
{t("note.showMore")}
</Link>
<button
type="button"

View File

@ -1,5 +1,6 @@
import { COL_TYPES } from "@lume/utils";
import * as DropdownMenu from "@radix-ui/react-dropdown-menu";
import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
import { useArk } from "../../../hooks/useArk";
import { useProfile } from "../../../hooks/useProfile";
@ -10,6 +11,7 @@ export function MentionUser({ pubkey }: { pubkey: string }) {
const cleanPubkey = ark.getCleanPubkey(pubkey);
const { isLoading, isError, user } = useProfile(pubkey);
const { t } = useTranslation();
const { addColumn } = useColumnContext();
return (
@ -27,7 +29,7 @@ export function MentionUser({ pubkey }: { pubkey: string }) {
to={`/users/${cleanPubkey}`}
className="inline-flex items-center gap-3 px-3 text-sm font-medium rounded-lg h-9 text-black/70 hover:bg-black/10 hover:text-black focus:outline-none dark:text-white/70 dark:hover:bg-white/10 dark:hover:text-white"
>
View profile
{t("note.buttons.viewProfile")}
</Link>
</DropdownMenu.Item>
<DropdownMenu.Item asChild>
@ -36,13 +38,13 @@ export function MentionUser({ pubkey }: { pubkey: string }) {
onClick={async () =>
await addColumn({
kind: COL_TYPES.user,
title: user?.name || user?.displayName || "Profile",
title: user?.name || user?.displayName || "User",
content: cleanPubkey,
})
}
className="inline-flex items-center gap-3 px-3 text-sm font-medium rounded-lg h-9 text-black/70 hover:bg-black/10 hover:text-black focus:outline-none dark:text-white/70 dark:hover:bg-white/10 dark:hover:text-white"
>
Pin
{t("note.buttons.pin")}
</button>
</DropdownMenu.Item>
</DropdownMenu.Content>

View File

@ -5,6 +5,7 @@ import { writeText } from "@tauri-apps/plugin-clipboard-manager";
import { nip19 } from "nostr-tools";
import { type EventPointer } from "nostr-tools/lib/types/nip19";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { Link, useNavigate } from "react-router-dom";
import { toast } from "sonner";
import { useColumnContext } from "../column/provider";
@ -13,7 +14,10 @@ import { useNoteContext } from "./provider";
export function NoteMenu() {
const event = useNoteContext();
const navigate = useNavigate();
const { t } = useTranslation();
const { addColumn } = useColumnContext();
const [open, setOpen] = useState(false);
const copyID = async () => {
@ -67,7 +71,7 @@ export function NoteMenu() {
onClick={() => copyLink()}
className="inline-flex items-center gap-3 px-3 text-sm font-medium rounded-lg h-9 text-black/70 hover:bg-black/10 hover:text-black focus:outline-none dark:text-white/70 dark:hover:bg-white/10 dark:hover:text-white"
>
View thread
{t("note.menu.viewThread")}
</button>
</DropdownMenu.Item>
<DropdownMenu.Item asChild>
@ -76,7 +80,7 @@ export function NoteMenu() {
onClick={() => navigate(`/events/${event.id}`)}
className="inline-flex items-center gap-3 px-3 text-sm font-medium rounded-lg h-9 text-black/70 hover:bg-black/10 hover:text-black focus:outline-none dark:text-white/70 dark:hover:bg-white/10 dark:hover:text-white"
>
Copy shareable link
{t("note.menu.copyLink")}
</button>
</DropdownMenu.Item>
<DropdownMenu.Item asChild>
@ -85,7 +89,7 @@ export function NoteMenu() {
onClick={() => copyID()}
className="inline-flex items-center gap-3 px-3 text-sm font-medium rounded-lg h-9 text-black/70 hover:bg-black/10 hover:text-black focus:outline-none dark:text-white/70 dark:hover:bg-white/10 dark:hover:text-white"
>
Copy note ID
{t("note.menu.copyNoteId")}
</button>
</DropdownMenu.Item>
<DropdownMenu.Item asChild>
@ -94,7 +98,7 @@ export function NoteMenu() {
onClick={() => copyNpub()}
className="inline-flex items-center gap-3 px-3 text-sm font-medium rounded-lg h-9 text-black/70 hover:bg-black/10 hover:text-black focus:outline-none dark:text-white/70 dark:hover:bg-white/10 dark:hover:text-white"
>
Copy author ID
{t("note.menu.copyAuthorId")}
</button>
</DropdownMenu.Item>
<DropdownMenu.Item asChild>
@ -102,7 +106,7 @@ export function NoteMenu() {
to={`/users/${event.pubkey}`}
className="inline-flex items-center gap-3 px-3 text-sm font-medium rounded-lg h-9 text-black/70 hover:bg-black/10 hover:text-black focus:outline-none dark:text-white/70 dark:hover:bg-white/10 dark:hover:text-white"
>
View author
{t("note.menu.viewAuthor")}
</Link>
</DropdownMenu.Item>
<DropdownMenu.Item asChild>
@ -117,7 +121,7 @@ export function NoteMenu() {
}
className="inline-flex items-center gap-3 px-3 text-sm font-medium rounded-lg h-9 text-black/70 hover:bg-black/10 hover:text-black focus:outline-none dark:text-white/70 dark:hover:bg-white/10 dark:hover:text-white"
>
Pin author
{t("note.menu.pinAuthor")}
</button>
</DropdownMenu.Item>
<DropdownMenu.Separator className="h-px my-1 bg-black/10 dark:bg-white/10" />
@ -127,7 +131,7 @@ export function NoteMenu() {
onClick={() => copyRaw()}
className="inline-flex items-center gap-3 px-3 text-sm font-medium rounded-lg h-9 text-black/70 hover:bg-black/10 hover:text-black focus:outline-none dark:text-white/70 dark:hover:bg-white/10 dark:hover:text-white"
>
Copy raw event
{t("note.menu.copyRaw")}
</button>
</DropdownMenu.Item>
<DropdownMenu.Item asChild>
@ -136,7 +140,7 @@ export function NoteMenu() {
onClick={muteUser}
className="inline-flex items-center gap-3 px-3 text-sm font-medium text-red-500 rounded-lg h-9 hover:bg-red-500 hover:text-red-50 focus:outline-none"
>
Mute
{t("note.menu.mute")}
</button>
</DropdownMenu.Item>
</DropdownMenu.Content>

View File

@ -1,4 +1,5 @@
import { useQuery } from "@tanstack/react-query";
import { useTranslation } from "react-i18next";
import { useArk } from "../../hooks/useArk";
import { AppHandler } from "./appHandler";
import { useNoteContext } from "./provider";
@ -7,6 +8,7 @@ export function NIP89({ className }: { className?: string }) {
const ark = useArk();
const event = useNoteContext();
const { t } = useTranslation();
const { isLoading, isError, data } = useQuery({
queryKey: ["app-recommend", event.id],
queryFn: () => {
@ -33,7 +35,7 @@ export function NIP89({ className }: { className?: string }) {
<div className="flex flex-col rounded-lg bg-neutral-100 dark:bg-neutral-900">
<div className="inline-flex items-center justify-between h-10 px-3 border-b shrink-0 border-neutral-200 dark:border-neutral-800">
<p className="text-sm font-medium text-amber-400">
Lume isn't support this event
{t("nip89.unsupported")}
</p>
<p className="text-sm text-neutral-600 dark:text-neutral-400">
{event.kind}
@ -41,10 +43,10 @@ export function NIP89({ className }: { className?: string }) {
</div>
<div className="flex flex-col flex-1 gap-2 px-3 py-3">
<span className="text-sm font-medium uppercase text-neutral-600 dark:text-neutral-400">
Open with
{t("nip89.openWith")}
</span>
{data.map((item, index) => (
<AppHandler key={item[1] + index} tag={item} />
{data.map((item) => (
<AppHandler key={item[1]} tag={item} />
))}
</div>
</div>

View File

@ -3,6 +3,7 @@ import { NDKEventWithReplies } from "@lume/types";
import { cn } from "@lume/utils";
import * as Collapsible from "@radix-ui/react-collapsible";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { Note } from "..";
import { ChildReply } from "./childReply";
@ -11,6 +12,7 @@ export function Reply({
}: {
event: NDKEventWithReplies;
}) {
const [t] = useTranslation();
const [open, setOpen] = useState(false);
return (
@ -30,7 +32,9 @@ export function Reply({
className={cn("size-5", open ? "rotate-180 transform" : "")}
/>
{`${event.replies?.length} ${
event.replies?.length === 1 ? "reply" : "replies"
event.replies?.length === 1
? t("note.reply.single")
: t("note.reply.plural")
}`}
</div>
</Collapsible.Trigger>

View File

@ -2,6 +2,7 @@ import { RepostIcon } from "@lume/icons";
import { cn } from "@lume/utils";
import { NDKEvent, NostrEvent } from "@nostr-dev-kit/ndk";
import { useQuery } from "@tanstack/react-query";
import { useTranslation } from "react-i18next";
import { Note } from "..";
import { useArk } from "../../../hooks/useArk";
import { User } from "../../user";
@ -12,6 +13,7 @@ export function RepostNote({
}: { event: NDKEvent; className?: string }) {
const ark = useArk();
const { t } = useTranslation();
const {
isLoading,
isError,
@ -51,7 +53,7 @@ export function RepostNote({
<User.Avatar className="size-6 shrink-0 rounded object-cover" />
<div className="inline-flex items-baseline gap-1">
<User.Name className="font-medium text-neutral-900 dark:text-neutral-100" />
<span className="text-blue-500">reposted</span>
<span className="text-blue-500">{t("note.reposted")}</span>
</div>
</div>
</User.Root>
@ -59,10 +61,6 @@ export function RepostNote({
<div className="px-3 mb-3 select-text">
<div className="flex flex-col items-start justify-start px-3 py-3 bg-red-100 rounded-lg dark:bg-red-900">
<p className="text-red-500">Failed to get event</p>
<p className="text-sm">
You can consider enable Outbox in Settings for better event
discovery.
</p>
</div>
</div>
</Note.Root>
@ -85,7 +83,7 @@ export function RepostNote({
<User.Avatar className="size-6 shrink-0 rounded object-cover" />
<div className="inline-flex items-baseline gap-1">
<User.Name className="font-medium text-neutral-900 dark:text-neutral-100" />
<span className="text-blue-500">reposted</span>
<span className="text-blue-500">{t("note.reposted")}</span>
</div>
</div>
</User.Root>

View File

@ -1,5 +1,6 @@
import { PinIcon } from "@lume/icons";
import { COL_TYPES, cn } from "@lume/utils";
import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
import { Note } from ".";
import { useArk } from "../../hooks/useArk";
@ -18,6 +19,7 @@ export function NoteThread({
tags: event.tags,
});
const { t } = useTranslation();
const { addColumn } = useColumnContext();
if (!thread) return null;
@ -36,7 +38,7 @@ export function NoteThread({
to={`/events/${thread?.rootEventId || thread?.replyEventId}`}
className="self-start text-blue-500 hover:text-blue-600"
>
Show thread
{t("note.showThread")}
</Link>
<button
type="button"

View File

@ -1,6 +1,7 @@
import { LoaderIcon } from "@lume/icons";
import { cn } from "@lume/utils";
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { useArk } from "../../hooks/useArk";
export function UserFollowButton({
@ -9,6 +10,7 @@ export function UserFollowButton({
}: { target: string; className?: string }) {
const ark = useArk();
const [t] = useTranslation();
const [loading, setLoading] = useState(false);
const [followed, setFollowed] = useState(false);
@ -43,14 +45,14 @@ export function UserFollowButton({
type="button"
disabled={loading}
onClick={toggleFollow}
className={cn("", className)}
className={cn("w-max", className)}
>
{loading ? (
<LoaderIcon className="size-4 animate-spin" />
) : followed ? (
"Unfollow"
t("user.unfollow")
) : (
"Follow"
t("user.follow")
)}
</button>
);

View File

@ -1,4 +1,4 @@
import { UnverifiedIcon, VerifiedIcon } from "@lume/icons";
import { VerifiedIcon } from "@lume/icons";
import { cn, displayNpub } from "@lume/utils";
import { useQuery } from "@tanstack/react-query";
import { useArk } from "../../hooks/useArk";

View File

@ -1,3 +1,4 @@
import { NDKUserProfile } from "@nostr-dev-kit/ndk";
import { useQuery, useQueryClient } from "@tanstack/react-query";
import { useArk } from "./useArk";
@ -20,7 +21,7 @@ export function useProfile(pubkey: string) {
return profile;
},
initialData: () => {
return queryClient.getQueryData(["user", pubkey]);
return queryClient.getQueryData(["user", pubkey]) as NDKUserProfile;
},
refetchOnMount: false,
refetchOnWindowFocus: false,

View File

@ -26,6 +26,7 @@
"react-dom": "^18.2.0",
"react-hook-form": "^7.49.3",
"react-hotkeys-hook": "^4.4.4",
"react-i18next": "^14.0.1",
"react-router-dom": "^6.21.3",
"slate": "^0.101.5",
"slate-react": "^0.101.6",

View File

@ -5,6 +5,7 @@ import * as Avatar from "@radix-ui/react-avatar";
import * as DropdownMenu from "@radix-ui/react-dropdown-menu";
import { minidenticon } from "minidenticons";
import { useMemo } from "react";
import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
import { Logout } from "./logout";
@ -19,6 +20,7 @@ export function ActiveAccount() {
[],
);
const { t } = useTranslation();
const { user } = useProfile(ark.account.pubkey);
return (
@ -62,7 +64,7 @@ export function ActiveAccount() {
className="inline-flex items-center gap-3 px-3 text-sm font-medium rounded-lg h-9 text-black/70 hover:bg-black/10 hover:text-black focus:outline-none dark:text-white/70 dark:hover:bg-white/10 dark:hover:text-white"
>
<UserIcon className="size-4" />
Edit profile
{t("user.editProfile")}
</Link>
</DropdownMenu.Item>
<DropdownMenu.Item asChild>
@ -71,7 +73,7 @@ export function ActiveAccount() {
className="inline-flex items-center gap-3 px-3 text-sm font-medium rounded-lg h-9 text-black/70 hover:bg-black/10 hover:text-black focus:outline-none dark:text-white/70 dark:hover:bg-white/10 dark:hover:text-white"
>
<SettingsIcon className="size-4" />
Settings
{t("user.settings")}
</Link>
</DropdownMenu.Item>
<DropdownMenu.Separator className="h-px my-1 bg-black/10 dark:bg-white/10" />

View File

@ -3,6 +3,7 @@ import { LogoutIcon } from "@lume/icons";
import { useStorage } from "@lume/storage";
import * as AlertDialog from "@radix-ui/react-alert-dialog";
import { useQueryClient } from "@tanstack/react-query";
import { useTranslation } from "react-i18next";
import { useNavigate } from "react-router-dom";
import { toast } from "sonner";
@ -12,6 +13,8 @@ export function Logout() {
const queryClient = useQueryClient();
const navigate = useNavigate();
const { t } = useTranslation();
const logout = async () => {
try {
// logout
@ -38,7 +41,7 @@ export function Logout() {
className="inline-flex items-center gap-3 px-3 text-sm font-medium rounded-lg h-9 text-black/70 hover:bg-black/10 hover:text-black focus:outline-none dark:text-white/70 dark:hover:bg-white/10 dark:hover:text-white"
>
<LogoutIcon className="size-4" />
Logout
{t("user.logout")}
</button>
</AlertDialog.Trigger>
<AlertDialog.Portal>
@ -47,11 +50,10 @@ export function Logout() {
<div className="relative h-min w-full max-w-md rounded-xl bg-neutral-100 dark:bg-neutral-900">
<div className="flex flex-col gap-1 border-b border-white/5 px-5 py-4">
<AlertDialog.Title className="text-lg font-semibold text-neutral-900 dark:text-neutral-100">
Are you sure!
{t("user.logoutConfirmTitle")}
</AlertDialog.Title>
<AlertDialog.Description className="text-sm leading-tight text-neutral-600 dark:text-neutral-400">
You can always log back in at any time. If you just want to
switch accounts, you can do that by adding an existing account.
{t("user.logoutConfirmSubtitle")}
</AlertDialog.Description>
</div>
<div className="flex justify-end gap-2 px-5 py-3">
@ -60,7 +62,7 @@ export function Logout() {
type="button"
className="inline-flex h-9 items-center justify-center rounded-lg px-4 text-sm font-medium text-neutral-900 outline-none hover:bg-neutral-200 dark:text-neutral-100 dark:hover:bg-neutral-800"
>
Cancel
{t("global.cancel")}
</button>
</AlertDialog.Cancel>
<AlertDialog.Action asChild>
@ -69,7 +71,7 @@ export function Logout() {
onClick={() => logout()}
className="inline-flex h-9 items-center justify-center rounded-lg bg-red-500 px-4 text-sm font-medium text-white outline-none hover:bg-red-600"
>
Logout
{t("user.logout")}
</button>
</AlertDialog.Action>
</div>

View File

@ -1,6 +1,7 @@
import { useArk } from "@lume/ark";
import { LoaderIcon } from "@lume/icons";
import { Dispatch, SetStateAction, useState } from "react";
import { useTranslation } from "react-i18next";
import { toast } from "sonner";
export function AvatarUploadButton({
@ -9,6 +10,8 @@ export function AvatarUploadButton({
setPicture: Dispatch<SetStateAction<string>>;
}) {
const ark = useArk();
const [t] = useTranslation();
const [loading, setLoading] = useState(false);
const uploadAvatar = async () => {
@ -36,7 +39,7 @@ export function AvatarUploadButton({
{loading ? (
<LoaderIcon className="size-4 animate-spin" />
) : (
"Change avatar"
t("user.avatarButton")
)}
</button>
);

View File

@ -6,6 +6,7 @@ import { COL_TYPES, cn, editorValueAtom } from "@lume/utils";
import { NDKEvent, NDKKind } from "@nostr-dev-kit/ndk";
import { useAtom } from "jotai";
import { useEffect, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import {
Descendant,
Editor,
@ -200,6 +201,7 @@ export function EditorForm() {
withMentions(withNostrEvent(withImages(withReact(createEditor())))),
);
const { t } = useTranslation();
const { addColumn } = useColumnContext();
const filters = contacts
@ -247,9 +249,7 @@ export function EditorForm() {
const publish = await event.publish();
if (publish) {
toast.success(
`Event has been published successfully to ${publish.size} relays.`,
);
toast.success(t("editor.successMessage"));
// add current post as column thread
addColumn({
@ -321,7 +321,7 @@ export function EditorForm() {
>
<div className="flex items-center justify-between h-16 pl-7 pr-3 border-b shrink-0 border-neutral-100 dark:border-neutral-900 bg-neutral-50 dark:bg-neutral-950">
<div>
<h3 className="font-medium">New Post</h3>
<h3 className="font-medium">{t("editor.title")}</h3>
</div>
<div className="flex items-center">
<div className="inline-flex items-center gap-2">
@ -336,7 +336,7 @@ export function EditorForm() {
{loading ? (
<LoaderIcon className="size-4 animate-spin" />
) : (
"Post"
t("global.post")
)}
</button>
</div>
@ -349,7 +349,7 @@ export function EditorForm() {
autoCorrect="none"
spellCheck={false}
renderElement={(props) => <Element {...props} />}
placeholder="What are you up to?"
placeholder={t("editor.placeholder")}
className="focus:outline-none"
/>
{target && filters.length > 0 && (

View File

@ -6,6 +6,7 @@ import { cn } from "@lume/utils";
import { NDKEvent, NDKKind } from "@nostr-dev-kit/ndk";
import { Portal } from "@radix-ui/react-dropdown-menu";
import { useEffect, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import {
Descendant,
Editor,
@ -207,6 +208,8 @@ export function ReplyForm({
withMentions(withNostrEvent(withImages(withReact(createEditor())))),
);
const { t } = useTranslation();
const filters = contacts
?.filter((c) => c?.name?.toLowerCase().startsWith(search.toLowerCase()))
?.slice(0, 10);
@ -334,7 +337,7 @@ export function ReplyForm({
autoCorrect="none"
spellCheck={false}
renderElement={(props) => <Element {...props} />}
placeholder="Post your reply"
placeholder={t("editor.replyPlaceholder")}
className="focus:outline-none h-28"
/>
{target && filters.length > 0 && (
@ -383,7 +386,7 @@ export function ReplyForm({
{loading ? (
<LoaderIcon className="size-4 animate-spin" />
) : (
"Post"
t("global.post")
)}
</button>
</div>

View File

@ -1,11 +1,14 @@
import { InfoIcon } from "@lume/icons";
import { cn } from "@lume/utils";
import { useTranslation } from "react-i18next";
export function EmptyFeed({
text,
subtext,
className,
}: { text?: string; subtext?: string; className?: string }) {
const { t } = useTranslation();
return (
<div
className={cn(
@ -16,12 +19,10 @@ export function EmptyFeed({
<InfoIcon className="size-8 text-blue-500" />
<div className="text-center">
<p className="font-semibold text-lg">
{text ? text : "This feed is empty"}
{text ? text : t("global.emptyFeedTitle")}
</p>
<p className="leading-tight text-sm">
{subtext
? subtext
: "You can follow more users to build up your timeline"}
{subtext ? subtext : t("global.emptyFeedSubtitle")}
</p>
</div>
</div>

View File

@ -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>

View File

@ -10,6 +10,7 @@ import {
import { NDKCacheUserProfile } from "@lume/types";
import { cn } from "@lume/utils";
import { useTranslation } from "react-i18next";
type MentionListRef = {
onKeyDown: (props: { event: Event }) => boolean;
@ -22,6 +23,7 @@ const List = (
},
ref: Ref<unknown>,
) => {
const [t] = useTranslation();
const [selectedIndex, setSelectedIndex] = useState(0);
const selectItem = (index) => {
@ -107,7 +109,9 @@ const List = (
</button>
))
) : (
<div className="text-center text-sm font-medium">No result</div>
<div className="text-center text-sm font-medium">
{t("global.noResult")}
</div>
)}
</div>
);

View File

@ -1,74 +0,0 @@
import { UnverifiedIcon, VerifiedIcon } from "@lume/icons";
import { cn } from "@lume/utils";
import { useQuery } from "@tanstack/react-query";
import { fetch } from "@tauri-apps/plugin-http";
import { memo } from "react";
interface NIP05 {
names: {
[key: string]: string;
};
}
export const NIP05 = memo(function NIP05({
pubkey,
nip05,
className,
}: {
pubkey: string;
nip05: string;
className?: string;
}) {
const { status, data } = useQuery({
queryKey: ["nip05", nip05],
queryFn: async ({ signal }: { signal: AbortSignal }) => {
try {
const localPath = nip05.split("@")[0];
const service = nip05.split("@")[1];
const verifyURL = `https://${service}/.well-known/nostr.json?name=${localPath}`;
const res = await fetch(verifyURL, {
method: "GET",
headers: {
"Content-Type": "application/json; charset=utf-8",
},
signal,
});
if (!res.ok)
throw new Error(`Failed to fetch NIP-05 service: ${nip05}`);
const data: NIP05 = await res.json();
if (data.names) {
if (data.names[localPath.toLowerCase()] === pubkey) return true;
if (data.names[localPath] === pubkey) return true;
return false;
}
return false;
} catch (e) {
throw new Error(`Failed to verify NIP-05, error: ${e}`);
}
},
refetchOnMount: false,
refetchOnReconnect: false,
refetchOnWindowFocus: false,
staleTime: Infinity,
});
if (status === "pending") {
<div className="h-4 w-4 animate-pulse rounded-full bg-neutral-100 dark:bg-neutral-900" />;
}
return (
<div className="inline-flex items-center gap-1">
<p className={cn("text-sm font-medium", className)}>
{nip05.startsWith("_@") ? nip05.replace("_@", "") : nip05}
</p>
{data === true ? (
<VerifiedIcon className="h-4 w-4 text-teal-500" />
) : (
<UnverifiedIcon className="h-4 w-4 text-red-500" />
)}
</div>
);
});

View File

@ -5,12 +5,14 @@ import { useQueryClient } from "@tanstack/react-query";
import { motion } from "framer-motion";
import { useSetAtom } from "jotai";
import { useState } from "react";
import { useTranslation } from "react-i18next";
export function OnboardingFinishScreen() {
const storage = useStorage();
const queryClient = useQueryClient();
const setOnboarding = useSetAtom(onboardingAtom);
const [t] = useTranslation();
const [loading, setLoading] = useState(false);
const finish = async () => {
@ -33,9 +35,9 @@ export function OnboardingFinishScreen() {
>
<CheckIcon className="size-12 text-teal-500" />
<div className="text-center">
<p className="text-lg font-medium">Profile setup complete!</p>
<p className="text-lg font-medium">{t("onboarding.finish.title")}</p>
<p className="leading-tight text-neutral-600 dark:text-neutral-400">
You can exit the setup here and start using Lume.
{t("onboarding.finish.subtitle")}
</p>
</div>
<div className="mt-4 flex flex-col gap-2 items-center">
@ -44,7 +46,11 @@ export function OnboardingFinishScreen() {
onClick={finish}
className="inline-flex items-center justify-center gap-2 w-44 font-medium h-11 rounded-xl bg-blue-100 text-blue-500 hover:bg-blue-200 dark:bg-blue-900 dark:text-blue-500 dark:hover:bg-blue-800"
>
{loading ? <LoaderIcon className="size-4 animate-spin" /> : "Close"}
{loading ? (
<LoaderIcon className="size-4 animate-spin" />
) : (
t("global.close")
)}
</button>
<a
href="https://github.com/luminous-devs/lume/issues"
@ -52,7 +58,7 @@ export function OnboardingFinishScreen() {
className="inline-flex items-center justify-center gap-2 w-44 px-5 font-medium h-11 rounded-xl hover:bg-neutral-100 dark:hover:bg-neutral-900 text-neutral-700 dark:text-neutral-600"
rel="noreferrer"
>
Report a issue
{t("onboarding.finish.report")}
</a>
</div>
</motion.div>

View File

@ -1,310 +0,0 @@
import { User, useArk } from "@lume/ark";
import {
ArrowLeftIcon,
CancelIcon,
ChevronDownIcon,
LoaderIcon,
PlusIcon,
} from "@lume/icons";
import { cn } from "@lume/utils";
import * as Accordion from "@radix-ui/react-accordion";
import { useQuery } from "@tanstack/react-query";
import { motion } from "framer-motion";
import { nip19 } from "nostr-tools";
import { useState } from "react";
import { useNavigate } from "react-router-dom";
import { toast } from "sonner";
const POPULAR_USERS = [
"npub180cvv07tjdrrgpa0j7j7tmnyl2yr6yr7l8j4s3evf6u64th6gkwsyjh6w6",
"npub1sg6plzptd64u62a878hep2kev88swjh3tw00gjsfl8f237lmu63q0uf63m",
"npub1xtscya34g58tk0z605fvr788k263gsu6cy9x0mhnm87echrgufzsevkk5s",
"npub1gcxzte5zlkncx26j68ez60fzkvtkm9e0vrwdcvsjakxf9mu9qewqlfnj5z",
"npub1az9xj85cmxv8e9j9y80lvqp97crsqdu2fpu3srwthd99qfu9qsgstam8y8",
"npub1a2cww4kn9wqte4ry70vyfwqyqvpswksna27rtxd8vty6c74era8sdcw83a",
"npub168ghgug469n4r2tuyw05dmqhqv5jcwm7nxytn67afmz8qkc4a4zqsu2dlc",
"npub133vj8ycevdle0cq8mtgddq0xtn34kxkwxvak983dx0u5vhqnycyqj6tcza",
"npub18ams6ewn5aj2n3wt2qawzglx9mr4nzksxhvrdc4gzrecw7n5tvjqctp424",
"npub1r0rs5q2gk0e3dk3nlc7gnu378ec6cnlenqp8a3cjhyzu6f8k5sgs4sq9ac",
"npub1prya33fnqerq0fljwjtp77ehtu7jlsjt5ydhwveuwmqdsdm6k8esk42xcv",
"npub19mduaf5569jx9xz555jcx3v06mvktvtpu0zgk47n4lcpjsz43zzqhj6vzk",
];
const LUME_USERS = [
"npub1zfss807aer0j26mwp2la0ume0jqde3823rmu97ra6sgyyg956e0s6xw445",
];
export function OnboardingFollowScreen() {
const ark = useArk();
const navigate = useNavigate();
const { isLoading, isError, data } = useQuery({
queryKey: ["trending-users"],
queryFn: async ({ signal }: { signal: AbortSignal }) => {
const res = await fetch("https://api.nostr.band/v0/trending/profiles", {
signal,
});
if (!res.ok) {
throw new Error("Failed to fetch trending users from nostr.band API.");
}
return res.json();
},
});
const [loading, setLoading] = useState(false);
const [follows, setFollows] = useState<string[]>([]);
// toggle follow state
const toggleFollow = (pubkey: string) => {
const arr = follows.includes(pubkey)
? follows.filter((i) => i !== pubkey)
: [...follows, pubkey];
setFollows(arr);
};
const submit = async () => {
try {
setLoading(true);
if (!follows.length) return navigate("/finish");
const publish = await ark.newContactList({
tags: follows.map((item) => {
if (item.startsWith("npub1"))
return ["p", nip19.decode(item).data as string];
return ["p", item];
}),
});
if (publish) {
setLoading(false);
return navigate("/finish");
}
} catch (e) {
setLoading(false);
toast.error(String(e));
}
};
return (
<motion.div className="w-full h-full flex flex-col">
<div className="h-12 shrink-0 px-8 border-b border-neutral-100 dark:border-neutral-900 flex font-medium text-neutral-700 dark:text-neutral-600 w-full items-center">
Dive into the nostrverse
</div>
<div className="w-full flex-1 mb-0 min-h-0 flex flex-col justify-between h-full">
<motion.div
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: -20 }}
className="flex-1 overflow-y-auto px-8"
>
<p className="leading-snug text-neutral-700 dark:text-neutral-500 my-4">
Nostr is fun when we are together. Try following some users that
interest you to build up your timeline.
</p>
<Accordion.Root type="single" defaultValue="recommended" collapsible>
<Accordion.Item
value="recommended"
className="mb-3 overflow-hidden rounded-xl"
>
<Accordion.Trigger className="flex h-11 w-full items-center justify-between px-3 rounded-t-xl font-medium bg-neutral-50 dark:bg-neutral-950">
Recommended
<ChevronDownIcon className="size-4" />
</Accordion.Trigger>
<Accordion.Content>
<div className="flex w-full flex-col overflow-y-auto rounded-b-xl px-3 bg-neutral-100 dark:bg-neutral-900">
{POPULAR_USERS.map((pubkey) => (
<div
key={pubkey}
className="flex h-max w-full shrink-0 flex-col my-3 gap-4 overflow-hidden rounded-lg bg-white dark:bg-black"
>
<User.Provider pubkey={pubkey}>
<User.Root>
<User.Cover className="h-20 w-full rounded-t-lg" />
<div className="flex h-full w-full flex-col gap-2.5 px-3 -mt-6">
<User.Avatar className="size-10 shrink-0 rounded-lg" />
<div className="flex flex-col items-start text-start">
<User.Name className="max-w-[15rem] truncate text-lg font-semibold leadning-tight" />
<User.About className="break-p text-neutral-700 dark:text-neutral-600 max-w-none select-text whitespace-pre-line" />
</div>
</div>
</User.Root>
</User.Provider>
<div className="h-16 shrink-0 px-3 flex items-center border-t border-neutral-100 dark:border-neutral-900">
<button
type="button"
onClick={() => toggleFollow(pubkey)}
className={cn(
"inline-flex h-9 shrink-0 w-28 items-center justify-center gap-1 rounded-lg font-medium",
follows.includes(pubkey)
? "text-red-500 bg-red-100 hover:text-white hover:bg-red-500"
: "text-blue-500 bg-blue-100 hover:text-white hover:bg-blue-500",
)}
>
{follows.includes(pubkey) ? (
<>
<CancelIcon className="size-4" />
Unfollow
</>
) : (
<>
<PlusIcon className="size-4" />
Follow
</>
)}
</button>
</div>
</div>
))}
</div>
</Accordion.Content>
</Accordion.Item>
<Accordion.Item
value="trending"
className="mb-3 overflow-hidden rounded-xl"
>
<Accordion.Trigger className="flex h-11 w-full items-center justify-between px-3 rounded-t-xl font-medium bg-neutral-50 dark:bg-neutral-950">
Trending users
<ChevronDownIcon className="size-4" />
</Accordion.Trigger>
<Accordion.Content>
<div className="flex w-full flex-col overflow-y-auto rounded-b-xl px-3 bg-neutral-100 dark:bg-neutral-900">
{isLoading ? (
<div className="flex h-full w-full items-center justify-center">
<LoaderIcon className="size-4 animate-spin" />
</div>
) : isError ? (
<div className="flex h-full w-full items-center justify-center">
Error. Cannot get trending users
</div>
) : (
data?.profiles.map((item: { pubkey: string }) => (
<div
key={item.pubkey}
className="flex h-max w-full shrink-0 flex-col my-3 gap-4 overflow-hidden rounded-lg bg-white dark:bg-black"
>
<User.Provider pubkey={item.pubkey}>
<User.Root>
<User.Cover className="h-20 w-full rounded-t-lg" />
<div className="flex h-full w-full flex-col gap-2.5 px-3 -mt-6">
<User.Avatar className="size-10 shrink-0 rounded-lg" />
<div className="flex flex-col items-start text-start">
<User.Name className="max-w-[15rem] truncate text-lg font-semibold leadning-tight" />
<User.About className="break-p text-neutral-700 dark:text-neutral-600 max-w-none select-text whitespace-pre-line" />
</div>
</div>
</User.Root>
</User.Provider>
<div className="h-16 shrink-0 px-3 flex items-center border-t border-neutral-100 dark:border-neutral-900">
<button
type="button"
onClick={() => toggleFollow(item.pubkey)}
className={cn(
"inline-flex h-9 shrink-0 w-28 items-center justify-center gap-1 rounded-lg font-medium",
follows.includes(item.pubkey)
? "text-red-500 bg-red-100 hover:text-white hover:bg-red-500"
: "text-blue-500 bg-blue-100 hover:text-white hover:bg-blue-500",
)}
>
{follows.includes(item.pubkey) ? (
<>
<CancelIcon className="size-4" />
Unfollow
</>
) : (
<>
<PlusIcon className="size-4" />
Follow
</>
)}
</button>
</div>
</div>
))
)}
</div>
</Accordion.Content>
</Accordion.Item>
<Accordion.Item
value="lume"
className="mb-3 overflow-hidden rounded-xl"
>
<Accordion.Trigger className="flex h-11 w-full items-center justify-between px-3 rounded-t-xl font-medium bg-neutral-50 dark:bg-neutral-950">
Lume HQ
<ChevronDownIcon className="size-4" />
</Accordion.Trigger>
<Accordion.Content>
<div className="flex w-full flex-col overflow-y-auto rounded-b-xl px-3 bg-neutral-100 dark:bg-neutral-900">
{LUME_USERS.map((pubkey) => (
<div
key={pubkey}
className="flex h-max w-full shrink-0 flex-col my-3 gap-4 overflow-hidden rounded-lg bg-white dark:bg-black"
>
<User.Provider pubkey={pubkey}>
<User.Root>
<User.Cover className="h-20 w-full rounded-t-lg" />
<div className="flex h-full w-full flex-col gap-2.5 px-3 -mt-6">
<User.Avatar className="size-10 shrink-0 rounded-lg" />
<div className="flex flex-col items-start text-start">
<User.Name className="max-w-[15rem] truncate text-lg font-semibold leadning-tight" />
<User.About className="break-p text-neutral-700 dark:text-neutral-600 max-w-none select-text whitespace-pre-line" />
</div>
</div>
</User.Root>
</User.Provider>
<div className="h-16 shrink-0 px-3 flex items-center border-t border-neutral-100 dark:border-neutral-900">
<button
type="button"
onClick={() => toggleFollow(pubkey)}
className={cn(
"inline-flex h-9 shrink-0 w-28 items-center justify-center gap-1 rounded-lg font-medium",
follows.includes(pubkey)
? "text-red-500 bg-red-100 hover:text-white hover:bg-red-500"
: "text-blue-500 bg-blue-100 hover:text-white hover:bg-blue-500",
)}
>
{follows.includes(pubkey) ? (
<>
<CancelIcon className="size-4" />
Unfollow
</>
) : (
<>
<PlusIcon className="size-4" />
Follow
</>
)}
</button>
</div>
</div>
))}
</div>
</Accordion.Content>
</Accordion.Item>
</Accordion.Root>
</motion.div>
<div className="h-16 w-full shrink-0 flex items-center px-8 justify-center gap-2 border-t border-neutral-100 dark:border-neutral-900 bg-neutral-50 dark:bg-neutral-950">
<button
type="button"
onClick={() => navigate(-1)}
className="inline-flex h-9 flex-1 gap-2 shrink-0 items-center justify-center rounded-lg bg-neutral-100 font-medium dark:bg-neutral-900 dark:hover:bg-neutral-800 hover:bg-blue-200"
>
<ArrowLeftIcon className="size-4" />
Back
</button>
<button
type="button"
onClick={() => submit()}
className="inline-flex h-9 flex-1 shrink-0 items-center justify-center rounded-lg bg-blue-500 font-semibold text-white hover:bg-blue-600 disabled:opacity-50"
>
{loading ? (
<LoaderIcon className="size-4 animate-spin" />
) : (
"Continue"
)}
</button>
</div>
</div>
</motion.div>
);
}

View File

@ -2,10 +2,13 @@ import { ArrowRightIcon, PopperFilledIcon } from "@lume/icons";
import { onboardingAtom } from "@lume/utils";
import { motion } from "framer-motion";
import { useAtom } from "jotai";
import { useTranslation } from "react-i18next";
import { useNavigate } from "react-router-dom";
export function OnboardingHomeScreen() {
const navigate = useNavigate();
const [t] = useTranslation();
const [onboarding, setOnboarding] = useAtom(onboardingAtom);
return (
@ -17,11 +20,9 @@ export function OnboardingHomeScreen() {
>
<PopperFilledIcon className="size-12 text-blue-500" />
<div className="text-center">
<p className="text-lg font-medium">
Your account was successfully created!
</p>
<p className="text-lg font-medium">{t("onboarding.home.title")}</p>
<p className="leading-tight text-neutral-600 dark:text-neutral-400">
For starters, let's set up your profile.
{t("onboarding.home.subtitle")}
</p>
</div>
<div className="mt-4 flex flex-col gap-2 items-center">
@ -32,7 +33,7 @@ export function OnboardingHomeScreen() {
}
className="inline-flex items-center justify-center gap-2 w-44 font-medium h-11 rounded-xl bg-blue-100 text-blue-500 hover:bg-blue-200 dark:bg-blue-900 dark:text-blue-500 dark:hover:bg-blue-800"
>
Profile Settings
{t("onboarding.home.profileSettings")}
<ArrowRightIcon className="size-4" />
</button>
<button
@ -40,7 +41,7 @@ export function OnboardingHomeScreen() {
onClick={() => setOnboarding({ open: false, newUser: false })}
className="inline-flex items-center justify-center gap-2 w-44 px-5 font-medium h-11 rounded-xl hover:bg-neutral-100 dark:hover:bg-neutral-900 text-neutral-700 dark:text-neutral-600"
>
Skip
{t("global.skip")}
</button>
</div>
</motion.div>

View File

@ -2,6 +2,7 @@ import { ArrowLeftIcon, LoaderIcon } from "@lume/icons";
import { useStorage } from "@lume/storage";
import { TOPICS, cn } from "@lume/utils";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { useNavigate } from "react-router-dom";
import { toast } from "sonner";
@ -9,6 +10,7 @@ export function OnboardingInterestScreen() {
const storage = useStorage();
const navigate = useNavigate();
const [t] = useTranslation();
const [loading, setLoading] = useState(false);
const [hashtags, setHashtags] = useState([]);
@ -49,9 +51,9 @@ export function OnboardingInterestScreen() {
<div className="w-full h-full flex flex-col">
<div className="h-16 shrink-0 px-8 border-b border-neutral-100 dark:border-neutral-900 flex w-full items-center justify-between">
<div className="flex flex-col">
<h3 className="font-semibold">Interests</h3>
<h3 className="font-semibold">{t("interests.title")}</h3>
<p className="text-sm font-medium text-neutral-700 dark:text-neutral-300">
Pick things you'd like to see in your home feed.
{t("interests.subtitle")}
</p>
</div>
</div>
@ -74,7 +76,7 @@ export function OnboardingInterestScreen() {
onClick={() => toggleAll(topic.content)}
className="text-sm font-medium text-blue-500"
>
Follow All
{t("interests.followAll")}
</button>
</div>
<div className="flex flex-wrap items-center gap-3">
@ -105,7 +107,7 @@ export function OnboardingInterestScreen() {
className="inline-flex h-9 flex-1 gap-2 shrink-0 items-center justify-center rounded-lg bg-neutral-100 font-medium dark:bg-neutral-900 dark:hover:bg-neutral-800 hover:bg-blue-200"
>
<ArrowLeftIcon className="size-4" />
Back
{t("global.back")}
</button>
<button
type="button"
@ -115,7 +117,7 @@ export function OnboardingInterestScreen() {
{loading ? (
<LoaderIcon className="size-4 animate-spin" />
) : (
"Continue"
t("global.continue")
)}
</button>
</div>

View File

@ -7,6 +7,7 @@ import { motion } from "framer-motion";
import { minidenticon } from "minidenticons";
import { useState } from "react";
import { useForm } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { useNavigate } from "react-router-dom";
import { toast } from "sonner";
import { AvatarUploadButton } from "../avatarUploadButton";
@ -20,6 +21,7 @@ export function OnboardingProfileScreen() {
const queryClient = useQueryClient();
const navigate = useNavigate();
const { t } = useTranslation();
const { register, handleSubmit } = useForm();
const svgURI = `data:image/svg+xml;utf8,${encodeURIComponent(
@ -71,9 +73,9 @@ export function OnboardingProfileScreen() {
<div className="w-full h-full flex flex-col gap-4">
<div className="h-16 shrink-0 px-8 border-b border-neutral-100 dark:border-neutral-900 flex w-full items-center justify-between">
<div className="flex flex-col">
<h3 className="font-semibold">About you</h3>
<h3 className="font-semibold">{t("onboarding.profile.title")}</h3>
<p className="text-sm font-medium text-neutral-700 dark:text-neutral-300">
Tell Lume about yourself to start building your home feed.
{t("onboarding.profile.subtitle")}
</p>
</div>
</div>
@ -89,7 +91,7 @@ export function OnboardingProfileScreen() {
className="flex flex-col px-8 gap-4"
>
<div className="flex flex-col gap-1">
<span className="font-medium">Avatar</span>
<span className="font-medium">{t("user.avatar")}</span>
<div className="flex h-36 w-full flex-col items-center justify-center gap-3 rounded-lg bg-neutral-100 dark:bg-neutral-950">
{picture.length ? (
<img
@ -109,7 +111,7 @@ export function OnboardingProfileScreen() {
</div>
<div className="flex flex-col gap-1">
<label htmlFor="name" className="font-medium">
Name *
{t("user.name")} *
</label>
<input
type={"text"}
@ -121,7 +123,7 @@ export function OnboardingProfileScreen() {
</div>
<div className="flex flex-col gap-1">
<label htmlFor="about" className="font-medium">
Bio
{t("user.bio")}
</label>
<textarea
{...register("about")}
@ -132,7 +134,7 @@ export function OnboardingProfileScreen() {
</div>
<div className="flex flex-col gap-1">
<label htmlFor="website" className="font-medium">
Website
{t("user.website")}
</label>
<input
type="url"
@ -150,7 +152,7 @@ export function OnboardingProfileScreen() {
className="inline-flex h-9 flex-1 gap-2 shrink-0 items-center justify-center rounded-lg bg-neutral-100 font-medium dark:bg-neutral-900 dark:hover:bg-neutral-800 hover:bg-blue-200"
>
<ArrowLeftIcon className="size-4" />
Back
{t("global.back")}
</button>
<button
type="submit"
@ -159,7 +161,7 @@ export function OnboardingProfileScreen() {
{loading ? (
<LoaderIcon className="h-4 w-4 animate-spin" />
) : (
"Continue"
t("global.continue")
)}
</button>
</div>

View File

@ -4,6 +4,7 @@ import { NDKEventWithReplies } from "@lume/types";
import { cn } from "@lume/utils";
import { NDKKind, type NDKSubscription } from "@nostr-dev-kit/ndk";
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { ReplyForm } from "./editor/replyForm";
export function ReplyList({
@ -11,6 +12,8 @@ export function ReplyList({
className,
}: { eventId: string; className?: string }) {
const ark = useArk();
const [t] = useTranslation();
const [data, setData] = useState<null | NDKEventWithReplies[]>(null);
useEffect(() => {
@ -68,7 +71,7 @@ export function ReplyList({
<div className="flex flex-col items-center justify-center gap-2 py-6">
<h3 className="text-3xl">👋</h3>
<p className="leading-none text-neutral-600 dark:text-neutral-400">
Be the first to Reply!
{t("note.reply.empty")}
</p>
</div>
</div>

View File

@ -1,6 +1,7 @@
import { User } from "@lume/ark";
import { ArrowLeftIcon, ArrowRightIcon, LoaderIcon } from "@lume/icons";
import { useQuery, useQueryClient } from "@tanstack/react-query";
import { useTranslation } from "react-i18next";
import { useNavigate } from "react-router-dom";
import { toast } from "sonner";
import { WindowVirtualizer } from "virtua";
@ -28,6 +29,7 @@ export function SuggestRoute({ queryKey }: { queryKey: string[] }) {
const queryClient = useQueryClient();
const navigate = useNavigate();
const { t } = useTranslation();
const { isLoading, isError, data } = useQuery({
queryKey: ["trending-users"],
queryFn: async ({ signal }: { signal: AbortSignal }) => {
@ -71,7 +73,7 @@ export function SuggestRoute({ queryKey }: { queryKey: string[] }) {
</div>
<div className="relative px-3">
<div className="flex items-center h-16">
<h3 className="font-semibold text-xl">Suggested Follows</h3>
<h3 className="font-semibold text-xl">{t("suggestion.title")}</h3>
</div>
<div className="flex flex-col divide-y divide-neutral-100 dark:divide-neutral-900">
{isLoading ? (
@ -80,7 +82,7 @@ export function SuggestRoute({ queryKey }: { queryKey: string[] }) {
</div>
) : isError ? (
<div className="flex h-44 w-full items-center justify-center">
Error. Cannot get trending users
{t("suggestion.error")}
</div>
) : (
data?.profiles.map((item: { pubkey: string }) => (
@ -115,7 +117,7 @@ export function SuggestRoute({ queryKey }: { queryKey: string[] }) {
onClick={submit}
className="inline-flex items-center justify-center gap-2 px-6 font-medium shadow-xl dark:shadow-none shadow-neutral-500/50 text-white transform bg-blue-500 rounded-full active:translate-y-1 w-44 h-11 hover:bg-blue-600 focus:outline-none disabled:cursor-not-allowed"
>
Save & Go back
{t("suggestion.button")}
</button>
</div>
</div>

View File

@ -9,6 +9,7 @@ import { FETCH_LIMIT } from "@lume/utils";
import { NDKEvent, NDKKind } from "@nostr-dev-kit/ndk";
import { useInfiniteQuery } from "@tanstack/react-query";
import { useMemo } from "react";
import { useTranslation } from "react-i18next";
import { useNavigate, useParams } from "react-router-dom";
import { WindowVirtualizer } from "virtua";
@ -17,6 +18,7 @@ export function UserRoute() {
const navigate = useNavigate();
const { id } = useParams();
const { t } = useTranslation();
const { data, hasNextPage, isLoading, isFetchingNextPage, fetchNextPage } =
useInfiniteQuery({
queryKey: ["user-posts", id],
@ -107,7 +109,7 @@ export function UserRoute() {
</User.Provider>
<div className="pt-2 mt-2 border-t border-neutral-100 dark:border-neutral-900">
<h3 className="text-lg font-semibold text-neutral-900 dark:text-neutral-100">
Latest posts
{t("user.latestPosts")}
</h3>
<div className="flex h-full w-full flex-col justify-between gap-1.5 pb-10">
{isLoading ? (
@ -130,7 +132,7 @@ export function UserRoute() {
) : (
<>
<ArrowRightCircleIcon className="size-5" />
Load more
{t("global.loadMore")}
</>
)}
</button>

View File

@ -4,17 +4,20 @@ import { COL_TYPES, searchAtom } from "@lume/utils";
import { type NDKEvent, NDKKind } from "@nostr-dev-kit/ndk";
import { useAtom } from "jotai";
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { useDebounce } from "use-debounce";
import { Command } from "../cmdk";
export function SearchDialog() {
const ark = useArk();
const [open, setOpen] = useAtom(searchAtom);
const [loading, setLoading] = useState(false);
const [events, setEvents] = useState<NDKEvent[]>([]);
const [search, setSearch] = useState("");
const [value] = useDebounce(search, 1200);
const ark = useArk();
const { t } = useTranslation();
const { vlistRef, columns, addColumn } = useColumnContext();
const searchEvents = async () => {
@ -90,7 +93,7 @@ export function SearchDialog() {
<Command.Input
value={search}
onValueChange={setSearch}
placeholder="Type something to search..."
placeholder={t("search.placeholder")}
className="w-full h-12 bg-neutral-100 dark:bg-neutral-900 rounded-xl border-none focus:outline-none focus:ring-0 placeholder:text-neutral-500 dark:placeholder:text-neutral-600"
/>
</div>
@ -101,7 +104,7 @@ export function SearchDialog() {
</Command.Loading>
) : !events.length ? (
<Command.Empty className="flex items-center justify-center h-full text-sm">
No results found.
{t("global.noResult")}
</Command.Empty>
) : (
<>
@ -161,7 +164,7 @@ export function SearchDialog() {
<div className="size-16 bg-blue-100 dark:bg-blue-900 rounded-full inline-flex items-center justify-center text-blue-500">
<SearchIcon className="size-6" />
</div>
Try searching for people, notes, or keywords
{t("search.empty")}
</div>
) : null}
</Command.List>

View File

@ -1,5 +1,10 @@
export const FETCH_LIMIT = 20;
export const LANGUAGES = [
{ label: "English", code: "en" },
{ label: "Japanese", code: "ja" },
];
export const NOSTR_MENTIONS = [
"@npub1",
"nostr:npub1",
@ -26,7 +31,7 @@ export const NOSTR_EVENTS = [
"Nostr:nevent1",
];
export const BITCOINS = ['lnbc', 'bc1p', 'bc1q'];
export const BITCOINS = ["lnbc", "bc1p", "bc1q"];
export const IMAGES = ["jpg", "jpeg", "gif", "png", "webp", "avif", "tiff"];
@ -374,4 +379,5 @@ export const QUOTES = [
"Are you a fan of following topics, instead of people? Use https://zapddit.com",
];
// @ts-ignore, it works
export const VITE_FLATPAK_RESOURCE = import.meta.env.VITE_FLATPAK_RESOURCE;

View File

@ -150,6 +150,12 @@ importers:
framer-motion:
specifier: ^10.18.0
version: 10.18.0(react-dom@18.2.0)(react@18.2.0)
i18next:
specifier: ^23.8.0
version: 23.8.0
i18next-resources-to-backend:
specifier: ^1.2.0
version: 1.2.0
jotai:
specifier: ^2.6.3
version: 2.6.3(@types/react@18.2.48)(react@18.2.0)
@ -177,6 +183,9 @@ importers:
react-hook-form:
specifier: ^7.49.3
version: 7.49.3(react@18.2.0)
react-i18next:
specifier: ^14.0.1
version: 14.0.1(i18next@23.8.0)(react-dom@18.2.0)(react@18.2.0)
react-router-dom:
specifier: ^6.21.3
version: 6.21.3(react-dom@18.2.0)(react@18.2.0)
@ -359,6 +368,9 @@ importers:
react-currency-input-field:
specifier: ^3.6.14
version: 3.6.14(react@18.2.0)
react-i18next:
specifier: ^14.0.1
version: 14.0.1(i18next@23.8.0)(react-dom@18.2.0)(react@18.2.0)
react-router-dom:
specifier: ^6.21.3
version: 6.21.3(react-dom@18.2.0)(react@18.2.0)
@ -993,6 +1005,9 @@ importers:
react-hotkeys-hook:
specifier: ^4.4.4
version: 4.4.4(react-dom@18.2.0)(react@18.2.0)
react-i18next:
specifier: ^14.0.1
version: 14.0.1(i18next@23.8.0)(react-dom@18.2.0)(react@18.2.0)
react-router-dom:
specifier: ^6.21.3
version: 6.21.3(react-dom@18.2.0)(react@18.2.0)
@ -5180,6 +5195,12 @@ packages:
resolution: {integrity: sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ==}
dev: false
/html-parse-stringify@3.0.1:
resolution: {integrity: sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==}
dependencies:
void-elements: 3.1.0
dev: false
/html-void-elements@3.0.0:
resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==}
dev: false
@ -5203,6 +5224,18 @@ packages:
engines: {node: '>=16.17.0'}
dev: false
/i18next-resources-to-backend@1.2.0:
resolution: {integrity: sha512-8f1l03s+QxDmCfpSXCh9V+AFcxAwIp0UaroWuyOx+hmmv8484GcELHs+lnu54FrNij8cDBEXvEwhzZoXsKcVpg==}
dependencies:
'@babel/runtime': 7.23.9
dev: false
/i18next@23.8.0:
resolution: {integrity: sha512-1H+39doU9dQZrRprpnZ2aZetbX9I1N3bM/YGHN/ZkMJ//wJqrxDEqgI5mmSsh/rglsFBiNxI6UtFZfUO2A6XbA==}
dependencies:
'@babel/runtime': 7.23.9
dev: false
/iconv-lite@0.4.23:
resolution: {integrity: sha512-neyTUVFtahjf0mB3dZT77u+8O0QB89jFdnBkd5P1JgYPbPaia3gXXOVL2fq8VyU2gMMD7SaN7QukTB/pmXYvDA==}
engines: {node: '>=0.10.0'}
@ -6970,6 +7003,26 @@ packages:
react-dom: 18.2.0(react@18.2.0)
dev: false
/react-i18next@14.0.1(i18next@23.8.0)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-TMV8hFismBmpMdIehoFHin/okfvgjFhp723RYgIqB4XyhDobVMyukyM3Z8wtTRmajyFMZrBl/OaaXF2P6WjUAw==}
peerDependencies:
i18next: '>= 23.2.3'
react: '>= 16.8.0'
react-dom: '*'
react-native: '*'
peerDependenciesMeta:
react-dom:
optional: true
react-native:
optional: true
dependencies:
'@babel/runtime': 7.23.9
html-parse-stringify: 3.0.1
i18next: 23.8.0
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
dev: false
/react-remove-scroll-bar@2.3.4(@types/react@18.2.48)(react@18.2.0):
resolution: {integrity: sha512-63C4YQBUt0m6ALadE9XV56hV8BgJWDmmTPY758iIJjfQKt2nYwoUrPk0LXRXcB/yIj82T1/Ixfdpdk68LwIB0A==}
engines: {node: '>=10'}
@ -8594,6 +8647,11 @@ packages:
vite: 5.0.12(@types/node@20.11.8)
dev: false
/void-elements@3.1.0:
resolution: {integrity: sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==}
engines: {node: '>=0.10.0'}
dev: false
/volar-service-css@0.0.17(@volar/language-service@1.11.1):
resolution: {integrity: sha512-bEDJykygMzn2+a9ud6KwZZLli9eqarxApAXZuf2CqJJh6Trw1elmbBCo9SlPfqMrIhpFnwV0Sa+Xoc9x5WPeGw==}
peerDependencies:

295
src-tauri/locales/cn.json Normal file
View File

@ -0,0 +1,295 @@
{
"global": {
"relay": "Relay",
"back": "Back",
"continue": "Continue",
"loading": "Loading",
"error": "Error",
"moveLeft": "Move Left",
"moveRight": "Move Right",
"newColumn": "New Column",
"inspect": "Inspect",
"loadMore": "Load more",
"delete": "Delete",
"refresh": "Refresh",
"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",
"apiKey": "API Key",
"skip": "Skip",
"close": "Close"
},
"nip89": {
"unsupported": "Lume isn't support this event",
"openWith": "Open with"
},
"note": {
"showThread": "Show thread",
"showMore": "Show more",
"error": "Failed to fetch event.",
"posted": "posted",
"replied": "replied",
"reposted": "reposted",
"menu": {
"viewThread": "View thread",
"copyLink": "Copy shareable link",
"copyNoteId": "Copy note ID",
"copyAuthorId": "Copy author ID",
"viewAuthor": "View author",
"pinAuthor": "Pin author",
"copyRaw": "Copy raw event",
"mute": "Mute"
},
"buttons": {
"pin": "Pin",
"pinTooltip": "Pin Note",
"repost": "Repost",
"quote": "Quote",
"viewProfile": "View profile"
},
"zap": {
"zap": "Zap",
"tooltip": "Send zap",
"modalTitle": "Send zap to",
"messagePlaceholder": "Enter message (optional)",
"buttonFinish": "Zapped",
"buttonLoading": "Processing...",
"invoiceButton": "Scan to zap",
"invoiceFooter": "You must use Bitcoin wallet which support Lightning\nsuch as: Blue Wallet, Bitkit, Phoenix,..."
},
"reply": {
"single": "reply",
"plural": "replies",
"empty": "Be the first to Reply!"
}
},
"user": {
"avatar": "Avatar",
"displayName": "Display Name",
"name": "Name",
"bio": "Bio",
"lna": "Lightning address",
"website": "Website",
"verified": "Verified",
"unverified": "Unverified",
"follow": "Follow",
"unfollow": "Unfollow",
"latestPosts": "Latest posts",
"avatarButton": "Change avatar",
"coverButton": "Change cover",
"editProfile": "Edit profile",
"settings": "Settings",
"logout": "Log out",
"logoutConfirmTitle": "Are you sure!",
"logoutConfirmSubtitle": "You can always log back in at any time. If you just want to switch accounts, you can do that by adding an existing account."
},
"editor": {
"title": "New Post",
"placeholder": "What are you up to?",
"successMessage": "Your note has been published successfully.",
"replyPlaceholder": "Post your reply"
},
"search": {
"placeholder": "Type something to search...",
"empty": "Try searching for people, notes, or keywords"
},
"welcome": {
"title": "Lume is a magnificent client for Nostr to meet, explore\nand freely share your thoughts with everyone.",
"signup": "Join Nostr",
"login": "Login",
"footer": "Before joining Nostr, you can take time to learn more about Nostr"
},
"login": {
"title": "Welcome back, anon!",
"footer": "Lume will put your Private Key in Secure Storage depended on your OS Platform. It will be secured by Password or Biometric ID",
"loginWithAddress": "Login with Nostr Address",
"loginWithBunker": "Login with nsecBunker",
"or": "Or continue with",
"loginWithPrivkey": "Login with Private Key"
},
"loginWithAddress": {
"title": "Enter your Nostr Address"
},
"loginWithBunker": {
"title": "Enter your nsecbunker token"
},
"loginWithPrivkey": {
"title": "Enter your Private Key",
"subtitle": "Lume will put your private key to <1>{{service}}</1>.\nIt will be secured by your OS."
},
"signup": {
"title": "Let's Get Started",
"subtitle": "Choose one of methods below to create your account",
"selfManageMethod": "Self-Managed",
"selfManageMethodDescription": "You create your keys and keep them safe.",
"providerMethod": "Managed by Provider",
"providerMethodDescription": "A 3rd party provider will handle your sign in keys for you."
},
"signupWithSelfManage": {
"title": "This is your new Account Key",
"subtitle": "Keep your key in safe place. If you lose this key, you will lose access to your account.",
"confirm1": "I understand the risk of lost private key.",
"confirm2": "I will make sure keep it safe and not sharing with anyone.",
"confirm3": "I understand I cannot recover private key.",
"button": "Save key & Continue"
},
"signupWithProvider": {
"title": "Let's set up your account on Nostr",
"username": "Username *",
"chooseProvider": "Choose a Provider",
"usernameFooter": "Use to login to Lume and other Nostr apps. You can choose provider you trust to manage your account",
"email": "Backup Email (optional)",
"emailFooter": "Use for recover your account if you lose your password"
},
"onboardingSettings": {
"title": "You're almost ready to use Lume.",
"subtitle": "Let's start personalizing your experience.",
"notification": {
"title": "Push notification",
"subtitle": "Enabling push notifications will allow you to receive notifications from Lume."
},
"lowPower": {
"title": "Low Power Mode",
"subtitle": "Limited relay connection and hide all media, sustainable for low network environment."
},
"translation": {
"title": "Translation (nostr.wine)",
"subtitle": "Translate text to your preferred language, powered by Nostr Wine."
},
"footer": "There are many more settings you can configure from the 'Settings' Screen. Be sure to visit it later."
},
"relays": {
"global": "Global",
"follows": "Follows",
"sidebar": {
"title": "Connected relays",
"empty": "Empty."
},
"relayView": {
"empty": "Could not load relay information 😬",
"owner": "Owner",
"contact": "Contact",
"software": "Software",
"nips": "Supported NIPs",
"limit": "Limitation",
"payment": "Open payment website",
"paymentNote": "You need to make a payment to connect this relay"
}
},
"suggestion": {
"title": "Suggested Follows",
"error": "Error. Cannot get trending users",
"button": "Save & Go back"
},
"interests": {
"title": "Interests",
"subtitle": "Pick things you'd like to see in your home feed.",
"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"
}
},
"onboarding": {
"home": {
"title": "Your account was successfully created!",
"subtitle": "For starters, let's set up your profile.",
"profileSettings": "Profile Settings"
},
"profile": {
"title": "About you",
"subtitle": "Tell Lume about yourself to start building your home feed."
},
"finish": {
"title": "Profile setup complete!",
"subtitle": "You can exit the setup here and start using Lume.",
"report": "Report a issue"
}
},
"activity": {
"title": "Activity",
"empty": "Yo! Nothing new yet.",
"mention": "mention you",
"repost": "reposted",
"zap": "zapped",
"newReply": "New reply",
"boost": "Boost",
"boostSubtitle": "@ Someone has reposted to your note",
"conversation": "Conversation",
"conversationSubtitle": "@ Someone has replied to your note"
}
}

295
src-tauri/locales/en.json Normal file
View File

@ -0,0 +1,295 @@
{
"global": {
"relay": "Relay",
"back": "Back",
"continue": "Continue",
"loading": "Loading",
"error": "Error",
"moveLeft": "Move Left",
"moveRight": "Move Right",
"newColumn": "New Column",
"inspect": "Inspect",
"loadMore": "Load more",
"delete": "Delete",
"refresh": "Refresh",
"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",
"apiKey": "API Key",
"skip": "Skip",
"close": "Close"
},
"nip89": {
"unsupported": "Lume isn't support this event",
"openWith": "Open with"
},
"note": {
"showThread": "Show thread",
"showMore": "Show more",
"error": "Failed to fetch event.",
"posted": "posted",
"replied": "replied",
"reposted": "reposted",
"menu": {
"viewThread": "View thread",
"copyLink": "Copy shareable link",
"copyNoteId": "Copy note ID",
"copyAuthorId": "Copy author ID",
"viewAuthor": "View author",
"pinAuthor": "Pin author",
"copyRaw": "Copy raw event",
"mute": "Mute"
},
"buttons": {
"pin": "Pin",
"pinTooltip": "Pin Note",
"repost": "Repost",
"quote": "Quote",
"viewProfile": "View profile"
},
"zap": {
"zap": "Zap",
"tooltip": "Send zap",
"modalTitle": "Send zap to",
"messagePlaceholder": "Enter message (optional)",
"buttonFinish": "Zapped",
"buttonLoading": "Processing...",
"invoiceButton": "Scan to zap",
"invoiceFooter": "You must use Bitcoin wallet which support Lightning\nsuch as: Blue Wallet, Bitkit, Phoenix,..."
},
"reply": {
"single": "reply",
"plural": "replies",
"empty": "Be the first to Reply!"
}
},
"user": {
"avatar": "Avatar",
"displayName": "Display Name",
"name": "Name",
"bio": "Bio",
"lna": "Lightning address",
"website": "Website",
"verified": "Verified",
"unverified": "Unverified",
"follow": "Follow",
"unfollow": "Unfollow",
"latestPosts": "Latest posts",
"avatarButton": "Change avatar",
"coverButton": "Change cover",
"editProfile": "Edit profile",
"settings": "Settings",
"logout": "Log out",
"logoutConfirmTitle": "Are you sure!",
"logoutConfirmSubtitle": "You can always log back in at any time. If you just want to switch accounts, you can do that by adding an existing account."
},
"editor": {
"title": "New Post",
"placeholder": "What are you up to?",
"successMessage": "Your note has been published successfully.",
"replyPlaceholder": "Post your reply"
},
"search": {
"placeholder": "Type something to search...",
"empty": "Try searching for people, notes, or keywords"
},
"welcome": {
"title": "Lume is a magnificent client for Nostr to meet, explore\nand freely share your thoughts with everyone.",
"signup": "Join Nostr",
"login": "Login",
"footer": "Before joining Nostr, you can take time to learn more about Nostr"
},
"login": {
"title": "Welcome back, anon!",
"footer": "Lume will put your Private Key in Secure Storage depended on your OS Platform. It will be secured by Password or Biometric ID",
"loginWithAddress": "Login with Nostr Address",
"loginWithBunker": "Login with nsecBunker",
"or": "Or continue with",
"loginWithPrivkey": "Login with Private Key"
},
"loginWithAddress": {
"title": "Enter your Nostr Address"
},
"loginWithBunker": {
"title": "Enter your nsecbunker token"
},
"loginWithPrivkey": {
"title": "Enter your Private Key",
"subtitle": "Lume will put your private key to <1>{{service}}</1>.\nIt will be secured by your OS."
},
"signup": {
"title": "Let's Get Started",
"subtitle": "Choose one of methods below to create your account",
"selfManageMethod": "Self-Managed",
"selfManageMethodDescription": "You create your keys and keep them safe.",
"providerMethod": "Managed by Provider",
"providerMethodDescription": "A 3rd party provider will handle your sign in keys for you."
},
"signupWithSelfManage": {
"title": "This is your new Account Key",
"subtitle": "Keep your key in safe place. If you lose this key, you will lose access to your account.",
"confirm1": "I understand the risk of lost private key.",
"confirm2": "I will make sure keep it safe and not sharing with anyone.",
"confirm3": "I understand I cannot recover private key.",
"button": "Save key & Continue"
},
"signupWithProvider": {
"title": "Let's set up your account on Nostr",
"username": "Username *",
"chooseProvider": "Choose a Provider",
"usernameFooter": "Use to login to Lume and other Nostr apps. You can choose provider you trust to manage your account",
"email": "Backup Email (optional)",
"emailFooter": "Use for recover your account if you lose your password"
},
"onboardingSettings": {
"title": "You're almost ready to use Lume.",
"subtitle": "Let's start personalizing your experience.",
"notification": {
"title": "Push notification",
"subtitle": "Enabling push notifications will allow you to receive notifications from Lume."
},
"lowPower": {
"title": "Low Power Mode",
"subtitle": "Limited relay connection and hide all media, sustainable for low network environment."
},
"translation": {
"title": "Translation (nostr.wine)",
"subtitle": "Translate text to your preferred language, powered by Nostr Wine."
},
"footer": "There are many more settings you can configure from the 'Settings' Screen. Be sure to visit it later."
},
"relays": {
"global": "Global",
"follows": "Follows",
"sidebar": {
"title": "Connected relays",
"empty": "Empty."
},
"relayView": {
"empty": "Could not load relay information 😬",
"owner": "Owner",
"contact": "Contact",
"software": "Software",
"nips": "Supported NIPs",
"limit": "Limitation",
"payment": "Open payment website",
"paymentNote": "You need to make a payment to connect this relay"
}
},
"suggestion": {
"title": "Suggested Follows",
"error": "Error. Cannot get trending users",
"button": "Save & Go back"
},
"interests": {
"title": "Interests",
"subtitle": "Pick things you'd like to see in your home feed.",
"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"
}
},
"onboarding": {
"home": {
"title": "Your account was successfully created!",
"subtitle": "For starters, let's set up your profile.",
"profileSettings": "Profile Settings"
},
"profile": {
"title": "About you",
"subtitle": "Tell Lume about yourself to start building your home feed."
},
"finish": {
"title": "Profile setup complete!",
"subtitle": "You can exit the setup here and start using Lume.",
"report": "Report a issue"
}
},
"activity": {
"title": "Activity",
"empty": "Yo! Nothing new yet.",
"mention": "mention you",
"repost": "reposted",
"zap": "zapped",
"newReply": "New reply",
"boost": "Boost",
"boostSubtitle": "@ Someone has reposted to your note",
"conversation": "Conversation",
"conversationSubtitle": "@ Someone has replied to your note"
}
}

295
src-tauri/locales/ja.json Normal file
View File

@ -0,0 +1,295 @@
{
"global": {
"relay": "Relay",
"back": "Back",
"continue": "Continue",
"loading": "Loading",
"error": "Error",
"moveLeft": "Move Left",
"moveRight": "Move Right",
"newColumn": "New Column",
"inspect": "Inspect",
"loadMore": "Load more",
"delete": "Delete",
"refresh": "Refresh",
"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",
"apiKey": "API Key",
"skip": "Skip",
"close": "Close"
},
"nip89": {
"unsupported": "Lume isn't support this event",
"openWith": "Open with"
},
"note": {
"showThread": "Show thread",
"showMore": "Show more",
"error": "Failed to fetch event.",
"posted": "posted",
"replied": "replied",
"reposted": "reposted",
"menu": {
"viewThread": "View thread",
"copyLink": "Copy shareable link",
"copyNoteId": "Copy note ID",
"copyAuthorId": "Copy author ID",
"viewAuthor": "View author",
"pinAuthor": "Pin author",
"copyRaw": "Copy raw event",
"mute": "Mute"
},
"buttons": {
"pin": "Pin",
"pinTooltip": "Pin Note",
"repost": "Repost",
"quote": "Quote",
"viewProfile": "View profile"
},
"zap": {
"zap": "Zap",
"tooltip": "Send zap",
"modalTitle": "Send zap to",
"messagePlaceholder": "Enter message (optional)",
"buttonFinish": "Zapped",
"buttonLoading": "Processing...",
"invoiceButton": "Scan to zap",
"invoiceFooter": "You must use Bitcoin wallet which support Lightning\nsuch as: Blue Wallet, Bitkit, Phoenix,..."
},
"reply": {
"single": "reply",
"plural": "replies",
"empty": "Be the first to Reply!"
}
},
"user": {
"avatar": "Avatar",
"displayName": "Display Name",
"name": "Name",
"bio": "Bio",
"lna": "Lightning address",
"website": "Website",
"verified": "Verified",
"unverified": "Unverified",
"follow": "Follow",
"unfollow": "Unfollow",
"latestPosts": "Latest posts",
"avatarButton": "Change avatar",
"coverButton": "Change cover",
"editProfile": "Edit profile",
"settings": "Settings",
"logout": "Log out",
"logoutConfirmTitle": "Are you sure!",
"logoutConfirmSubtitle": "You can always log back in at any time. If you just want to switch accounts, you can do that by adding an existing account."
},
"editor": {
"title": "New Post",
"placeholder": "What are you up to?",
"successMessage": "Your note has been published successfully.",
"replyPlaceholder": "Post your reply"
},
"search": {
"placeholder": "Type something to search...",
"empty": "Try searching for people, notes, or keywords"
},
"welcome": {
"title": "Lume is a magnificent client for Nostr to meet, explore\nand freely share your thoughts with everyone.",
"signup": "Join Nostr",
"login": "Login",
"footer": "Before joining Nostr, you can take time to learn more about Nostr"
},
"login": {
"title": "Welcome back, anon!",
"footer": "Lume will put your Private Key in Secure Storage depended on your OS Platform. It will be secured by Password or Biometric ID",
"loginWithAddress": "Login with Nostr Address",
"loginWithBunker": "Login with nsecBunker",
"or": "Or continue with",
"loginWithPrivkey": "Login with Private Key"
},
"loginWithAddress": {
"title": "Enter your Nostr Address"
},
"loginWithBunker": {
"title": "Enter your nsecbunker token"
},
"loginWithPrivkey": {
"title": "Enter your Private Key",
"subtitle": "Lume will put your private key to <1>{{service}}</1>.\nIt will be secured by your OS."
},
"signup": {
"title": "Let's Get Started",
"subtitle": "Choose one of methods below to create your account",
"selfManageMethod": "Self-Managed",
"selfManageMethodDescription": "You create your keys and keep them safe.",
"providerMethod": "Managed by Provider",
"providerMethodDescription": "A 3rd party provider will handle your sign in keys for you."
},
"signupWithSelfManage": {
"title": "This is your new Account Key",
"subtitle": "Keep your key in safe place. If you lose this key, you will lose access to your account.",
"confirm1": "I understand the risk of lost private key.",
"confirm2": "I will make sure keep it safe and not sharing with anyone.",
"confirm3": "I understand I cannot recover private key.",
"button": "Save key & Continue"
},
"signupWithProvider": {
"title": "Let's set up your account on Nostr",
"username": "Username *",
"chooseProvider": "Choose a Provider",
"usernameFooter": "Use to login to Lume and other Nostr apps. You can choose provider you trust to manage your account",
"email": "Backup Email (optional)",
"emailFooter": "Use for recover your account if you lose your password"
},
"onboardingSettings": {
"title": "You're almost ready to use Lume.",
"subtitle": "Let's start personalizing your experience.",
"notification": {
"title": "Push notification",
"subtitle": "Enabling push notifications will allow you to receive notifications from Lume."
},
"lowPower": {
"title": "Low Power Mode",
"subtitle": "Limited relay connection and hide all media, sustainable for low network environment."
},
"translation": {
"title": "Translation (nostr.wine)",
"subtitle": "Translate text to your preferred language, powered by Nostr Wine."
},
"footer": "There are many more settings you can configure from the 'Settings' Screen. Be sure to visit it later."
},
"relays": {
"global": "Global",
"follows": "Follows",
"sidebar": {
"title": "Connected relays",
"empty": "Empty."
},
"relayView": {
"empty": "Could not load relay information 😬",
"owner": "Owner",
"contact": "Contact",
"software": "Software",
"nips": "Supported NIPs",
"limit": "Limitation",
"payment": "Open payment website",
"paymentNote": "You need to make a payment to connect this relay"
}
},
"suggestion": {
"title": "Suggested Follows",
"error": "Error. Cannot get trending users",
"button": "Save & Go back"
},
"interests": {
"title": "Interests",
"subtitle": "Pick things you'd like to see in your home feed.",
"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"
}
},
"onboarding": {
"home": {
"title": "Your account was successfully created!",
"subtitle": "For starters, let's set up your profile.",
"profileSettings": "Profile Settings"
},
"profile": {
"title": "About you",
"subtitle": "Tell Lume about yourself to start building your home feed."
},
"finish": {
"title": "Profile setup complete!",
"subtitle": "You can exit the setup here and start using Lume.",
"report": "Report a issue"
}
},
"activity": {
"title": "Activity",
"empty": "Yo! Nothing new yet.",
"mention": "mention you",
"repost": "reposted",
"zap": "zapped",
"newReply": "New reply",
"boost": "Boost",
"boostSubtitle": "@ Someone has reposted to your note",
"conversation": "Conversation",
"conversationSubtitle": "@ Someone has replied to your note"
}
}

295
src-tauri/locales/ru.json Normal file
View File

@ -0,0 +1,295 @@
{
"global": {
"relay": "Relay",
"back": "Back",
"continue": "Continue",
"loading": "Loading",
"error": "Error",
"moveLeft": "Move Left",
"moveRight": "Move Right",
"newColumn": "New Column",
"inspect": "Inspect",
"loadMore": "Load more",
"delete": "Delete",
"refresh": "Refresh",
"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",
"apiKey": "API Key",
"skip": "Skip",
"close": "Close"
},
"nip89": {
"unsupported": "Lume isn't support this event",
"openWith": "Open with"
},
"note": {
"showThread": "Show thread",
"showMore": "Show more",
"error": "Failed to fetch event.",
"posted": "posted",
"replied": "replied",
"reposted": "reposted",
"menu": {
"viewThread": "View thread",
"copyLink": "Copy shareable link",
"copyNoteId": "Copy note ID",
"copyAuthorId": "Copy author ID",
"viewAuthor": "View author",
"pinAuthor": "Pin author",
"copyRaw": "Copy raw event",
"mute": "Mute"
},
"buttons": {
"pin": "Pin",
"pinTooltip": "Pin Note",
"repost": "Repost",
"quote": "Quote",
"viewProfile": "View profile"
},
"zap": {
"zap": "Zap",
"tooltip": "Send zap",
"modalTitle": "Send zap to",
"messagePlaceholder": "Enter message (optional)",
"buttonFinish": "Zapped",
"buttonLoading": "Processing...",
"invoiceButton": "Scan to zap",
"invoiceFooter": "You must use Bitcoin wallet which support Lightning\nsuch as: Blue Wallet, Bitkit, Phoenix,..."
},
"reply": {
"single": "reply",
"plural": "replies",
"empty": "Be the first to Reply!"
}
},
"user": {
"avatar": "Avatar",
"displayName": "Display Name",
"name": "Name",
"bio": "Bio",
"lna": "Lightning address",
"website": "Website",
"verified": "Verified",
"unverified": "Unverified",
"follow": "Follow",
"unfollow": "Unfollow",
"latestPosts": "Latest posts",
"avatarButton": "Change avatar",
"coverButton": "Change cover",
"editProfile": "Edit profile",
"settings": "Settings",
"logout": "Log out",
"logoutConfirmTitle": "Are you sure!",
"logoutConfirmSubtitle": "You can always log back in at any time. If you just want to switch accounts, you can do that by adding an existing account."
},
"editor": {
"title": "New Post",
"placeholder": "What are you up to?",
"successMessage": "Your note has been published successfully.",
"replyPlaceholder": "Post your reply"
},
"search": {
"placeholder": "Type something to search...",
"empty": "Try searching for people, notes, or keywords"
},
"welcome": {
"title": "Lume is a magnificent client for Nostr to meet, explore\nand freely share your thoughts with everyone.",
"signup": "Join Nostr",
"login": "Login",
"footer": "Before joining Nostr, you can take time to learn more about Nostr"
},
"login": {
"title": "Welcome back, anon!",
"footer": "Lume will put your Private Key in Secure Storage depended on your OS Platform. It will be secured by Password or Biometric ID",
"loginWithAddress": "Login with Nostr Address",
"loginWithBunker": "Login with nsecBunker",
"or": "Or continue with",
"loginWithPrivkey": "Login with Private Key"
},
"loginWithAddress": {
"title": "Enter your Nostr Address"
},
"loginWithBunker": {
"title": "Enter your nsecbunker token"
},
"loginWithPrivkey": {
"title": "Enter your Private Key",
"subtitle": "Lume will put your private key to <1>{{service}}</1>.\nIt will be secured by your OS."
},
"signup": {
"title": "Let's Get Started",
"subtitle": "Choose one of methods below to create your account",
"selfManageMethod": "Self-Managed",
"selfManageMethodDescription": "You create your keys and keep them safe.",
"providerMethod": "Managed by Provider",
"providerMethodDescription": "A 3rd party provider will handle your sign in keys for you."
},
"signupWithSelfManage": {
"title": "This is your new Account Key",
"subtitle": "Keep your key in safe place. If you lose this key, you will lose access to your account.",
"confirm1": "I understand the risk of lost private key.",
"confirm2": "I will make sure keep it safe and not sharing with anyone.",
"confirm3": "I understand I cannot recover private key.",
"button": "Save key & Continue"
},
"signupWithProvider": {
"title": "Let's set up your account on Nostr",
"username": "Username *",
"chooseProvider": "Choose a Provider",
"usernameFooter": "Use to login to Lume and other Nostr apps. You can choose provider you trust to manage your account",
"email": "Backup Email (optional)",
"emailFooter": "Use for recover your account if you lose your password"
},
"onboardingSettings": {
"title": "You're almost ready to use Lume.",
"subtitle": "Let's start personalizing your experience.",
"notification": {
"title": "Push notification",
"subtitle": "Enabling push notifications will allow you to receive notifications from Lume."
},
"lowPower": {
"title": "Low Power Mode",
"subtitle": "Limited relay connection and hide all media, sustainable for low network environment."
},
"translation": {
"title": "Translation (nostr.wine)",
"subtitle": "Translate text to your preferred language, powered by Nostr Wine."
},
"footer": "There are many more settings you can configure from the 'Settings' Screen. Be sure to visit it later."
},
"relays": {
"global": "Global",
"follows": "Follows",
"sidebar": {
"title": "Connected relays",
"empty": "Empty."
},
"relayView": {
"empty": "Could not load relay information 😬",
"owner": "Owner",
"contact": "Contact",
"software": "Software",
"nips": "Supported NIPs",
"limit": "Limitation",
"payment": "Open payment website",
"paymentNote": "You need to make a payment to connect this relay"
}
},
"suggestion": {
"title": "Suggested Follows",
"error": "Error. Cannot get trending users",
"button": "Save & Go back"
},
"interests": {
"title": "Interests",
"subtitle": "Pick things you'd like to see in your home feed.",
"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"
}
},
"onboarding": {
"home": {
"title": "Your account was successfully created!",
"subtitle": "For starters, let's set up your profile.",
"profileSettings": "Profile Settings"
},
"profile": {
"title": "About you",
"subtitle": "Tell Lume about yourself to start building your home feed."
},
"finish": {
"title": "Profile setup complete!",
"subtitle": "You can exit the setup here and start using Lume.",
"report": "Report a issue"
}
},
"activity": {
"title": "Activity",
"empty": "Yo! Nothing new yet.",
"mention": "mention you",
"repost": "reposted",
"zap": "zapped",
"newReply": "New reply",
"boost": "Boost",
"boostSubtitle": "@ Someone has reposted to your note",
"conversation": "Conversation",
"conversationSubtitle": "@ Someone has replied to your note"
}
}

295
src-tauri/locales/vi.json Normal file
View File

@ -0,0 +1,295 @@
{
"global": {
"relay": "Relay",
"back": "Back",
"continue": "Continue",
"loading": "Loading",
"error": "Error",
"moveLeft": "Move Left",
"moveRight": "Move Right",
"newColumn": "New Column",
"inspect": "Inspect",
"loadMore": "Load more",
"delete": "Delete",
"refresh": "Refresh",
"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",
"apiKey": "API Key",
"skip": "Skip",
"close": "Close"
},
"nip89": {
"unsupported": "Lume isn't support this event",
"openWith": "Open with"
},
"note": {
"showThread": "Show thread",
"showMore": "Show more",
"error": "Failed to fetch event.",
"posted": "posted",
"replied": "replied",
"reposted": "reposted",
"menu": {
"viewThread": "View thread",
"copyLink": "Copy shareable link",
"copyNoteId": "Copy note ID",
"copyAuthorId": "Copy author ID",
"viewAuthor": "View author",
"pinAuthor": "Pin author",
"copyRaw": "Copy raw event",
"mute": "Mute"
},
"buttons": {
"pin": "Pin",
"pinTooltip": "Pin Note",
"repost": "Repost",
"quote": "Quote",
"viewProfile": "View profile"
},
"zap": {
"zap": "Zap",
"tooltip": "Send zap",
"modalTitle": "Send zap to",
"messagePlaceholder": "Enter message (optional)",
"buttonFinish": "Zapped",
"buttonLoading": "Processing...",
"invoiceButton": "Scan to zap",
"invoiceFooter": "You must use Bitcoin wallet which support Lightning\nsuch as: Blue Wallet, Bitkit, Phoenix,..."
},
"reply": {
"single": "reply",
"plural": "replies",
"empty": "Be the first to Reply!"
}
},
"user": {
"avatar": "Avatar",
"displayName": "Display Name",
"name": "Name",
"bio": "Bio",
"lna": "Lightning address",
"website": "Website",
"verified": "Verified",
"unverified": "Unverified",
"follow": "Follow",
"unfollow": "Unfollow",
"latestPosts": "Latest posts",
"avatarButton": "Change avatar",
"coverButton": "Change cover",
"editProfile": "Edit profile",
"settings": "Settings",
"logout": "Log out",
"logoutConfirmTitle": "Are you sure!",
"logoutConfirmSubtitle": "You can always log back in at any time. If you just want to switch accounts, you can do that by adding an existing account."
},
"editor": {
"title": "New Post",
"placeholder": "What are you up to?",
"successMessage": "Your note has been published successfully.",
"replyPlaceholder": "Post your reply"
},
"search": {
"placeholder": "Type something to search...",
"empty": "Try searching for people, notes, or keywords"
},
"welcome": {
"title": "Lume is a magnificent client for Nostr to meet, explore\nand freely share your thoughts with everyone.",
"signup": "Join Nostr",
"login": "Login",
"footer": "Before joining Nostr, you can take time to learn more about Nostr"
},
"login": {
"title": "Welcome back, anon!",
"footer": "Lume will put your Private Key in Secure Storage depended on your OS Platform. It will be secured by Password or Biometric ID",
"loginWithAddress": "Login with Nostr Address",
"loginWithBunker": "Login with nsecBunker",
"or": "Or continue with",
"loginWithPrivkey": "Login with Private Key"
},
"loginWithAddress": {
"title": "Enter your Nostr Address"
},
"loginWithBunker": {
"title": "Enter your nsecbunker token"
},
"loginWithPrivkey": {
"title": "Enter your Private Key",
"subtitle": "Lume will put your private key to <1>{{service}}</1>.\nIt will be secured by your OS."
},
"signup": {
"title": "Let's Get Started",
"subtitle": "Choose one of methods below to create your account",
"selfManageMethod": "Self-Managed",
"selfManageMethodDescription": "You create your keys and keep them safe.",
"providerMethod": "Managed by Provider",
"providerMethodDescription": "A 3rd party provider will handle your sign in keys for you."
},
"signupWithSelfManage": {
"title": "This is your new Account Key",
"subtitle": "Keep your key in safe place. If you lose this key, you will lose access to your account.",
"confirm1": "I understand the risk of lost private key.",
"confirm2": "I will make sure keep it safe and not sharing with anyone.",
"confirm3": "I understand I cannot recover private key.",
"button": "Save key & Continue"
},
"signupWithProvider": {
"title": "Let's set up your account on Nostr",
"username": "Username *",
"chooseProvider": "Choose a Provider",
"usernameFooter": "Use to login to Lume and other Nostr apps. You can choose provider you trust to manage your account",
"email": "Backup Email (optional)",
"emailFooter": "Use for recover your account if you lose your password"
},
"onboardingSettings": {
"title": "You're almost ready to use Lume.",
"subtitle": "Let's start personalizing your experience.",
"notification": {
"title": "Push notification",
"subtitle": "Enabling push notifications will allow you to receive notifications from Lume."
},
"lowPower": {
"title": "Low Power Mode",
"subtitle": "Limited relay connection and hide all media, sustainable for low network environment."
},
"translation": {
"title": "Translation (nostr.wine)",
"subtitle": "Translate text to your preferred language, powered by Nostr Wine."
},
"footer": "There are many more settings you can configure from the 'Settings' Screen. Be sure to visit it later."
},
"relays": {
"global": "Global",
"follows": "Follows",
"sidebar": {
"title": "Connected relays",
"empty": "Empty."
},
"relayView": {
"empty": "Could not load relay information 😬",
"owner": "Owner",
"contact": "Contact",
"software": "Software",
"nips": "Supported NIPs",
"limit": "Limitation",
"payment": "Open payment website",
"paymentNote": "You need to make a payment to connect this relay"
}
},
"suggestion": {
"title": "Suggested Follows",
"error": "Error. Cannot get trending users",
"button": "Save & Go back"
},
"interests": {
"title": "Interests",
"subtitle": "Pick things you'd like to see in your home feed.",
"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"
}
},
"onboarding": {
"home": {
"title": "Your account was successfully created!",
"subtitle": "For starters, let's set up your profile.",
"profileSettings": "Profile Settings"
},
"profile": {
"title": "About you",
"subtitle": "Tell Lume about yourself to start building your home feed."
},
"finish": {
"title": "Profile setup complete!",
"subtitle": "You can exit the setup here and start using Lume.",
"report": "Report a issue"
}
},
"activity": {
"title": "Activity",
"empty": "Yo! Nothing new yet.",
"mention": "mention you",
"repost": "reposted",
"zap": "zapped",
"newReply": "New reply",
"boost": "Boost",
"boostSubtitle": "@ Someone has reposted to your note",
"conversation": "Conversation",
"conversationSubtitle": "@ Someone has replied to your note"
}
}

View File

View File

@ -1,108 +0,0 @@
[info]
relay_url = "<url>"
name = "depot"
description = "Nostr Relay inside Lume. Powered by nostr-rs-relay"
pubkey = ""
favicon = "favicon.ico"
relay_icon = "https://example.test/img.png"
#contact = "mailto:contact@example.com"
[diagnostics]
#tracing = false
[database]
engine = "sqlite"
data_directory = "."
max_conn = 8
min_conn = 0
[logging]
#folder_path = "./log"
#file_prefix = "nostr-relay"
[network]
address = "0.0.0.0"
port = 6090
#remote_ip_header = "x-forwarded-for"
#remote_ip_header = "cf-connecting-ip"
#ping_interval = 300
[options]
reject_future_seconds = 1800
[limits]
messages_per_sec = 10
subscriptions_per_min = 10
limit_scrapers = false
[authorization]
pubkey_whitelist = []
nip42_auth = true
nip42_dms = true
[verified_users]
mode = "passive"
#domain_blacklist = ["wellorder.net"]
#domain_whitelist = ["example.com"]
verify_expiration = "1 week"
#verify_update_frequency = "24 hours"
max_consecutive_failures = 3
[grpc]
# gRPC interfaces for externalized decisions and other extensions to
# functionality.
#
# Events can be authorized through an external service, by providing
# the URL below. In the event the server is not accessible, events
# will be permitted. The protobuf3 schema used is available in
# `proto/nauthz.proto`.
# event_admission_server = "http://[::1]:50051"
# If the event admission server denies writes
# in any case (excluding spam filtering).
# This is reflected in the relay information document.
# restricts_write = true
[pay_to_relay]
# Enable pay to relay
#enabled = false
# The cost to be admitted to relay
#admission_cost = 4200
# The cost in sats per post
#cost_per_event = 0
# Url of lnbits api
#node_url = "<node url>"
# LNBits api secret
#api_secret = "<ln bits api>"
# Nostr direct message on signup
#direct_message=false
# Terms of service
#terms_message = """
#This service (and supporting services) are provided "as is", without warranty of any kind, express or implied.
#
#By using this service, you agree:
#* Not to engage in spam or abuse the relay service
#* Not to disseminate illegal content
#* That requests to delete content cannot be guaranteed
#* To use the service in compliance with all applicable laws
#* To grant necessary rights to your content for unlimited time
#* To be of legal age and have capacity to use this service
#* That the service may be terminated at any time without notice
#* That the content you publish may be removed at any time without notice
#* To have your IP address collected to detect abuse or misuse
#* To cooperate with the relay to combat abuse or misuse
#* You may be exposed to content that you might find triggering or distasteful
#* The relay operator is not liable for content produced by users of the relay
#"""
# Whether or not new sign ups should be allowed
#sign_ups = false
# optional if `direct_message=false`
#secret_key = "<nostr nsec>"

View File

@ -26,7 +26,8 @@
"$VIDEO/*",
"$RESOURCE",
"$RESOURCE/*",
"$RESOURCE/**"
"$RESOURCE/**",
"$RESOURCE/locales/*"
]
},
"http": {
@ -34,7 +35,7 @@
},
"shell": {
"open": true,
"scope": [{ "name": "bin/depot", "sidecar": true, "args": true }]
"scope": []
},
"updater": {
"endpoints": [
@ -51,7 +52,7 @@
"depends": []
},
"externalBin": [],
"resources": ["resources/*"],
"resources": ["resources/*", "./locales/*"],
"icon": [
"icons/32x32.png",
"icons/128x128.png",