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:
@@ -125,3 +125,4 @@ export * from "./src/key";
|
||||
export * from "./src/remote";
|
||||
export * from "./src/nsfw";
|
||||
export * from "./src/visit";
|
||||
export * from "./src/pow";
|
||||
|
||||
@@ -1,19 +1,9 @@
|
||||
export function PlusSquareIcon(props: JSX.IntrinsicElements["svg"]) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
{...props}
|
||||
>
|
||||
<svg width="24" height="24" fill="none" viewBox="0 0 24 24" {...props}>
|
||||
<path
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="M12 15v-3m0 0V9m0 3H9m3 0h3m-3 9c-2.796 0-4.193 0-5.296-.457a6 6 0 01-3.247-3.247C3 16.194 3 14.796 3 12c0-2.796 0-4.193.457-5.296a6 6 0 013.247-3.247C7.807 3 9.204 3 12 3c2.796 0 4.194 0 5.296.457a6 6 0 013.247 3.247C21 7.807 21 9.204 21 12c0 2.796 0 4.194-.457 5.296a6 6 0 01-3.247 3.247C16.194 21 14.796 21 12 21z"
|
||||
fill="currentColor"
|
||||
d="m4.842 20.032.34-.668-.34.668Zm-.874-.874.668-.34-.668.34Zm16.064 0-.668-.34.668.34Zm-.874.874-.34-.668.34.668Zm.874-15.19-.668.34.668-.34Zm-.874-.874-.34.668.34-.668Zm-15.19.874-.668-.34.668.34Zm.874-.874-.34-.668.34.668ZM15.25 12.75a.75.75 0 0 0 0-1.5v1.5Zm-6.493-1.5a.75.75 0 0 0 0 1.5v-1.5Zm2.493 3.993a.75.75 0 0 0 1.5 0h-1.5Zm1.5-6.485a.75.75 0 0 0-1.5 0h1.5ZM19.5 6.95v10.1H21V6.95h-1.5ZM17.05 19.5H6.95V21h10.1v-1.5ZM4.5 17.05V6.95H3v10.1h1.5ZM6.95 4.5h10.1V3H6.95v1.5Zm0 15c-.572 0-.957 0-1.253-.025-.287-.023-.424-.065-.514-.111L4.502 20.7c.337.172.693.24 1.073.27.371.03.827.03 1.375.03v-1.5ZM3 17.05c0 .548 0 1.003.03 1.375.03.38.098.736.27 1.073l1.336-.68c-.046-.091-.088-.228-.111-.516-.024-.295-.025-.68-.025-1.252H3Zm2.183 2.314a1.25 1.25 0 0 1-.547-.547l-1.336.681A2.75 2.75 0 0 0 4.502 20.7l.68-1.336ZM19.5 17.05c0 .572 0 .957-.025 1.252-.023.288-.065.425-.111.515l1.336.681c.172-.337.24-.693.27-1.073.03-.372.03-.827.03-1.375h-1.5ZM17.05 21c.548 0 1.003 0 1.375-.03.38-.03.736-.098 1.073-.27l-.68-1.336c-.091.046-.228.088-.516.111-.295.024-.68.025-1.252.025V21Zm2.314-2.183a1.25 1.25 0 0 1-.547.547l.681 1.336a2.751 2.751 0 0 0 1.202-1.2l-1.336-.681ZM21 6.95c0-.548 0-1.004-.03-1.375-.03-.38-.098-.736-.27-1.073l-1.336.68c.046.091.088.228.111.515.024.296.025.68.025 1.253H21ZM17.05 4.5c.572 0 .957 0 1.252.025.288.023.425.065.515.111l.681-1.336c-.337-.172-.693-.24-1.073-.27C18.053 3 17.598 3 17.05 3v1.5Zm3.65.002A2.75 2.75 0 0 0 19.5 3.3l-.681 1.336c.235.12.426.311.546.547l1.336-.681ZM4.5 6.95c0-.572 0-.957.025-1.253.023-.287.065-.424.111-.514L3.3 4.502c-.172.337-.24.693-.27 1.073C3 5.946 3 6.402 3 6.95h1.5ZM6.95 3c-.548 0-1.004 0-1.375.03-.38.03-.736.098-1.073.27l.68 1.336c.091-.046.228-.088.515-.111.296-.024.68-.025 1.253-.025V3ZM4.636 5.183a1.25 1.25 0 0 1 .547-.547L4.502 3.3A2.75 2.75 0 0 0 3.3 4.502l1.336.68ZM15.25 11.25H8.757v1.5h6.493v-1.5Zm-2.5 3.993V8.758h-1.5v6.485h1.5Z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
19
packages/icons/src/pow.tsx
Normal file
19
packages/icons/src/pow.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
export function PowIcon(props: JSX.IntrinsicElements["svg"]) {
|
||||
return (
|
||||
<svg width="24" height="24" fill="none" viewBox="0 0 24 24" {...props}>
|
||||
<path
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="1.5"
|
||||
d="M19 2.75a2.25 2.25 0 1 1 0 4.5 2.25 2.25 0 0 1 0-4.5ZM5.567 18.434a6.196 6.196 0 0 1 0-8.763L9.4 5.837a1.807 1.807 0 1 1 2.556 2.556l-3.834 3.834a2.582 2.582 0 1 0 3.651 3.65l3.834-3.833a1.807 1.807 0 0 1 2.556 2.556l-3.834 3.834a6.177 6.177 0 0 1-4.382 1.815 6.177 6.177 0 0 1-4.381-1.815Z"
|
||||
/>
|
||||
<path
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeWidth="1.5"
|
||||
d="m13.965 13.687 2.556 2.556M7.758 7.48l2.556 2.556"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -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": {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -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 };
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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}"e=${quote}`;
|
||||
} else {
|
||||
url = `/editor?reply_to=${reply_to}`;
|
||||
}
|
||||
|
||||
if (quote?.length) {
|
||||
url = `/editor?quote=${quote}`;
|
||||
}
|
||||
|
||||
if (!reply_to?.length && !quote?.length) {
|
||||
url = "/editor";
|
||||
}
|
||||
|
||||
|
||||
5
packages/types/index.d.ts
vendored
5
packages/types/index.d.ts
vendored
@@ -52,6 +52,11 @@ export interface EventWithReplies extends NostrEvent {
|
||||
replies: Array<NostrEvent>;
|
||||
}
|
||||
|
||||
export interface EventTag {
|
||||
id: string;
|
||||
relayHint: string;
|
||||
}
|
||||
|
||||
export interface Metadata {
|
||||
name?: string;
|
||||
display_name?: string;
|
||||
|
||||
@@ -25,7 +25,7 @@ export const Carousel = <T,>({ items, renderItem }: CarouselProps<T>) => {
|
||||
snapPointIndexes,
|
||||
} = useSnapCarousel();
|
||||
return (
|
||||
<div className="group relative">
|
||||
<div className="relative group">
|
||||
<ul
|
||||
ref={scrollRef}
|
||||
className="relative flex overflow-auto snap-x scrollbar-none"
|
||||
@@ -39,9 +39,10 @@ export const Carousel = <T,>({ items, renderItem }: CarouselProps<T>) => {
|
||||
</ul>
|
||||
<div
|
||||
aria-hidden
|
||||
className="hidden group-hover:flex z-10 absolute left-1/2 top-1/2 transform -translate-x-1/2 -translate-y-1/2 w-full justify-between items-center px-5"
|
||||
className="absolute z-10 items-center justify-between hidden w-full px-5 transform -translate-x-1/2 -translate-y-1/2 group-hover:flex left-1/2 top-1/2"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
className={cn(
|
||||
"size-11 rounded-full bg-black/50 backdrop-blur-sm flex items-center justify-center text-white",
|
||||
activePageIndex <= 0 ? "opacity-50" : "",
|
||||
@@ -51,6 +52,7 @@ export const Carousel = <T,>({ items, renderItem }: CarouselProps<T>) => {
|
||||
<ArrowLeftIcon className="size-6" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={cn(
|
||||
"size-11 rounded-full bg-black/50 backdrop-blur-sm flex items-center justify-center text-white",
|
||||
activePageIndex <= 0 ? "opacity-50" : "",
|
||||
@@ -60,7 +62,7 @@ export const Carousel = <T,>({ items, renderItem }: CarouselProps<T>) => {
|
||||
<ArrowRightIcon className="size-6" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="absolute top-3 right-3 flex justify-center bg-black mix-blend-multiply bg-opacity-20 backdrop-blur-sm h-6 w-12 items-center rounded-full text-sm font-medium text-white">
|
||||
<div className="absolute flex items-center justify-center w-12 h-6 text-sm font-medium text-white bg-black rounded-full top-3 right-3 mix-blend-multiply bg-opacity-20 backdrop-blur-sm">
|
||||
{activePageIndex + 1} / {pages.length}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -33,7 +33,7 @@ export function Spinner({
|
||||
<span
|
||||
aria-hidden
|
||||
style={{ display: "contents", visibility: "hidden" }}
|
||||
// Workaround to use `inert` until https://github.com/facebook/react/pull/24730 is merged.
|
||||
// biome-ignore lint/correctness/noConstantCondition: Workaround to use `inert` until https://github.com/facebook/react/pull/24730 is merged.
|
||||
{...{ inert: true ? "" : undefined }}
|
||||
>
|
||||
{children}
|
||||
|
||||
@@ -2,16 +2,6 @@ export * from "./src/constants";
|
||||
export * from "./src/delay";
|
||||
export * from "./src/formater";
|
||||
export * from "./src/editor";
|
||||
export * from "./src/nip01";
|
||||
export * from "./src/nip94";
|
||||
export * from "./src/notification";
|
||||
export * from "./src/cn";
|
||||
export * from "./src/image";
|
||||
export * from "./src/parser";
|
||||
export * from "./src/groupBy";
|
||||
export * from "./src/invoice";
|
||||
export * from "./src/update";
|
||||
|
||||
// Hooks
|
||||
export * from "./src/hooks/useNetworkStatus";
|
||||
export * from "./src/hooks/useOpenGraph";
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
"access": "public"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tanstack/react-query": "^5.40.1",
|
||||
"bitcoin-units": "^1.0.0",
|
||||
"clsx": "^2.1.1",
|
||||
"dayjs": "^1.11.11",
|
||||
@@ -17,7 +16,7 @@
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"slate": "^0.103.0",
|
||||
"slate-react": "^0.104.0"
|
||||
"slate-react": "^0.105.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@lume/tsconfig": "workspace:^",
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
export const groupBy = <T>(
|
||||
array: T[],
|
||||
predicate: (value: T, index: number, array: T[]) => string,
|
||||
) =>
|
||||
array.reduce(
|
||||
(acc, value, index, array) => {
|
||||
(acc[predicate(value, index, array)] ||= []).push(value);
|
||||
return acc;
|
||||
},
|
||||
{} as { [key: string]: T[] },
|
||||
);
|
||||
|
||||
export const groupByToMap = <T, Q>(
|
||||
array: T[],
|
||||
predicate: (value: T, index: number, array: T[]) => Q,
|
||||
) =>
|
||||
array.reduce((map, value, index, array) => {
|
||||
const key = predicate(value, index, array);
|
||||
map.get(key)?.push(value) ?? map.set(key, [value]);
|
||||
return map;
|
||||
}, new Map<Q, T[]>());
|
||||
@@ -1,25 +0,0 @@
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
const getOnLineStatus = () =>
|
||||
typeof navigator !== "undefined" && typeof navigator.onLine === "boolean"
|
||||
? navigator.onLine
|
||||
: true;
|
||||
|
||||
export function useNetworkStatus() {
|
||||
const [status, setStatus] = useState(getOnLineStatus());
|
||||
|
||||
const setOnline = () => setStatus(true);
|
||||
const setOffline = () => setStatus(false);
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener("online", setOnline);
|
||||
window.addEventListener("offline", setOffline);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("online", setOnline);
|
||||
window.removeEventListener("offline", setOffline);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return status;
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
import type { Opengraph } from "@lume/types";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
|
||||
export function useOpenGraph(url: string) {
|
||||
const { isLoading, isError, data } = useQuery({
|
||||
queryKey: ["opg", url],
|
||||
queryFn: async () => {
|
||||
try {
|
||||
const res: Opengraph = await invoke("fetch_opg", { url });
|
||||
return res;
|
||||
} catch {
|
||||
throw new Error("fetch preview failed");
|
||||
}
|
||||
},
|
||||
staleTime: Number.POSITIVE_INFINITY,
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnMount: false,
|
||||
refetchOnReconnect: false,
|
||||
});
|
||||
|
||||
return {
|
||||
isLoading,
|
||||
isError,
|
||||
data,
|
||||
};
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
export function getImageMeta(
|
||||
url: string,
|
||||
): Promise<{ width: number; height: number }> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const img = new Image();
|
||||
img.onload = () => resolve(img);
|
||||
img.onerror = () => reject();
|
||||
img.src = url;
|
||||
});
|
||||
}
|
||||
@@ -7,10 +7,10 @@ export function decodeZapInvoice(tags?: string[][]) {
|
||||
|
||||
const decodedInvoice = decode(invoice);
|
||||
const amountSection = decodedInvoice.sections.find(
|
||||
(s: any) => s.name === "amount",
|
||||
(s: { name: string }) => s.name === "amount",
|
||||
);
|
||||
|
||||
const amount = parseInt(amountSection.value);
|
||||
const amount = Number.parseInt(amountSection.value);
|
||||
const displayValue = getBitcoinDisplayValues(amount);
|
||||
|
||||
return displayValue;
|
||||
|
||||
@@ -1,96 +0,0 @@
|
||||
import { nip19 } from "nostr-tools";
|
||||
import type { EventPointer, ProfilePointer } from "nostr-tools/lib/types/nip19";
|
||||
|
||||
// Borrow from NDK
|
||||
// https://github.com/nostr-dev-kit/ndk/blob/master/ndk/src/events/content-tagger.ts
|
||||
export async function generateContentTags(content: string) {
|
||||
const promises: Promise<void>[] = [];
|
||||
const tags: string[][] = [];
|
||||
|
||||
const tagRegex = /(@|nostr:)(npub|nprofile|note|nevent|naddr)[a-zA-Z0-9]+/g;
|
||||
const hashtagRegex = /#(\w+)/g;
|
||||
|
||||
const addTagIfNew = (t: string[]) => {
|
||||
if (!tags.find((t2) => t2[0] === t[0] && t2[1] === t[1])) {
|
||||
tags.push(t);
|
||||
}
|
||||
};
|
||||
|
||||
content = content.replace(tagRegex, (tag) => {
|
||||
try {
|
||||
const entity = tag.split(/(@|nostr:)/)[2];
|
||||
const { type, data } = nip19.decode(entity);
|
||||
let t: string[] | undefined;
|
||||
|
||||
switch (type) {
|
||||
case "npub":
|
||||
t = ["p", data as string];
|
||||
break;
|
||||
case "nprofile":
|
||||
t = ["p", (data as ProfilePointer).pubkey as string];
|
||||
break;
|
||||
case "note":
|
||||
promises.push(
|
||||
new Promise(async (resolve) => {
|
||||
addTagIfNew(["e", data, "", "mention"]);
|
||||
resolve();
|
||||
}),
|
||||
);
|
||||
break;
|
||||
case "nevent":
|
||||
promises.push(
|
||||
new Promise(async (resolve) => {
|
||||
let { id, relays, author } = data as EventPointer;
|
||||
|
||||
// If the nevent doesn't have a relay specified, try to get one
|
||||
if (!relays || relays.length === 0) {
|
||||
relays = [""];
|
||||
}
|
||||
|
||||
addTagIfNew(["e", id, relays[0], "mention"]);
|
||||
if (author) addTagIfNew(["p", author]);
|
||||
resolve();
|
||||
}),
|
||||
);
|
||||
break;
|
||||
case "naddr":
|
||||
promises.push(
|
||||
new Promise(async (resolve) => {
|
||||
const id = [data.kind, data.pubkey, data.identifier].join(":");
|
||||
let relays = data.relays ?? [];
|
||||
|
||||
// If the naddr doesn't have a relay specified, try to get one
|
||||
if (relays.length === 0) {
|
||||
relays = [""];
|
||||
}
|
||||
|
||||
addTagIfNew(["a", id, relays[0], "mention"]);
|
||||
addTagIfNew(["p", data.pubkey]);
|
||||
resolve();
|
||||
}),
|
||||
);
|
||||
break;
|
||||
default:
|
||||
return tag;
|
||||
}
|
||||
|
||||
if (t) addTagIfNew(t);
|
||||
|
||||
return `nostr:${entity}`;
|
||||
} catch (error) {
|
||||
return tag;
|
||||
}
|
||||
});
|
||||
|
||||
await Promise.all(promises);
|
||||
|
||||
content = content.replace(hashtagRegex, (tag, word) => {
|
||||
const t: string[] = ["t", word];
|
||||
if (!tags.find((t2) => t2[0] === t[0] && t2[1] === t[1])) {
|
||||
tags.push(t);
|
||||
}
|
||||
return tag; // keep the original tag in the content
|
||||
});
|
||||
|
||||
return { content, tags };
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
export function fileType(url: string) {
|
||||
if (url.match(/\.(jpg|jpeg|gif|png|webp|avif|tiff)$/)) {
|
||||
return "image";
|
||||
}
|
||||
|
||||
if (url.match(/\.(mp4|mov|webm|wmv|flv|mts|avi|ogv|mkv)$/)) {
|
||||
return "video";
|
||||
}
|
||||
|
||||
if (url.match(/\.(mp3|ogg|wav)$/)) {
|
||||
return "audio";
|
||||
}
|
||||
|
||||
return "link";
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
import {
|
||||
isPermissionGranted,
|
||||
requestPermission,
|
||||
sendNotification,
|
||||
} from "@tauri-apps/plugin-notification";
|
||||
|
||||
export async function sendNativeNotification(content: string, title?: string) {
|
||||
let permissionGranted = await isPermissionGranted();
|
||||
if (!permissionGranted) {
|
||||
const permission = await requestPermission();
|
||||
permissionGranted = permission === "granted";
|
||||
}
|
||||
if (permissionGranted) {
|
||||
sendNotification({ title: title || "Lume", body: content });
|
||||
}
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
import { Meta } from "@lume/types";
|
||||
import { IMAGES, NOSTR_EVENTS, NOSTR_MENTIONS, VIDEOS } from "./constants";
|
||||
import { fetch } from "@tauri-apps/plugin-http";
|
||||
|
||||
export async function parser(
|
||||
content: string,
|
||||
abortController?: AbortController,
|
||||
) {
|
||||
const words = content.split(/( |\n)/);
|
||||
const urls = content.match(/(https?:\/\/\S+)/gi);
|
||||
|
||||
// Extract hashtags
|
||||
const hashtags = words.filter((word) => word.startsWith("#"));
|
||||
|
||||
// Extract nostr events
|
||||
const events = words.filter((word) =>
|
||||
NOSTR_EVENTS.some((el) => word.startsWith(el)),
|
||||
);
|
||||
|
||||
// Extract nostr mentions
|
||||
const mentions = words.filter((word) =>
|
||||
NOSTR_MENTIONS.some((el) => word.startsWith(el)),
|
||||
);
|
||||
|
||||
// Extract images and videos from content
|
||||
const images: string[] = [];
|
||||
const videos: string[] = [];
|
||||
|
||||
let text: string = content;
|
||||
|
||||
if (urls) {
|
||||
for (const url of urls) {
|
||||
const ext = new URL(url).pathname.split(".")[1];
|
||||
|
||||
if (IMAGES.includes(ext)) {
|
||||
text = text.replace(url, "");
|
||||
images.push(url);
|
||||
break;
|
||||
}
|
||||
|
||||
if (VIDEOS.includes(ext)) {
|
||||
text = text.replace(url, "");
|
||||
videos.push(url);
|
||||
break;
|
||||
}
|
||||
|
||||
if (urls.length <= 3) {
|
||||
try {
|
||||
const res = await fetch(url, {
|
||||
method: "HEAD",
|
||||
priority: "high",
|
||||
signal: abortController.signal,
|
||||
// proxy: settings.proxy;
|
||||
});
|
||||
|
||||
if (res.headers.get("Content-Type").startsWith("image")) {
|
||||
text = text.replace(url, "");
|
||||
images.push(url);
|
||||
break;
|
||||
}
|
||||
} catch {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const meta: Meta = {
|
||||
content: text.trim(),
|
||||
images,
|
||||
videos,
|
||||
events,
|
||||
mentions,
|
||||
hashtags,
|
||||
};
|
||||
|
||||
return meta;
|
||||
}
|
||||
Reference in New Issue
Block a user