Make Lume Faster (#208)

* chore: fix some lint issues

* feat: refactor contact list

* feat: refactor relay hint

* feat: add missing commands

* feat: use new cache layer for react query

* feat: refactor column

* feat: improve relay hint

* fix: replace break with continue in parser

* refactor: publish function

* feat: add reply command

* feat: improve editor

* fix: quote

* chore: update deps

* refactor: note component

* feat: improve repost

* feat: improve cache

* fix: backup screen

* refactor: column manager
This commit is contained in:
雨宮蓮
2024-06-17 13:52:06 +07:00
committed by GitHub
parent 7c99ed39e4
commit 843895d876
79 changed files with 1738 additions and 1975 deletions

View File

@@ -5,7 +5,9 @@
"main": "./src/index.ts",
"dependencies": {
"@lume/utils": "workspace:^",
"@tanstack/react-query": "^5.40.1",
"@tanstack/query-persist-client-core": "^5.45.0",
"@tanstack/react-query": "^5.45.0",
"nostr-tools": "^2.7.0",
"react": "^18.3.1"
},
"devDependencies": {

View File

@@ -1,5 +1,5 @@
import { Metadata } from "@lume/types";
import { Result, commands } from "./commands";
import type { Metadata } from "@lume/types";
import { type Result, commands } from "./commands";
import { Window } from "@tauri-apps/api/window";
export class NostrAccount {
@@ -123,24 +123,24 @@ export class NostrAccount {
const query = await commands.getBalance();
if (query.status === "ok") {
return parseInt(query.data);
return Number.parseInt(query.data);
} else {
return 0;
}
}
static async getContactList() {
const query = await commands.getContactList();
static async isContactListEmpty() {
const query = await commands.isContactListEmpty();
if (query.status === "ok") {
return query.data;
} else {
return [];
return true;
}
}
static async follow(pubkey: string, alias?: string) {
const query = await commands.follow(pubkey, alias);
static async checkContact(pubkey: string) {
const query = await commands.checkContact(pubkey);
if (query.status === "ok") {
return query.data;
@@ -149,8 +149,8 @@ export class NostrAccount {
}
}
static async unfollow(pubkey: string) {
const query = await commands.unfollow(pubkey);
static async toggleContact(pubkey: string, alias?: string) {
const query = await commands.toggleContact(pubkey, alias);
if (query.status === "ok") {
return query.data;

View File

@@ -76,6 +76,14 @@ try {
else return { status: "error", error: e as any };
}
},
async getPrivateKey(npub: string) : Promise<Result<string, string>> {
try {
return { status: "ok", data: await TAURI_INVOKE("get_private_key", { npub }) };
} catch (e) {
if(e instanceof Error) throw e;
else return { status: "error", error: e as any };
}
},
async connectRemoteAccount(uri: string) : Promise<Result<string, string>> {
try {
return { status: "ok", data: await TAURI_INVOKE("connect_remote_account", { uri }) };
@@ -140,9 +148,9 @@ try {
else return { status: "error", error: e as any };
}
},
async setContactList(pubkeys: string[]) : Promise<Result<boolean, string>> {
async setContactList(publicKeys: string[]) : Promise<Result<boolean, string>> {
try {
return { status: "ok", data: await TAURI_INVOKE("set_contact_list", { pubkeys }) };
return { status: "ok", data: await TAURI_INVOKE("set_contact_list", { publicKeys }) };
} catch (e) {
if(e instanceof Error) throw e;
else return { status: "error", error: e as any };
@@ -156,17 +164,25 @@ try {
else return { status: "error", error: e as any };
}
},
async follow(id: string, alias: string | null) : Promise<Result<string, string>> {
async isContactListEmpty() : Promise<Result<boolean, null>> {
try {
return { status: "ok", data: await TAURI_INVOKE("follow", { id, alias }) };
return { status: "ok", data: await TAURI_INVOKE("is_contact_list_empty") };
} catch (e) {
if(e instanceof Error) throw e;
else return { status: "error", error: e as any };
}
},
async unfollow(id: string) : Promise<Result<string, string>> {
async checkContact(hex: string) : Promise<Result<boolean, null>> {
try {
return { status: "ok", data: await TAURI_INVOKE("unfollow", { id }) };
return { status: "ok", data: await TAURI_INVOKE("check_contact", { hex }) };
} catch (e) {
if(e instanceof Error) throw e;
else return { status: "error", error: e as any };
}
},
async toggleContact(hex: string, alias: string | null) : Promise<Result<string, string>> {
try {
return { status: "ok", data: await TAURI_INVOKE("toggle_contact", { hex, alias }) };
} catch (e) {
if(e instanceof Error) throw e;
else return { status: "error", error: e as any };
@@ -244,6 +260,14 @@ try {
else return { status: "error", error: e as any };
}
},
async getEventMeta(content: string) : Promise<Result<Meta, null>> {
try {
return { status: "ok", data: await TAURI_INVOKE("get_event_meta", { content }) };
} catch (e) {
if(e instanceof Error) throw e;
else return { status: "error", error: e as any };
}
},
async getEvent(id: string) : Promise<Result<RichEvent, string>> {
try {
return { status: "ok", data: await TAURI_INVOKE("get_event", { id }) };
@@ -252,6 +276,14 @@ try {
else return { status: "error", error: e as any };
}
},
async getEventFrom(id: string, relayHint: string) : Promise<Result<RichEvent, string>> {
try {
return { status: "ok", data: await TAURI_INVOKE("get_event_from", { id, relayHint }) };
} catch (e) {
if(e instanceof Error) throw e;
else return { status: "error", error: e as any };
}
},
async getReplies(id: string) : Promise<Result<RichEvent[], string>> {
try {
return { status: "ok", data: await TAURI_INVOKE("get_replies", { id }) };
@@ -268,9 +300,17 @@ try {
else return { status: "error", error: e as any };
}
},
async getLocalEvents(pubkeys: string[], until: string | null) : Promise<Result<RichEvent[], string>> {
async getLocalEvents(until: string | null) : Promise<Result<RichEvent[], string>> {
try {
return { status: "ok", data: await TAURI_INVOKE("get_local_events", { pubkeys, until }) };
return { status: "ok", data: await TAURI_INVOKE("get_local_events", { until }) };
} catch (e) {
if(e instanceof Error) throw e;
else return { status: "error", error: e as any };
}
},
async getGroupEvents(publicKeys: string[], until: string | null) : Promise<Result<RichEvent[], string>> {
try {
return { status: "ok", data: await TAURI_INVOKE("get_group_events", { publicKeys, until }) };
} catch (e) {
if(e instanceof Error) throw e;
else return { status: "error", error: e as any };
@@ -292,9 +332,17 @@ try {
else return { status: "error", error: e as any };
}
},
async publish(content: string, tags: string[][]) : Promise<Result<string, string>> {
async publish(content: string, warning: string | null, difficulty: number | null) : Promise<Result<string, string>> {
try {
return { status: "ok", data: await TAURI_INVOKE("publish", { content, tags }) };
return { status: "ok", data: await TAURI_INVOKE("publish", { content, warning, difficulty }) };
} catch (e) {
if(e instanceof Error) throw e;
else return { status: "error", error: e as any };
}
},
async reply(content: string, to: string, root: string | null) : Promise<Result<string, string>> {
try {
return { status: "ok", data: await TAURI_INVOKE("reply", { content, to, root }) };
} catch (e) {
if(e instanceof Error) throw e;
else return { status: "error", error: e as any };

View File

@@ -1,6 +1,11 @@
import type { EventWithReplies, Kind, Meta, NostrEvent } from "@lume/types";
import { commands } from "./commands";
import { generateContentTags } from "@lume/utils";
import type {
EventTag,
EventWithReplies,
Kind,
Meta,
NostrEvent,
} from "@lume/types";
import { type Result, commands } from "./commands";
export class LumeEvent {
public id: string;
@@ -10,8 +15,8 @@ export class LumeEvent {
public tags: string[][];
public content: string;
public sig: string;
public relay?: string;
public meta: Meta;
public relay?: string;
#raw: NostrEvent;
constructor(event: NostrEvent) {
@@ -19,49 +24,52 @@ export class LumeEvent {
Object.assign(this, event);
}
get isQuote() {
return this.tags.filter((tag) => tag[0] === "q").length > 0;
}
get isConversation() {
const tags = this.tags.filter(
(tag) => tag[0] === "e" && tag[3] !== "mention",
);
return tags.length > 0;
}
get mentions() {
return this.tags.filter((tag) => tag[0] === "p").map((tag) => tag[1]);
}
static getEventThread(tags: string[][], gossip = true) {
let root: string = null;
let reply: string = null;
get repostId() {
return this.tags.find((tag) => tag[0] === "e")[1];
}
// Get all event references from tags, ignore mention
const events = tags.filter((el) => el[0] === "e" && el[3] !== "mention");
get thread() {
let root: EventTag = null;
let reply: EventTag = null;
if (gossip) {
const relays = tags
.filter((el) => el[0] === "e" && el[2]?.length)
.map((tag) => tag[2]);
// Get all event references from tags, ignore mention.
const events = this.tags.filter(
(el) => el[0] === "e" && el[3] !== "mention",
);
if (relays.length >= 1) {
for (const relay of relays) {
try {
if (relay.length) {
const url = new URL(relay);
commands
.connectRelay(url.toString())
.then(() => console.log("[relay hint]: ", url));
}
} catch (e) {
console.log("[relay hint] error: ", relay);
}
}
if (events.length === 1) {
root = { id: events[0][1], relayHint: events[0][2] };
}
if (events.length === 2) {
root = { id: events[0][1], relayHint: events[0][2] };
reply = { id: events[1][1], relayHint: events[1][2] };
}
if (events.length > 2) {
for (const tag of events) {
if (tag[3] === "root") root = { id: tag[1], relayHint: tag[2] };
if (tag[3] === "reply") reply = { id: tag[1], relayHint: tag[2] };
}
}
if (events.length === 1) {
root = events[0][1];
}
if (events.length > 1) {
root = events.find((el) => el[3] === "root")?.[1] ?? events[0][1];
reply = events.find((el) => el[3] === "reply")?.[1] ?? events[1][1];
}
// Fix some rare case when root === reply
if (root && reply && root === reply) {
// Fix some rare case when root same as reply
if (root && reply && root.id === reply.id) {
reply = null;
}
@@ -71,7 +79,17 @@ export class LumeEvent {
};
}
static async getReplies(id: string) {
get quote() {
const tag = this.tags.filter(
(tag) => tag[0] === "q" || tag[3] === "mention",
);
const id = tag[0][1];
const relayHint = tag[0][2];
return { id, relayHint };
}
public async getReplies(id: string) {
const query = await commands.getReplies(id);
if (query.status === "ok") {
@@ -99,14 +117,6 @@ export class LumeEvent {
for (const tag of tags) {
const rootIndex = events.findIndex((el) => el.id === tag[1]);
// Relay Hint
if (tag[2]?.length) {
const url = new URL(tag[2]);
commands
.connectRelay(url.toString())
.then(() => console.log("[relay hint]: ", url));
}
if (rootIndex !== -1) {
const rootEvent = events[rootIndex];
@@ -129,63 +139,8 @@ export class LumeEvent {
}
}
static async publish(
content: string,
reply_to?: string,
quote?: boolean,
nsfw?: boolean,
) {
const g = await generateContentTags(content);
const eventContent = g.content;
const eventTags = g.tags;
if (reply_to) {
const queryReply = await commands.getEvent(reply_to);
if (queryReply.status === "ok") {
const replyEvent = JSON.parse(queryReply.data.raw) as NostrEvent;
const relayHint =
replyEvent.tags.find((ev) => ev[0] === "e")?.[0][2] ?? "";
if (quote) {
eventTags.push(["e", replyEvent.id, relayHint, "mention"]);
eventTags.push(["q", replyEvent.id]);
} else {
const rootEvent = replyEvent.tags.find((ev) => ev[3] === "root");
if (rootEvent) {
eventTags.push([
"e",
rootEvent[1],
rootEvent[2] || relayHint,
"root",
]);
}
eventTags.push(["e", replyEvent.id, relayHint, "reply"]);
eventTags.push(["p", replyEvent.pubkey]);
}
}
}
if (nsfw) {
eventTags.push(["L", "content-warning"]);
eventTags.push(["l", "reason", "content-warning"]);
eventTags.push(["content-warning", "nsfw"]);
}
const query = await commands.publish(eventContent, eventTags);
if (query.status === "ok") {
return query.data;
} else {
throw new Error(query.error);
}
}
static async zap(id: string, amount: number, message: string) {
const query = await commands.zapEvent(id, amount.toString(), message);
public async zap(amount: number, message: string) {
const query = await commands.zapEvent(this.id, amount.toString(), message);
if (query.status === "ok") {
return query.data;
@@ -223,4 +178,26 @@ export class LumeEvent {
throw new Error(query.error);
}
}
static async publish(
content: string,
warning?: string,
difficulty?: number,
reply_to?: string,
root_to?: string,
) {
let query: Result<string, string>;
if (reply_to) {
query = await commands.reply(content, reply_to, root_to);
} else {
query = await commands.publish(content, warning, difficulty);
}
if (query.status === "ok") {
return query.data;
} else {
throw new Error(query.error);
}
}
}

View File

@@ -1,12 +1,13 @@
import { useQuery } from "@tanstack/react-query";
import { NostrQuery } from "../query";
import { experimental_createPersister } from "@tanstack/query-persist-client-core";
export function useEvent(id: string) {
export function useEvent(id: string, relayHint?: string) {
const { isLoading, isError, data } = useQuery({
queryKey: ["event", id],
queryFn: async () => {
try {
const event = await NostrQuery.getEvent(id);
const event = await NostrQuery.getEvent(id, relayHint);
return event;
} catch (e) {
throw new Error(e);
@@ -17,6 +18,10 @@ export function useEvent(id: string) {
refetchOnReconnect: false,
staleTime: Number.POSITIVE_INFINITY,
retry: 2,
persister: experimental_createPersister({
storage: localStorage,
maxAge: 1000 * 60 * 60 * 12, // 12 hours
}),
});
return { isLoading, isError, data };

View File

@@ -1,53 +0,0 @@
import { useInfiniteQuery } from "@tanstack/react-query";
import { commands } from "../commands";
import { dedupEvents } from "../dedup";
import { NostrEvent } from "@lume/types";
export function useInfiniteEvents(
contacts: string[],
label: string,
account: string,
nsfw?: boolean,
) {
const pubkeys = contacts;
const {
data,
isLoading,
isFetching,
isFetchingNextPage,
hasNextPage,
fetchNextPage,
} = useInfiniteQuery({
queryKey: [label, account],
initialPageParam: 0,
queryFn: async ({ pageParam }: { pageParam: number }) => {
try {
const until: string = pageParam > 0 ? pageParam.toString() : undefined;
const query = await commands.getLocalEvents(pubkeys, until);
if (query.status === "ok") {
const nostrEvents = query.data as unknown as NostrEvent[];
const events = dedupEvents(nostrEvents, nsfw);
return events;
} else {
throw new Error(query.error);
}
} catch (e) {
throw new Error(e);
}
},
getNextPageParam: (lastPage) => lastPage?.at(-1)?.created_at - 1,
select: (data) => data?.pages.flatMap((page) => page),
refetchOnWindowFocus: false,
});
return {
data,
isLoading,
isFetching,
isFetchingNextPage,
hasNextPage,
fetchNextPage,
};
}

View File

@@ -1,6 +1,7 @@
import type { Metadata } from "@lume/types";
import { useQuery } from "@tanstack/react-query";
import { commands } from "../commands";
import { experimental_createPersister } from "@tanstack/query-persist-client-core";
export function useProfile(pubkey: string, embed?: string) {
const {
@@ -8,7 +9,7 @@ export function useProfile(pubkey: string, embed?: string) {
isError,
data: profile,
} = useQuery({
queryKey: ["user", pubkey],
queryKey: ["profile", pubkey],
queryFn: async () => {
try {
if (embed) return JSON.parse(embed) as Metadata;
@@ -30,6 +31,10 @@ export function useProfile(pubkey: string, embed?: string) {
refetchOnReconnect: false,
staleTime: Number.POSITIVE_INFINITY,
retry: 2,
persister: experimental_createPersister({
storage: localStorage,
maxAge: 1000 * 60 * 60 * 24, // 24 hours
}),
});
return { isLoading, isError, profile };

View File

@@ -4,5 +4,4 @@ export * from "./query";
export * from "./window";
export * from "./commands";
export * from "./hooks/useEvent";
export * from "./hooks/useInfiniteEvents";
export * from "./hooks/useProfile";

View File

@@ -5,13 +5,15 @@ import type {
Relay,
Settings,
} from "@lume/types";
import { commands } from "./commands";
import { type Result, type RichEvent, commands } from "./commands";
import { resolveResource } from "@tauri-apps/api/path";
import { readFile, readTextFile } from "@tauri-apps/plugin-fs";
import { isPermissionGranted } from "@tauri-apps/plugin-notification";
import { open } from "@tauri-apps/plugin-dialog";
import { invoke } from "@tauri-apps/api/core";
import { relaunch } from "@tauri-apps/plugin-process";
import { nip19 } from "nostr-tools";
import { LumeEvent } from "./event";
enum NSTORE_KEYS {
settings = "lume_user_settings",
@@ -19,6 +21,24 @@ enum NSTORE_KEYS {
}
export class NostrQuery {
static #toLumeEvents(richEvents: RichEvent[]) {
const events = richEvents.map((item) => {
const nostrEvent = JSON.parse(item.raw) as NostrEvent;
if (item.parsed) {
nostrEvent.meta = item.parsed;
} else {
nostrEvent.meta = null;
}
const lumeEvent = new LumeEvent(nostrEvent);
return lumeEvent;
});
return events;
}
static async upload(filePath?: string) {
const allowExts = [
"png",
@@ -78,7 +98,9 @@ export class NostrQuery {
const query = await commands.getNotifications();
if (query.status === "ok") {
const events = query.data.map((item) => JSON.parse(item) as NostrEvent);
const data = query.data.map((item) => JSON.parse(item) as NostrEvent);
const events = data.map((ev) => new LumeEvent(ev));
return events;
} else {
console.error(query.error);
@@ -98,9 +120,32 @@ export class NostrQuery {
}
}
static async getEvent(id: string) {
const normalize: string = id.replace("nostr:", "").replace(/[^\w\s]/gi, "");
const query = await commands.getEvent(normalize);
static async getEvent(id: string, hint?: string) {
// Validate ID
const normalizeId: string = id
.replace("nostr:", "")
.replace(/[^\w\s]/gi, "");
// Define query
let query: Result<RichEvent, string>;
let relayHint: string = hint;
if (normalizeId.startsWith("nevent1")) {
const decoded = nip19.decode(normalizeId);
if (decoded.type === "nevent") relayHint = decoded.data.relays[0];
}
// Build query
if (relayHint) {
try {
const url = new URL(relayHint);
query = await commands.getEventFrom(normalizeId, url.toString());
} catch {
query = await commands.getEvent(normalizeId);
}
} else {
query = await commands.getEvent(normalizeId);
}
if (query.status === "ok") {
const data = query.data;
@@ -110,13 +155,46 @@ export class NostrQuery {
raw.meta = data.parsed;
}
return raw;
const event = new LumeEvent(raw);
return event;
} else {
console.log("[getEvent]: ", query.error);
return null;
}
}
static async getRepostEvent(event: LumeEvent) {
try {
const embed: NostrEvent = JSON.parse(event.content);
const query = await commands.getEventMeta(embed.content);
if (query.status === "ok") {
embed.meta = query.data;
const lumeEvent = new LumeEvent(embed);
return lumeEvent;
}
} catch {
const query = await commands.getEvent(event.repostId);
if (query.status === "ok") {
const data = query.data;
const raw = JSON.parse(data.raw) as NostrEvent;
if (data?.parsed) {
raw.meta = data.parsed;
}
const event = new LumeEvent(raw);
return event;
} else {
console.log("[getRepostEvent]: ", query.error);
return null;
}
}
}
static async getUserEvents(pubkey: string, asOf?: number) {
const until: string = asOf && asOf > 0 ? asOf.toString() : undefined;
const query = await commands.getEventsBy(pubkey, until);
@@ -140,9 +218,21 @@ export class NostrQuery {
}
}
static async getLocalEvents(pubkeys: string[], asOf?: number) {
static async getLocalEvents(asOf?: number) {
const until: string = asOf && asOf > 0 ? asOf.toString() : undefined;
const query = await commands.getLocalEvents(pubkeys, until);
const query = await commands.getLocalEvents(until);
if (query.status === "ok") {
const data = NostrQuery.#toLumeEvents(query.data);
return data;
} else {
return [];
}
}
static async getGroupEvents(pubkeys: string[], asOf?: number) {
const until: string = asOf && asOf > 0 ? asOf.toString() : undefined;
const query = await commands.getGroupEvents(pubkeys, until);
if (query.status === "ok") {
const data = query.data.map((item) => {
@@ -211,8 +301,6 @@ export class NostrQuery {
}
static async verifyNip05(pubkey: string, nip05?: string) {
if (!nip05) return false;
const query = await commands.verifyNip05(pubkey, nip05);
if (query.status === "ok") {
@@ -299,7 +387,9 @@ export class NostrQuery {
return systemColumns;
}
return columns;
// Filter "open" column
// Reason: deprecated
return columns.filter((col) => col.label !== "open");
} else {
return systemColumns;
}

View File

@@ -1,8 +1,9 @@
import { NostrEvent } from "@lume/types";
import type { NostrEvent } from "@lume/types";
import type { LumeEvent } from "./event";
import { commands } from "./commands";
export class LumeWindow {
static async openEvent(event: NostrEvent) {
static async openEvent(event: NostrEvent | LumeEvent) {
const eTags = event.tags.filter((tag) => tag[0] === "e" || tag[0] === "q");
const root: string =
eTags.find((el) => el[3] === "root")?.[1] ?? eTags[0]?.[1];
@@ -38,12 +39,18 @@ export class LumeWindow {
}
}
static async openEditor(reply_to?: string, quote = false) {
static async openEditor(reply_to?: string, quote?: string) {
let url: string;
if (reply_to) {
url = `/editor?reply_to=${reply_to}&quote=${quote}`;
} else {
url = `/editor?reply_to=${reply_to}`;
}
if (quote?.length) {
url = `/editor?quote=${quote}`;
}
if (!reply_to?.length && !quote?.length) {
url = "/editor";
}