feat: improve editor
This commit is contained in:
@@ -13,179 +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 }) => {
|
||||||
try {
|
try {
|
||||||
const ark = context.ark;
|
const ark = context.ark;
|
||||||
const resourcePath = await resolveResource(
|
const resourcePath = await resolveResource(
|
||||||
"resources/system_columns.json",
|
"resources/system_columns.json",
|
||||||
);
|
);
|
||||||
const systemColumns: LumeColumn[] = JSON.parse(
|
const systemColumns: LumeColumn[] = JSON.parse(
|
||||||
await readTextFile(resourcePath),
|
await readTextFile(resourcePath),
|
||||||
);
|
);
|
||||||
const userColumns = await ark.get_columns();
|
const userColumns = await ark.get_columns();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
storedColumns: !userColumns.length ? systemColumns : userColumns,
|
storedColumns: !userColumns.length ? systemColumns : userColumns,
|
||||||
};
|
};
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(String(e));
|
console.error(String(e));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
component: Screen,
|
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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,10 +2,10 @@ import { BellIcon, ComposeFilledIcon, PlusIcon, SearchIcon } from "@lume/icons";
|
|||||||
import { Event, Kind } from "@lume/types";
|
import { Event, Kind } from "@lume/types";
|
||||||
import { User } from "@lume/ui";
|
import { User } from "@lume/ui";
|
||||||
import {
|
import {
|
||||||
cn,
|
cn,
|
||||||
decodeZapInvoice,
|
decodeZapInvoice,
|
||||||
displayNpub,
|
displayNpub,
|
||||||
sendNativeNotification,
|
sendNativeNotification,
|
||||||
} from "@lume/utils";
|
} 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 { UnlistenFn } from "@tauri-apps/api/event";
|
||||||
@@ -13,171 +13,171 @@ import { getCurrent } from "@tauri-apps/api/window";
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
export const Route = createFileRoute("/$account")({
|
export const Route = createFileRoute("/$account")({
|
||||||
beforeLoad: async ({ context }) => {
|
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();
|
||||||
|
|
||||||
return { accounts };
|
return { accounts };
|
||||||
},
|
},
|
||||||
component: Screen,
|
component: Screen,
|
||||||
});
|
});
|
||||||
|
|
||||||
function Screen() {
|
function Screen() {
|
||||||
const { ark, platform } = Route.useRouteContext();
|
const { ark, platform } = Route.useRouteContext();
|
||||||
const navigate = Route.useNavigate();
|
const navigate = Route.useNavigate();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-screen w-screen flex-col">
|
<div className="flex h-screen w-screen flex-col">
|
||||||
<div
|
<div
|
||||||
data-tauri-drag-region
|
data-tauri-drag-region
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex h-11 shrink-0 items-center justify-between pr-2",
|
"flex h-11 shrink-0 items-center justify-between pr-2",
|
||||||
platform === "macos" ? "ml-2 pl-20" : "pl-4",
|
platform === "macos" ? "ml-2 pl-20" : "pl-4",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<Accounts />
|
<Accounts />
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => navigate({ to: "/landing" })}
|
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"
|
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" />
|
<PlusIcon className="size-5" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => ark.open_editor()}
|
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"
|
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" />
|
<ComposeFilledIcon className="size-4" />
|
||||||
New Post
|
New Post
|
||||||
</button>
|
</button>
|
||||||
<Bell />
|
<Bell />
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => ark.open_search()}
|
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"
|
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" />
|
<SearchIcon className="size-5" />
|
||||||
</button>
|
</button>
|
||||||
<div id="toolbar" />
|
<div id="toolbar" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function Accounts() {
|
function Accounts() {
|
||||||
const navigate = Route.useNavigate();
|
const navigate = Route.useNavigate();
|
||||||
const { ark, accounts } = Route.useRouteContext();
|
const { ark, accounts } = Route.useRouteContext();
|
||||||
const { account } = Route.useParams();
|
const { account } = Route.useParams();
|
||||||
|
|
||||||
const changeAccount = async (npub: string) => {
|
const changeAccount = async (npub: string) => {
|
||||||
if (npub === account) return;
|
if (npub === account) return;
|
||||||
|
|
||||||
const select = await ark.load_selected_account(npub);
|
const select = await ark.load_selected_account(npub);
|
||||||
|
|
||||||
if (select) {
|
if (select) {
|
||||||
return navigate({ to: "/$account/home", params: { account: npub } });
|
return navigate({ to: "/$account/home", params: { account: npub } });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div data-tauri-drag-region className="flex items-center gap-3">
|
<div data-tauri-drag-region className="flex items-center gap-3">
|
||||||
{accounts.map((user) => (
|
{accounts.map((user) => (
|
||||||
<button key={user} type="button" onClick={() => changeAccount(user)}>
|
<button key={user} type="button" onClick={() => changeAccount(user)}>
|
||||||
<User.Provider pubkey={user}>
|
<User.Provider pubkey={user}>
|
||||||
<User.Root
|
<User.Root
|
||||||
className={cn(
|
className={cn(
|
||||||
"rounded-full",
|
"rounded-full",
|
||||||
user === account
|
user === account
|
||||||
? "ring-1 ring-teal-500 ring-offset-2 ring-offset-neutral-200 dark:ring-offset-neutral-950"
|
? "ring-1 ring-teal-500 ring-offset-2 ring-offset-neutral-200 dark:ring-offset-neutral-950"
|
||||||
: "",
|
: "",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<User.Avatar
|
<User.Avatar
|
||||||
className={cn(
|
className={cn(
|
||||||
"aspect-square h-auto rounded-full object-cover",
|
"aspect-square h-auto rounded-full object-cover",
|
||||||
user === account ? "w-7" : "w-8",
|
user === account ? "w-7" : "w-8",
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</User.Root>
|
</User.Root>
|
||||||
</User.Provider>
|
</User.Provider>
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function Bell() {
|
function Bell() {
|
||||||
const { ark } = Route.useRouteContext();
|
const { ark } = Route.useRouteContext();
|
||||||
const { account } = Route.useParams();
|
const { account } = Route.useParams();
|
||||||
|
|
||||||
const [isRing, setIsRing] = useState(false);
|
const [isRing, setIsRing] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let unlisten: UnlistenFn = undefined;
|
let unlisten: UnlistenFn = undefined;
|
||||||
|
|
||||||
async function listenNotify() {
|
async function listenNotify() {
|
||||||
unlisten = await getCurrent().listen<string>(
|
unlisten = await getCurrent().listen<string>(
|
||||||
"activity",
|
"activity",
|
||||||
async (payload) => {
|
async (payload) => {
|
||||||
setIsRing(true);
|
setIsRing(true);
|
||||||
|
|
||||||
const event: Event = JSON.parse(payload.payload);
|
const event: Event = JSON.parse(payload.payload);
|
||||||
const user = await ark.get_profile(event.pubkey);
|
const user = await ark.get_profile(event.pubkey);
|
||||||
const userName =
|
const userName =
|
||||||
user.display_name || user.name || displayNpub(event.pubkey, 16);
|
user.display_name || user.name || displayNpub(event.pubkey, 16);
|
||||||
|
|
||||||
switch (event.kind) {
|
switch (event.kind) {
|
||||||
case Kind.Text: {
|
case Kind.Text: {
|
||||||
sendNativeNotification("Mentioned you in a note", userName);
|
sendNativeNotification("Mentioned you in a note", userName);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case Kind.Repost: {
|
case Kind.Repost: {
|
||||||
sendNativeNotification("Reposted your note", userName);
|
sendNativeNotification("Reposted your note", userName);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case Kind.ZapReceipt: {
|
case Kind.ZapReceipt: {
|
||||||
const amount = decodeZapInvoice(event.tags);
|
const amount = decodeZapInvoice(event.tags);
|
||||||
sendNativeNotification(
|
sendNativeNotification(
|
||||||
`Zapped ₿ ${amount.bitcoinFormatted}`,
|
`Zapped ₿ ${amount.bitcoinFormatted}`,
|
||||||
userName,
|
userName,
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!unlisten) listenNotify();
|
if (!unlisten) listenNotify();
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
if (unlisten) unlisten();
|
if (unlisten) unlisten();
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setIsRing(false);
|
setIsRing(false);
|
||||||
ark.open_activity(account);
|
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"
|
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" />
|
<BellIcon className="size-5" />
|
||||||
{isRing ? (
|
{isRing ? (
|
||||||
<span className="absolute right-0 top-0 block size-2 rounded-full bg-teal-500 ring-1 ring-black/5" />
|
<span className="absolute right-0 top-0 block size-2 rounded-full bg-teal-500 ring-1 ring-black/5" />
|
||||||
) : null}
|
) : null}
|
||||||
</button>
|
</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?: string[];
|
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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,9 +10,8 @@ import { useSlateStatic } from "slate-react";
|
|||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
|
||||||
export function MediaButton({ className }: { className?: string }) {
|
export function MediaButton({ className }: { className?: string }) {
|
||||||
const { ark } = useRouteContext({ strict: false });
|
|
||||||
const editor = useSlateStatic();
|
const editor = useSlateStatic();
|
||||||
|
const { ark } = useRouteContext({ strict: false });
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
const uploadToNostrBuild = async () => {
|
const uploadToNostrBuild = async () => {
|
||||||
|
|||||||
83
apps/desktop2/src/routes/editor/-components/mention.tsx
Normal file
83
apps/desktop2/src/routes/editor/-components/mention.tsx
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
import { MentionIcon } from "@lume/icons";
|
||||||
|
import { cn, insertMention } from "@lume/utils";
|
||||||
|
import * as Tooltip from "@radix-ui/react-tooltip";
|
||||||
|
import * as DropdownMenu from "@radix-ui/react-dropdown-menu";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { useRouteContext } from "@tanstack/react-router";
|
||||||
|
import { User } from "@lume/ui";
|
||||||
|
import { useSlateStatic } from "slate-react";
|
||||||
|
import type { Contact } from "@lume/types";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
|
||||||
|
export function MentionButton({ className }: { className?: string }) {
|
||||||
|
const editor = useSlateStatic();
|
||||||
|
const { ark } = useRouteContext({ strict: false });
|
||||||
|
const [contacts, setContacts] = useState<string[]>([]);
|
||||||
|
|
||||||
|
const select = async (user: string) => {
|
||||||
|
try {
|
||||||
|
const metadata = await ark.get_profile(user);
|
||||||
|
const contact: Contact = { pubkey: user, profile: metadata };
|
||||||
|
|
||||||
|
insertMention(editor, contact);
|
||||||
|
} catch (e) {
|
||||||
|
toast.error(String(e));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
async function getContacts() {
|
||||||
|
const data = await ark.get_contact_list();
|
||||||
|
setContacts(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
getContacts();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DropdownMenu.Root>
|
||||||
|
<Tooltip.Provider>
|
||||||
|
<Tooltip.Root delayDuration={150}>
|
||||||
|
<DropdownMenu.Trigger asChild>
|
||||||
|
<Tooltip.Trigger asChild>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={cn(
|
||||||
|
"inline-flex items-center justify-center",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<MentionIcon className="size-4" />
|
||||||
|
</button>
|
||||||
|
</Tooltip.Trigger>
|
||||||
|
</DropdownMenu.Trigger>
|
||||||
|
<Tooltip.Portal>
|
||||||
|
<Tooltip.Content className="inline-flex h-7 select-none items-center justify-center rounded-md bg-neutral-950 px-3.5 text-sm text-neutral-50 will-change-[transform,opacity] data-[state=delayed-open]:data-[side=bottom]:animate-slideUpAndFade data-[state=delayed-open]:data-[side=left]:animate-slideRightAndFade data-[state=delayed-open]:data-[side=right]:animate-slideLeftAndFade data-[state=delayed-open]:data-[side=top]:animate-slideDownAndFade dark:bg-neutral-50 dark:text-neutral-950">
|
||||||
|
Mention
|
||||||
|
<Tooltip.Arrow className="fill-neutral-950 dark:fill-neutral-50" />
|
||||||
|
</Tooltip.Content>
|
||||||
|
</Tooltip.Portal>
|
||||||
|
</Tooltip.Root>
|
||||||
|
</Tooltip.Provider>
|
||||||
|
<DropdownMenu.Portal>
|
||||||
|
<DropdownMenu.Content className="flex w-[220px] h-[220px] scrollbar-none flex-col overflow-y-auto rounded-xl bg-black py-1 shadow-md shadow-neutral-500/20 focus:outline-none dark:bg-white">
|
||||||
|
{contacts.map((contact) => (
|
||||||
|
<DropdownMenu.Item
|
||||||
|
key={contact}
|
||||||
|
onClick={() => select(contact)}
|
||||||
|
className="shrink-0 h-11 flex items-center hover:bg-white/10 px-2"
|
||||||
|
>
|
||||||
|
<User.Provider pubkey={contact}>
|
||||||
|
<User.Root className="flex items-center gap-2">
|
||||||
|
<User.Avatar className="shrink-0 size-8 rounded-full" />
|
||||||
|
<User.Name className="text-sm font-medium text-white dark:text-black" />
|
||||||
|
</User.Root>
|
||||||
|
</User.Provider>
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
))}
|
||||||
|
<DropdownMenu.Arrow className="fill-neutral-950 dark:fill-neutral-50" />
|
||||||
|
</DropdownMenu.Content>
|
||||||
|
</DropdownMenu.Portal>
|
||||||
|
</DropdownMenu.Root>
|
||||||
|
);
|
||||||
|
}
|
||||||
40
apps/desktop2/src/routes/editor/-components/pow.tsx
Normal file
40
apps/desktop2/src/routes/editor/-components/pow.tsx
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import { NsfwIcon } from "@lume/icons";
|
||||||
|
import { cn } from "@lume/utils";
|
||||||
|
import * as Tooltip from "@radix-ui/react-tooltip";
|
||||||
|
import type { Dispatch, SetStateAction } from "react";
|
||||||
|
|
||||||
|
export function PowToggle({
|
||||||
|
pow,
|
||||||
|
setPow,
|
||||||
|
className,
|
||||||
|
}: {
|
||||||
|
pow: boolean;
|
||||||
|
setPow: Dispatch<SetStateAction<boolean>>;
|
||||||
|
className?: string;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<Tooltip.Provider>
|
||||||
|
<Tooltip.Root delayDuration={150}>
|
||||||
|
<Tooltip.Trigger asChild>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setPow((prev) => !prev)}
|
||||||
|
className={cn(
|
||||||
|
"inline-flex items-center justify-center",
|
||||||
|
className,
|
||||||
|
pow ? "bg-blue-500 text-white" : "",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<NsfwIcon className="size-4" />
|
||||||
|
</button>
|
||||||
|
</Tooltip.Trigger>
|
||||||
|
<Tooltip.Portal>
|
||||||
|
<Tooltip.Content className="inline-flex h-7 select-none items-center justify-center rounded-md bg-neutral-950 px-3.5 text-sm text-neutral-50 will-change-[transform,opacity] data-[state=delayed-open]:data-[side=bottom]:animate-slideUpAndFade data-[state=delayed-open]:data-[side=left]:animate-slideRightAndFade data-[state=delayed-open]:data-[side=right]:animate-slideLeftAndFade data-[state=delayed-open]:data-[side=top]:animate-slideDownAndFade dark:bg-neutral-50 dark:text-neutral-950">
|
||||||
|
Proof of Work
|
||||||
|
<Tooltip.Arrow className="fill-neutral-950 dark:fill-neutral-50" />
|
||||||
|
</Tooltip.Content>
|
||||||
|
</Tooltip.Portal>
|
||||||
|
</Tooltip.Root>
|
||||||
|
</Tooltip.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,27 +1,18 @@
|
|||||||
import { ComposeFilledIcon, TrashIcon } from "@lume/icons";
|
import { ComposeFilledIcon, TrashIcon } from "@lume/icons";
|
||||||
import { Spinner, User } from "@lume/ui";
|
import { Spinner } from "@lume/ui";
|
||||||
import { MentionNote } from "@lume/ui/src/note/mentions/note";
|
import { MentionNote } from "@lume/ui/src/note/mentions/note";
|
||||||
import {
|
import {
|
||||||
Portal,
|
|
||||||
cn,
|
cn,
|
||||||
insertImage,
|
insertImage,
|
||||||
insertMention,
|
|
||||||
insertNostrEvent,
|
insertNostrEvent,
|
||||||
isImageUrl,
|
isImageUrl,
|
||||||
sendNativeNotification,
|
sendNativeNotification,
|
||||||
} from "@lume/utils";
|
} from "@lume/utils";
|
||||||
import { createFileRoute } from "@tanstack/react-router";
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
import { nip19 } from "nostr-tools";
|
import { nip19 } from "nostr-tools";
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import {
|
import { type Descendant, Node, Transforms, createEditor } from "slate";
|
||||||
type Descendant,
|
|
||||||
Editor,
|
|
||||||
Node,
|
|
||||||
Range,
|
|
||||||
Transforms,
|
|
||||||
createEditor,
|
|
||||||
} from "slate";
|
|
||||||
import {
|
import {
|
||||||
Editable,
|
Editable,
|
||||||
ReactEditor,
|
ReactEditor,
|
||||||
@@ -33,6 +24,7 @@ import {
|
|||||||
} from "slate-react";
|
} from "slate-react";
|
||||||
import { MediaButton } from "./-components/media";
|
import { MediaButton } from "./-components/media";
|
||||||
import { NsfwToggle } from "./-components/nsfw";
|
import { NsfwToggle } from "./-components/nsfw";
|
||||||
|
import { MentionButton } from "./-components/mention";
|
||||||
|
|
||||||
type EditorSearch = {
|
type EditorSearch = {
|
||||||
reply_to: string;
|
reply_to: string;
|
||||||
@@ -73,32 +65,20 @@ export const Route = createFileRoute("/editor/")({
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
component: Screen,
|
component: Screen,
|
||||||
pendingComponent: Pending,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
function Screen() {
|
function Screen() {
|
||||||
const ref = useRef<HTMLDivElement | null>();
|
|
||||||
const { reply_to, quote } = Route.useSearch();
|
const { reply_to, quote } = Route.useSearch();
|
||||||
const { ark, initialValue, contacts } = Route.useRouteContext();
|
const { ark, initialValue } = Route.useRouteContext();
|
||||||
|
|
||||||
const [t] = useTranslation();
|
const [t] = useTranslation();
|
||||||
const [editorValue, setEditorValue] = useState(initialValue);
|
const [editorValue, setEditorValue] = useState(initialValue);
|
||||||
const [target, setTarget] = useState<Range | undefined>();
|
|
||||||
const [index, setIndex] = useState(0);
|
|
||||||
const [search, setSearch] = useState("");
|
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [nsfw, setNsfw] = useState(false);
|
const [nsfw, setNsfw] = useState(false);
|
||||||
const [editor] = useState(() =>
|
const [editor] = useState(() =>
|
||||||
withMentions(withNostrEvent(withImages(withReact(createEditor())))),
|
withMentions(withNostrEvent(withImages(withReact(createEditor())))),
|
||||||
);
|
);
|
||||||
|
|
||||||
const filters =
|
|
||||||
contacts
|
|
||||||
?.filter((c) =>
|
|
||||||
c?.profile.name?.toLowerCase().startsWith(search.toLowerCase()),
|
|
||||||
)
|
|
||||||
?.slice(0, 5) ?? [];
|
|
||||||
|
|
||||||
const reset = () => {
|
const reset = () => {
|
||||||
// @ts-expect-error, backlog
|
// @ts-expect-error, backlog
|
||||||
editor.children = [{ type: "paragraph", children: [{ text: "" }] }];
|
editor.children = [{ type: "paragraph", children: [{ text: "" }] }];
|
||||||
@@ -138,11 +118,15 @@ function Screen() {
|
|||||||
const eventId = await ark.publish(content, reply_to, quote);
|
const eventId = await ark.publish(content, reply_to, quote);
|
||||||
|
|
||||||
if (eventId) {
|
if (eventId) {
|
||||||
await sendNativeNotification("You've publish new post successfully.");
|
await sendNativeNotification(
|
||||||
|
"Your note has been published successfully.",
|
||||||
|
"Lume",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// stop loading
|
// stop loading
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
|
|
||||||
// reset form
|
// reset form
|
||||||
reset();
|
reset();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -151,58 +135,20 @@ function Screen() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (target && filters.length > 0) {
|
|
||||||
const el = ref.current;
|
|
||||||
const domRange = ReactEditor.toDOMRange(editor, target);
|
|
||||||
const rect = domRange.getBoundingClientRect();
|
|
||||||
el.style.top = `${rect.top + window.scrollY + 24}px`;
|
|
||||||
el.style.left = `${rect.left + window.scrollX}px`;
|
|
||||||
}
|
|
||||||
}, [filters.length, editor, index, search, target]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-screen w-screen flex-col bg-gradient-to-tr from-neutral-200 to-neutral-100 dark:from-neutral-950 dark:to-neutral-900">
|
<div className="w-full h-full">
|
||||||
<Slate
|
<Slate editor={editor} initialValue={editorValue}>
|
||||||
editor={editor}
|
|
||||||
initialValue={editorValue}
|
|
||||||
onChange={() => {
|
|
||||||
const { selection } = editor;
|
|
||||||
|
|
||||||
if (selection && Range.isCollapsed(selection)) {
|
|
||||||
const [start] = Range.edges(selection);
|
|
||||||
const wordBefore = Editor.before(editor, start, { unit: "word" });
|
|
||||||
const before = wordBefore && Editor.before(editor, wordBefore);
|
|
||||||
const beforeRange = before && Editor.range(editor, before, start);
|
|
||||||
const beforeText =
|
|
||||||
beforeRange && Editor.string(editor, beforeRange);
|
|
||||||
const beforeMatch = beforeText?.match(/^@(\w+)$/);
|
|
||||||
const after = Editor.after(editor, start);
|
|
||||||
const afterRange = Editor.range(editor, start, after);
|
|
||||||
const afterText = Editor.string(editor, afterRange);
|
|
||||||
const afterMatch = afterText.match(/^(\s|$)/);
|
|
||||||
|
|
||||||
if (beforeMatch && afterMatch) {
|
|
||||||
setTarget(beforeRange);
|
|
||||||
setSearch(beforeMatch[1]);
|
|
||||||
setIndex(0);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setTarget(null);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div
|
<div
|
||||||
data-tauri-drag-region
|
data-tauri-drag-region
|
||||||
className="flex h-14 w-full shrink-0 items-center justify-end gap-2 px-2"
|
className="flex h-14 w-full shrink-0 items-center justify-end gap-2 px-2 border-b border-black/10 dark:border-white/10"
|
||||||
>
|
>
|
||||||
<NsfwToggle
|
<NsfwToggle
|
||||||
nsfw={nsfw}
|
nsfw={nsfw}
|
||||||
setNsfw={setNsfw}
|
setNsfw={setNsfw}
|
||||||
className="size-8 rounded-full bg-neutral-200 hover:bg-neutral-300 dark:bg-neutral-800 dark:hover:bg-neutral-700"
|
className="size-8 rounded-full bg-black/10 hover:bg-black/20 dark:bg-white/10 dark:hover:bg-white/20"
|
||||||
/>
|
/>
|
||||||
<MediaButton className="size-8 rounded-full bg-neutral-200 hover:bg-neutral-300 dark:bg-neutral-800 dark:hover:bg-neutral-700" />
|
<MentionButton className="size-8 rounded-full bg-black/10 hover:bg-black/20 dark:bg-white/10 dark:hover:bg-white/20" />
|
||||||
|
<MediaButton className="size-8 rounded-full bg-black/10 hover:bg-black/20 dark:bg-white/10 dark:hover:bg-white/20" />
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => publish()}
|
onClick={() => publish()}
|
||||||
@@ -216,53 +162,25 @@ function Screen() {
|
|||||||
{t("global.post")}
|
{t("global.post")}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex h-full min-h-0 w-full">
|
<div className="flex h-full w-full flex-1 flex-col">
|
||||||
<div className="flex h-full w-full flex-1 flex-col gap-2 px-2 pb-2">
|
{reply_to && !quote ? (
|
||||||
{reply_to && !quote ? <MentionNote eventId={reply_to} /> : null}
|
<div className="px-4 py-2">
|
||||||
<div className="h-full w-full flex-1 overflow-hidden overflow-y-auto rounded-xl bg-white p-5 shadow-[rgba(50,_50,_105,_0.15)_0px_2px_5px_0px,_rgba(0,_0,_0,_0.05)_0px_1px_1px_0px] dark:bg-black dark:shadow-none dark:ring-1 dark:ring-white/5">
|
<MentionNote eventId={reply_to} />
|
||||||
<Editable
|
|
||||||
key={JSON.stringify(editorValue)}
|
|
||||||
autoFocus={true}
|
|
||||||
autoCapitalize="none"
|
|
||||||
autoCorrect="none"
|
|
||||||
spellCheck={false}
|
|
||||||
renderElement={(props) => <Element {...props} />}
|
|
||||||
placeholder={
|
|
||||||
reply_to ? "Type your reply..." : t("editor.placeholder")
|
|
||||||
}
|
|
||||||
className="focus:outline-none"
|
|
||||||
/>
|
|
||||||
{target && filters.length > 0 && (
|
|
||||||
<Portal>
|
|
||||||
<div
|
|
||||||
ref={ref}
|
|
||||||
className="absolute left-[-9999px] top-[-9999px] z-10 w-[250px] rounded-xl border border-neutral-50 bg-white p-2 shadow-lg dark:border-neutral-900 dark:bg-neutral-950"
|
|
||||||
>
|
|
||||||
{filters.map((contact) => (
|
|
||||||
<button
|
|
||||||
key={contact.pubkey}
|
|
||||||
type="button"
|
|
||||||
onClick={() => {
|
|
||||||
Transforms.select(editor, target);
|
|
||||||
insertMention(editor, contact);
|
|
||||||
setTarget(null);
|
|
||||||
}}
|
|
||||||
className="flex w-full flex-col rounded-lg p-2 hover:bg-neutral-100 dark:hover:bg-neutral-900"
|
|
||||||
>
|
|
||||||
<User.Provider pubkey={contact.pubkey}>
|
|
||||||
<User.Root className="flex w-full items-center gap-2">
|
|
||||||
<User.Avatar className="size-7 shrink-0 rounded-full object-cover" />
|
|
||||||
<div className="flex w-full flex-col items-start">
|
|
||||||
<User.Name className="max-w-[8rem] truncate text-sm font-medium" />
|
|
||||||
</div>
|
|
||||||
</User.Root>
|
|
||||||
</User.Provider>
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</Portal>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
) : null}
|
||||||
|
<div className="overflow-y-auto p-4">
|
||||||
|
<Editable
|
||||||
|
key={JSON.stringify(editorValue)}
|
||||||
|
autoFocus={true}
|
||||||
|
autoCapitalize="none"
|
||||||
|
autoCorrect="none"
|
||||||
|
spellCheck={false}
|
||||||
|
renderElement={(props) => <Element {...props} />}
|
||||||
|
placeholder={
|
||||||
|
reply_to ? "Type your reply..." : t("editor.placeholder")
|
||||||
|
}
|
||||||
|
className="focus:outline-none"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Slate>
|
</Slate>
|
||||||
@@ -270,20 +188,6 @@ function Screen() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function Pending() {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
data-tauri-drag-region
|
|
||||||
className="flex h-full w-full items-center justify-center gap-2.5"
|
|
||||||
>
|
|
||||||
<button type="button" disabled>
|
|
||||||
<Spinner className="size-5" />
|
|
||||||
</button>
|
|
||||||
<p>Loading cache...</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const withNostrEvent = (editor: ReactEditor) => {
|
const withNostrEvent = (editor: ReactEditor) => {
|
||||||
const { insertData, isVoid } = editor;
|
const { insertData, isVoid } = editor;
|
||||||
|
|
||||||
@@ -429,7 +333,7 @@ const Element = (props) => {
|
|||||||
return <Event {...props} />;
|
return <Event {...props} />;
|
||||||
default:
|
default:
|
||||||
return (
|
return (
|
||||||
<p {...attributes} className="text-lg">
|
<p {...attributes} className="text-[15px]">
|
||||||
{children}
|
{children}
|
||||||
</p>
|
</p>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -7,112 +7,112 @@ import { useState } from "react";
|
|||||||
import { toast } from "sonner";
|
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();
|
||||||
|
|
||||||
if (!accounts.length) {
|
if (!accounts.length) {
|
||||||
throw redirect({
|
throw redirect({
|
||||||
to: "/landing",
|
to: "/landing",
|
||||||
replace: true,
|
replace: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Run notification service
|
// Run notification service
|
||||||
await invoke("run_notification", { accounts });
|
await invoke("run_notification", { accounts });
|
||||||
|
|
||||||
return { accounts };
|
return { accounts };
|
||||||
},
|
},
|
||||||
component: Screen,
|
component: Screen,
|
||||||
});
|
});
|
||||||
|
|
||||||
function Screen() {
|
function Screen() {
|
||||||
const navigate = Route.useNavigate();
|
const navigate = Route.useNavigate();
|
||||||
const { ark, accounts } = Route.useRouteContext();
|
const { ark, accounts } = Route.useRouteContext();
|
||||||
|
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
const select = async (npub: string) => {
|
const select = async (npub: string) => {
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
||||||
const loadAccount = await ark.load_selected_account(npub);
|
const loadAccount = await ark.load_selected_account(npub);
|
||||||
if (loadAccount) {
|
if (loadAccount) {
|
||||||
return navigate({
|
return navigate({
|
||||||
to: "/$account/home",
|
to: "/$account/home",
|
||||||
params: { account: npub },
|
params: { account: npub },
|
||||||
replace: true,
|
replace: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
toast.error(String(e));
|
toast.error(String(e));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const currentDate = new Date().toLocaleString("default", {
|
const currentDate = new Date().toLocaleString("default", {
|
||||||
weekday: "long",
|
weekday: "long",
|
||||||
month: "long",
|
month: "long",
|
||||||
day: "numeric",
|
day: "numeric",
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative flex h-full w-full items-center justify-center">
|
<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="relative z-20 flex flex-col items-center gap-16">
|
||||||
<div className="text-center text-white">
|
<div className="text-center text-white">
|
||||||
<h2 className="mb-1 text-2xl">{currentDate}</h2>
|
<h2 className="mb-1 text-2xl">{currentDate}</h2>
|
||||||
<h2 className="text-2xl font-semibold">Welcome back!</h2>
|
<h2 className="text-2xl font-semibold">Welcome back!</h2>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-center gap-6">
|
<div className="flex items-center justify-center gap-6">
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<div className="inline-flex size-6 items-center justify-center">
|
<div className="inline-flex size-6 items-center justify-center">
|
||||||
<Spinner className="size-6" />
|
<Spinner className="size-6 text-white" />
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{accounts.map((account) => (
|
{accounts.map((account) => (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
key={account}
|
key={account}
|
||||||
onClick={() => select(account)}
|
onClick={() => select(account)}
|
||||||
>
|
>
|
||||||
<User.Provider pubkey={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.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.Avatar className="size-20 rounded-full object-cover" />
|
||||||
<User.Name className="max-w-[5rem] truncate text-lg font-medium leading-tight text-white" />
|
<User.Name className="max-w-[5rem] truncate text-lg font-medium leading-tight text-white" />
|
||||||
</User.Root>
|
</User.Root>
|
||||||
</User.Provider>
|
</User.Provider>
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
<Link to="/landing">
|
<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 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">
|
<div className="flex size-20 items-center justify-center rounded-full bg-white/20 dark:bg-black/20">
|
||||||
<PlusIcon className="size-5" />
|
<PlusIcon className="size-5" />
|
||||||
</div>
|
</div>
|
||||||
<p className="text-lg font-medium leading-tight">Add</p>
|
<p className="text-lg font-medium leading-tight">Add</p>
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="absolute z-10 h-full w-full bg-white/10 backdrop-blur-lg dark:bg-black/10" />
|
<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">
|
<div className="absolute inset-0 h-full w-full">
|
||||||
<img
|
<img
|
||||||
src="/lock-screen.jpg"
|
src="/lock-screen.jpg"
|
||||||
srcSet="/lock-screen@2x.jpg 2x"
|
srcSet="/lock-screen@2x.jpg 2x"
|
||||||
alt="Lock Screen Background"
|
alt="Lock Screen Background"
|
||||||
className="h-full w-full object-cover"
|
className="h-full w-full object-cover"
|
||||||
/>
|
/>
|
||||||
<a
|
<a
|
||||||
href="https://njump.me/nprofile1qqs9tuz9jpn57djg7nxunhyvuvk69g5zqaxdpvpqt9hwqv7395u9rpg6zq5uw"
|
href="https://njump.me/nprofile1qqs9tuz9jpn57djg7nxunhyvuvk69g5zqaxdpvpqt9hwqv7395u9rpg6zq5uw"
|
||||||
target="_blank"
|
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"
|
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"
|
rel="noreferrer"
|
||||||
>
|
>
|
||||||
Design by NoGood
|
Design by NoGood
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -4,21 +4,13 @@ export function MentionIcon(
|
|||||||
props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>,
|
props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>,
|
||||||
) {
|
) {
|
||||||
return (
|
return (
|
||||||
<svg
|
<svg width="24" height="24" fill="none" viewBox="0 0 24 24" {...props}>
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
width="24"
|
|
||||||
height="24"
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<path
|
<path
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
strokeLinecap="round"
|
strokeLinecap="round"
|
||||||
strokeLinejoin="round"
|
|
||||||
strokeWidth="1.5"
|
strokeWidth="1.5"
|
||||||
d="M11.85 13.251c-3.719.065-6.427 2.567-7.18 5.915-.13.575.338 1.084.927 1.084h6.901m-.647-6.999l.147-.001c.353 0 .696.022 1.03.064m-1.177-.063a7.889 7.889 0 00-1.852.249m3.028-.186c.334.042.658.104.972.186m-.972-.186a7.475 7.475 0 011.972.524m3.25 1.412v3m0 0v3m0-3h-3m3 0h3m-5.5-11.75a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0z"
|
d="M16.868 19.867A9.25 9.25 0 1 1 21.25 12c0 1.98-.984 4.024-3.279 3.816a3.312 3.312 0 0 1-2.978-3.767l.53-3.646m-.585 4.077c-.308 2.188-2.109 3.744-4.023 3.474-1.914-.269-3.217-2.26-2.91-4.448.308-2.187 2.11-3.743 4.023-3.474 1.914.27 3.217 2.26 2.91 4.448Z"
|
||||||
></path>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
228
packages/types/index.d.ts
vendored
228
packages/types/index.d.ts
vendored
@@ -1,167 +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,
|
||||||
ZapReceipt = 9735,
|
ZapReceipt = 9735,
|
||||||
// NIP-89: App Metadata
|
// NIP-89: App Metadata
|
||||||
AppRecommendation = 31989,
|
AppRecommendation = 31989,
|
||||||
AppHandler = 31990,
|
AppHandler = 31990,
|
||||||
// #TODO: Add all nostr kinds
|
// #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,6 +23,7 @@ export function NoteReply({ large = false }: { large?: boolean }) {
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<ReplyIcon className="shrink-0 size-4" />
|
<ReplyIcon className="shrink-0 size-4" />
|
||||||
|
{large ? "Reply" : null}
|
||||||
</button>
|
</button>
|
||||||
</Tooltip.Trigger>
|
</Tooltip.Trigger>
|
||||||
<Tooltip.Portal>
|
<Tooltip.Portal>
|
||||||
|
|||||||
@@ -187,9 +187,9 @@ pub async fn publish(
|
|||||||
state: State<'_, Nostr>,
|
state: State<'_, Nostr>,
|
||||||
) -> Result<String, String> {
|
) -> Result<String, String> {
|
||||||
let client = &state.client;
|
let client = &state.client;
|
||||||
let final_tags = tags.into_iter().map(|val| Tag::parse(&val).unwrap());
|
let event_tags = tags.into_iter().map(|val| Tag::parse(&val).unwrap());
|
||||||
|
|
||||||
match client.publish_text_note(content, final_tags).await {
|
match client.publish_text_note(content, event_tags).await {
|
||||||
Ok(event_id) => Ok(event_id.to_bech32().unwrap()),
|
Ok(event_id) => Ok(event_id.to_bech32().unwrap()),
|
||||||
Err(err) => Err(err.to_string()),
|
Err(err) => Err(err.to_string()),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
use tauri::TitleBarStyle;
|
use tauri::TitleBarStyle;
|
||||||
use tauri::{Manager, Runtime, WebviewUrl, WebviewWindowBuilder};
|
use tauri::{
|
||||||
|
utils::config::WindowEffectsConfig, window::Effect, Manager, Runtime, WebviewUrl,
|
||||||
|
WebviewWindowBuilder,
|
||||||
|
};
|
||||||
use tauri_plugin_shell::ShellExt;
|
use tauri_plugin_shell::ShellExt;
|
||||||
|
|
||||||
pub fn create_tray<R: Runtime>(app: &tauri::AppHandle<R>) -> tauri::Result<()> {
|
pub fn create_tray<R: Runtime>(app: &tauri::AppHandle<R>) -> tauri::Result<()> {
|
||||||
@@ -60,18 +63,25 @@ pub fn create_tray<R: Runtime>(app: &tauri::AppHandle<R>) -> tauri::Result<()> {
|
|||||||
let _ =
|
let _ =
|
||||||
WebviewWindowBuilder::new(app, "editor-0", WebviewUrl::App(PathBuf::from("editor")))
|
WebviewWindowBuilder::new(app, "editor-0", WebviewUrl::App(PathBuf::from("editor")))
|
||||||
.title("Editor")
|
.title("Editor")
|
||||||
.min_inner_size(500., 400.)
|
.min_inner_size(560., 340.)
|
||||||
.inner_size(600., 400.)
|
.inner_size(560., 340.)
|
||||||
.hidden_title(true)
|
.hidden_title(true)
|
||||||
.title_bar_style(TitleBarStyle::Overlay)
|
.title_bar_style(TitleBarStyle::Overlay)
|
||||||
|
.transparent(true)
|
||||||
|
.effects(WindowEffectsConfig {
|
||||||
|
state: None,
|
||||||
|
effects: vec![Effect::WindowBackground],
|
||||||
|
radius: None,
|
||||||
|
color: None,
|
||||||
|
})
|
||||||
.build()
|
.build()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
#[cfg(not(target_os = "macos"))]
|
#[cfg(not(target_os = "macos"))]
|
||||||
let _ =
|
let _ =
|
||||||
WebviewWindowBuilder::new(app, "editor-0", WebviewUrl::App(PathBuf::from("editor")))
|
WebviewWindowBuilder::new(app, "editor-0", WebviewUrl::App(PathBuf::from("editor")))
|
||||||
.title("Editor")
|
.title("Editor")
|
||||||
.min_inner_size(500., 400.)
|
.min_inner_size(560., 340.)
|
||||||
.inner_size(600., 400.)
|
.inner_size(560., 340.)
|
||||||
.build()
|
.build()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
}
|
||||||
@@ -92,6 +102,13 @@ pub fn create_tray<R: Runtime>(app: &tauri::AppHandle<R>) -> tauri::Result<()> {
|
|||||||
.minimizable(false)
|
.minimizable(false)
|
||||||
.resizable(false)
|
.resizable(false)
|
||||||
.title_bar_style(TitleBarStyle::Overlay)
|
.title_bar_style(TitleBarStyle::Overlay)
|
||||||
|
.transparent(true)
|
||||||
|
.effects(WindowEffectsConfig {
|
||||||
|
state: None,
|
||||||
|
effects: vec![Effect::WindowBackground],
|
||||||
|
radius: None,
|
||||||
|
color: None,
|
||||||
|
})
|
||||||
.build()
|
.build()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
#[cfg(not(target_os = "macos"))]
|
#[cfg(not(target_os = "macos"))]
|
||||||
@@ -131,6 +148,13 @@ pub fn create_tray<R: Runtime>(app: &tauri::AppHandle<R>) -> tauri::Result<()> {
|
|||||||
.hidden_title(true)
|
.hidden_title(true)
|
||||||
.resizable(false)
|
.resizable(false)
|
||||||
.minimizable(false)
|
.minimizable(false)
|
||||||
|
.transparent(true)
|
||||||
|
.effects(WindowEffectsConfig {
|
||||||
|
state: None,
|
||||||
|
effects: vec![Effect::WindowBackground],
|
||||||
|
radius: None,
|
||||||
|
color: None,
|
||||||
|
})
|
||||||
.build()
|
.build()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
#[cfg(not(target_os = "macos"))]
|
#[cfg(not(target_os = "macos"))]
|
||||||
|
|||||||
Reference in New Issue
Block a user