- {search.reply_to ? (
-
-
+
+
+ {reply_to?.length ? (
+
) : null}
-
+
}
placeholder={
- search.reply_to ? "Type your reply..." : t("editor.placeholder")
+ reply_to ? "Type your reply..." : "What're you up to?"
}
className="focus:outline-none"
/>
+ {warning.enable ? (
+
+
+ Reason:
+
+
+ setWarning((prev) => ({ ...prev, reason: e.target.value }))
+ }
+ className="flex-1 text-sm bg-transparent border-none focus:outline-none focus:ring-0 placeholder:text-black/50 dark:placeholder:text-white/50"
+ />
+
+ ) : null}
+ {difficulty.enable ? (
+
+
+ Difficulty:
+
+ {
+ if (!/[0-9]/.test(event.key)) {
+ event.preventDefault();
+ }
+ }}
+ placeholder="21"
+ defaultValue={difficulty.num}
+ onChange={(e) =>
+ setWarning((prev) => ({ ...prev, num: Number(e.target.value) }))
+ }
+ className="flex-1 text-sm bg-transparent border-none focus:outline-none focus:ring-0 placeholder:text-black/50 dark:placeholder:text-white/50"
+ />
+
+ ) : null}
+
+
+
+
);
}
+function ChildNote({ id }: { id: string }) {
+ const { isLoading, isError, data } = useEvent(id);
+
+ if (isLoading) {
+ return
;
+ }
+
+ if (isError || !data) {
+ return
Event not found with your current relay set.
;
+ }
+
+ return (
+
+
+
+
+
+
+
+ {data.content}
+
+
+ );
+}
+
const withNostrEvent = (editor: ReactEditor) => {
const { insertData, isVoid } = editor;
@@ -190,7 +270,7 @@ const withNostrEvent = (editor: ReactEditor) => {
editor.insertData = (data) => {
const text = data.getData("text/plain");
- if (text.startsWith("nevent1") || text.startsWith("note1")) {
+ if (text.startsWith("nevent") || text.startsWith("note")) {
insertNostrEvent(editor, text);
} else {
insertData(data);
@@ -259,6 +339,7 @@ const Image = ({ attributes, element, children }) => {
selected && focused ? "ring-blue-500" : "ring-transparent",
)}
onClick={() => Transforms.removeNodes(editor, { at: path })}
+ onKeyDown={() => Transforms.removeNodes(editor, { at: path })}
/>
);
@@ -274,7 +355,7 @@ const Mention = ({ attributes, element }) => {
type="button"
contentEditable={false}
onClick={() => Transforms.removeNodes(editor, { at: path })}
- className="inline-block align-baseline text-blue-500 hover:text-blue-600"
+ className="inline-block text-blue-500 align-baseline hover:text-blue-600"
>{`@${element.name}`}
);
};
@@ -286,16 +367,13 @@ const Event = ({ attributes, element, children }) => {
return (
{children}
- {/* biome-ignore lint/a11y/useKeyWithClickEvents:
*/}
Transforms.removeNodes(editor, { at: path })}
- className="user-select-none relative my-2"
+ onKeyDown={() => Transforms.removeNodes(editor, { at: path })}
>
-
+
);
diff --git a/apps/desktop2/src/routes/global.tsx b/apps/desktop2/src/routes/global.tsx
index ee437265..8ae7ba43 100644
--- a/apps/desktop2/src/routes/global.tsx
+++ b/apps/desktop2/src/routes/global.tsx
@@ -2,12 +2,13 @@ import { Conversation } from "@/components/conversation";
import { Quote } from "@/components/quote";
import { RepostNote } from "@/components/repost";
import { TextNote } from "@/components/text";
-import { ArrowRightCircleIcon, ArrowRightIcon } from "@lume/icons";
+import { ArrowRightCircleIcon } from "@lume/icons";
import { NostrQuery } from "@lume/system";
import { type ColumnRouteSearch, type NostrEvent, Kind } from "@lume/types";
import { Spinner } from "@lume/ui";
import { useInfiniteQuery } from "@tanstack/react-query";
-import { Link, createFileRoute } from "@tanstack/react-router";
+import { createFileRoute } from "@tanstack/react-router";
+import { useCallback } from "react";
import { Virtualizer } from "virtua";
export const Route = createFileRoute("/global")({
@@ -18,10 +19,6 @@ export const Route = createFileRoute("/global")({
name: search.name,
};
},
- beforeLoad: async () => {
- const settings = await NostrQuery.getSettings();
- return { settings };
- },
component: Screen,
});
@@ -46,34 +43,39 @@ export function Screen() {
refetchOnWindowFocus: false,
});
- const renderItem = (event: NostrEvent) => {
- if (!event) return;
- switch (event.kind) {
- case Kind.Repost:
- return
;
- default: {
- const isConversation =
- event.tags.filter((tag) => tag[0] === "e" && tag[3] !== "mention")
- .length > 0;
- const isQuote = event.tags.filter((tag) => tag[0] === "q").length > 0;
+ const renderItem = useCallback(
+ (event: NostrEvent) => {
+ if (!event) return;
+ switch (event.kind) {
+ case Kind.Repost:
+ return
;
+ default: {
+ const isConversation =
+ event.tags.filter((tag) => tag[0] === "e" && tag[3] !== "mention")
+ .length > 0;
+ const isQuote = event.tags.filter((tag) => tag[0] === "q").length > 0;
- if (isConversation) {
- return
;
+ if (isConversation) {
+ return (
+
+ );
+ }
+
+ if (isQuote) {
+ return
;
+ }
+
+ return
;
}
-
- if (isQuote) {
- return
;
- }
-
- return
;
}
- }
- };
+ },
+ [data],
+ );
return (
-
+
{isFetching && !isLoading && !isFetchingNextPage ? (
-
+
Fetching new notes...
@@ -81,7 +83,7 @@ export function Screen() {
) : null}
{isLoading ? (
-
+
Loading...
@@ -98,7 +100,7 @@ export function Screen() {
type="button"
onClick={() => fetchNextPage()}
disabled={isFetchingNextPage || isLoading}
- className="inline-flex h-12 w-full items-center justify-center gap-2 rounded-xl bg-black/5 px-3 font-medium hover:bg-black/10 focus:outline-none dark:bg-white/10 dark:hover:bg-white/20"
+ className="inline-flex items-center justify-center w-full h-12 gap-2 px-3 font-medium rounded-xl bg-black/5 hover:bg-black/10 focus:outline-none dark:bg-white/10 dark:hover:bg-white/20"
>
{isFetchingNextPage ? (
@@ -117,31 +119,12 @@ export function Screen() {
function Empty() {
return (
-
-
-
-
+
+
+
Your newsfeed is empty
-
- Here are few suggestions to get started.
-
-
-
-
-
- Show trending notes
-
-
-
- Discover trending users
-
);
diff --git a/apps/desktop2/src/routes/group.tsx b/apps/desktop2/src/routes/group.tsx
index f41bc5ea..b40a303f 100644
--- a/apps/desktop2/src/routes/group.tsx
+++ b/apps/desktop2/src/routes/group.tsx
@@ -2,12 +2,13 @@ import { Conversation } from "@/components/conversation";
import { Quote } from "@/components/quote";
import { RepostNote } from "@/components/repost";
import { TextNote } from "@/components/text";
-import { ArrowRightCircleIcon, ArrowRightIcon } from "@lume/icons";
-import { NostrAccount, NostrQuery } from "@lume/system";
+import { ArrowRightCircleIcon } from "@lume/icons";
+import { NostrQuery } from "@lume/system";
import { type ColumnRouteSearch, type NostrEvent, Kind } from "@lume/types";
import { Spinner } from "@lume/ui";
import { useInfiniteQuery } from "@tanstack/react-query";
-import { Link, createFileRoute, redirect } from "@tanstack/react-router";
+import { createFileRoute, redirect } from "@tanstack/react-router";
+import { useCallback } from "react";
import { Virtualizer } from "virtua";
export const Route = createFileRoute("/group")({
@@ -21,7 +22,6 @@ export const Route = createFileRoute("/group")({
beforeLoad: async ({ search }) => {
const key = `lume_group_${search.label}`;
const groups = (await NostrQuery.getNstore(key)) as string[];
- const settings = await NostrQuery.getSettings();
if (!groups?.length) {
throw redirect({
@@ -33,10 +33,7 @@ export const Route = createFileRoute("/group")({
});
}
- return {
- groups,
- settings,
- };
+ return { groups };
},
component: Screen,
});
@@ -55,7 +52,7 @@ export function Screen() {
queryKey: [label, account],
initialPageParam: 0,
queryFn: async ({ pageParam }: { pageParam: number }) => {
- const events = await NostrQuery.getLocalEvents(groups, pageParam);
+ const events = await NostrQuery.getGroupEvents(groups, pageParam);
return events;
},
getNextPageParam: (lastPage) => lastPage?.at(-1)?.created_at - 1,
@@ -64,34 +61,39 @@ export function Screen() {
refetchOnWindowFocus: false,
});
- const renderItem = (event: NostrEvent) => {
- if (!event) return;
- switch (event.kind) {
- case Kind.Repost:
- return
;
- default: {
- const isConversation =
- event.tags.filter((tag) => tag[0] === "e" && tag[3] !== "mention")
- .length > 0;
- const isQuote = event.tags.filter((tag) => tag[0] === "q").length > 0;
+ const renderItem = useCallback(
+ (event: NostrEvent) => {
+ if (!event) return;
+ switch (event.kind) {
+ case Kind.Repost:
+ return
;
+ default: {
+ const isConversation =
+ event.tags.filter((tag) => tag[0] === "e" && tag[3] !== "mention")
+ .length > 0;
+ const isQuote = event.tags.filter((tag) => tag[0] === "q").length > 0;
- if (isConversation) {
- return
;
+ if (isConversation) {
+ return (
+
+ );
+ }
+
+ if (isQuote) {
+ return
;
+ }
+
+ return
;
}
-
- if (isQuote) {
- return
;
- }
-
- return
;
}
- }
- };
+ },
+ [data],
+ );
return (
-
+
{isFetching && !isLoading && !isFetchingNextPage ? (
-
+
Fetching new notes...
@@ -99,7 +101,7 @@ export function Screen() {
) : null}
{isLoading ? (
-
+
Loading...
@@ -116,7 +118,7 @@ export function Screen() {
type="button"
onClick={() => fetchNextPage()}
disabled={isFetchingNextPage || isLoading}
- className="inline-flex h-12 w-full items-center justify-center gap-2 rounded-xl bg-neutral-100 px-3 font-medium hover:bg-neutral-50 focus:outline-none dark:bg-white/10 dark:hover:bg-white/20"
+ className="inline-flex items-center justify-center w-full h-12 gap-2 px-3 font-medium rounded-xl bg-neutral-100 hover:bg-neutral-50 focus:outline-none dark:bg-white/10 dark:hover:bg-white/20"
>
{isFetchingNextPage ? (
@@ -135,31 +137,12 @@ export function Screen() {
function Empty() {
return (
-
-
-
-
+
+
+
Your newsfeed is empty
-
- Here are few suggestions to get started.
-
-
-
-
-
- Show trending notes
-
-
-
- Discover trending users
-
);
diff --git a/apps/desktop2/src/routes/newsfeed.tsx b/apps/desktop2/src/routes/newsfeed.tsx
index 8161a3a1..b975314e 100644
--- a/apps/desktop2/src/routes/newsfeed.tsx
+++ b/apps/desktop2/src/routes/newsfeed.tsx
@@ -3,12 +3,12 @@ import { Quote } from "@/components/quote";
import { RepostNote } from "@/components/repost";
import { TextNote } from "@/components/text";
import { ArrowRightCircleIcon } from "@lume/icons";
-import { NostrAccount, NostrQuery } from "@lume/system";
-import { type ColumnRouteSearch, type NostrEvent, Kind } from "@lume/types";
+import { type LumeEvent, NostrAccount, NostrQuery } from "@lume/system";
+import { type ColumnRouteSearch, Kind } from "@lume/types";
import { Spinner } from "@lume/ui";
import { useInfiniteQuery } from "@tanstack/react-query";
-import { redirect } from "@tanstack/react-router";
-import { createFileRoute } from "@tanstack/react-router";
+import { createFileRoute, redirect } from "@tanstack/react-router";
+import { useCallback } from "react";
import { Virtualizer } from "virtua";
export const Route = createFileRoute("/newsfeed")({
@@ -20,10 +20,8 @@ export const Route = createFileRoute("/newsfeed")({
};
},
beforeLoad: async ({ search }) => {
- const settings = await NostrQuery.getSettings();
- const contacts = await NostrAccount.getContactList();
-
- if (!contacts.length) {
+ const isContactListEmpty = await NostrAccount.isContactListEmpty();
+ if (isContactListEmpty) {
throw redirect({
to: "/create-newsfeed/users",
search: {
@@ -32,15 +30,12 @@ export const Route = createFileRoute("/newsfeed")({
},
});
}
-
- return { settings, contacts };
},
component: Screen,
});
export function Screen() {
const { label, account } = Route.useSearch();
- const { contacts, settings } = Route.useRouteContext();
const {
data,
isLoading,
@@ -52,7 +47,7 @@ export function Screen() {
queryKey: [label, account],
initialPageParam: 0,
queryFn: async ({ pageParam }: { pageParam: number }) => {
- const events = await NostrQuery.getLocalEvents(contacts, pageParam);
+ const events = await NostrQuery.getLocalEvents(pageParam);
return events;
},
getNextPageParam: (lastPage) => lastPage?.at(-1)?.created_at - 1,
@@ -60,41 +55,32 @@ export function Screen() {
refetchOnWindowFocus: false,
});
- const renderItem = (event: NostrEvent) => {
- if (!event) return;
- switch (event.kind) {
- case Kind.Repost:
- return
;
- default: {
- const isConversation =
- event.tags.filter((tag) => tag[0] === "e" && tag[3] !== "mention")
- .length > 0;
- const isQuote = event.tags.filter((tag) => tag[0] === "q").length > 0;
-
- if (isConversation) {
- return (
-
- );
+ const renderItem = useCallback(
+ (event: LumeEvent) => {
+ if (!event) return;
+ switch (event.kind) {
+ case Kind.Repost:
+ return
;
+ default: {
+ if (event.isConversation) {
+ return (
+
+ );
+ }
+ if (event.isQuote) {
+ return
;
+ }
+ return
;
}
-
- if (isQuote) {
- return
;
- }
-
- return
;
}
- }
- };
+ },
+ [data],
+ );
return (
-
+
{isFetching && !isLoading && !isFetchingNextPage ? (
-
+
Fetching new notes...
@@ -102,7 +88,7 @@ export function Screen() {
) : null}
{isLoading ? (
-
+
Loading...
@@ -121,7 +107,7 @@ export function Screen() {
type="button"
onClick={() => fetchNextPage()}
disabled={isFetchingNextPage || isLoading}
- className="inline-flex h-12 w-full items-center justify-center gap-2 rounded-xl bg-black/5 px-3 font-medium hover:bg-black/10 focus:outline-none dark:bg-white/10 dark:hover:bg-white/20"
+ className="inline-flex items-center justify-center w-full gap-2 px-3 font-medium h-9 rounded-xl bg-black/5 hover:bg-black/10 focus:outline-none dark:bg-white/10 dark:hover:bg-white/20"
>
{isFetchingNextPage ? (
diff --git a/apps/desktop2/src/routes/panel.tsx b/apps/desktop2/src/routes/panel.tsx
index 0720aba9..f1816929 100644
--- a/apps/desktop2/src/routes/panel.tsx
+++ b/apps/desktop2/src/routes/panel.tsx
@@ -1,7 +1,7 @@
import { Note } from "@/components/note";
import { User } from "@/components/user";
-import { LumeWindow, NostrQuery, useEvent } from "@lume/system";
-import { Kind, NostrEvent } from "@lume/types";
+import { type LumeEvent, LumeWindow, NostrQuery, useEvent } from "@lume/system";
+import { Kind } from "@lume/types";
import { createFileRoute } from "@tanstack/react-router";
import { getCurrent } from "@tauri-apps/api/window";
import { useEffect, useMemo, useState } from "react";
@@ -19,7 +19,7 @@ export const Route = createFileRoute("/panel")({
function Screen() {
const [account, setAccount] = useState
(null);
- const [events, setEvents] = useState([]);
+ const [events, setEvents] = useState([]);
const texts = useMemo(
() => events.filter((ev) => ev.kind === Kind.Text),
@@ -27,7 +27,7 @@ function Screen() {
);
const zaps = useMemo(() => {
- const groups = new Map();
+ const groups = new Map();
const list = events.filter((ev) => ev.kind === Kind.ZapReceipt);
for (const event of list) {
@@ -46,7 +46,7 @@ function Screen() {
}, [events]);
const reactions = useMemo(() => {
- const groups = new Map();
+ const groups = new Map();
const list = events.filter(
(ev) => ev.kind === Kind.Repost || ev.kind === Kind.Reaction,
);
@@ -86,7 +86,7 @@ function Screen() {
);
const unlistenNewEvent = getCurrent().listen("notification", (data) => {
- const event: NostrEvent = JSON.parse(data.payload as string);
+ const event: LumeEvent = JSON.parse(data.payload as string);
setEvents((prev) => [event, ...prev]);
});
@@ -98,28 +98,28 @@ function Screen() {
if (!account) {
return (
-
+
Please log in.
);
}
return (
-
-
+
+
Notifications
-
+
@@ -127,7 +127,7 @@ function Screen() {
{texts.map((event) => (
-
+
))}
{[...reactions.entries()].map(([root, events]) => (
-
-
+
+
{events.map((event) => (
-
+
-
-
+
+
{event.kind === Kind.Reaction ? (
event.content === "+" ? (
"๐"
@@ -178,7 +178,7 @@ function Screen() {
event.content
)
) : (
-
+
)}
@@ -193,19 +193,20 @@ function Screen() {
{[...zaps.entries()].map(([root, events]) => (
-
-
+
+
{events.map((event) => (
tag[0] == "P")[1]}
+ key={event.id}
+ pubkey={event.tags.find((tag) => tag[0] === "P")[1]}
>
-
+
โฟ {decodeZapInvoice(event.tags).bitcoinFormatted}
@@ -228,9 +229,9 @@ function RootNote({ id }: { id: string }) {
if (isLoading) {
return (
-
-
-
+
);
}
@@ -238,10 +239,10 @@ function RootNote({ id }: { id: string }) {
if (isError || !data) {
return (
-
+
-
+
Event not found with your current relay set
@@ -253,7 +254,7 @@ function RootNote({ id }: { id: string }) {
-
+
{data.content}
@@ -262,7 +263,7 @@ function RootNote({ id }: { id: string }) {
);
}
-function TextNote({ event }: { event: NostrEvent }) {
+function TextNote({ event }: { event: LumeEvent }) {
const pTags = event.tags
.filter((tag) => tag[0] === "p")
.map((tag) => tag[1])
@@ -275,14 +276,14 @@ function TextNote({ event }: { event: NostrEvent }) {
onClick={() => LumeWindow.openEvent(event)}
>
-
+
-
-
-
-
-
+
+
+
+
+
{formatCreatedAt(event.created_at)}
@@ -292,9 +293,9 @@ function TextNote({ event }: { event: NostrEvent }) {
{pTags.map((replyTo) => (
-
+
-
+
))}
diff --git a/apps/desktop2/src/routes/settings/backup.tsx b/apps/desktop2/src/routes/settings/backup.tsx
index 57c041b6..88e7ebb1 100644
--- a/apps/desktop2/src/routes/settings/backup.tsx
+++ b/apps/desktop2/src/routes/settings/backup.tsx
@@ -1,6 +1,6 @@
import { User } from "@/components/user";
import { NostrAccount } from "@lume/system";
-import { displayNsec } from "@lume/utils";
+import { displayNpub, displayNsec } from "@lume/utils";
import { createFileRoute } from "@tanstack/react-router";
import { invoke } from "@tauri-apps/api/core";
import { writeText } from "@tauri-apps/plugin-clipboard-manager";
@@ -13,50 +13,34 @@ interface Account {
}
export const Route = createFileRoute("/settings/backup")({
- component: Screen,
- loader: async () => {
- const npubs = await NostrAccount.getAccounts();
- const accounts: Account[] = [];
-
- for (const npub of npubs) {
- const nsec: string = await invoke("get_stored_nsec", { npub });
- accounts.push({ npub, nsec });
- }
-
- return accounts;
+ beforeLoad: async () => {
+ const accounts = await NostrAccount.getAccounts();
+ return { accounts };
},
+ component: Screen,
});
function Screen() {
- const accounts = Route.useLoaderData();
+ const { accounts } = Route.useRouteContext();
return (
-
+
{accounts.map((account) => (
-
+
))}
);
}
-function List({ account }: { account: Account }) {
- const [key, setKey] = useState(account.nsec);
+function Account({ account }: { account: string }) {
const [copied, setCopied] = useState(false);
- const [passphase, setPassphase] = useState("");
-
- const encrypt = async () => {
- const encrypted: string = await invoke("get_encrypted_key", {
- npub: account.npub,
- password: passphase,
- });
- setKey(encrypted);
- };
const copyKey = async () => {
try {
- await writeText(key);
+ const data: string = await invoke("get_private_key", { npub: account });
+ await writeText(data);
setCopied(true);
} catch (e) {
toast.error(e);
@@ -64,65 +48,26 @@ function List({ account }: { account: Account }) {
};
return (
-
-
+
+
-
+
-
+
+ {displayNpub(account, 16)}
+
-
-
-
-
-
-
-
-
-
-
-
- setPassphase(e.target.value)}
- className="h-9 w-full rounded-lg border-neutral-300 bg-transparent px-3 placeholder:text-neutral-500 focus:border-blue-500 focus:ring focus:ring-blue-200 dark:border-neutral-700 dark:placeholder:text-neutral-400 dark:focus:ring-blue-800"
- />
-
-
-
+
+
);
diff --git a/apps/desktop2/src/routes/settings/general.tsx b/apps/desktop2/src/routes/settings/general.tsx
index d742905a..2c7c6c88 100644
--- a/apps/desktop2/src/routes/settings/general.tsx
+++ b/apps/desktop2/src/routes/settings/general.tsx
@@ -48,13 +48,6 @@ function Screen() {
}));
};
- const toggleZap = () => {
- setNewSettings((prev) => ({
- ...prev,
- zap: !newSettings.zap,
- }));
- };
-
const toggleNsfw = () => {
setNewSettings((prev) => ({
...prev,
@@ -84,14 +77,14 @@ function Screen() {
}, [newSettings]);
return (
-
+
-
+
General
-
-
+
+
Notification
@@ -99,7 +92,7 @@ function Screen() {
notifications from Lume directly.
-
+
toggleNofitication()}
@@ -109,7 +102,7 @@ function Screen() {
-
+
Relay Hint
@@ -117,7 +110,7 @@ function Screen() {
Relay Hint when fetching a new event.
-
+
toggleGossip()}
@@ -127,7 +120,7 @@ function Screen() {
-
+
Enhanced Privacy
@@ -135,7 +128,7 @@ function Screen() {
previews in plain text.
-
+
toggleEnhancedPrivacy()}
@@ -145,14 +138,14 @@ function Screen() {
-
+
Auto Update
Automatically download and install new version.
-
+
toggleAutoUpdate()}
@@ -162,7 +155,7 @@ function Screen() {
-
+
Filter sensitive content
@@ -170,7 +163,7 @@ function Screen() {
Warning tag, it's may include NSFW content.
-
+
toggleNsfw()}
@@ -183,39 +176,21 @@ function Screen() {
-
+
Interface
-
-
-
-
Zap
-
- Show the Zap button in each note and user's profile screen,
- use for send bitcoin tip to other users.
-
-
-
- toggleZap()}
- className="relative h-7 w-12 shrink-0 cursor-default rounded-full bg-black/10 outline-none data-[state=checked]:bg-blue-500 dark:bg-white/10"
- >
-
-
-
-
-
+
+
Appearance
* Require restarting the app to take effect.
-
+