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 { listen } from "@tauri-apps/api/event";
|
||||
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 { nanoid } from "nanoid";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
@ -13,173 +13,179 @@ import { useDebouncedCallback } from "use-debounce";
|
||||
import { VList, type VListHandle } from "virtua";
|
||||
|
||||
export const Route = createFileRoute("/$account/home")({
|
||||
beforeLoad: async ({ context }) => {
|
||||
const ark = context.ark;
|
||||
const resourcePath = await resolveResource("resources/system_columns.json");
|
||||
const systemColumns: LumeColumn[] = JSON.parse(
|
||||
await readTextFile(resourcePath),
|
||||
);
|
||||
const userColumns = await ark.get_columns();
|
||||
beforeLoad: async ({ context }) => {
|
||||
try {
|
||||
const ark = context.ark;
|
||||
const resourcePath = await resolveResource(
|
||||
"resources/system_columns.json",
|
||||
);
|
||||
const systemColumns: LumeColumn[] = JSON.parse(
|
||||
await readTextFile(resourcePath),
|
||||
);
|
||||
const userColumns = await ark.get_columns();
|
||||
|
||||
return {
|
||||
storedColumns: !userColumns.length ? systemColumns : userColumns,
|
||||
};
|
||||
},
|
||||
component: Screen,
|
||||
return {
|
||||
storedColumns: !userColumns.length ? systemColumns : userColumns,
|
||||
};
|
||||
} catch (e) {
|
||||
console.error(String(e));
|
||||
}
|
||||
},
|
||||
component: Screen,
|
||||
});
|
||||
|
||||
function Screen() {
|
||||
const vlistRef = useRef<VListHandle>(null);
|
||||
const vlistRef = useRef<VListHandle>(null);
|
||||
|
||||
const { account } = Route.useParams();
|
||||
const { ark, storedColumns } = Route.useRouteContext();
|
||||
const { account } = Route.useParams();
|
||||
const { ark, storedColumns } = Route.useRouteContext();
|
||||
|
||||
const [selectedIndex, setSelectedIndex] = useState(-1);
|
||||
const [columns, setColumns] = useState(storedColumns);
|
||||
const [isScroll, setIsScroll] = useState(false);
|
||||
const [isResize, setIsResize] = useState(false);
|
||||
const [selectedIndex, setSelectedIndex] = useState(-1);
|
||||
const [columns, setColumns] = useState(storedColumns);
|
||||
const [isScroll, setIsScroll] = useState(false);
|
||||
const [isResize, setIsResize] = useState(false);
|
||||
|
||||
const goLeft = () => {
|
||||
const prevIndex = Math.max(selectedIndex - 1, 0);
|
||||
setSelectedIndex(prevIndex);
|
||||
vlistRef.current.scrollToIndex(prevIndex, {
|
||||
align: "center",
|
||||
});
|
||||
};
|
||||
const goLeft = () => {
|
||||
const prevIndex = Math.max(selectedIndex - 1, 0);
|
||||
setSelectedIndex(prevIndex);
|
||||
vlistRef.current.scrollToIndex(prevIndex, {
|
||||
align: "center",
|
||||
});
|
||||
};
|
||||
|
||||
const goRight = () => {
|
||||
const nextIndex = Math.min(selectedIndex + 1, columns.length - 1);
|
||||
setSelectedIndex(nextIndex);
|
||||
vlistRef.current.scrollToIndex(nextIndex, {
|
||||
align: "center",
|
||||
});
|
||||
};
|
||||
const goRight = () => {
|
||||
const nextIndex = Math.min(selectedIndex + 1, columns.length - 1);
|
||||
setSelectedIndex(nextIndex);
|
||||
vlistRef.current.scrollToIndex(nextIndex, {
|
||||
align: "center",
|
||||
});
|
||||
};
|
||||
|
||||
const add = useDebouncedCallback((column: LumeColumn) => {
|
||||
// update col label
|
||||
column.label = `${column.label}-${nanoid()}`;
|
||||
const add = useDebouncedCallback((column: LumeColumn) => {
|
||||
// update col label
|
||||
column.label = `${column.label}-${nanoid()}`;
|
||||
|
||||
// create new cols
|
||||
const cols = [...columns];
|
||||
const openColIndex = cols.findIndex((col) => col.label === "open");
|
||||
const newCols = [
|
||||
...cols.slice(0, openColIndex),
|
||||
column,
|
||||
...cols.slice(openColIndex),
|
||||
];
|
||||
// create new cols
|
||||
const cols = [...columns];
|
||||
const openColIndex = cols.findIndex((col) => col.label === "open");
|
||||
const newCols = [
|
||||
...cols.slice(0, openColIndex),
|
||||
column,
|
||||
...cols.slice(openColIndex),
|
||||
];
|
||||
|
||||
setColumns(newCols);
|
||||
setSelectedIndex(newCols.length);
|
||||
setIsScroll(true);
|
||||
setColumns(newCols);
|
||||
setSelectedIndex(newCols.length);
|
||||
setIsScroll(true);
|
||||
|
||||
// scroll to the newest column
|
||||
vlistRef.current.scrollToIndex(newCols.length - 1, {
|
||||
align: "end",
|
||||
});
|
||||
}, 150);
|
||||
// scroll to the newest column
|
||||
vlistRef.current.scrollToIndex(newCols.length - 1, {
|
||||
align: "end",
|
||||
});
|
||||
}, 150);
|
||||
|
||||
const remove = useDebouncedCallback((label: string) => {
|
||||
const newCols = columns.filter((t) => t.label !== label);
|
||||
const remove = useDebouncedCallback((label: string) => {
|
||||
const newCols = columns.filter((t) => t.label !== label);
|
||||
|
||||
setColumns(newCols);
|
||||
setSelectedIndex(newCols.length);
|
||||
setIsScroll(true);
|
||||
setColumns(newCols);
|
||||
setSelectedIndex(newCols.length);
|
||||
setIsScroll(true);
|
||||
|
||||
// scroll to the first column
|
||||
vlistRef.current.scrollToIndex(newCols.length - 1, {
|
||||
align: "start",
|
||||
});
|
||||
}, 150);
|
||||
// scroll to the first column
|
||||
vlistRef.current.scrollToIndex(newCols.length - 1, {
|
||||
align: "start",
|
||||
});
|
||||
}, 150);
|
||||
|
||||
const updateName = useDebouncedCallback((label: string, title: string) => {
|
||||
const currentColIndex = columns.findIndex((col) => col.label === label);
|
||||
const updateName = useDebouncedCallback((label: string, title: string) => {
|
||||
const currentColIndex = columns.findIndex((col) => col.label === label);
|
||||
|
||||
const updatedCol = Object.assign({}, columns[currentColIndex]);
|
||||
updatedCol.name = title;
|
||||
const updatedCol = Object.assign({}, columns[currentColIndex]);
|
||||
updatedCol.name = title;
|
||||
|
||||
const newCols = columns.slice();
|
||||
newCols[currentColIndex] = updatedCol;
|
||||
const newCols = columns.slice();
|
||||
newCols[currentColIndex] = updatedCol;
|
||||
|
||||
setColumns(newCols);
|
||||
}, 150);
|
||||
setColumns(newCols);
|
||||
}, 150);
|
||||
|
||||
const startResize = useDebouncedCallback(
|
||||
() => setIsResize((prev) => !prev),
|
||||
150,
|
||||
);
|
||||
const startResize = useDebouncedCallback(
|
||||
() => setIsResize((prev) => !prev),
|
||||
150,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
// save state
|
||||
ark.set_columns(columns);
|
||||
}, [columns]);
|
||||
useEffect(() => {
|
||||
// save state
|
||||
ark.set_columns(columns);
|
||||
}, [columns]);
|
||||
|
||||
useEffect(() => {
|
||||
let unlistenColEvent: Awaited<ReturnType<typeof listen>> | undefined =
|
||||
undefined;
|
||||
let unlistenWindowResize: Awaited<ReturnType<typeof listen>> | undefined =
|
||||
undefined;
|
||||
useEffect(() => {
|
||||
let unlistenColEvent: Awaited<ReturnType<typeof listen>> | undefined =
|
||||
undefined;
|
||||
let unlistenWindowResize: Awaited<ReturnType<typeof listen>> | undefined =
|
||||
undefined;
|
||||
|
||||
(async () => {
|
||||
if (unlistenColEvent && unlistenWindowResize) return;
|
||||
(async () => {
|
||||
if (unlistenColEvent && unlistenWindowResize) return;
|
||||
|
||||
unlistenColEvent = await listen<EventColumns>("columns", (data) => {
|
||||
if (data.payload.type === "add") add(data.payload.column);
|
||||
if (data.payload.type === "remove") remove(data.payload.label);
|
||||
if (data.payload.type === "set_title")
|
||||
updateName(data.payload.label, data.payload.title);
|
||||
});
|
||||
unlistenColEvent = await listen<EventColumns>("columns", (data) => {
|
||||
if (data.payload.type === "add") add(data.payload.column);
|
||||
if (data.payload.type === "remove") remove(data.payload.label);
|
||||
if (data.payload.type === "set_title")
|
||||
updateName(data.payload.label, data.payload.title);
|
||||
});
|
||||
|
||||
unlistenWindowResize = await getCurrent().listen("tauri://resize", () => {
|
||||
startResize();
|
||||
});
|
||||
})();
|
||||
unlistenWindowResize = await getCurrent().listen("tauri://resize", () => {
|
||||
startResize();
|
||||
});
|
||||
})();
|
||||
|
||||
return () => {
|
||||
if (unlistenColEvent) unlistenColEvent();
|
||||
if (unlistenWindowResize) unlistenWindowResize();
|
||||
};
|
||||
}, []);
|
||||
return () => {
|
||||
if (unlistenColEvent) unlistenColEvent();
|
||||
if (unlistenWindowResize) unlistenWindowResize();
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="h-full w-full">
|
||||
<VList
|
||||
ref={vlistRef}
|
||||
horizontal
|
||||
tabIndex={-1}
|
||||
itemSize={440}
|
||||
overscan={3}
|
||||
onScroll={() => setIsScroll(true)}
|
||||
onScrollEnd={() => setIsScroll(false)}
|
||||
className="scrollbar-none h-full w-full overflow-x-auto focus:outline-none"
|
||||
>
|
||||
{columns.map((column) => (
|
||||
<Col
|
||||
key={column.label}
|
||||
column={column}
|
||||
account={account}
|
||||
isScroll={isScroll}
|
||||
isResize={isResize}
|
||||
/>
|
||||
))}
|
||||
</VList>
|
||||
<Toolbar>
|
||||
<div className="flex items-center gap-1">
|
||||
<button
|
||||
type="button"
|
||||
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"
|
||||
>
|
||||
<ArrowLeftIcon className="size-5" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
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"
|
||||
>
|
||||
<ArrowRightIcon className="size-5" />
|
||||
</button>
|
||||
</div>
|
||||
</Toolbar>
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<div className="h-full w-full">
|
||||
<VList
|
||||
ref={vlistRef}
|
||||
horizontal
|
||||
tabIndex={-1}
|
||||
itemSize={440}
|
||||
overscan={3}
|
||||
onScroll={() => setIsScroll(true)}
|
||||
onScrollEnd={() => setIsScroll(false)}
|
||||
className="scrollbar-none h-full w-full overflow-x-auto focus:outline-none"
|
||||
>
|
||||
{columns.map((column) => (
|
||||
<Col
|
||||
key={column.label}
|
||||
column={column}
|
||||
account={account}
|
||||
isScroll={isScroll}
|
||||
isResize={isResize}
|
||||
/>
|
||||
))}
|
||||
</VList>
|
||||
<Toolbar>
|
||||
<div className="flex items-center gap-1">
|
||||
<button
|
||||
type="button"
|
||||
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"
|
||||
>
|
||||
<ArrowLeftIcon className="size-5" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
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"
|
||||
>
|
||||
<ArrowRightIcon className="size-5" />
|
||||
</button>
|
||||
</div>
|
||||
</Toolbar>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -1,118 +1,183 @@
|
||||
import { BellIcon, ComposeFilledIcon, PlusIcon, SearchIcon } from "@lume/icons";
|
||||
import { Event, Kind } from "@lume/types";
|
||||
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 { UnlistenFn } from "@tauri-apps/api/event";
|
||||
import { getCurrent } from "@tauri-apps/api/window";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
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() {
|
||||
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">
|
||||
<div
|
||||
data-tauri-drag-region
|
||||
className={cn(
|
||||
"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 />
|
||||
<button
|
||||
type="button"
|
||||
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>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
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
|
||||
</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>
|
||||
<div className="flex-1">
|
||||
<Outlet />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<div className="flex h-screen w-screen flex-col">
|
||||
<div
|
||||
data-tauri-drag-region
|
||||
className={cn(
|
||||
"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 />
|
||||
<button
|
||||
type="button"
|
||||
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>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
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
|
||||
</button>
|
||||
<Bell />
|
||||
<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>
|
||||
<div className="flex-1">
|
||||
<Outlet />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function Accounts() {
|
||||
const navigate = Route.useNavigate();
|
||||
const { ark } = Route.useRouteContext();
|
||||
const { account } = Route.useParams();
|
||||
function Accounts() {
|
||||
const navigate = Route.useNavigate();
|
||||
const { ark, accounts } = Route.useRouteContext();
|
||||
const { account } = Route.useParams();
|
||||
|
||||
const [accounts, setAccounts] = useState<string[]>([]);
|
||||
const changeAccount = async (npub: string) => {
|
||||
if (npub === account) return;
|
||||
|
||||
const changeAccount = async (npub: string) => {
|
||||
if (npub === account) return;
|
||||
const select = await ark.load_selected_account(npub);
|
||||
if (select)
|
||||
return navigate({ to: "/$account/home", params: { account: npub } });
|
||||
};
|
||||
const select = await ark.load_selected_account(npub);
|
||||
|
||||
useEffect(() => {
|
||||
async function getAllAccounts() {
|
||||
const data = await ark.get_all_accounts();
|
||||
if (data) setAccounts(data);
|
||||
}
|
||||
if (select) {
|
||||
return navigate({ to: "/$account/home", params: { account: npub } });
|
||||
}
|
||||
};
|
||||
|
||||
getAllAccounts();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div data-tauri-drag-region className="flex items-center gap-3">
|
||||
{accounts.map((user) => (
|
||||
<button key={user} type="button" onClick={() => changeAccount(user)}>
|
||||
<User.Provider pubkey={user}>
|
||||
<User.Root
|
||||
className={cn(
|
||||
"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 === account ? "w-7" : "w-8",
|
||||
)}
|
||||
/>
|
||||
</User.Root>
|
||||
</User.Provider>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<div data-tauri-drag-region className="flex items-center gap-3">
|
||||
{accounts.map((user) => (
|
||||
<button key={user} type="button" onClick={() => changeAccount(user)}>
|
||||
<User.Provider pubkey={user}>
|
||||
<User.Root
|
||||
className={cn(
|
||||
"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 === account ? "w-7" : "w-8",
|
||||
)}
|
||||
/>
|
||||
</User.Root>
|
||||
</User.Provider>
|
||||
</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";
|
||||
|
||||
type EditorElement = {
|
||||
type: string;
|
||||
children: Descendant[];
|
||||
eventId?: string;
|
||||
type: string;
|
||||
children: Descendant[];
|
||||
eventId?: string;
|
||||
};
|
||||
|
||||
interface RouterContext {
|
||||
// System
|
||||
ark: Ark;
|
||||
queryClient: QueryClient;
|
||||
// App info
|
||||
platform?: Platform;
|
||||
locale?: string;
|
||||
// Settings
|
||||
settings?: Settings;
|
||||
interests?: Interests;
|
||||
// Profile
|
||||
accounts?: Account[];
|
||||
profile?: Metadata;
|
||||
// Editor
|
||||
initialValue?: EditorElement[];
|
||||
// System
|
||||
ark: Ark;
|
||||
queryClient: QueryClient;
|
||||
// App info
|
||||
platform?: Platform;
|
||||
locale?: string;
|
||||
// Settings
|
||||
settings?: Settings;
|
||||
interests?: Interests;
|
||||
// Profile
|
||||
accounts?: string[];
|
||||
profile?: Metadata;
|
||||
// Editor
|
||||
initialValue?: EditorElement[];
|
||||
}
|
||||
|
||||
export const Route = createRootRouteWithContext<RouterContext>()({
|
||||
component: () => <Outlet />,
|
||||
pendingComponent: Pending,
|
||||
wrapInSuspense: true,
|
||||
component: () => <Outlet />,
|
||||
pendingComponent: Pending,
|
||||
wrapInSuspense: true,
|
||||
});
|
||||
|
||||
function Pending() {
|
||||
return (
|
||||
<div className="flex h-screen w-screen flex-col items-center justify-center">
|
||||
<Spinner className="size-5" />
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<div className="flex h-screen w-screen flex-col items-center justify-center">
|
||||
<Spinner className="size-5" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -4,131 +4,115 @@ import { Link } from "@tanstack/react-router";
|
||||
import { createFileRoute, redirect } from "@tanstack/react-router";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
export const Route = createFileRoute("/")({
|
||||
beforeLoad: async ({ context }) => {
|
||||
const ark = context.ark;
|
||||
const accounts = await ark.get_all_accounts();
|
||||
beforeLoad: async ({ context }) => {
|
||||
const ark = context.ark;
|
||||
const accounts = await ark.get_all_accounts();
|
||||
|
||||
// Run notification service
|
||||
if (accounts.length > 0) {
|
||||
await invoke("run_notification", { accounts });
|
||||
}
|
||||
if (!accounts.length) {
|
||||
throw redirect({
|
||||
to: "/landing",
|
||||
replace: true,
|
||||
});
|
||||
}
|
||||
|
||||
switch (accounts.length) {
|
||||
// Guest account
|
||||
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);
|
||||
// Run notification service
|
||||
await invoke("run_notification", { accounts });
|
||||
|
||||
if (loadedAccount) {
|
||||
throw redirect({
|
||||
to: "/$account/home",
|
||||
params: { account },
|
||||
replace: true,
|
||||
});
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
// Account selection
|
||||
default:
|
||||
return { accounts };
|
||||
}
|
||||
},
|
||||
component: Screen,
|
||||
return { accounts };
|
||||
},
|
||||
component: Screen,
|
||||
});
|
||||
|
||||
function Screen() {
|
||||
const navigate = Route.useNavigate();
|
||||
const context = Route.useRouteContext();
|
||||
const navigate = Route.useNavigate();
|
||||
const { ark, accounts } = Route.useRouteContext();
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const select = async (npub: string) => {
|
||||
setLoading(true);
|
||||
const select = async (npub: string) => {
|
||||
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) {
|
||||
return navigate({
|
||||
to: "/$account/home",
|
||||
params: { account: npub },
|
||||
replace: true,
|
||||
});
|
||||
}
|
||||
};
|
||||
const currentDate = new Date().toLocaleString("default", {
|
||||
weekday: "long",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
});
|
||||
|
||||
const currentDate = new Date().toLocaleString("default", {
|
||||
weekday: "long",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="relative flex h-full w-full items-center justify-center">
|
||||
<div className="relative z-20 flex flex-col items-center gap-16">
|
||||
<div className="text-center text-white">
|
||||
<h2 className="mb-1 text-2xl">{currentDate}</h2>
|
||||
<h2 className="text-2xl font-semibold">Welcome back!</h2>
|
||||
</div>
|
||||
<div className="flex items-center justify-center gap-6">
|
||||
{loading ? (
|
||||
<div className="inline-flex size-6 items-center justify-center">
|
||||
<Spinner className="size-6" />
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{context.accounts.map((account) => (
|
||||
<button
|
||||
type="button"
|
||||
key={account}
|
||||
onClick={() => select(account)}
|
||||
>
|
||||
<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" />
|
||||
</User.Root>
|
||||
</User.Provider>
|
||||
</button>
|
||||
))}
|
||||
<Link to="/landing">
|
||||
<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">
|
||||
<div className="flex size-20 items-center justify-center rounded-full bg-white/20 dark:bg-black/20">
|
||||
<PlusIcon className="size-5" />
|
||||
</div>
|
||||
<p className="text-lg font-medium leading-tight">Add</p>
|
||||
</div>
|
||||
</Link>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="absolute z-10 h-full w-full bg-white/10 backdrop-blur-lg dark:bg-black/10" />
|
||||
<div className="absolute inset-0 h-full w-full">
|
||||
<img
|
||||
src="/lock-screen.jpg"
|
||||
srcSet="/lock-screen@2x.jpg 2x"
|
||||
alt="Lock Screen Background"
|
||||
className="h-full w-full object-cover"
|
||||
/>
|
||||
<a
|
||||
href="https://njump.me/nprofile1qqs9tuz9jpn57djg7nxunhyvuvk69g5zqaxdpvpqt9hwqv7395u9rpg6zq5uw"
|
||||
target="_blank"
|
||||
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"
|
||||
rel="noreferrer"
|
||||
>
|
||||
Design by NoGood
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<div className="relative flex h-full w-full items-center justify-center">
|
||||
<div className="relative z-20 flex flex-col items-center gap-16">
|
||||
<div className="text-center text-white">
|
||||
<h2 className="mb-1 text-2xl">{currentDate}</h2>
|
||||
<h2 className="text-2xl font-semibold">Welcome back!</h2>
|
||||
</div>
|
||||
<div className="flex items-center justify-center gap-6">
|
||||
{loading ? (
|
||||
<div className="inline-flex size-6 items-center justify-center">
|
||||
<Spinner className="size-6" />
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{accounts.map((account) => (
|
||||
<button
|
||||
type="button"
|
||||
key={account}
|
||||
onClick={() => select(account)}
|
||||
>
|
||||
<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" />
|
||||
</User.Root>
|
||||
</User.Provider>
|
||||
</button>
|
||||
))}
|
||||
<Link to="/landing">
|
||||
<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">
|
||||
<div className="flex size-20 items-center justify-center rounded-full bg-white/20 dark:bg-black/20">
|
||||
<PlusIcon className="size-5" />
|
||||
</div>
|
||||
<p className="text-lg font-medium leading-tight">Add</p>
|
||||
</div>
|
||||
</Link>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="absolute z-10 h-full w-full bg-white/10 backdrop-blur-lg dark:bg-black/10" />
|
||||
<div className="absolute inset-0 h-full w-full">
|
||||
<img
|
||||
src="/lock-screen.jpg"
|
||||
srcSet="/lock-screen@2x.jpg 2x"
|
||||
alt="Lock Screen Background"
|
||||
className="h-full w-full object-cover"
|
||||
/>
|
||||
<a
|
||||
href="https://njump.me/nprofile1qqs9tuz9jpn57djg7nxunhyvuvk69g5zqaxdpvpqt9hwqv7395u9rpg6zq5uw"
|
||||
target="_blank"
|
||||
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"
|
||||
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 {
|
||||
notification: boolean;
|
||||
enhancedPrivacy: boolean;
|
||||
autoUpdate: boolean;
|
||||
zap: boolean;
|
||||
nsfw: boolean;
|
||||
[key: string]: string | number | boolean;
|
||||
notification: boolean;
|
||||
enhancedPrivacy: boolean;
|
||||
autoUpdate: boolean;
|
||||
zap: boolean;
|
||||
nsfw: boolean;
|
||||
[key: string]: string | number | boolean;
|
||||
}
|
||||
|
||||
export interface Keys {
|
||||
npub: string;
|
||||
nsec: string;
|
||||
npub: string;
|
||||
nsec: string;
|
||||
}
|
||||
|
||||
export enum Kind {
|
||||
Metadata = 0,
|
||||
Text = 1,
|
||||
RecommendRelay = 2,
|
||||
Contacts = 3,
|
||||
Repost = 6,
|
||||
Reaction = 7,
|
||||
// NIP-89: App Metadata
|
||||
AppRecommendation = 31989,
|
||||
AppHandler = 31990,
|
||||
// #TODO: Add all nostr kinds
|
||||
Metadata = 0,
|
||||
Text = 1,
|
||||
RecommendRelay = 2,
|
||||
Contacts = 3,
|
||||
Repost = 6,
|
||||
Reaction = 7,
|
||||
ZapReceipt = 9735,
|
||||
// NIP-89: App Metadata
|
||||
AppRecommendation = 31989,
|
||||
AppHandler = 31990,
|
||||
// #TODO: Add all nostr kinds
|
||||
}
|
||||
|
||||
export interface Event {
|
||||
id: string;
|
||||
pubkey: string;
|
||||
created_at: number;
|
||||
kind: Kind;
|
||||
tags: string[][];
|
||||
content: string;
|
||||
sig: string;
|
||||
relay?: string;
|
||||
id: string;
|
||||
pubkey: string;
|
||||
created_at: number;
|
||||
kind: Kind;
|
||||
tags: string[][];
|
||||
content: string;
|
||||
sig: string;
|
||||
relay?: string;
|
||||
}
|
||||
|
||||
export interface EventWithReplies extends Event {
|
||||
replies: Array<Event>;
|
||||
replies: Array<Event>;
|
||||
}
|
||||
|
||||
export interface Metadata {
|
||||
name?: string;
|
||||
display_name?: string;
|
||||
about?: string;
|
||||
website?: string;
|
||||
picture?: string;
|
||||
banner?: string;
|
||||
nip05?: string;
|
||||
lud06?: string;
|
||||
lud16?: string;
|
||||
name?: string;
|
||||
display_name?: string;
|
||||
about?: string;
|
||||
website?: string;
|
||||
picture?: string;
|
||||
banner?: string;
|
||||
nip05?: string;
|
||||
lud06?: string;
|
||||
lud16?: string;
|
||||
}
|
||||
|
||||
export interface Contact {
|
||||
pubkey: string;
|
||||
profile: Metadata;
|
||||
pubkey: string;
|
||||
profile: Metadata;
|
||||
}
|
||||
|
||||
export interface Account {
|
||||
npub: string;
|
||||
nsec?: string;
|
||||
contacts?: string[];
|
||||
interests?: Interests;
|
||||
npub: string;
|
||||
nsec?: string;
|
||||
contacts?: string[];
|
||||
interests?: Interests;
|
||||
}
|
||||
|
||||
export interface Interests {
|
||||
hashtags: string[];
|
||||
users: string[];
|
||||
words: string[];
|
||||
hashtags: string[];
|
||||
users: string[];
|
||||
words: string[];
|
||||
}
|
||||
|
||||
export interface RichContent {
|
||||
parsed: string;
|
||||
images: string[];
|
||||
videos: string[];
|
||||
links: string[];
|
||||
notes: string[];
|
||||
parsed: string;
|
||||
images: string[];
|
||||
videos: string[];
|
||||
links: string[];
|
||||
notes: string[];
|
||||
}
|
||||
|
||||
export interface AppRouteSearch {
|
||||
account: string;
|
||||
account: string;
|
||||
}
|
||||
|
||||
export interface ColumnRouteSearch {
|
||||
account: string;
|
||||
label: string;
|
||||
name: string;
|
||||
redirect?: string;
|
||||
account: string;
|
||||
label: string;
|
||||
name: string;
|
||||
redirect?: string;
|
||||
}
|
||||
|
||||
export interface LumeColumn {
|
||||
label: string;
|
||||
name: string;
|
||||
content: URL | string;
|
||||
description?: string;
|
||||
author?: string;
|
||||
logo?: string;
|
||||
cover?: string;
|
||||
coverRetina?: string;
|
||||
featured?: boolean;
|
||||
label: string;
|
||||
name: string;
|
||||
content: URL | string;
|
||||
description?: string;
|
||||
author?: string;
|
||||
logo?: string;
|
||||
cover?: string;
|
||||
coverRetina?: string;
|
||||
featured?: boolean;
|
||||
}
|
||||
|
||||
export interface EventColumns {
|
||||
type: "add" | "remove" | "update" | "left" | "right" | "set_title";
|
||||
label?: string;
|
||||
title?: string;
|
||||
column?: LumeColumn;
|
||||
type: "add" | "remove" | "update" | "left" | "right" | "set_title";
|
||||
label?: string;
|
||||
title?: string;
|
||||
column?: LumeColumn;
|
||||
}
|
||||
|
||||
export interface Opengraph {
|
||||
url: string;
|
||||
title?: string;
|
||||
description?: string;
|
||||
image?: string;
|
||||
url: string;
|
||||
title?: string;
|
||||
description?: string;
|
||||
image?: string;
|
||||
}
|
||||
|
||||
export interface NostrBuildResponse {
|
||||
ok: boolean;
|
||||
data?: {
|
||||
message: string;
|
||||
status: string;
|
||||
data: Array<{
|
||||
blurhash: string;
|
||||
dimensions: {
|
||||
width: number;
|
||||
height: number;
|
||||
};
|
||||
mime: string;
|
||||
name: string;
|
||||
sha256: string;
|
||||
size: number;
|
||||
url: string;
|
||||
}>;
|
||||
};
|
||||
ok: boolean;
|
||||
data?: {
|
||||
message: string;
|
||||
status: string;
|
||||
data: Array<{
|
||||
blurhash: string;
|
||||
dimensions: {
|
||||
width: number;
|
||||
height: number;
|
||||
};
|
||||
mime: string;
|
||||
name: string;
|
||||
sha256: string;
|
||||
size: number;
|
||||
url: string;
|
||||
}>;
|
||||
};
|
||||
}
|
||||
|
||||
export interface NIP11 {
|
||||
name: string;
|
||||
description: string;
|
||||
pubkey: string;
|
||||
contact: string;
|
||||
supported_nips: number[];
|
||||
software: string;
|
||||
version: string;
|
||||
limitation: {
|
||||
[key: string]: string | number | boolean;
|
||||
};
|
||||
relay_countries: string[];
|
||||
language_tags: string[];
|
||||
tags: string[];
|
||||
posting_policy: string;
|
||||
payments_url: string;
|
||||
icon: string[];
|
||||
name: string;
|
||||
description: string;
|
||||
pubkey: string;
|
||||
contact: string;
|
||||
supported_nips: number[];
|
||||
software: string;
|
||||
version: string;
|
||||
limitation: {
|
||||
[key: string]: string | number | boolean;
|
||||
};
|
||||
relay_countries: string[];
|
||||
language_tags: string[];
|
||||
tags: string[];
|
||||
posting_policy: string;
|
||||
payments_url: string;
|
||||
icon: string[];
|
||||
}
|
||||
|
||||
export interface NIP05 {
|
||||
names: {
|
||||
[key: string]: string;
|
||||
};
|
||||
nip46: {
|
||||
[key: string]: {
|
||||
[key: string]: string[];
|
||||
};
|
||||
};
|
||||
names: {
|
||||
[key: string]: string;
|
||||
};
|
||||
nip46: {
|
||||
[key: string]: {
|
||||
[key: string]: string[];
|
||||
};
|
||||
};
|
||||
}
|
||||
|
@ -23,12 +23,7 @@ pub fn run_notification(accounts: Vec<String>, app: tauri::AppHandle) -> Result<
|
||||
.collect();
|
||||
let subscription = Filter::new()
|
||||
.pubkeys(pubkeys)
|
||||
.kinds(vec![
|
||||
Kind::TextNote,
|
||||
Kind::Repost,
|
||||
Kind::ZapReceipt,
|
||||
Kind::EncryptedDirectMessage,
|
||||
])
|
||||
.kinds(vec![Kind::TextNote, Kind::Repost, Kind::ZapReceipt])
|
||||
.since(Timestamp::now());
|
||||
let activity_id = SubscriptionId::new("activity");
|
||||
|
||||
@ -47,7 +42,8 @@ pub fn run_notification(accounts: Vec<String>, app: tauri::AppHandle) -> Result<
|
||||
} = notification
|
||||
{
|
||||
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)
|
||||
|
Loading…
Reference in New Issue
Block a user