feat: add notification screen

This commit is contained in:
reya
2024-05-06 15:17:34 +07:00
parent 28337e5915
commit c843626bca
37 changed files with 729 additions and 263 deletions

View File

@@ -1,6 +1,5 @@
import {
Kind,
type Account,
type Contact,
type Event,
type EventWithReplies,
@@ -33,17 +32,12 @@ export class Ark {
public async get_all_accounts() {
try {
const accounts: Account[] = [];
const cmd: string[] = await invoke("get_accounts");
const accounts: string[] = cmd.map((item) => item.replace(".npub", ""));
if (cmd) {
for (const item of cmd) {
accounts.push({ npub: item.replace(".npub", "") });
}
return accounts;
}
} catch {
return [];
return accounts;
} catch (e) {
throw new Error(String(e));
}
}
@@ -52,14 +46,22 @@ export class Ark {
const cmd: boolean = await invoke("load_selected_account", {
npub,
});
await invoke("connect_user_relays");
return cmd;
} catch (e) {
throw new Error(String(e));
}
}
public async get_activities(account: string, kind: "1" | "6" | "9735" = "1") {
try {
const events: Event[] = await invoke("get_activities", { account, kind });
return events;
} catch (e) {
console.error(String(e));
return null;
}
}
public async nostr_connect(uri: string) {
try {
const remoteKey = uri.replace("bunker://", "").split("?")[0];
@@ -130,13 +132,14 @@ export class Ark {
if (asOf && asOf > 0) until = asOf.toString();
const nostrEvents: Event[] = await invoke("get_events_from", {
public_key: pubkey,
publicKey: pubkey,
limit,
as_of: until,
});
return nostrEvents.sort((a, b) => b.created_at - a.created_at);
} catch {
} catch (e) {
console.error(String(e));
return [];
}
}
@@ -377,39 +380,30 @@ export class Ark {
}
}
public parse_event_thread({
content,
tags,
}: {
content: string;
tags: string[][];
}) {
let rootEventId: string = null;
let replyEventId: string = null;
public parse_event_thread(tags: string[][]) {
let root: string = null;
let reply: string = null;
// Get all event references from tags, ignore mention
const events = tags.filter((el) => el[0] === "e" && el[3] !== "mention");
if (!events.length) return null;
if (events.length === 1) {
return {
rootEventId: events[0][1],
replyEventId: null,
};
root = events[0][1];
}
if (events.length > 1) {
rootEventId = events.find((el) => el[3] === "root")?.[1];
replyEventId = events.find((el) => el[3] === "reply")?.[1];
if (!rootEventId && !replyEventId) {
rootEventId = events[0][1];
replyEventId = events[1][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) {
reply = null;
}
return {
rootEventId,
replyEventId,
root,
reply,
};
}
@@ -879,4 +873,19 @@ export class Ark {
throw new Error(String(e));
}
}
public async open_activity(account: string) {
try {
const label = "activity";
await invoke("open_window", {
label,
title: "Activity",
url: `/activity/${account}/texts`,
width: 400,
height: 600,
});
} catch (e) {
throw new Error(String(e));
}
}
}

View File

@@ -7,9 +7,8 @@ export function BellIcon(
<svg width="24" height="24" fill="none" viewBox="0 0 24 24" {...props}>
<path
stroke="currentColor"
strokeLinejoin="round"
strokeWidth="2"
d="M16 18c-.673 1.766-2.21 3-4 3s-3.327-1.234-4-3m-1.716 0h11.432a2 2 0 0 0 1.982-2.264l-.905-6.789a6.853 6.853 0 0 0-13.586 0l-.905 6.789A2 2 0 0 0 6.284 18Z"
strokeWidth="1.5"
d="M16 18.25c-.673 1.766-2.21 3-4 3s-3.327-1.234-4-3m-2.152 0h12.304a2 2 0 0 0 1.974-2.319l-1.17-7.258a7.045 7.045 0 0 0-13.911 0l-1.171 7.258a2 2 0 0 0 1.974 2.319Z"
/>
</svg>
);

View File

@@ -1,22 +1,22 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"display": "Default",
"compilerOptions": {
"target": "es2017",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"moduleResolution": "node",
"skipLibCheck": true,
"strict": false,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"incremental": true,
"esModuleInterop": true,
"module": "esnext",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"strictNullChecks": false,
},
"exclude": ["node_modules", "src-tauri"]
"$schema": "https://json.schemastore.org/tsconfig",
"display": "Default",
"compilerOptions": {
"target": "es2017",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"moduleResolution": "node",
"skipLibCheck": true,
"strict": false,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"incremental": true,
"esModuleInterop": true,
"module": "esnext",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"strictNullChecks": false
},
"exclude": ["node_modules", "src-tauri"]
}

View File

@@ -14,11 +14,16 @@ export function Container({
className?: string;
}) {
return (
<div className={cn("flex h-screen w-screen flex-col", className)}>
<div
className={cn(
"bg-transparent flex h-screen w-screen flex-col",
className,
)}
>
{withDrag ? (
<div
data-tauri-drag-region
className="flex h-11 w-full shrink-0 items-center justify-end pr-2"
className="bg-transparent flex h-11 w-full shrink-0 items-center justify-end pr-2"
>
{withNavigate ? (
<div className="flex items-center gap-1">

View File

@@ -0,0 +1,26 @@
import { cn } from "@lume/utils";
import { useNoteContext } from "./provider";
import { User } from "../user";
export function NoteActivity({ className }: { className?: string }) {
const event = useNoteContext();
const mentions = event.tags
.filter((tag) => tag[0] === "p")
.map((tag) => tag[1]);
return (
<div className={cn("-mt-3 mb-2", className)}>
<div className="text-neutral-700 dark:text-neutral-300 inline-flex items-baseline gap-2 w-full overflow-hidden">
<div className="shrink-0 text-sm font-medium">To:</div>
{mentions.splice(0, 4).map((mention) => (
<User.Provider key={mention} pubkey={mention}>
<User.Root>
<User.Name className="text-sm font-medium" />
</User.Root>
</User.Provider>
))}
{mentions.length > 4 ? "..." : ""}
</div>
</div>
);
}

View File

@@ -18,7 +18,7 @@ export function NoteReply({ large = false }: { large?: boolean }) {
className={cn(
"inline-flex items-center justify-center text-neutral-800 dark:text-neutral-200",
large
? "bg-neutral-100 dark:bg-white/10 h-7 gap-1.5 w-24 text-sm font-medium hover:text-blue-500 hover:bg-neutral-200 dark:hover:bg-white/20"
? "rounded-full bg-neutral-100 dark:bg-white/10 h-7 gap-1.5 w-24 text-sm font-medium hover:text-blue-500 hover:bg-neutral-200 dark:hover:bg-white/20"
: "size-7",
)}
>

View File

@@ -31,7 +31,7 @@ export function NoteZap({ large = false }: { large?: boolean }) {
className={cn(
"inline-flex items-center justify-center text-neutral-800 dark:text-neutral-200",
large
? "bg-neutral-100 dark:bg-white/10 h-7 gap-1.5 w-24 text-sm font-medium hover:text-blue-500 hover:bg-neutral-200 dark:hover:bg-white/20"
? "rounded-full bg-neutral-100 dark:bg-white/10 h-7 gap-1.5 w-24 text-sm font-medium hover:text-blue-500 hover:bg-neutral-200 dark:hover:bg-white/20"
: "size-7",
)}
>

View File

@@ -1,8 +1,6 @@
import { useEvent } from "@lume/ark";
import { cn } from "@lume/utils";
import { useTranslation } from "react-i18next";
import { Note } from ".";
import { User } from "../user";
export function NoteChild({
eventId,

View File

@@ -82,7 +82,6 @@ export function NoteContent({
target="_blank"
rel="noreferrer"
className="line-clamp-1 text-blue-500 hover:text-blue-600"
onClick={(e) => e.stopPropagation()}
>
{match}
</a>

View File

@@ -145,7 +145,9 @@ export function NoteContentLarge({
return (
<div className={cn("select-text", className)}>
<div className="text-[15px] text-balance content-break leading-normal">{content}</div>
<div className="text-[15px] text-balance content-break leading-normal">
{content}
</div>
</div>
);
}

View File

@@ -1,3 +1,4 @@
import { NoteActivity } from "./activity";
import { NoteOpenThread } from "./buttons/open";
import { NoteReply } from "./buttons/reply";
import { NoteRepost } from "./buttons/repost";
@@ -24,4 +25,5 @@ export const Note = {
Open: NoteOpenThread,
Child: NoteChild,
Thread: NoteThread,
Activity: NoteActivity,
};

View File

@@ -8,6 +8,8 @@ export * from "./src/notification";
export * from "./src/cn";
export * from "./src/image";
export * from "./src/parser";
export * from "./src/groupBy";
export * from "./src/invoice";
// Hooks
export * from "./src/hooks/useNetworkStatus";

View File

@@ -12,6 +12,7 @@
"bitcoin-units": "^1.0.0",
"clsx": "^2.1.1",
"dayjs": "^1.11.11",
"light-bolt11-decoder": "^3.1.1",
"nostr-tools": "^2.5.1",
"react": "^18.3.1",
"react-dom": "^18.3.1",

View File

@@ -0,0 +1,21 @@
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[]>());

View File

@@ -0,0 +1,17 @@
import { decode } from "light-bolt11-decoder";
import { getBitcoinDisplayValues } from "./formater";
export function decodeZapInvoice(tags?: string[][]) {
const invoice = tags.find((tag) => tag[0] === "bolt11")?.[1];
if (!invoice) return;
const decodedInvoice = decode(invoice);
const amountSection = decodedInvoice.sections.find(
(s: any) => s.name === "amount",
);
const amount = parseInt(amountSection.value);
const displayValue = getBitcoinDisplayValues(amount);
return displayValue;
}