mirror of
https://github.com/luminous-devs/lume.git
synced 2024-09-29 16:30:55 +00:00
feat: add bell
This commit is contained in:
parent
c843626bca
commit
afb7c87fa3
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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
227
packages/types/index.d.ts
vendored
227
packages/types/index.d.ts
vendored
@ -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[];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
|
Loading…
Reference in New Issue
Block a user