feat: add bell
This commit is contained in:
@@ -5,7 +5,7 @@ import type { EventColumns, LumeColumn } from "@lume/types";
|
|||||||
import { createFileRoute } from "@tanstack/react-router";
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
import { listen } from "@tauri-apps/api/event";
|
import { listen } from "@tauri-apps/api/event";
|
||||||
import { resolveResource } from "@tauri-apps/api/path";
|
import { resolveResource } from "@tauri-apps/api/path";
|
||||||
import { getCurrent } from "@tauri-apps/api/webviewWindow";
|
import { getCurrent } from "@tauri-apps/api/window";
|
||||||
import { readTextFile } from "@tauri-apps/plugin-fs";
|
import { readTextFile } from "@tauri-apps/plugin-fs";
|
||||||
import { nanoid } from "nanoid";
|
import { nanoid } from "nanoid";
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
@@ -14,8 +14,11 @@ import { VList, type VListHandle } from "virtua";
|
|||||||
|
|
||||||
export const Route = createFileRoute("/$account/home")({
|
export const Route = createFileRoute("/$account/home")({
|
||||||
beforeLoad: async ({ context }) => {
|
beforeLoad: async ({ context }) => {
|
||||||
|
try {
|
||||||
const ark = context.ark;
|
const ark = context.ark;
|
||||||
const resourcePath = await resolveResource("resources/system_columns.json");
|
const resourcePath = await resolveResource(
|
||||||
|
"resources/system_columns.json",
|
||||||
|
);
|
||||||
const systemColumns: LumeColumn[] = JSON.parse(
|
const systemColumns: LumeColumn[] = JSON.parse(
|
||||||
await readTextFile(resourcePath),
|
await readTextFile(resourcePath),
|
||||||
);
|
);
|
||||||
@@ -24,6 +27,9 @@ export const Route = createFileRoute("/$account/home")({
|
|||||||
return {
|
return {
|
||||||
storedColumns: !userColumns.length ? systemColumns : userColumns,
|
storedColumns: !userColumns.length ? systemColumns : userColumns,
|
||||||
};
|
};
|
||||||
|
} catch (e) {
|
||||||
|
console.error(String(e));
|
||||||
|
}
|
||||||
},
|
},
|
||||||
component: Screen,
|
component: Screen,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,17 +1,29 @@
|
|||||||
import { BellIcon, ComposeFilledIcon, PlusIcon, SearchIcon } from "@lume/icons";
|
import { BellIcon, ComposeFilledIcon, PlusIcon, SearchIcon } from "@lume/icons";
|
||||||
|
import { Event, Kind } from "@lume/types";
|
||||||
import { User } from "@lume/ui";
|
import { User } from "@lume/ui";
|
||||||
import { cn } from "@lume/utils";
|
import {
|
||||||
|
cn,
|
||||||
|
decodeZapInvoice,
|
||||||
|
displayNpub,
|
||||||
|
sendNativeNotification,
|
||||||
|
} from "@lume/utils";
|
||||||
import { Outlet, createFileRoute } from "@tanstack/react-router";
|
import { Outlet, createFileRoute } from "@tanstack/react-router";
|
||||||
|
import { UnlistenFn } from "@tauri-apps/api/event";
|
||||||
|
import { getCurrent } from "@tauri-apps/api/window";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
export const Route = createFileRoute("/$account")({
|
export const Route = createFileRoute("/$account")({
|
||||||
|
beforeLoad: async ({ context }) => {
|
||||||
|
const ark = context.ark;
|
||||||
|
const accounts = await ark.get_all_accounts();
|
||||||
|
|
||||||
|
return { accounts };
|
||||||
|
},
|
||||||
component: Screen,
|
component: Screen,
|
||||||
});
|
});
|
||||||
|
|
||||||
function Screen() {
|
function Screen() {
|
||||||
const { account } = Route.useParams();
|
|
||||||
const { ark, platform } = Route.useRouteContext();
|
const { ark, platform } = Route.useRouteContext();
|
||||||
|
|
||||||
const navigate = Route.useNavigate();
|
const navigate = Route.useNavigate();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -42,14 +54,7 @@ function Screen() {
|
|||||||
<ComposeFilledIcon className="size-4" />
|
<ComposeFilledIcon className="size-4" />
|
||||||
New Post
|
New Post
|
||||||
</button>
|
</button>
|
||||||
<button
|
<Bell />
|
||||||
type="button"
|
|
||||||
onClick={() => ark.open_activity(account)}
|
|
||||||
className="relative inline-flex size-8 items-center justify-center rounded-full text-neutral-800 hover:bg-black/10 dark:text-neutral-200 dark:hover:bg-white/10"
|
|
||||||
>
|
|
||||||
<BellIcon className="size-5" />
|
|
||||||
{/* <span className="absolute right-0 top-0 block size-2 rounded-full bg-teal-500 ring-1 ring-black/5"></span> */}
|
|
||||||
</button>
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => ark.open_search()}
|
onClick={() => ark.open_search()}
|
||||||
@@ -67,28 +72,20 @@ function Screen() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Accounts() {
|
function Accounts() {
|
||||||
const navigate = Route.useNavigate();
|
const navigate = Route.useNavigate();
|
||||||
const { ark } = Route.useRouteContext();
|
const { ark, accounts } = Route.useRouteContext();
|
||||||
const { account } = Route.useParams();
|
const { account } = Route.useParams();
|
||||||
|
|
||||||
const [accounts, setAccounts] = useState<string[]>([]);
|
|
||||||
|
|
||||||
const changeAccount = async (npub: string) => {
|
const changeAccount = async (npub: string) => {
|
||||||
if (npub === account) return;
|
if (npub === account) return;
|
||||||
|
|
||||||
const select = await ark.load_selected_account(npub);
|
const select = await ark.load_selected_account(npub);
|
||||||
if (select)
|
|
||||||
|
if (select) {
|
||||||
return navigate({ to: "/$account/home", params: { account: npub } });
|
return navigate({ to: "/$account/home", params: { account: npub } });
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
async function getAllAccounts() {
|
|
||||||
const data = await ark.get_all_accounts();
|
|
||||||
if (data) setAccounts(data);
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
getAllAccounts();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div data-tauri-drag-region className="flex items-center gap-3">
|
<div data-tauri-drag-region className="flex items-center gap-3">
|
||||||
@@ -116,3 +113,71 @@ export function Accounts() {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function Bell() {
|
||||||
|
const { ark } = Route.useRouteContext();
|
||||||
|
const { account } = Route.useParams();
|
||||||
|
|
||||||
|
const [isRing, setIsRing] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let unlisten: UnlistenFn = undefined;
|
||||||
|
|
||||||
|
async function listenNotify() {
|
||||||
|
unlisten = await getCurrent().listen<string>(
|
||||||
|
"activity",
|
||||||
|
async (payload) => {
|
||||||
|
setIsRing(true);
|
||||||
|
|
||||||
|
const event: Event = JSON.parse(payload.payload);
|
||||||
|
const user = await ark.get_profile(event.pubkey);
|
||||||
|
const userName =
|
||||||
|
user.display_name || user.name || displayNpub(event.pubkey, 16);
|
||||||
|
|
||||||
|
switch (event.kind) {
|
||||||
|
case Kind.Text: {
|
||||||
|
sendNativeNotification("Mentioned you in a note", userName);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case Kind.Repost: {
|
||||||
|
sendNativeNotification("Reposted your note", userName);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case Kind.ZapReceipt: {
|
||||||
|
const amount = decodeZapInvoice(event.tags);
|
||||||
|
sendNativeNotification(
|
||||||
|
`Zapped ₿ ${amount.bitcoinFormatted}`,
|
||||||
|
userName,
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!unlisten) listenNotify();
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (unlisten) unlisten();
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
setIsRing(false);
|
||||||
|
ark.open_activity(account);
|
||||||
|
}}
|
||||||
|
className="relative inline-flex size-8 items-center justify-center rounded-full text-neutral-800 hover:bg-black/10 dark:text-neutral-200 dark:hover:bg-white/10"
|
||||||
|
>
|
||||||
|
<BellIcon className="size-5" />
|
||||||
|
{isRing ? (
|
||||||
|
<span className="absolute right-0 top-0 block size-2 rounded-full bg-teal-500 ring-1 ring-black/5" />
|
||||||
|
) : null}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ interface RouterContext {
|
|||||||
settings?: Settings;
|
settings?: Settings;
|
||||||
interests?: Interests;
|
interests?: Interests;
|
||||||
// Profile
|
// Profile
|
||||||
accounts?: Account[];
|
accounts?: string[];
|
||||||
profile?: Metadata;
|
profile?: Metadata;
|
||||||
// Editor
|
// Editor
|
||||||
initialValue?: EditorElement[];
|
initialValue?: EditorElement[];
|
||||||
|
|||||||
@@ -4,59 +4,39 @@ import { Link } from "@tanstack/react-router";
|
|||||||
import { createFileRoute, redirect } from "@tanstack/react-router";
|
import { createFileRoute, redirect } from "@tanstack/react-router";
|
||||||
import { invoke } from "@tauri-apps/api/core";
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
|
||||||
export const Route = createFileRoute("/")({
|
export const Route = createFileRoute("/")({
|
||||||
beforeLoad: async ({ context }) => {
|
beforeLoad: async ({ context }) => {
|
||||||
const ark = context.ark;
|
const ark = context.ark;
|
||||||
const accounts = await ark.get_all_accounts();
|
const accounts = await ark.get_all_accounts();
|
||||||
|
|
||||||
// Run notification service
|
if (!accounts.length) {
|
||||||
if (accounts.length > 0) {
|
|
||||||
await invoke("run_notification", { accounts });
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (accounts.length) {
|
|
||||||
// Guest account
|
|
||||||
case 0:
|
|
||||||
throw redirect({
|
throw redirect({
|
||||||
to: "/landing",
|
to: "/landing",
|
||||||
replace: true,
|
replace: true,
|
||||||
});
|
});
|
||||||
// Only 1 account, skip account selection screen
|
|
||||||
case 1: {
|
|
||||||
const account = accounts[0];
|
|
||||||
const loadedAccount = await ark.load_selected_account(account);
|
|
||||||
|
|
||||||
if (loadedAccount) {
|
|
||||||
throw redirect({
|
|
||||||
to: "/$account/home",
|
|
||||||
params: { account },
|
|
||||||
replace: true,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
// Run notification service
|
||||||
}
|
await invoke("run_notification", { accounts });
|
||||||
// Account selection
|
|
||||||
default:
|
|
||||||
return { accounts };
|
return { accounts };
|
||||||
}
|
|
||||||
},
|
},
|
||||||
component: Screen,
|
component: Screen,
|
||||||
});
|
});
|
||||||
|
|
||||||
function Screen() {
|
function Screen() {
|
||||||
const navigate = Route.useNavigate();
|
const navigate = Route.useNavigate();
|
||||||
const context = Route.useRouteContext();
|
const { ark, accounts } = Route.useRouteContext();
|
||||||
|
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
const select = async (npub: string) => {
|
const select = async (npub: string) => {
|
||||||
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
||||||
const ark = context.ark;
|
|
||||||
const loadAccount = await ark.load_selected_account(npub);
|
const loadAccount = await ark.load_selected_account(npub);
|
||||||
|
|
||||||
if (loadAccount) {
|
if (loadAccount) {
|
||||||
return navigate({
|
return navigate({
|
||||||
to: "/$account/home",
|
to: "/$account/home",
|
||||||
@@ -64,6 +44,10 @@ function Screen() {
|
|||||||
replace: true,
|
replace: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
} catch (e) {
|
||||||
|
setLoading(false);
|
||||||
|
toast.error(String(e));
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const currentDate = new Date().toLocaleString("default", {
|
const currentDate = new Date().toLocaleString("default", {
|
||||||
@@ -86,7 +70,7 @@ function Screen() {
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{context.accounts.map((account) => (
|
{accounts.map((account) => (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
key={account}
|
key={account}
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ enum NSTORE_KEYS {
|
|||||||
export class Ark {
|
export class Ark {
|
||||||
public windows: WebviewWindow[];
|
public windows: WebviewWindow[];
|
||||||
public settings: Settings;
|
public settings: Settings;
|
||||||
|
public accounts: string[];
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.windows = [];
|
this.windows = [];
|
||||||
@@ -35,6 +36,8 @@ export class Ark {
|
|||||||
const cmd: string[] = await invoke("get_accounts");
|
const cmd: string[] = await invoke("get_accounts");
|
||||||
const accounts: string[] = cmd.map((item) => item.replace(".npub", ""));
|
const accounts: string[] = cmd.map((item) => item.replace(".npub", ""));
|
||||||
|
|
||||||
|
if (!this.accounts) this.accounts = accounts;
|
||||||
|
|
||||||
return accounts;
|
return accounts;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw new Error(String(e));
|
throw new Error(String(e));
|
||||||
|
|||||||
1
packages/types/index.d.ts
vendored
1
packages/types/index.d.ts
vendored
@@ -19,6 +19,7 @@ export enum Kind {
|
|||||||
Contacts = 3,
|
Contacts = 3,
|
||||||
Repost = 6,
|
Repost = 6,
|
||||||
Reaction = 7,
|
Reaction = 7,
|
||||||
|
ZapReceipt = 9735,
|
||||||
// NIP-89: App Metadata
|
// NIP-89: App Metadata
|
||||||
AppRecommendation = 31989,
|
AppRecommendation = 31989,
|
||||||
AppHandler = 31990,
|
AppHandler = 31990,
|
||||||
|
|||||||
@@ -23,12 +23,7 @@ pub fn run_notification(accounts: Vec<String>, app: tauri::AppHandle) -> Result<
|
|||||||
.collect();
|
.collect();
|
||||||
let subscription = Filter::new()
|
let subscription = Filter::new()
|
||||||
.pubkeys(pubkeys)
|
.pubkeys(pubkeys)
|
||||||
.kinds(vec![
|
.kinds(vec![Kind::TextNote, Kind::Repost, Kind::ZapReceipt])
|
||||||
Kind::TextNote,
|
|
||||||
Kind::Repost,
|
|
||||||
Kind::ZapReceipt,
|
|
||||||
Kind::EncryptedDirectMessage,
|
|
||||||
])
|
|
||||||
.since(Timestamp::now());
|
.since(Timestamp::now());
|
||||||
let activity_id = SubscriptionId::new("activity");
|
let activity_id = SubscriptionId::new("activity");
|
||||||
|
|
||||||
@@ -47,7 +42,8 @@ pub fn run_notification(accounts: Vec<String>, app: tauri::AppHandle) -> Result<
|
|||||||
} = notification
|
} = notification
|
||||||
{
|
{
|
||||||
if subscription_id == activity_id {
|
if subscription_id == activity_id {
|
||||||
let _ = app.emit_to("main", "activity", event.as_json());
|
println!("new notification: {}", event.as_json());
|
||||||
|
let _ = app.emit("activity", event.as_json());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(false)
|
Ok(false)
|
||||||
|
|||||||
Reference in New Issue
Block a user