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

@@ -78,7 +78,7 @@ export function Col({
});
}
};
}, [webview]);
}, []);
return (
<div className="h-full w-[440px] shrink-0 p-2">

View File

@@ -12,10 +12,7 @@ export function Conversation({
className?: string;
}) {
const { ark } = useRouteContext({ strict: false });
const thread = ark.parse_event_thread({
content: event.content,
tags: event.tags,
});
const thread = ark.parse_event_thread(event.tags);
return (
<Note.Provider event={event}>
@@ -26,9 +23,7 @@ export function Conversation({
)}
>
<div className="flex flex-col gap-3">
{thread?.rootEventId ? (
<Note.Child eventId={thread?.rootEventId} isRoot />
) : null}
{thread?.root ? <Note.Child eventId={thread?.root} isRoot /> : null}
<div className="flex items-center gap-2 px-3">
<div className="inline-flex items-center gap-1.5 shrink-0 text-sm font-medium text-neutral-600 dark:text-neutral-400">
<ThreadIcon className="size-4" />
@@ -36,9 +31,7 @@ export function Conversation({
</div>
<div className="flex-1 h-px bg-neutral-100 dark:bg-white/5" />
</div>
{thread?.replyEventId ? (
<Note.Child eventId={thread?.replyEventId} />
) : null}
{thread?.reply ? <Note.Child eventId={thread?.reply} /> : null}
<div>
<div className="px-3 h-14 flex items-center justify-between">
<Note.User />

View File

@@ -0,0 +1,34 @@
import type { Event } from "@lume/types";
import { Note } from "@lume/ui";
import { cn } from "@lume/utils";
export function Notification({
event,
className,
}: {
event: Event;
className?: string;
}) {
return (
<Note.Provider event={event}>
<Note.Root
className={cn(
"bg-white dark:bg-black/20 backdrop-blur-lg rounded-xl flex flex-col gap-3 shadow-primary dark:ring-1 ring-neutral-800/50",
className,
)}
>
<div className="flex flex-col gap-3">
<div>
<div className="px-3 h-14 flex items-center justify-between">
<Note.User />
</div>
<Note.Content className="px-3" />
</div>
</div>
<div className="flex items-center h-14 px-3">
<Note.Open />
</div>
</Note.Root>
</Note.Provider>
);
}

View File

@@ -1,5 +1,4 @@
import { ComposeFilledIcon, PlusIcon, SearchIcon } from "@lume/icons";
import type { Account } from "@lume/types";
import { BellIcon, ComposeFilledIcon, PlusIcon, SearchIcon } from "@lume/icons";
import { User } from "@lume/ui";
import { cn } from "@lume/utils";
import { Outlet, createFileRoute } from "@tanstack/react-router";
@@ -10,9 +9,11 @@ export const Route = createFileRoute("/$account")({
});
function Screen() {
const navigate = Route.useNavigate();
const { account } = Route.useParams();
const { ark, platform } = Route.useRouteContext();
const navigate = Route.useNavigate();
return (
<div className="flex h-screen w-screen flex-col">
<div
@@ -31,15 +32,8 @@ function Screen() {
>
<PlusIcon className="size-5" />
</button>
<button
type="button"
onClick={() => ark.open_search()}
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"
>
<SearchIcon className="size-4" />
</button>
</div>
<div className="flex items-center gap-3">
<div className="flex items-center gap-2">
<button
type="button"
onClick={() => ark.open_editor()}
@@ -48,6 +42,21 @@ function Screen() {
<ComposeFilledIcon className="size-4" />
New Post
</button>
<button
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
type="button"
onClick={() => ark.open_search()}
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"
>
<SearchIcon className="size-5" />
</button>
<div id="toolbar" />
</div>
</div>
@@ -63,7 +72,7 @@ export function Accounts() {
const { ark } = Route.useRouteContext();
const { account } = Route.useParams();
const [accounts, setAccounts] = useState<Account[]>([]);
const [accounts, setAccounts] = useState<string[]>([]);
const changeAccount = async (npub: string) => {
if (npub === account) return;
@@ -84,16 +93,12 @@ export function Accounts() {
return (
<div data-tauri-drag-region className="flex items-center gap-3">
{accounts.map((user) => (
<button
key={user.npub}
type="button"
onClick={() => changeAccount(user.npub)}
>
<User.Provider pubkey={user.npub}>
<button key={user} type="button" onClick={() => changeAccount(user)}>
<User.Provider pubkey={user}>
<User.Root
className={cn(
"rounded-full",
user.npub === account
user === account
? "ring-1 ring-teal-500 ring-offset-2 ring-offset-neutral-200 dark:ring-offset-neutral-950"
: "",
)}
@@ -101,7 +106,7 @@ export function Accounts() {
<User.Avatar
className={cn(
"aspect-square h-auto rounded-full object-cover",
user.npub === account ? "w-7" : "w-8",
user === account ? "w-7" : "w-8",
)}
/>
</User.Root>

View File

@@ -0,0 +1,5 @@
import { createFileRoute } from "@tanstack/react-router";
export const Route = createFileRoute("/activity/$account/messages")({
component: () => <div>Hello /activity/$account/messages!</div>,
});

View File

@@ -0,0 +1,60 @@
import { Note, Spinner } from "@lume/ui";
import { Await, createFileRoute, defer } from "@tanstack/react-router";
import { Suspense } from "react";
import { Virtualizer } from "virtua";
export const Route = createFileRoute("/activity/$account/texts")({
loader: async ({ context, params }) => {
const ark = context.ark;
return { data: defer(ark.get_activities(params.account, "1")) };
},
component: Screen,
});
function Screen() {
const { data } = Route.useLoaderData();
return (
<Virtualizer overscan={3}>
<Suspense
fallback={
<div className="flex h-20 w-full flex-col items-center justify-center gap-1">
<button
type="button"
className="inline-flex items-center gap-2 text-sm font-medium"
disabled
>
<Spinner className="size-5" />
Loading...
</button>
</div>
}
>
<Await promise={data}>
{(events) =>
events.map((event) => (
<div
key={event.id}
className="flex flex-col gap-2 mb-3 bg-white dark:bg-black/20 backdrop-blur-lg rounded-xl shadow-primary dark:ring-1 ring-neutral-800/50"
>
<Note.Provider event={event}>
<Note.Root>
<div className="px-3 h-14 flex items-center justify-between">
<Note.User />
<Note.Menu />
</div>
<Note.Activity className="px-3" />
<Note.Content className="px-3" quote={false} clean />
<div className="mt-3 flex items-center gap-4 h-14 px-3">
<Note.Open />
</div>
</Note.Root>
</Note.Provider>
</div>
))
}
</Await>
</Suspense>
</Virtualizer>
);
}

View File

@@ -0,0 +1,50 @@
import { Box, Container } from "@lume/ui";
import { cn } from "@lume/utils";
import { Link, Outlet } from "@tanstack/react-router";
import { createFileRoute } from "@tanstack/react-router";
export const Route = createFileRoute("/activity/$account")({
component: Screen,
});
function Screen() {
const { account } = Route.useParams();
return (
<Container withDrag withNavigate={false}>
<Box className="scrollbar-none shadow-none bg-black/5 dark:bg-white/5 backdrop-blur-sm flex flex-col overflow-y-auto">
<div className="h-14 shrink-0 flex w-full items-center gap-1 px-3">
<div className="inline-flex h-full w-full items-center gap-1">
<Link to="/activity/$account/texts" params={{ account }}>
{({ isActive }) => (
<div
className={cn(
"inline-flex h-7 w-max items-center justify-center gap-2 rounded-full px-3 text-sm font-medium",
isActive ? "bg-neutral-50 dark:bg-white/10" : "opacity-50",
)}
>
Notes
</div>
)}
</Link>
<Link to="/activity/$account/zaps" params={{ account }}>
{({ isActive }) => (
<div
className={cn(
"inline-flex h-7 w-max items-center justify-center gap-2 rounded-full px-3 text-sm font-medium",
isActive ? "bg-neutral-50 dark:bg-white/10" : "opacity-50",
)}
>
Zaps
</div>
)}
</Link>
</div>
</div>
<div className="px-2 flex-1 overflow-y-auto w-full h-full scrollbar-none">
<Outlet />
</div>
</Box>
</Container>
);
}

View File

@@ -0,0 +1,64 @@
import { Note, Spinner, User } from "@lume/ui";
import { decodeZapInvoice } from "@lume/utils";
import { Await, createFileRoute, defer } from "@tanstack/react-router";
import { Suspense } from "react";
import { Virtualizer } from "virtua";
export const Route = createFileRoute("/activity/$account/zaps")({
loader: async ({ context, params }) => {
const ark = context.ark;
return { data: defer(ark.get_activities(params.account, "9735")) };
},
component: Screen,
});
function Screen() {
const { data } = Route.useLoaderData();
return (
<Virtualizer overscan={3}>
<Suspense
fallback={
<div className="flex h-20 w-full flex-col items-center justify-center gap-1">
<button
type="button"
className="inline-flex items-center gap-2 text-sm font-medium"
disabled
>
<Spinner className="size-5" />
Loading...
</button>
</div>
}
>
<Await promise={data}>
{(events) =>
events.map((event) => (
<div
key={event.id}
className="flex flex-col gap-2 mb-3 bg-white dark:bg-black/20 backdrop-blur-lg rounded-xl shadow-primary dark:ring-1 ring-neutral-800/50"
>
<User.Provider pubkey={event.pubkey}>
<User.Root className="flex flex-col">
<div className="text-lg h-20 font-medium leading-tight flex w-full items-center justify-center">
{decodeZapInvoice(event.tags).bitcoinFormatted}
</div>
<div className="h-11 border-t border-neutral-100 dark:border-neutral-900 flex items-center gap-1 px-2">
<div className="inline-flex items-center gap-2">
<User.Avatar className="size-7 rounded-full shrink-0" />
<User.Name className="text-sm font-medium" />
</div>
<div className="text-sm text-neutral-700 dark:text-neutral-300">
zapped you
</div>
</div>
</User.Root>
</User.Provider>
</div>
))
}
</Await>
</Suspense>
</Virtualizer>
);
}

View File

@@ -2,6 +2,7 @@ import { PlusIcon } from "@lume/icons";
import { Spinner, User } from "@lume/ui";
import { Link } from "@tanstack/react-router";
import { createFileRoute, redirect } from "@tanstack/react-router";
import { invoke } from "@tauri-apps/api/core";
import { useState } from "react";
export const Route = createFileRoute("/")({
@@ -9,6 +10,11 @@ export const Route = createFileRoute("/")({
const ark = context.ark;
const accounts = await ark.get_all_accounts();
// Run notification service
if (accounts.length > 0) {
await invoke("run_notification", { accounts });
}
switch (accounts.length) {
// Guest account
case 0:
@@ -18,7 +24,7 @@ export const Route = createFileRoute("/")({
});
// Only 1 account, skip account selection screen
case 1: {
const account = accounts[0].npub;
const account = accounts[0];
const loadedAccount = await ark.load_selected_account(account);
if (loadedAccount) {
@@ -83,10 +89,10 @@ function Screen() {
{context.accounts.map((account) => (
<button
type="button"
key={account.npub}
onClick={() => select(account.npub)}
key={account}
onClick={() => select(account)}
>
<User.Provider pubkey={account.npub}>
<User.Provider pubkey={account}>
<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" />
<User.Name className="max-w-[5rem] truncate text-lg font-medium leading-tight text-white" />

View File

@@ -1,6 +1,5 @@
import { RepostNote } from "@/components/repost";
import { TextNote } from "@/components/text";
import { type Event, Kind } from "@lume/types";
import { type Event } from "@lume/types";
import { Spinner } from "@lume/ui";
import { Await, createFileRoute } from "@tanstack/react-router";
import { defer } from "@tanstack/react-router";

View File

@@ -21,7 +21,7 @@ export const Route = createFileRoute("/trending")({
component: Screen,
});
export function Screen() {
function Screen() {
return (
<div className="flex flex-col h-full">
<div className="h-11 shrink-0 inline-flex w-full items-center gap-1 px-3">

View File

@@ -1,43 +0,0 @@
import { Box, Container, User } from "@lume/ui";
import { createLazyFileRoute } from "@tanstack/react-router";
import { WindowVirtualizer } from "virtua";
import { EventList } from "./-components/eventList";
export const Route = createLazyFileRoute("/users/$pubkey")({
component: Screen,
});
function Screen() {
const { pubkey } = Route.useParams();
return (
<Container withDrag>
<Box className="px-0 scrollbar-none">
<WindowVirtualizer>
<User.Provider pubkey={pubkey}>
<User.Root>
<User.Cover className="h-44 w-full object-cover" />
<div className="relative -mt-8 flex flex-col gap-4 px-3">
<User.Avatar className="size-14 rounded-full" />
<div className="inline-flex items-start justify-between">
<div>
<User.Name className="font-semibold leading-tight" />
<User.NIP05 className="text-sm leading-tight text-neutral-600 dark:text-neutral-400" />
</div>
<User.Button className="h-9 w-24 rounded-full bg-black text-sm font-medium text-white hover:bg-neutral-900 dark:bg-neutral-900" />
</div>
<User.About />
</div>
</User.Root>
</User.Provider>
<div className="mt-4">
<div className="px-3">
<h3 className="text-lg font-semibold">Latest notes</h3>
</div>
<EventList id={pubkey} />
</div>
</WindowVirtualizer>
</Box>
</Container>
);
}

View File

@@ -0,0 +1,95 @@
import { Box, Container, Spinner, User } from "@lume/ui";
import { createFileRoute, defer } from "@tanstack/react-router";
import { WindowVirtualizer } from "virtua";
import { Conversation } from "@/components/conversation";
import { Quote } from "@/components/quote";
import { RepostNote } from "@/components/repost";
import { TextNote } from "@/components/text";
import { Event, Kind } from "@lume/types";
import { Suspense } from "react";
import { Await } from "@tanstack/react-router";
export const Route = createFileRoute("/users/$pubkey")({
beforeLoad: async ({ context }) => {
const ark = context.ark;
const settings = await ark.get_settings();
return { settings };
},
loader: async ({ params, context }) => {
const ark = context.ark;
return { data: defer(ark.get_events_from(params.pubkey, 50)) };
},
component: Screen,
});
function Screen() {
const { pubkey } = Route.useParams();
const { data } = Route.useLoaderData();
const renderItem = (event: Event) => {
if (!event) return;
switch (event.kind) {
case Kind.Repost:
return <RepostNote key={event.id} event={event} />;
default: {
const isConversation =
event.tags.filter((tag) => tag[0] === "e" && tag[3] !== "mention")
.length > 0;
const isQuote = event.tags.filter((tag) => tag[0] === "q").length > 0;
if (isConversation) {
return <Conversation key={event.id} event={event} className="mb-3" />;
}
if (isQuote) {
return <Quote key={event.id} event={event} className="mb-3" />;
}
return <TextNote key={event.id} event={event} className="mb-3" />;
}
}
};
return (
<Container withDrag>
<Box className="px-0 scrollbar-none bg-black/5 dark:bg-white/5 backdrop-blur-sm">
<WindowVirtualizer>
<User.Provider pubkey={pubkey}>
<User.Root>
<User.Cover className="h-44 w-full object-cover" />
<div className="relative -mt-8 flex flex-col px-3">
<User.Avatar className="size-14 rounded-full" />
<div className="mb-4 inline-flex items-center justify-between">
<div className="flex items-center gap-1">
<User.Name className="text-lg font-semibold leading-tight" />
<User.NIP05 />
</div>
<User.Button className="h-9 w-24 rounded-full inline-flex items-center justify-center bg-black text-sm font-medium text-white hover:bg-neutral-900 dark:bg-neutral-900" />
</div>
<User.About />
</div>
</User.Root>
</User.Provider>
<div className="px-3 mt-5">
<div className="mb-3">
<h3 className="text-lg font-semibold">Latest notes</h3>
</div>
<Suspense
fallback={
<div className="flex h-20 w-full items-center justify-center gap-1.5 text-sm font-medium">
<Spinner className="size-5" />
Loading...
</div>
}
>
<Await promise={data}>
{(events) => events.map((event) => renderItem(event))}
</Await>
</Suspense>
</div>
</WindowVirtualizer>
</Box>
</Container>
);
}

View File

@@ -19,10 +19,8 @@ export function EventList({ id }: { id: string }) {
},
getNextPageParam: (lastPage) => {
const lastEvent = lastPage?.at(-1);
if (!lastEvent) return;
return lastEvent.created_at - 1;
return lastEvent ? lastEvent.created_at - 1 : null;
},
select: (data) => data?.pages.flatMap((page) => page),
refetchOnWindowFocus: false,
});