feat: add notification screen
This commit is contained in:
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
|
||||
@@ -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">
|
||||
|
||||
26
packages/ui/src/note/activity.tsx
Normal file
26
packages/ui/src/note/activity.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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",
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -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",
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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",
|
||||
|
||||
21
packages/utils/src/groupBy.ts
Normal file
21
packages/utils/src/groupBy.ts
Normal 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[]>());
|
||||
17
packages/utils/src/invoice.ts
Normal file
17
packages/utils/src/invoice.ts
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user