feat: add bell

This commit is contained in:
reya 2024-05-07 08:29:58 +07:00
parent c843626bca
commit afb7c87fa3
7 changed files with 1436 additions and 1381 deletions

View File

@ -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";
@ -13,173 +13,179 @@ import { useDebouncedCallback } from "use-debounce";
import { VList, type VListHandle } from "virtua"; import { VList, type VListHandle } from "virtua";
export const Route = createFileRoute("/$account/home")({ export const Route = createFileRoute("/$account/home")({
beforeLoad: async ({ context }) => { beforeLoad: async ({ context }) => {
const ark = context.ark; try {
const resourcePath = await resolveResource("resources/system_columns.json"); const ark = context.ark;
const systemColumns: LumeColumn[] = JSON.parse( const resourcePath = await resolveResource(
await readTextFile(resourcePath), "resources/system_columns.json",
); );
const userColumns = await ark.get_columns(); const systemColumns: LumeColumn[] = JSON.parse(
await readTextFile(resourcePath),
);
const userColumns = await ark.get_columns();
return { return {
storedColumns: !userColumns.length ? systemColumns : userColumns, storedColumns: !userColumns.length ? systemColumns : userColumns,
}; };
}, } catch (e) {
component: Screen, console.error(String(e));
}
},
component: Screen,
}); });
function Screen() { function Screen() {
const vlistRef = useRef<VListHandle>(null); const vlistRef = useRef<VListHandle>(null);
const { account } = Route.useParams(); const { account } = Route.useParams();
const { ark, storedColumns } = Route.useRouteContext(); const { ark, storedColumns } = Route.useRouteContext();
const [selectedIndex, setSelectedIndex] = useState(-1); const [selectedIndex, setSelectedIndex] = useState(-1);
const [columns, setColumns] = useState(storedColumns); const [columns, setColumns] = useState(storedColumns);
const [isScroll, setIsScroll] = useState(false); const [isScroll, setIsScroll] = useState(false);
const [isResize, setIsResize] = useState(false); const [isResize, setIsResize] = useState(false);
const goLeft = () => { const goLeft = () => {
const prevIndex = Math.max(selectedIndex - 1, 0); const prevIndex = Math.max(selectedIndex - 1, 0);
setSelectedIndex(prevIndex); setSelectedIndex(prevIndex);
vlistRef.current.scrollToIndex(prevIndex, { vlistRef.current.scrollToIndex(prevIndex, {
align: "center", align: "center",
}); });
}; };
const goRight = () => { const goRight = () => {
const nextIndex = Math.min(selectedIndex + 1, columns.length - 1); const nextIndex = Math.min(selectedIndex + 1, columns.length - 1);
setSelectedIndex(nextIndex); setSelectedIndex(nextIndex);
vlistRef.current.scrollToIndex(nextIndex, { vlistRef.current.scrollToIndex(nextIndex, {
align: "center", align: "center",
}); });
}; };
const add = useDebouncedCallback((column: LumeColumn) => { const add = useDebouncedCallback((column: LumeColumn) => {
// update col label // update col label
column.label = `${column.label}-${nanoid()}`; column.label = `${column.label}-${nanoid()}`;
// create new cols // create new cols
const cols = [...columns]; const cols = [...columns];
const openColIndex = cols.findIndex((col) => col.label === "open"); const openColIndex = cols.findIndex((col) => col.label === "open");
const newCols = [ const newCols = [
...cols.slice(0, openColIndex), ...cols.slice(0, openColIndex),
column, column,
...cols.slice(openColIndex), ...cols.slice(openColIndex),
]; ];
setColumns(newCols); setColumns(newCols);
setSelectedIndex(newCols.length); setSelectedIndex(newCols.length);
setIsScroll(true); setIsScroll(true);
// scroll to the newest column // scroll to the newest column
vlistRef.current.scrollToIndex(newCols.length - 1, { vlistRef.current.scrollToIndex(newCols.length - 1, {
align: "end", align: "end",
}); });
}, 150); }, 150);
const remove = useDebouncedCallback((label: string) => { const remove = useDebouncedCallback((label: string) => {
const newCols = columns.filter((t) => t.label !== label); const newCols = columns.filter((t) => t.label !== label);
setColumns(newCols); setColumns(newCols);
setSelectedIndex(newCols.length); setSelectedIndex(newCols.length);
setIsScroll(true); setIsScroll(true);
// scroll to the first column // scroll to the first column
vlistRef.current.scrollToIndex(newCols.length - 1, { vlistRef.current.scrollToIndex(newCols.length - 1, {
align: "start", align: "start",
}); });
}, 150); }, 150);
const updateName = useDebouncedCallback((label: string, title: string) => { const updateName = useDebouncedCallback((label: string, title: string) => {
const currentColIndex = columns.findIndex((col) => col.label === label); const currentColIndex = columns.findIndex((col) => col.label === label);
const updatedCol = Object.assign({}, columns[currentColIndex]); const updatedCol = Object.assign({}, columns[currentColIndex]);
updatedCol.name = title; updatedCol.name = title;
const newCols = columns.slice(); const newCols = columns.slice();
newCols[currentColIndex] = updatedCol; newCols[currentColIndex] = updatedCol;
setColumns(newCols); setColumns(newCols);
}, 150); }, 150);
const startResize = useDebouncedCallback( const startResize = useDebouncedCallback(
() => setIsResize((prev) => !prev), () => setIsResize((prev) => !prev),
150, 150,
); );
useEffect(() => { useEffect(() => {
// save state // save state
ark.set_columns(columns); ark.set_columns(columns);
}, [columns]); }, [columns]);
useEffect(() => { useEffect(() => {
let unlistenColEvent: Awaited<ReturnType<typeof listen>> | undefined = let unlistenColEvent: Awaited<ReturnType<typeof listen>> | undefined =
undefined; undefined;
let unlistenWindowResize: Awaited<ReturnType<typeof listen>> | undefined = let unlistenWindowResize: Awaited<ReturnType<typeof listen>> | undefined =
undefined; undefined;
(async () => { (async () => {
if (unlistenColEvent && unlistenWindowResize) return; if (unlistenColEvent && unlistenWindowResize) return;
unlistenColEvent = await listen<EventColumns>("columns", (data) => { unlistenColEvent = await listen<EventColumns>("columns", (data) => {
if (data.payload.type === "add") add(data.payload.column); if (data.payload.type === "add") add(data.payload.column);
if (data.payload.type === "remove") remove(data.payload.label); if (data.payload.type === "remove") remove(data.payload.label);
if (data.payload.type === "set_title") if (data.payload.type === "set_title")
updateName(data.payload.label, data.payload.title); updateName(data.payload.label, data.payload.title);
}); });
unlistenWindowResize = await getCurrent().listen("tauri://resize", () => { unlistenWindowResize = await getCurrent().listen("tauri://resize", () => {
startResize(); startResize();
}); });
})(); })();
return () => { return () => {
if (unlistenColEvent) unlistenColEvent(); if (unlistenColEvent) unlistenColEvent();
if (unlistenWindowResize) unlistenWindowResize(); if (unlistenWindowResize) unlistenWindowResize();
}; };
}, []); }, []);
return ( return (
<div className="h-full w-full"> <div className="h-full w-full">
<VList <VList
ref={vlistRef} ref={vlistRef}
horizontal horizontal
tabIndex={-1} tabIndex={-1}
itemSize={440} itemSize={440}
overscan={3} overscan={3}
onScroll={() => setIsScroll(true)} onScroll={() => setIsScroll(true)}
onScrollEnd={() => setIsScroll(false)} onScrollEnd={() => setIsScroll(false)}
className="scrollbar-none h-full w-full overflow-x-auto focus:outline-none" className="scrollbar-none h-full w-full overflow-x-auto focus:outline-none"
> >
{columns.map((column) => ( {columns.map((column) => (
<Col <Col
key={column.label} key={column.label}
column={column} column={column}
account={account} account={account}
isScroll={isScroll} isScroll={isScroll}
isResize={isResize} isResize={isResize}
/> />
))} ))}
</VList> </VList>
<Toolbar> <Toolbar>
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<button <button
type="button" type="button"
onClick={() => goLeft()} onClick={() => goLeft()}
className="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" className="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"
> >
<ArrowLeftIcon className="size-5" /> <ArrowLeftIcon className="size-5" />
</button> </button>
<button <button
type="button" type="button"
onClick={() => goRight()} onClick={() => goRight()}
className="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" className="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"
> >
<ArrowRightIcon className="size-5" /> <ArrowRightIcon className="size-5" />
</button> </button>
</div> </div>
</Toolbar> </Toolbar>
</div> </div>
); );
} }

View File

@ -1,118 +1,183 @@
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")({
component: Screen, beforeLoad: async ({ context }) => {
const ark = context.ark;
const accounts = await ark.get_all_accounts();
return { accounts };
},
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 (
<div className="flex h-screen w-screen flex-col">
return ( <div
<div className="flex h-screen w-screen flex-col"> data-tauri-drag-region
<div className={cn(
data-tauri-drag-region "flex h-11 shrink-0 items-center justify-between pr-2",
className={cn( platform === "macos" ? "ml-2 pl-20" : "pl-4",
"flex h-11 shrink-0 items-center justify-between pr-2", )}
platform === "macos" ? "ml-2 pl-20" : "pl-4", >
)} <div className="flex items-center gap-3">
> <Accounts />
<div className="flex items-center gap-3"> <button
<Accounts /> type="button"
<button onClick={() => navigate({ to: "/landing" })}
type="button" className="inline-flex size-8 items-center justify-center rounded-full bg-black/10 text-neutral-800 hover:bg-black/20 dark:bg-white/10 dark:text-neutral-200 dark:hover:bg-white/20"
onClick={() => navigate({ to: "/landing" })} >
className="inline-flex size-8 items-center justify-center rounded-full bg-black/10 text-neutral-800 hover:bg-black/20 dark:bg-white/10 dark:text-neutral-200 dark:hover:bg-white/20" <PlusIcon className="size-5" />
> </button>
<PlusIcon className="size-5" /> </div>
</button> <div className="flex items-center gap-2">
</div> <button
<div className="flex items-center gap-2"> type="button"
<button onClick={() => ark.open_editor()}
type="button" className="inline-flex h-8 w-max items-center justify-center gap-1 rounded-full bg-blue-500 px-3 text-sm font-medium text-white hover:bg-blue-600"
onClick={() => ark.open_editor()} >
className="inline-flex h-8 w-max items-center justify-center gap-1 rounded-full bg-blue-500 px-3 text-sm font-medium text-white hover:bg-blue-600" <ComposeFilledIcon className="size-4" />
> New Post
<ComposeFilledIcon className="size-4" /> </button>
New Post <Bell />
</button> <button
<button type="button"
type="button" onClick={() => ark.open_search()}
onClick={() => ark.open_activity(account)} className="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"
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" >
> <SearchIcon className="size-5" />
<BellIcon className="size-5" /> </button>
{/* <span className="absolute right-0 top-0 block size-2 rounded-full bg-teal-500 ring-1 ring-black/5"></span> */} <div id="toolbar" />
</button> </div>
<button </div>
type="button" <div className="flex-1">
onClick={() => ark.open_search()} <Outlet />
className="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" </div>
> </div>
<SearchIcon className="size-5" /> );
</button>
<div id="toolbar" />
</div>
</div>
<div className="flex-1">
<Outlet />
</div>
</div>
);
} }
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) => {
if (npub === account) return;
const changeAccount = async (npub: string) => { const select = await ark.load_selected_account(npub);
if (npub === account) return;
const select = await ark.load_selected_account(npub);
if (select)
return navigate({ to: "/$account/home", params: { account: npub } });
};
useEffect(() => { if (select) {
async function getAllAccounts() { return navigate({ to: "/$account/home", params: { account: npub } });
const data = await ark.get_all_accounts(); }
if (data) setAccounts(data); };
}
getAllAccounts(); return (
}, []); <div data-tauri-drag-region className="flex items-center gap-3">
{accounts.map((user) => (
return ( <button key={user} type="button" onClick={() => changeAccount(user)}>
<div data-tauri-drag-region className="flex items-center gap-3"> <User.Provider pubkey={user}>
{accounts.map((user) => ( <User.Root
<button key={user} type="button" onClick={() => changeAccount(user)}> className={cn(
<User.Provider pubkey={user}> "rounded-full",
<User.Root user === account
className={cn( ? "ring-1 ring-teal-500 ring-offset-2 ring-offset-neutral-200 dark:ring-offset-neutral-950"
"rounded-full", : "",
user === account )}
? "ring-1 ring-teal-500 ring-offset-2 ring-offset-neutral-200 dark:ring-offset-neutral-950" >
: "", <User.Avatar
)} className={cn(
> "aspect-square h-auto rounded-full object-cover",
<User.Avatar user === account ? "w-7" : "w-8",
className={cn( )}
"aspect-square h-auto rounded-full object-cover", />
user === account ? "w-7" : "w-8", </User.Root>
)} </User.Provider>
/> </button>
</User.Root> ))}
</User.Provider> </div>
</button> );
))} }
</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>
);
} }

View File

@ -7,38 +7,38 @@ import type { Platform } from "@tauri-apps/plugin-os";
import type { Descendant } from "slate"; import type { Descendant } from "slate";
type EditorElement = { type EditorElement = {
type: string; type: string;
children: Descendant[]; children: Descendant[];
eventId?: string; eventId?: string;
}; };
interface RouterContext { interface RouterContext {
// System // System
ark: Ark; ark: Ark;
queryClient: QueryClient; queryClient: QueryClient;
// App info // App info
platform?: Platform; platform?: Platform;
locale?: string; locale?: string;
// Settings // Settings
settings?: Settings; settings?: Settings;
interests?: Interests; interests?: Interests;
// Profile // Profile
accounts?: Account[]; accounts?: string[];
profile?: Metadata; profile?: Metadata;
// Editor // Editor
initialValue?: EditorElement[]; initialValue?: EditorElement[];
} }
export const Route = createRootRouteWithContext<RouterContext>()({ export const Route = createRootRouteWithContext<RouterContext>()({
component: () => <Outlet />, component: () => <Outlet />,
pendingComponent: Pending, pendingComponent: Pending,
wrapInSuspense: true, wrapInSuspense: true,
}); });
function Pending() { function Pending() {
return ( return (
<div className="flex h-screen w-screen flex-col items-center justify-center"> <div className="flex h-screen w-screen flex-col items-center justify-center">
<Spinner className="size-5" /> <Spinner className="size-5" />
</div> </div>
); );
} }

View File

@ -4,131 +4,115 @@ 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) { throw redirect({
await invoke("run_notification", { accounts }); to: "/landing",
} replace: true,
});
}
switch (accounts.length) { // Run notification service
// Guest account await invoke("run_notification", { accounts });
case 0:
throw redirect({
to: "/landing",
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) { return { accounts };
throw redirect({ },
to: "/$account/home", component: Screen,
params: { account },
replace: true,
});
}
break;
}
// Account selection
default:
return { accounts };
}
},
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) => {
setLoading(true); try {
setLoading(true);
const ark = context.ark; const loadAccount = await ark.load_selected_account(npub);
const loadAccount = await ark.load_selected_account(npub); if (loadAccount) {
return navigate({
to: "/$account/home",
params: { account: npub },
replace: true,
});
}
} catch (e) {
setLoading(false);
toast.error(String(e));
}
};
if (loadAccount) { const currentDate = new Date().toLocaleString("default", {
return navigate({ weekday: "long",
to: "/$account/home", month: "long",
params: { account: npub }, day: "numeric",
replace: true, });
});
}
};
const currentDate = new Date().toLocaleString("default", { return (
weekday: "long", <div className="relative flex h-full w-full items-center justify-center">
month: "long", <div className="relative z-20 flex flex-col items-center gap-16">
day: "numeric", <div className="text-center text-white">
}); <h2 className="mb-1 text-2xl">{currentDate}</h2>
<h2 className="text-2xl font-semibold">Welcome back!</h2>
return ( </div>
<div className="relative flex h-full w-full items-center justify-center"> <div className="flex items-center justify-center gap-6">
<div className="relative z-20 flex flex-col items-center gap-16"> {loading ? (
<div className="text-center text-white"> <div className="inline-flex size-6 items-center justify-center">
<h2 className="mb-1 text-2xl">{currentDate}</h2> <Spinner className="size-6" />
<h2 className="text-2xl font-semibold">Welcome back!</h2> </div>
</div> ) : (
<div className="flex items-center justify-center gap-6"> <>
{loading ? ( {accounts.map((account) => (
<div className="inline-flex size-6 items-center justify-center"> <button
<Spinner className="size-6" /> type="button"
</div> key={account}
) : ( onClick={() => select(account)}
<> >
{context.accounts.map((account) => ( <User.Provider pubkey={account}>
<button <User.Root className="flex h-36 w-32 flex-col items-center justify-center gap-4 rounded-2xl p-2 hover:bg-white/10 dark:hover:bg-black/10">
type="button" <User.Avatar className="size-20 rounded-full object-cover" />
key={account} <User.Name className="max-w-[5rem] truncate text-lg font-medium leading-tight text-white" />
onClick={() => select(account)} </User.Root>
> </User.Provider>
<User.Provider pubkey={account}> </button>
<User.Root className="flex h-36 w-32 flex-col items-center justify-center gap-4 rounded-2xl p-2 hover:bg-white/10 dark:hover:bg-black/10"> ))}
<User.Avatar className="size-20 rounded-full object-cover" /> <Link to="/landing">
<User.Name className="max-w-[5rem] truncate text-lg font-medium leading-tight text-white" /> <div className="flex h-36 w-32 flex-col items-center justify-center gap-4 rounded-2xl p-2 text-white hover:bg-white/10 dark:hover:bg-black/10">
</User.Root> <div className="flex size-20 items-center justify-center rounded-full bg-white/20 dark:bg-black/20">
</User.Provider> <PlusIcon className="size-5" />
</button> </div>
))} <p className="text-lg font-medium leading-tight">Add</p>
<Link to="/landing"> </div>
<div className="flex h-36 w-32 flex-col items-center justify-center gap-4 rounded-2xl p-2 text-white hover:bg-white/10 dark:hover:bg-black/10"> </Link>
<div className="flex size-20 items-center justify-center rounded-full bg-white/20 dark:bg-black/20"> </>
<PlusIcon className="size-5" /> )}
</div> </div>
<p className="text-lg font-medium leading-tight">Add</p> </div>
</div> <div className="absolute z-10 h-full w-full bg-white/10 backdrop-blur-lg dark:bg-black/10" />
</Link> <div className="absolute inset-0 h-full w-full">
</> <img
)} src="/lock-screen.jpg"
</div> srcSet="/lock-screen@2x.jpg 2x"
</div> alt="Lock Screen Background"
<div className="absolute z-10 h-full w-full bg-white/10 backdrop-blur-lg dark:bg-black/10" /> className="h-full w-full object-cover"
<div className="absolute inset-0 h-full w-full"> />
<img <a
src="/lock-screen.jpg" href="https://njump.me/nprofile1qqs9tuz9jpn57djg7nxunhyvuvk69g5zqaxdpvpqt9hwqv7395u9rpg6zq5uw"
srcSet="/lock-screen@2x.jpg 2x" target="_blank"
alt="Lock Screen Background" className="absolute bottom-3 right-3 z-50 rounded-md bg-white/20 px-2 py-1 text-xs font-medium text-white dark:bg-black/20"
className="h-full w-full object-cover" rel="noreferrer"
/> >
<a Design by NoGood
href="https://njump.me/nprofile1qqs9tuz9jpn57djg7nxunhyvuvk69g5zqaxdpvpqt9hwqv7395u9rpg6zq5uw" </a>
target="_blank" </div>
className="absolute bottom-3 right-3 z-50 rounded-md bg-white/20 px-2 py-1 text-xs font-medium text-white dark:bg-black/20" </div>
rel="noreferrer" );
>
Design by NoGood
</a>
</div>
</div>
);
} }

File diff suppressed because it is too large Load Diff

View File

@ -1,166 +1,167 @@
export interface Settings { export interface Settings {
notification: boolean; notification: boolean;
enhancedPrivacy: boolean; enhancedPrivacy: boolean;
autoUpdate: boolean; autoUpdate: boolean;
zap: boolean; zap: boolean;
nsfw: boolean; nsfw: boolean;
[key: string]: string | number | boolean; [key: string]: string | number | boolean;
} }
export interface Keys { export interface Keys {
npub: string; npub: string;
nsec: string; nsec: string;
} }
export enum Kind { export enum Kind {
Metadata = 0, Metadata = 0,
Text = 1, Text = 1,
RecommendRelay = 2, RecommendRelay = 2,
Contacts = 3, Contacts = 3,
Repost = 6, Repost = 6,
Reaction = 7, Reaction = 7,
// NIP-89: App Metadata ZapReceipt = 9735,
AppRecommendation = 31989, // NIP-89: App Metadata
AppHandler = 31990, AppRecommendation = 31989,
// #TODO: Add all nostr kinds AppHandler = 31990,
// #TODO: Add all nostr kinds
} }
export interface Event { export interface Event {
id: string; id: string;
pubkey: string; pubkey: string;
created_at: number; created_at: number;
kind: Kind; kind: Kind;
tags: string[][]; tags: string[][];
content: string; content: string;
sig: string; sig: string;
relay?: string; relay?: string;
} }
export interface EventWithReplies extends Event { export interface EventWithReplies extends Event {
replies: Array<Event>; replies: Array<Event>;
} }
export interface Metadata { export interface Metadata {
name?: string; name?: string;
display_name?: string; display_name?: string;
about?: string; about?: string;
website?: string; website?: string;
picture?: string; picture?: string;
banner?: string; banner?: string;
nip05?: string; nip05?: string;
lud06?: string; lud06?: string;
lud16?: string; lud16?: string;
} }
export interface Contact { export interface Contact {
pubkey: string; pubkey: string;
profile: Metadata; profile: Metadata;
} }
export interface Account { export interface Account {
npub: string; npub: string;
nsec?: string; nsec?: string;
contacts?: string[]; contacts?: string[];
interests?: Interests; interests?: Interests;
} }
export interface Interests { export interface Interests {
hashtags: string[]; hashtags: string[];
users: string[]; users: string[];
words: string[]; words: string[];
} }
export interface RichContent { export interface RichContent {
parsed: string; parsed: string;
images: string[]; images: string[];
videos: string[]; videos: string[];
links: string[]; links: string[];
notes: string[]; notes: string[];
} }
export interface AppRouteSearch { export interface AppRouteSearch {
account: string; account: string;
} }
export interface ColumnRouteSearch { export interface ColumnRouteSearch {
account: string; account: string;
label: string; label: string;
name: string; name: string;
redirect?: string; redirect?: string;
} }
export interface LumeColumn { export interface LumeColumn {
label: string; label: string;
name: string; name: string;
content: URL | string; content: URL | string;
description?: string; description?: string;
author?: string; author?: string;
logo?: string; logo?: string;
cover?: string; cover?: string;
coverRetina?: string; coverRetina?: string;
featured?: boolean; featured?: boolean;
} }
export interface EventColumns { export interface EventColumns {
type: "add" | "remove" | "update" | "left" | "right" | "set_title"; type: "add" | "remove" | "update" | "left" | "right" | "set_title";
label?: string; label?: string;
title?: string; title?: string;
column?: LumeColumn; column?: LumeColumn;
} }
export interface Opengraph { export interface Opengraph {
url: string; url: string;
title?: string; title?: string;
description?: string; description?: string;
image?: string; image?: string;
} }
export interface NostrBuildResponse { export interface NostrBuildResponse {
ok: boolean; ok: boolean;
data?: { data?: {
message: string; message: string;
status: string; status: string;
data: Array<{ data: Array<{
blurhash: string; blurhash: string;
dimensions: { dimensions: {
width: number; width: number;
height: number; height: number;
}; };
mime: string; mime: string;
name: string; name: string;
sha256: string; sha256: string;
size: number; size: number;
url: string; url: string;
}>; }>;
}; };
} }
export interface NIP11 { export interface NIP11 {
name: string; name: string;
description: string; description: string;
pubkey: string; pubkey: string;
contact: string; contact: string;
supported_nips: number[]; supported_nips: number[];
software: string; software: string;
version: string; version: string;
limitation: { limitation: {
[key: string]: string | number | boolean; [key: string]: string | number | boolean;
}; };
relay_countries: string[]; relay_countries: string[];
language_tags: string[]; language_tags: string[];
tags: string[]; tags: string[];
posting_policy: string; posting_policy: string;
payments_url: string; payments_url: string;
icon: string[]; icon: string[];
} }
export interface NIP05 { export interface NIP05 {
names: { names: {
[key: string]: string; [key: string]: string;
}; };
nip46: { nip46: {
[key: string]: { [key: string]: {
[key: string]: string[]; [key: string]: string[];
}; };
}; };
} }

View File

@ -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)