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