mirror of
https://github.com/luminous-devs/lume.git
synced 2024-09-29 16:30:55 +00:00
feat: add editor screen
This commit is contained in:
parent
64286aa354
commit
84584a4d1f
@ -24,6 +24,8 @@
|
|||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-i18next": "^14.0.5",
|
"react-i18next": "^14.0.5",
|
||||||
|
"slate": "^0.101.5",
|
||||||
|
"slate-react": "^0.101.6",
|
||||||
"sonner": "^1.4.0",
|
"sonner": "^1.4.0",
|
||||||
"virtua": "^0.27.0"
|
"virtua": "^0.27.0"
|
||||||
},
|
},
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
BellFilledIcon,
|
BellFilledIcon,
|
||||||
BellIcon,
|
BellIcon,
|
||||||
|
EditIcon,
|
||||||
HomeFilledIcon,
|
HomeFilledIcon,
|
||||||
HomeIcon,
|
HomeIcon,
|
||||||
SpaceFilledIcon,
|
SpaceFilledIcon,
|
||||||
@ -10,12 +11,14 @@ import { Link } from "@tanstack/react-router";
|
|||||||
import { Outlet, createFileRoute } from "@tanstack/react-router";
|
import { Outlet, createFileRoute } from "@tanstack/react-router";
|
||||||
import { cn } from "@lume/utils";
|
import { cn } from "@lume/utils";
|
||||||
import { Accounts } from "@/components/accounts";
|
import { Accounts } from "@/components/accounts";
|
||||||
|
import { useArk } from "@lume/ark";
|
||||||
|
|
||||||
export const Route = createFileRoute("/app")({
|
export const Route = createFileRoute("/app")({
|
||||||
component: App,
|
component: App,
|
||||||
});
|
});
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
|
const ark = useArk();
|
||||||
const context = Route.useRouteContext();
|
const context = Route.useRouteContext();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -28,7 +31,17 @@ function App() {
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<Navigation />
|
<Navigation />
|
||||||
<Accounts />
|
<div className="flex items-center gap-3">
|
||||||
|
<Accounts />
|
||||||
|
<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"
|
||||||
|
>
|
||||||
|
<EditIcon className="size-4" />
|
||||||
|
New
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex h-full min-h-0 w-full">
|
<div className="flex h-full min-h-0 w-full">
|
||||||
<div className="h-full w-full flex-1 px-2 pb-2">
|
<div className="h-full w-full flex-1 px-2 pb-2">
|
||||||
@ -45,7 +58,7 @@ function Navigation() {
|
|||||||
data-tauri-drag-region
|
data-tauri-drag-region
|
||||||
className="flex h-full flex-1 items-center gap-2"
|
className="flex h-full flex-1 items-center gap-2"
|
||||||
>
|
>
|
||||||
<Link to="/app/home">
|
<Link to="/app/home/local">
|
||||||
{({ isActive }) => (
|
{({ isActive }) => (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
|
@ -29,7 +29,7 @@ function LocalTimeline() {
|
|||||||
return events;
|
return events;
|
||||||
},
|
},
|
||||||
getNextPageParam: (lastPage) => {
|
getNextPageParam: (lastPage) => {
|
||||||
const lastEvent = lastPage.at(-1);
|
const lastEvent = lastPage?.at(-1);
|
||||||
if (!lastEvent) return;
|
if (!lastEvent) return;
|
||||||
return lastEvent.created_at - 1;
|
return lastEvent.created_at - 1;
|
||||||
},
|
},
|
||||||
@ -49,8 +49,9 @@ function LocalTimeline() {
|
|||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<div className="flex h-20 w-full items-center justify-center">
|
<div className="flex h-20 w-full flex-col items-center justify-center gap-1">
|
||||||
<LoaderIcon className="size-5 animate-spin" />
|
<LoaderIcon className="size-5 animate-spin" />
|
||||||
|
<p>Loading...</p>
|
||||||
</div>
|
</div>
|
||||||
) : !data.length ? (
|
) : !data.length ? (
|
||||||
<div className="flex flex-col gap-3">
|
<div className="flex flex-col gap-3">
|
||||||
|
@ -26,7 +26,7 @@ function Create() {
|
|||||||
try {
|
try {
|
||||||
await ark.save_account(keys);
|
await ark.save_account(keys);
|
||||||
navigate({
|
navigate({
|
||||||
to: "/app/home",
|
to: "/app/home/local",
|
||||||
search: { onboarding: true },
|
search: { onboarding: true },
|
||||||
replace: true,
|
replace: true,
|
||||||
});
|
});
|
||||||
|
78
apps/desktop2/src/routes/editor/-components/media.tsx
Normal file
78
apps/desktop2/src/routes/editor/-components/media.tsx
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
import { useArk } from "@lume/ark";
|
||||||
|
import { AddMediaIcon, LoaderIcon } from "@lume/icons";
|
||||||
|
import { cn, insertImage, isImagePath } from "@lume/utils";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { useSlateStatic } from "slate-react";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { getCurrent } from "@tauri-apps/api/window";
|
||||||
|
import { UnlistenFn } from "@tauri-apps/api/event";
|
||||||
|
|
||||||
|
export function MediaButton({ className }: { className?: string }) {
|
||||||
|
const ark = useArk();
|
||||||
|
const editor = useSlateStatic();
|
||||||
|
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
const uploadToNostrBuild = async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
const image = await ark.upload();
|
||||||
|
|
||||||
|
if (image) {
|
||||||
|
insertImage(editor, image);
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(false);
|
||||||
|
} catch (e) {
|
||||||
|
setLoading(false);
|
||||||
|
toast.error(`Upload failed, error: ${e}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let unlisten: UnlistenFn = undefined;
|
||||||
|
|
||||||
|
async function listenFileDrop() {
|
||||||
|
const window = getCurrent();
|
||||||
|
if (!unlisten) {
|
||||||
|
unlisten = await window.listen("tauri://file-drop", async (event) => {
|
||||||
|
// @ts-ignore, lfg !!!
|
||||||
|
const items: string[] = event.payload.paths;
|
||||||
|
// start loading
|
||||||
|
setLoading(true);
|
||||||
|
// upload all images
|
||||||
|
for (const item of items) {
|
||||||
|
if (isImagePath(item)) {
|
||||||
|
const image = await ark.upload(item);
|
||||||
|
insertImage(editor, image);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// stop loading
|
||||||
|
setLoading(false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
listenFileDrop();
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (unlisten) unlisten();
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => uploadToNostrBuild()}
|
||||||
|
disabled={loading}
|
||||||
|
className={cn("inline-flex items-center justify-center", className)}
|
||||||
|
>
|
||||||
|
{loading ? (
|
||||||
|
<LoaderIcon className="size-5 animate-spin" />
|
||||||
|
) : (
|
||||||
|
<AddMediaIcon className="size-5" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
379
apps/desktop2/src/routes/editor/index.lazy.tsx
Normal file
379
apps/desktop2/src/routes/editor/index.lazy.tsx
Normal file
@ -0,0 +1,379 @@
|
|||||||
|
import { useArk } from "@lume/ark";
|
||||||
|
import { LoaderIcon, TrashIcon } from "@lume/icons";
|
||||||
|
import {
|
||||||
|
Portal,
|
||||||
|
cn,
|
||||||
|
insertImage,
|
||||||
|
insertMention,
|
||||||
|
insertNostrEvent,
|
||||||
|
isImagePath,
|
||||||
|
isImageUrl,
|
||||||
|
sendNativeNotification,
|
||||||
|
} from "@lume/utils";
|
||||||
|
import { createLazyFileRoute } from "@tanstack/react-router";
|
||||||
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { MediaButton } from "./-components/media";
|
||||||
|
import { MentionNote } from "@lume/ui/src/note/mentions/note";
|
||||||
|
import {
|
||||||
|
Descendant,
|
||||||
|
Editor,
|
||||||
|
Node,
|
||||||
|
Range,
|
||||||
|
Transforms,
|
||||||
|
createEditor,
|
||||||
|
} from "slate";
|
||||||
|
import {
|
||||||
|
ReactEditor,
|
||||||
|
useSlateStatic,
|
||||||
|
useSelected,
|
||||||
|
useFocused,
|
||||||
|
withReact,
|
||||||
|
Slate,
|
||||||
|
Editable,
|
||||||
|
} from "slate-react";
|
||||||
|
import { Contact } from "@lume/types";
|
||||||
|
import { User } from "@lume/ui";
|
||||||
|
|
||||||
|
export const Route = createLazyFileRoute("/editor/")({
|
||||||
|
component: Screen,
|
||||||
|
});
|
||||||
|
|
||||||
|
function Screen() {
|
||||||
|
const ark = useArk();
|
||||||
|
const ref = useRef<HTMLDivElement | null>();
|
||||||
|
|
||||||
|
const [t] = useTranslation();
|
||||||
|
const [editorValue, setEditorValue] = useState([
|
||||||
|
{
|
||||||
|
type: "paragraph",
|
||||||
|
children: [{ text: "" }],
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
const [contacts, setContacts] = useState<Contact[]>([]);
|
||||||
|
const [target, setTarget] = useState<Range | undefined>();
|
||||||
|
const [index, setIndex] = useState(0);
|
||||||
|
const [search, setSearch] = useState("");
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [editor] = useState(() =>
|
||||||
|
withMentions(withNostrEvent(withImages(withReact(createEditor())))),
|
||||||
|
);
|
||||||
|
|
||||||
|
const filters = contacts
|
||||||
|
?.filter((c) =>
|
||||||
|
c?.profile.name?.toLowerCase().startsWith(search.toLowerCase()),
|
||||||
|
)
|
||||||
|
?.slice(0, 10);
|
||||||
|
|
||||||
|
const reset = () => {
|
||||||
|
// @ts-expect-error, backlog
|
||||||
|
editor.children = [{ type: "paragraph", children: [{ text: "" }] }];
|
||||||
|
setEditorValue([{ type: "paragraph", children: [{ text: "" }] }]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const serialize = (nodes: Descendant[]) => {
|
||||||
|
return nodes
|
||||||
|
.map((n) => {
|
||||||
|
// @ts-expect-error, backlog
|
||||||
|
if (n.type === "image") return n.url;
|
||||||
|
// @ts-expect-error, backlog
|
||||||
|
if (n.type === "event") return n.eventId;
|
||||||
|
|
||||||
|
// @ts-expect-error, backlog
|
||||||
|
if (n.children.length) {
|
||||||
|
// @ts-expect-error, backlog
|
||||||
|
return n.children
|
||||||
|
.map((n) => {
|
||||||
|
if (n.type === "mention") return n.npub;
|
||||||
|
return Node.string(n).trim();
|
||||||
|
})
|
||||||
|
.join(" ");
|
||||||
|
}
|
||||||
|
|
||||||
|
return Node.string(n);
|
||||||
|
})
|
||||||
|
.join("\n");
|
||||||
|
};
|
||||||
|
|
||||||
|
const publish = async () => {
|
||||||
|
try {
|
||||||
|
// start loading
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
const content = serialize(editor.children);
|
||||||
|
const eventId = await ark.publish(content);
|
||||||
|
|
||||||
|
if (eventId) {
|
||||||
|
await sendNativeNotification("You've publish new post successfully.");
|
||||||
|
return reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
// stop loading
|
||||||
|
setLoading(false);
|
||||||
|
} catch (e) {
|
||||||
|
setLoading(false);
|
||||||
|
await sendNativeNotification(String(e));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<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">
|
||||||
|
<Slate
|
||||||
|
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
|
||||||
|
data-tauri-drag-region
|
||||||
|
className="flex h-16 w-full shrink-0 items-center justify-end gap-3 px-2"
|
||||||
|
>
|
||||||
|
<MediaButton className="size-9 rounded-full bg-neutral-100 hover:bg-neutral-200 dark:bg-neutral-900 dark:hover:bg-neutral-800" />
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={publish}
|
||||||
|
className="inline-flex h-9 w-24 items-center justify-center rounded-full bg-blue-500 px-3 text-sm font-medium text-white hover:bg-blue-600"
|
||||||
|
>
|
||||||
|
{loading ? (
|
||||||
|
<LoaderIcon className="size-5 animate-spin" />
|
||||||
|
) : (
|
||||||
|
t("global.post")
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="flex h-full min-h-0 w-full">
|
||||||
|
<div className="h-full w-full flex-1 px-2 pb-2">
|
||||||
|
<div className="h-full w-full 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">
|
||||||
|
<Editable
|
||||||
|
key={JSON.stringify(editorValue)}
|
||||||
|
autoFocus={true}
|
||||||
|
autoCapitalize="none"
|
||||||
|
autoCorrect="none"
|
||||||
|
spellCheck={false}
|
||||||
|
renderElement={(props) => <Element {...props} />}
|
||||||
|
placeholder={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.5">
|
||||||
|
<User.Avatar className="size-8 shrink-0 rounded-lg 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>
|
||||||
|
</div>
|
||||||
|
</Slate>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const withNostrEvent = (editor: ReactEditor) => {
|
||||||
|
const { insertData, isVoid } = editor;
|
||||||
|
|
||||||
|
editor.isVoid = (element) => {
|
||||||
|
// @ts-expect-error, wtf
|
||||||
|
return element.type === "event" ? true : isVoid(element);
|
||||||
|
};
|
||||||
|
|
||||||
|
editor.insertData = (data) => {
|
||||||
|
const text = data.getData("text/plain");
|
||||||
|
|
||||||
|
if (text.startsWith("nevent1") || text.startsWith("note1")) {
|
||||||
|
insertNostrEvent(editor, text);
|
||||||
|
} else {
|
||||||
|
insertData(data);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return editor;
|
||||||
|
};
|
||||||
|
|
||||||
|
const withMentions = (editor: ReactEditor) => {
|
||||||
|
const { isInline, isVoid, markableVoid } = editor;
|
||||||
|
|
||||||
|
editor.isInline = (element) => {
|
||||||
|
// @ts-expect-error, wtf
|
||||||
|
return element.type === "mention" ? true : isInline(element);
|
||||||
|
};
|
||||||
|
|
||||||
|
editor.isVoid = (element) => {
|
||||||
|
// @ts-expect-error, wtf
|
||||||
|
return element.type === "mention" ? true : isVoid(element);
|
||||||
|
};
|
||||||
|
|
||||||
|
editor.markableVoid = (element) => {
|
||||||
|
// @ts-expect-error, wtf
|
||||||
|
return element.type === "mention" || markableVoid(element);
|
||||||
|
};
|
||||||
|
|
||||||
|
return editor;
|
||||||
|
};
|
||||||
|
|
||||||
|
const withImages = (editor: ReactEditor) => {
|
||||||
|
const { insertData, isVoid } = editor;
|
||||||
|
|
||||||
|
editor.isVoid = (element) => {
|
||||||
|
// @ts-expect-error, wtf
|
||||||
|
return element.type === "image" ? true : isVoid(element);
|
||||||
|
};
|
||||||
|
|
||||||
|
editor.insertData = (data) => {
|
||||||
|
const text = data.getData("text/plain");
|
||||||
|
|
||||||
|
if (isImageUrl(text)) {
|
||||||
|
insertImage(editor, text);
|
||||||
|
} else {
|
||||||
|
insertData(data);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return editor;
|
||||||
|
};
|
||||||
|
|
||||||
|
const Image = ({ attributes, children, element }) => {
|
||||||
|
const editor = useSlateStatic();
|
||||||
|
const path = ReactEditor.findPath(editor as ReactEditor, element);
|
||||||
|
|
||||||
|
const selected = useSelected();
|
||||||
|
const focused = useFocused();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div {...attributes}>
|
||||||
|
{children}
|
||||||
|
<div contentEditable={false} className="relative my-2">
|
||||||
|
<img
|
||||||
|
src={element.url}
|
||||||
|
alt={element.url}
|
||||||
|
className={cn(
|
||||||
|
"h-auto w-full rounded-lg border border-neutral-100 object-cover ring-2 dark:border-neutral-900",
|
||||||
|
selected && focused ? "ring-blue-500" : "ring-transparent",
|
||||||
|
)}
|
||||||
|
contentEditable={false}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
contentEditable={false}
|
||||||
|
onClick={() => Transforms.removeNodes(editor, { at: path })}
|
||||||
|
className="absolute right-2 top-2 inline-flex size-8 items-center justify-center rounded-lg bg-red-500 text-white hover:bg-red-600"
|
||||||
|
>
|
||||||
|
<TrashIcon className="size-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const Mention = ({ attributes, element }) => {
|
||||||
|
const editor = useSlateStatic();
|
||||||
|
const path = ReactEditor.findPath(editor as ReactEditor, element);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
{...attributes}
|
||||||
|
type="button"
|
||||||
|
contentEditable={false}
|
||||||
|
onClick={() => Transforms.removeNodes(editor, { at: path })}
|
||||||
|
className="inline-block align-baseline text-blue-500 hover:text-blue-600"
|
||||||
|
>{`@${element.name}`}</span>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const Event = ({ attributes, element, children }) => {
|
||||||
|
const editor = useSlateStatic();
|
||||||
|
const path = ReactEditor.findPath(editor as ReactEditor, element);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div {...attributes}>
|
||||||
|
{children}
|
||||||
|
{/* biome-ignore lint/a11y/useKeyWithClickEvents: <explanation> */}
|
||||||
|
<div
|
||||||
|
contentEditable={false}
|
||||||
|
onClick={() => Transforms.removeNodes(editor, { at: path })}
|
||||||
|
className="user-select-none relative my-2"
|
||||||
|
>
|
||||||
|
<MentionNote
|
||||||
|
eventId={element.eventId.replace("nostr:", "")}
|
||||||
|
openable={false}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const Element = (props) => {
|
||||||
|
const { attributes, children, element } = props;
|
||||||
|
|
||||||
|
switch (element.type) {
|
||||||
|
case "image":
|
||||||
|
return <Image {...props} />;
|
||||||
|
case "mention":
|
||||||
|
return <Mention {...props} />;
|
||||||
|
case "event":
|
||||||
|
return <Event {...props} />;
|
||||||
|
default:
|
||||||
|
return (
|
||||||
|
<p {...attributes} className="text-lg">
|
||||||
|
{children}
|
||||||
|
</p>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
@ -4,6 +4,7 @@ import { cn } from "@lume/utils";
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { EventWithReplies } from "@lume/types";
|
import { EventWithReplies } from "@lume/types";
|
||||||
|
import { Reply } from "./reply";
|
||||||
|
|
||||||
export function ReplyList({
|
export function ReplyList({
|
||||||
eventId,
|
eventId,
|
||||||
|
@ -6,7 +6,6 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@getalby/sdk": "^3.3.0",
|
"@getalby/sdk": "^3.3.0",
|
||||||
"@lume/icons": "workspace:^",
|
"@lume/icons": "workspace:^",
|
||||||
"@lume/storage": "workspace:^",
|
|
||||||
"@lume/utils": "workspace:^",
|
"@lume/utils": "workspace:^",
|
||||||
"@radix-ui/react-avatar": "^1.0.4",
|
"@radix-ui/react-avatar": "^1.0.4",
|
||||||
"@radix-ui/react-collapsible": "^1.0.3",
|
"@radix-ui/react-collapsible": "^1.0.3",
|
||||||
|
@ -7,6 +7,9 @@ import type {
|
|||||||
} from "@lume/types";
|
} from "@lume/types";
|
||||||
import { invoke } from "@tauri-apps/api/core";
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
import { WebviewWindow } from "@tauri-apps/api/webview";
|
import { WebviewWindow } from "@tauri-apps/api/webview";
|
||||||
|
import { open } from "@tauri-apps/plugin-dialog";
|
||||||
|
import { readFile } from "@tauri-apps/plugin-fs";
|
||||||
|
import { generateContentTags } from "@lume/utils";
|
||||||
|
|
||||||
export class Ark {
|
export class Ark {
|
||||||
public account: Account;
|
public account: Account;
|
||||||
@ -147,17 +150,27 @@ export class Ark {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return nostrEvents.sort((a, b) => b.created_at - a.created_at);
|
return nostrEvents.sort((a, b) => b.created_at - a.created_at);
|
||||||
} catch (e) {
|
} catch {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async publish(content: string) {
|
public async publish(content: string) {
|
||||||
try {
|
try {
|
||||||
const cmd: string = await invoke("publish", { content });
|
const g = await generateContentTags(content);
|
||||||
|
|
||||||
|
const eventContent = g.content;
|
||||||
|
const eventTags = g.tags;
|
||||||
|
|
||||||
|
const cmd: string = await invoke("publish", {
|
||||||
|
content: eventContent,
|
||||||
|
tags: eventTags,
|
||||||
|
});
|
||||||
|
|
||||||
return cmd;
|
return cmd;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(String(e));
|
console.error(String(e));
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -341,6 +354,61 @@ export class Ark {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async upload(filePath?: string) {
|
||||||
|
try {
|
||||||
|
const allowExts = [
|
||||||
|
"png",
|
||||||
|
"jpeg",
|
||||||
|
"jpg",
|
||||||
|
"gif",
|
||||||
|
"mp4",
|
||||||
|
"mp3",
|
||||||
|
"webm",
|
||||||
|
"mkv",
|
||||||
|
"avi",
|
||||||
|
"mov",
|
||||||
|
];
|
||||||
|
|
||||||
|
let selected =
|
||||||
|
filePath ||
|
||||||
|
(
|
||||||
|
await open({
|
||||||
|
multiple: false,
|
||||||
|
filters: [
|
||||||
|
{
|
||||||
|
name: "Media",
|
||||||
|
extensions: allowExts,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
).path;
|
||||||
|
|
||||||
|
if (!selected) return null;
|
||||||
|
|
||||||
|
const file = await readFile(selected);
|
||||||
|
const blob = new Blob([file]);
|
||||||
|
|
||||||
|
const data = new FormData();
|
||||||
|
data.append("fileToUpload", blob);
|
||||||
|
data.append("submit", "Upload Image");
|
||||||
|
|
||||||
|
const res = await fetch("https://nostr.build/api/v2/upload/files", {
|
||||||
|
method: "POST",
|
||||||
|
body: data,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) return null;
|
||||||
|
|
||||||
|
const json = await res.json();
|
||||||
|
const content = json.data[0];
|
||||||
|
|
||||||
|
return content.url as string;
|
||||||
|
} catch (e) {
|
||||||
|
console.error(String(e));
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public open_thread(id: string) {
|
public open_thread(id: string) {
|
||||||
return new WebviewWindow(`event-${id}`, {
|
return new WebviewWindow(`event-${id}`, {
|
||||||
title: "Thread",
|
title: "Thread",
|
||||||
@ -364,4 +432,17 @@ export class Ark {
|
|||||||
titleBarStyle: "overlay",
|
titleBarStyle: "overlay",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public open_editor() {
|
||||||
|
return new WebviewWindow("editor", {
|
||||||
|
title: "Editor",
|
||||||
|
url: "/editor",
|
||||||
|
minWidth: 500,
|
||||||
|
width: 600,
|
||||||
|
height: 400,
|
||||||
|
hiddenTitle: true,
|
||||||
|
titleBarStyle: "overlay",
|
||||||
|
fileDropEnabled: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,7 +6,6 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@lume/ark": "workspace:^",
|
"@lume/ark": "workspace:^",
|
||||||
"@lume/icons": "workspace:^",
|
"@lume/icons": "workspace:^",
|
||||||
"@lume/storage": "workspace:^",
|
|
||||||
"@lume/ui": "workspace:^",
|
"@lume/ui": "workspace:^",
|
||||||
"@lume/utils": "workspace:^",
|
"@lume/utils": "workspace:^",
|
||||||
"@tanstack/react-query": "^5.20.5",
|
"@tanstack/react-query": "^5.20.5",
|
||||||
|
@ -1,25 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "@lume/storage",
|
|
||||||
"version": "0.0.0",
|
|
||||||
"main": "./src/index.ts",
|
|
||||||
"private": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"publishConfig": {
|
|
||||||
"access": "public"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"@tauri-apps/plugin-store": "2.0.0-beta.0",
|
|
||||||
"react": "^18.2.0",
|
|
||||||
"scheduler": "^0.23.0",
|
|
||||||
"use-context-selector": "^1.4.1",
|
|
||||||
"virtua": "^0.27.0",
|
|
||||||
"zustand": "^4.5.0"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@lume/tsconfig": "workspace:*",
|
|
||||||
"@lume/types": "workspace:*",
|
|
||||||
"@lume/utils": "workspace:^",
|
|
||||||
"@types/react": "^18.2.55",
|
|
||||||
"typescript": "^5.3.3"
|
|
||||||
}
|
|
||||||
}
|
|
@ -1 +0,0 @@
|
|||||||
export * from "./provider";
|
|
@ -1,118 +0,0 @@
|
|||||||
import { LumeColumn } from "@lume/types";
|
|
||||||
import { locale, platform } from "@tauri-apps/plugin-os";
|
|
||||||
import { Store } from "@tauri-apps/plugin-store";
|
|
||||||
import {
|
|
||||||
MutableRefObject,
|
|
||||||
PropsWithChildren,
|
|
||||||
useCallback,
|
|
||||||
useRef,
|
|
||||||
useState,
|
|
||||||
} from "react";
|
|
||||||
import { createContext, useContextSelector } from "use-context-selector";
|
|
||||||
import { type VListHandle } from "virtua";
|
|
||||||
import { LumeStorage } from "./storage";
|
|
||||||
|
|
||||||
const platformName = await platform();
|
|
||||||
const osLocale = (await locale()).slice(0, 2);
|
|
||||||
|
|
||||||
const store = new Store("lume.dat");
|
|
||||||
const storage = new LumeStorage(store, platformName, osLocale);
|
|
||||||
await storage.init();
|
|
||||||
|
|
||||||
type StorageContext = {
|
|
||||||
storage: LumeStorage;
|
|
||||||
column: {
|
|
||||||
columns: LumeColumn[];
|
|
||||||
vlistRef: MutableRefObject<VListHandle>;
|
|
||||||
create: (column: LumeColumn) => void;
|
|
||||||
remove: (id: number) => void;
|
|
||||||
move: (id: number, position: "left" | "right") => void;
|
|
||||||
update: (id: number, title: string, content: string) => void;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const StorageContext = createContext<StorageContext>(null);
|
|
||||||
|
|
||||||
export const StorageProvider = ({ children }: PropsWithChildren<object>) => {
|
|
||||||
const vlistRef = useRef<VListHandle>(null);
|
|
||||||
|
|
||||||
const [columns, setColumns] = useState<LumeColumn[]>([
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
title: "Newsfeed",
|
|
||||||
content: "",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
title: "For You",
|
|
||||||
content: "",
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
const create = useCallback((column: LumeColumn) => {
|
|
||||||
setColumns((prev) => [...prev, column]);
|
|
||||||
vlistRef?.current.scrollToIndex(columns.length);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const remove = useCallback((id: number) => {
|
|
||||||
setColumns((prev) => prev.filter((t) => t.id !== id));
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const update = useCallback(
|
|
||||||
(id: number, title: string, content: string) => {
|
|
||||||
const newCols = columns.map((col) => {
|
|
||||||
if (col.id === id) {
|
|
||||||
return { ...col, title, content };
|
|
||||||
}
|
|
||||||
return col;
|
|
||||||
});
|
|
||||||
|
|
||||||
setColumns(newCols);
|
|
||||||
},
|
|
||||||
[columns],
|
|
||||||
);
|
|
||||||
|
|
||||||
const move = useCallback(
|
|
||||||
(id: number, position: "left" | "right") => {
|
|
||||||
const newCols = [...columns];
|
|
||||||
|
|
||||||
const col = newCols.find((el) => el.id === id);
|
|
||||||
const colIndex = newCols.findIndex((el) => el.id === id);
|
|
||||||
|
|
||||||
newCols.splice(colIndex, 1);
|
|
||||||
|
|
||||||
if (position === "left") newCols.splice(colIndex - 1, 0, col);
|
|
||||||
if (position === "right") newCols.splice(colIndex + 1, 0, col);
|
|
||||||
|
|
||||||
setColumns(newCols);
|
|
||||||
},
|
|
||||||
[columns],
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<StorageContext.Provider
|
|
||||||
value={{
|
|
||||||
storage,
|
|
||||||
column: { columns, vlistRef, create, remove, move, update },
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</StorageContext.Provider>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useStorage = () => {
|
|
||||||
const context = useContextSelector(StorageContext, (state) => state.storage);
|
|
||||||
if (context === undefined) {
|
|
||||||
throw new Error("Storage Provider is required");
|
|
||||||
}
|
|
||||||
return context;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useColumn = () => {
|
|
||||||
const context = useContextSelector(StorageContext, (state) => state.column);
|
|
||||||
if (context === undefined) {
|
|
||||||
throw new Error("Storage Provider is required");
|
|
||||||
}
|
|
||||||
return context;
|
|
||||||
};
|
|
@ -1,55 +0,0 @@
|
|||||||
import { Settings } from "@lume/types";
|
|
||||||
import { Platform } from "@tauri-apps/plugin-os";
|
|
||||||
import { Store } from "@tauri-apps/plugin-store";
|
|
||||||
|
|
||||||
export class LumeStorage {
|
|
||||||
#store: Store;
|
|
||||||
readonly platform: Platform;
|
|
||||||
readonly locale: string;
|
|
||||||
public settings: Settings;
|
|
||||||
|
|
||||||
constructor(store: Store, platform: Platform, locale: string) {
|
|
||||||
this.#store = store;
|
|
||||||
this.locale = locale;
|
|
||||||
this.platform = platform;
|
|
||||||
this.settings = {
|
|
||||||
autoupdate: false,
|
|
||||||
nsecbunker: false,
|
|
||||||
media: true,
|
|
||||||
hashtag: true,
|
|
||||||
lowPower: false,
|
|
||||||
translation: false,
|
|
||||||
translateApiKey: "",
|
|
||||||
instantZap: false,
|
|
||||||
defaultZapAmount: 21,
|
|
||||||
nwc: "",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public async init() {
|
|
||||||
this.loadSettings();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async loadSettings() {
|
|
||||||
const data = await this.#store.get("settings");
|
|
||||||
if (!data) return;
|
|
||||||
|
|
||||||
const settings = JSON.parse(data as string) as Settings;
|
|
||||||
|
|
||||||
if (Object.keys(settings).length) {
|
|
||||||
for (const [key, value] of Object.entries(settings)) {
|
|
||||||
this.settings[key] = value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async createSetting(key: string, value: string | number | boolean) {
|
|
||||||
this.settings[key] = value;
|
|
||||||
|
|
||||||
const settings: Settings = JSON.parse(await this.#store.get("settings"));
|
|
||||||
const newSettings = { ...settings, key: value };
|
|
||||||
|
|
||||||
await this.#store.set("settings", newSettings);
|
|
||||||
await this.#store.save();
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,7 +0,0 @@
|
|||||||
{
|
|
||||||
"extends": "@lume/tsconfig/base.json",
|
|
||||||
"compilerOptions": {
|
|
||||||
"outDir": "dist"
|
|
||||||
},
|
|
||||||
"exclude": ["node_modules", "dist"]
|
|
||||||
}
|
|
5
packages/types/index.d.ts
vendored
5
packages/types/index.d.ts
vendored
@ -54,6 +54,11 @@ export interface Metadata {
|
|||||||
lud16?: string;
|
lud16?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface Contact {
|
||||||
|
pubkey: string;
|
||||||
|
profile: Metadata;
|
||||||
|
}
|
||||||
|
|
||||||
export interface Account {
|
export interface Account {
|
||||||
npub: string;
|
npub: string;
|
||||||
contacts?: string[];
|
contacts?: string[];
|
||||||
|
@ -7,7 +7,6 @@
|
|||||||
"@getalby/sdk": "^3.3.0",
|
"@getalby/sdk": "^3.3.0",
|
||||||
"@lume/ark": "workspace:^",
|
"@lume/ark": "workspace:^",
|
||||||
"@lume/icons": "workspace:^",
|
"@lume/icons": "workspace:^",
|
||||||
"@lume/storage": "workspace:^",
|
|
||||||
"@lume/utils": "workspace:^",
|
"@lume/utils": "workspace:^",
|
||||||
"@nostr-dev-kit/ndk": "^2.4.1",
|
"@nostr-dev-kit/ndk": "^2.4.1",
|
||||||
"@radix-ui/react-accordion": "^1.1.2",
|
"@radix-ui/react-accordion": "^1.1.2",
|
||||||
|
@ -1,89 +1,80 @@
|
|||||||
import {
|
import {
|
||||||
ChevronDownIcon,
|
ChevronDownIcon,
|
||||||
MoveLeftIcon,
|
MoveLeftIcon,
|
||||||
MoveRightIcon,
|
MoveRightIcon,
|
||||||
RefreshIcon,
|
RefreshIcon,
|
||||||
TrashIcon,
|
TrashIcon,
|
||||||
} from "@lume/icons";
|
} from "@lume/icons";
|
||||||
import { useColumn } from "@lume/storage";
|
|
||||||
import * as DropdownMenu from "@radix-ui/react-dropdown-menu";
|
import * as DropdownMenu from "@radix-ui/react-dropdown-menu";
|
||||||
import { useQueryClient } from "@tanstack/react-query";
|
import { useQueryClient } from "@tanstack/react-query";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useColumnContext } from "./provider";
|
|
||||||
|
|
||||||
export function ColumnHeader({
|
export function ColumnHeader({ queryKey }: { queryKey?: string[] }) {
|
||||||
queryKey,
|
const { t } = useTranslation();
|
||||||
}: {
|
const queryClient = useQueryClient();
|
||||||
queryKey?: string[];
|
|
||||||
}) {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const { move, remove } = useColumn();
|
|
||||||
|
|
||||||
const column = useColumnContext();
|
const refresh = async () => {
|
||||||
const queryClient = useQueryClient();
|
if (queryKey) await queryClient.refetchQueries({ queryKey });
|
||||||
|
};
|
||||||
|
|
||||||
const refresh = async () => {
|
return (
|
||||||
if (queryKey) await queryClient.refetchQueries({ queryKey });
|
<DropdownMenu.Root>
|
||||||
};
|
<div className="flex h-11 w-full shrink-0 items-center justify-center gap-2 border-b border-neutral-100 px-3 dark:border-neutral-900">
|
||||||
|
<DropdownMenu.Trigger asChild>
|
||||||
return (
|
<div className="inline-flex items-center gap-1.5">
|
||||||
<DropdownMenu.Root>
|
<div className="text-[13px] font-medium">{column.title}</div>
|
||||||
<div className="flex items-center justify-center gap-2 px-3 w-full border-b h-11 shrink-0 border-neutral-100 dark:border-neutral-900">
|
<ChevronDownIcon className="size-5" />
|
||||||
<DropdownMenu.Trigger asChild>
|
</div>
|
||||||
<div className="inline-flex items-center gap-1.5">
|
</DropdownMenu.Trigger>
|
||||||
<div className="text-[13px] font-medium">{column.title}</div>
|
<DropdownMenu.Portal>
|
||||||
<ChevronDownIcon className="size-5" />
|
<DropdownMenu.Content
|
||||||
</div>
|
sideOffset={5}
|
||||||
</DropdownMenu.Trigger>
|
className="flex w-[200px] flex-col overflow-hidden rounded-2xl bg-white/50 p-2 ring-1 ring-black/10 backdrop-blur-2xl focus:outline-none dark:bg-black/50 dark:ring-white/10"
|
||||||
<DropdownMenu.Portal>
|
>
|
||||||
<DropdownMenu.Content
|
<DropdownMenu.Item asChild>
|
||||||
sideOffset={5}
|
<button
|
||||||
className="flex w-[200px] p-2 flex-col overflow-hidden rounded-2xl bg-white/50 dark:bg-black/50 ring-1 ring-black/10 dark:ring-white/10 backdrop-blur-2xl focus:outline-none"
|
type="button"
|
||||||
>
|
onClick={refresh}
|
||||||
<DropdownMenu.Item asChild>
|
className="inline-flex h-9 items-center gap-3 rounded-lg px-3 text-sm font-medium text-black/70 hover:bg-black/10 hover:text-black focus:outline-none dark:text-white/70 dark:hover:bg-white/10 dark:hover:text-white"
|
||||||
<button
|
>
|
||||||
type="button"
|
<RefreshIcon className="size-4" />
|
||||||
onClick={refresh}
|
{t("global.refresh")}
|
||||||
className="inline-flex items-center gap-3 px-3 text-sm font-medium rounded-lg h-9 text-black/70 hover:bg-black/10 hover:text-black focus:outline-none dark:text-white/70 dark:hover:bg-white/10 dark:hover:text-white"
|
</button>
|
||||||
>
|
</DropdownMenu.Item>
|
||||||
<RefreshIcon className="size-4" />
|
<DropdownMenu.Item asChild>
|
||||||
{t("global.refresh")}
|
<button
|
||||||
</button>
|
type="button"
|
||||||
</DropdownMenu.Item>
|
onClick={() => move(column.id, "left")}
|
||||||
<DropdownMenu.Item asChild>
|
className="inline-flex h-9 items-center gap-3 rounded-lg px-3 text-sm font-medium text-black/70 hover:bg-black/10 hover:text-black focus:outline-none dark:text-white/70 dark:hover:bg-white/10 dark:hover:text-white"
|
||||||
<button
|
>
|
||||||
type="button"
|
<MoveLeftIcon className="size-4" />
|
||||||
onClick={() => move(column.id, "left")}
|
{t("global.moveLeft")}
|
||||||
className="inline-flex items-center gap-3 px-3 text-sm font-medium rounded-lg h-9 text-black/70 hover:bg-black/10 hover:text-black focus:outline-none dark:text-white/70 dark:hover:bg-white/10 dark:hover:text-white"
|
</button>
|
||||||
>
|
</DropdownMenu.Item>
|
||||||
<MoveLeftIcon className="size-4" />
|
<DropdownMenu.Item asChild>
|
||||||
{t("global.moveLeft")}
|
<button
|
||||||
</button>
|
type="button"
|
||||||
</DropdownMenu.Item>
|
onClick={() => move(column.id, "right")}
|
||||||
<DropdownMenu.Item asChild>
|
className="inline-flex h-9 items-center gap-3 rounded-lg px-3 text-sm font-medium text-black/70 hover:bg-black/10 hover:text-black focus:outline-none dark:text-white/70 dark:hover:bg-white/10 dark:hover:text-white"
|
||||||
<button
|
>
|
||||||
type="button"
|
<MoveRightIcon className="size-4" />
|
||||||
onClick={() => move(column.id, "right")}
|
{t("global.moveRight")}
|
||||||
className="inline-flex items-center gap-3 px-3 text-sm font-medium rounded-lg h-9 text-black/70 hover:bg-black/10 hover:text-black focus:outline-none dark:text-white/70 dark:hover:bg-white/10 dark:hover:text-white"
|
</button>
|
||||||
>
|
</DropdownMenu.Item>
|
||||||
<MoveRightIcon className="size-4" />
|
<DropdownMenu.Separator className="my-1 h-px bg-black/10 dark:bg-white/10" />
|
||||||
{t("global.moveRight")}
|
<DropdownMenu.Item asChild>
|
||||||
</button>
|
<button
|
||||||
</DropdownMenu.Item>
|
type="button"
|
||||||
<DropdownMenu.Separator className="h-px my-1 bg-black/10 dark:bg-white/10" />
|
onClick={() => remove(column.id)}
|
||||||
<DropdownMenu.Item asChild>
|
className="inline-flex h-9 items-center gap-3 rounded-lg px-3 text-sm font-medium text-red-500 hover:bg-red-500 hover:text-red-50 focus:outline-none"
|
||||||
<button
|
>
|
||||||
type="button"
|
<TrashIcon className="size-4" />
|
||||||
onClick={() => remove(column.id)}
|
{t("global.delete")}
|
||||||
className="inline-flex items-center gap-3 px-3 text-sm font-medium text-red-500 rounded-lg h-9 hover:bg-red-500 hover:text-red-50 focus:outline-none"
|
</button>
|
||||||
>
|
</DropdownMenu.Item>
|
||||||
<TrashIcon className="size-4" />
|
</DropdownMenu.Content>
|
||||||
{t("global.delete")}
|
</DropdownMenu.Portal>
|
||||||
</button>
|
</div>
|
||||||
</DropdownMenu.Item>
|
</DropdownMenu.Root>
|
||||||
</DropdownMenu.Content>
|
);
|
||||||
</DropdownMenu.Portal>
|
|
||||||
</div>
|
|
||||||
</DropdownMenu.Root>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,4 @@
|
|||||||
import { useArk } from "@lume/ark";
|
|
||||||
import { LoaderIcon, TrashIcon } from "@lume/icons";
|
import { LoaderIcon, TrashIcon } from "@lume/icons";
|
||||||
import { useStorage } from "@lume/storage";
|
|
||||||
import { cn, editorValueAtom } from "@lume/utils";
|
import { cn, editorValueAtom } from "@lume/utils";
|
||||||
import { invoke } from "@tauri-apps/api/core";
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
import { useAtom } from "jotai";
|
import { useAtom } from "jotai";
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import { NDKCacheUserProfile } from "@lume/types";
|
|
||||||
import { ReactNode } from "react";
|
import { ReactNode } from "react";
|
||||||
import ReactDOM from "react-dom";
|
import ReactDOM from "react-dom";
|
||||||
import { BaseEditor, Transforms } from "slate";
|
import { BaseEditor, Transforms } from "slate";
|
||||||
|
@ -21,12 +21,10 @@ export function NoteChild({
|
|||||||
const richContent = useMemo(() => {
|
const richContent = useMemo(() => {
|
||||||
if (!data) return "";
|
if (!data) return "";
|
||||||
|
|
||||||
let parsedContent: string | ReactNode[] = data.content.replace(
|
let parsedContent: string | ReactNode[] =
|
||||||
/\n+/g,
|
data.content.substring(0, 160) + "...";
|
||||||
"\n",
|
|
||||||
);
|
|
||||||
|
|
||||||
const text = parsedContent as string;
|
const text = data.content;
|
||||||
const words = text.split(/( |\n)/);
|
const words = text.split(/( |\n)/);
|
||||||
|
|
||||||
const hashtags = words.filter((word) => word.startsWith("#"));
|
const hashtags = words.filter((word) => word.startsWith("#"));
|
||||||
@ -104,7 +102,7 @@ export function NoteChild({
|
|||||||
<div className="relative flex gap-3">
|
<div className="relative flex gap-3">
|
||||||
<div className="relative flex-1 rounded-md bg-neutral-200 px-2 py-2 dark:bg-neutral-800">
|
<div className="relative flex-1 rounded-md bg-neutral-200 px-2 py-2 dark:bg-neutral-800">
|
||||||
<div className="absolute right-0 top-[18px] h-3 w-3 -translate-y-1/2 translate-x-1/2 rotate-45 transform bg-neutral-200 dark:bg-neutral-800" />
|
<div className="absolute right-0 top-[18px] h-3 w-3 -translate-y-1/2 translate-x-1/2 rotate-45 transform bg-neutral-200 dark:bg-neutral-800" />
|
||||||
<div className="content-break mt-6 line-clamp-3 select-text leading-normal text-neutral-900 dark:text-neutral-100">
|
<div className="content-break mt-6 select-text leading-normal text-neutral-900 dark:text-neutral-100">
|
||||||
{richContent}
|
{richContent}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -103,9 +103,9 @@ export function MentionNote({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="my-1.5 flex w-full cursor-default flex-col rounded-xl bg-neutral-100 pt-1 ring-1 ring-black/5 dark:bg-neutral-900 dark:ring-white/5">
|
<div className="my-1 flex w-full cursor-default flex-col rounded-xl bg-neutral-100 px-3 pt-1 ring-1 ring-black/5 dark:bg-neutral-900 dark:ring-white/5">
|
||||||
<User.Provider pubkey={data.pubkey}>
|
<User.Provider pubkey={data.pubkey}>
|
||||||
<User.Root className="flex h-10 items-center gap-2 px-3">
|
<User.Root className="flex h-10 items-center gap-2">
|
||||||
<User.Avatar className="size-6 shrink-0 rounded-full object-cover" />
|
<User.Avatar className="size-6 shrink-0 rounded-full object-cover" />
|
||||||
<div className="inline-flex flex-1 items-center gap-2">
|
<div className="inline-flex flex-1 items-center gap-2">
|
||||||
<User.Name className="font-semibold text-neutral-900 dark:text-neutral-100" />
|
<User.Name className="font-semibold text-neutral-900 dark:text-neutral-100" />
|
||||||
@ -117,11 +117,11 @@ export function MentionNote({
|
|||||||
</div>
|
</div>
|
||||||
</User.Root>
|
</User.Root>
|
||||||
</User.Provider>
|
</User.Provider>
|
||||||
<div className="line-clamp-4 select-text whitespace-normal text-balance px-3 leading-normal">
|
<div className="line-clamp-4 select-text whitespace-normal text-balance leading-normal">
|
||||||
{richContent}
|
{richContent}
|
||||||
</div>
|
</div>
|
||||||
{openable ? (
|
{openable ? (
|
||||||
<div className="flex h-10 items-center justify-between px-3">
|
<div className="flex h-10 items-center justify-between">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => ark.open_thread(data.id)}
|
onClick={() => ark.open_thread(data.id)}
|
||||||
|
@ -37,7 +37,7 @@ export function ImagePreview({ url }: { url: string }) {
|
|||||||
// biome-ignore lint/a11y/useKeyWithClickEvents: <explanation>
|
// biome-ignore lint/a11y/useKeyWithClickEvents: <explanation>
|
||||||
<div
|
<div
|
||||||
onClick={open}
|
onClick={open}
|
||||||
className="group relative my-1.5 rounded-xl ring-1 ring-black/5 dark:ring-white/5"
|
className="group relative my-1 rounded-xl ring-1 ring-black/5 dark:ring-white/5"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
src={url}
|
src={url}
|
||||||
|
@ -54,7 +54,7 @@ export function LinkPreview({ url }: { url: string }) {
|
|||||||
href={url}
|
href={url}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
className="my-1.5 flex w-full flex-col overflow-hidden rounded-xl bg-neutral-100 ring-1 ring-black/5 dark:bg-neutral-900 dark:ring-white/5"
|
className="my-1 flex w-full flex-col overflow-hidden rounded-xl bg-neutral-100 ring-1 ring-black/5 dark:bg-neutral-900 dark:ring-white/5"
|
||||||
>
|
>
|
||||||
{isImage(data.image) ? (
|
{isImage(data.image) ? (
|
||||||
<img
|
<img
|
||||||
|
@ -9,7 +9,7 @@ import {
|
|||||||
|
|
||||||
export function VideoPreview({ url }: { url: string }) {
|
export function VideoPreview({ url }: { url: string }) {
|
||||||
return (
|
return (
|
||||||
<div className="my-1.5 w-full overflow-hidden rounded-xl ring-1 ring-black/5 dark:ring-white/5">
|
<div className="my-1 w-full overflow-hidden rounded-xl ring-1 ring-black/5 dark:ring-white/5">
|
||||||
<MediaController>
|
<MediaController>
|
||||||
<video
|
<video
|
||||||
slot="media"
|
slot="media"
|
||||||
|
@ -1,55 +0,0 @@
|
|||||||
import { useArk } from "@lume/ark";
|
|
||||||
import { LoaderIcon } from "@lume/icons";
|
|
||||||
import { cn } from "@lume/utils";
|
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
import { ReplyForm } from "./editor/replyForm";
|
|
||||||
import { Reply } from "./note/primitives/reply";
|
|
||||||
import { EventWithReplies } from "@lume/types";
|
|
||||||
|
|
||||||
export function ReplyList({
|
|
||||||
eventId,
|
|
||||||
className,
|
|
||||||
}: {
|
|
||||||
eventId: string;
|
|
||||||
className?: string;
|
|
||||||
}) {
|
|
||||||
const ark = useArk();
|
|
||||||
|
|
||||||
const [t] = useTranslation();
|
|
||||||
const [data, setData] = useState<null | EventWithReplies[]>(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
async function getReplies() {
|
|
||||||
const events = await ark.get_event_thread(eventId);
|
|
||||||
setData(events);
|
|
||||||
}
|
|
||||||
getReplies();
|
|
||||||
}, [eventId]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
"flex flex-col divide-y divide-neutral-100 dark:divide-neutral-900",
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{!data ? (
|
|
||||||
<div className="mt-4 flex h-16 items-center justify-center p-3">
|
|
||||||
<LoaderIcon className="h-5 w-5 animate-spin" />
|
|
||||||
</div>
|
|
||||||
) : data.length === 0 ? (
|
|
||||||
<div className="mt-4 flex w-full items-center justify-center">
|
|
||||||
<div className="flex flex-col items-center justify-center gap-2 py-6">
|
|
||||||
<h3 className="text-3xl">👋</h3>
|
|
||||||
<p className="leading-none text-neutral-600 dark:text-neutral-400">
|
|
||||||
{t("note.reply.empty")}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
data.map((event) => <Reply key={event.id} event={event} />)
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,7 +1,6 @@
|
|||||||
import { ArrowLeftIcon, ArrowRightIcon } from "@lume/icons";
|
import { ArrowLeftIcon, ArrowRightIcon } from "@lume/icons";
|
||||||
import { useNavigate, useParams } from "react-router-dom";
|
import { useNavigate, useParams } from "react-router-dom";
|
||||||
import { WindowVirtualizer } from "virtua";
|
import { WindowVirtualizer } from "virtua";
|
||||||
import { ReplyList } from "../replyList";
|
|
||||||
import { ThreadNote } from "../note/primitives/thread";
|
import { ThreadNote } from "../note/primitives/thread";
|
||||||
|
|
||||||
export function EventRoute() {
|
export function EventRoute() {
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
export * from "./src/constants";
|
export * from "./src/constants";
|
||||||
export * from "./src/delay";
|
export * from "./src/delay";
|
||||||
export * from "./src/formater";
|
export * from "./src/formater";
|
||||||
|
export * from "./src/editor";
|
||||||
|
export * from "./src/nip01";
|
||||||
export * from "./src/nip94";
|
export * from "./src/nip94";
|
||||||
export * from "./src/notification";
|
export * from "./src/notification";
|
||||||
export * from "./src/hooks/useNetworkStatus";
|
export * from "./src/hooks/useNetworkStatus";
|
||||||
|
@ -13,12 +13,16 @@
|
|||||||
"dayjs": "^1.11.10",
|
"dayjs": "^1.11.10",
|
||||||
"jotai": "^2.6.4",
|
"jotai": "^2.6.4",
|
||||||
"nostr-tools": "^2.1.9",
|
"nostr-tools": "^2.1.9",
|
||||||
"react": "^18.2.0"
|
"react": "^18.2.0",
|
||||||
|
"react-dom": "^18.2.0",
|
||||||
|
"slate": "^0.101.5",
|
||||||
|
"slate-react": "^0.101.6"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@lume/tsconfig": "workspace:^",
|
"@lume/tsconfig": "workspace:^",
|
||||||
"@lume/types": "workspace:^",
|
"@lume/types": "workspace:^",
|
||||||
"@types/react": "^18.2.55",
|
"@types/react": "^18.2.55",
|
||||||
|
"@types/react-dom": "^18.2.19",
|
||||||
"tailwind-merge": "^2.2.1",
|
"tailwind-merge": "^2.2.1",
|
||||||
"typescript": "^5.3.3"
|
"typescript": "^5.3.3"
|
||||||
}
|
}
|
||||||
|
97
packages/utils/src/editor.ts
Normal file
97
packages/utils/src/editor.ts
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
import ReactDOM from "react-dom";
|
||||||
|
import { ReactNode } from "react";
|
||||||
|
import { BaseEditor, Transforms } from "slate";
|
||||||
|
import { ReactEditor } from "slate-react";
|
||||||
|
import { Contact } from "@lume/types";
|
||||||
|
|
||||||
|
export const Portal = ({ children }: { children?: ReactNode }) => {
|
||||||
|
return typeof document === "object"
|
||||||
|
? ReactDOM.createPortal(children, document.body)
|
||||||
|
: null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const isImagePath = (path: string) => {
|
||||||
|
for (const suffix of ["jpg", "jpeg", "gif", "png", "webp", "avif", "tiff"]) {
|
||||||
|
if (path.endsWith(suffix)) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const isImageUrl = (url: string) => {
|
||||||
|
try {
|
||||||
|
if (!url) return false;
|
||||||
|
const ext = new URL(url).pathname.split(".").pop();
|
||||||
|
return ["jpg", "jpeg", "gif", "png", "webp", "avif", "tiff"].includes(ext);
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const insertImage = (editor: ReactEditor | BaseEditor, url: string) => {
|
||||||
|
const text = { text: "" };
|
||||||
|
const image = [
|
||||||
|
{
|
||||||
|
type: "image",
|
||||||
|
url,
|
||||||
|
children: [text],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const extraText = [
|
||||||
|
{
|
||||||
|
type: "paragraph",
|
||||||
|
children: [text],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// @ts-ignore, idk
|
||||||
|
ReactEditor.focus(editor);
|
||||||
|
Transforms.insertNodes(editor, image);
|
||||||
|
Transforms.insertNodes(editor, extraText);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const insertMention = (
|
||||||
|
editor: ReactEditor | BaseEditor,
|
||||||
|
contact: Contact,
|
||||||
|
) => {
|
||||||
|
const text = { text: "" };
|
||||||
|
const mention = {
|
||||||
|
type: "mention",
|
||||||
|
npub: `nostr:${contact.pubkey}`,
|
||||||
|
name: contact.profile.name || contact.profile.display_name || "anon",
|
||||||
|
children: [text],
|
||||||
|
};
|
||||||
|
const extraText = [
|
||||||
|
{
|
||||||
|
type: "paragraph",
|
||||||
|
children: [text],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// @ts-ignore, idk
|
||||||
|
ReactEditor.focus(editor);
|
||||||
|
Transforms.insertNodes(editor, mention);
|
||||||
|
Transforms.insertNodes(editor, extraText);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const insertNostrEvent = (
|
||||||
|
editor: ReactEditor | BaseEditor,
|
||||||
|
eventId: string,
|
||||||
|
) => {
|
||||||
|
const text = { text: "" };
|
||||||
|
const event = [
|
||||||
|
{
|
||||||
|
type: "event",
|
||||||
|
eventId: `nostr:${eventId}`,
|
||||||
|
children: [text],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const extraText = [
|
||||||
|
{
|
||||||
|
type: "paragraph",
|
||||||
|
children: [text],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
Transforms.insertNodes(editor, event);
|
||||||
|
Transforms.insertNodes(editor, extraText);
|
||||||
|
};
|
96
packages/utils/src/nip01.ts
Normal file
96
packages/utils/src/nip01.ts
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
import { nip19 } from "nostr-tools";
|
||||||
|
import { EventPointer, ProfilePointer } from "nostr-tools/lib/types/nip19";
|
||||||
|
|
||||||
|
// Borrow from NDK
|
||||||
|
// https://github.com/nostr-dev-kit/ndk/blob/master/ndk/src/events/content-tagger.ts
|
||||||
|
export async function generateContentTags(content: string) {
|
||||||
|
let promises: Promise<void>[] = [];
|
||||||
|
let tags: string[][] = [];
|
||||||
|
|
||||||
|
const tagRegex = /(@|nostr:)(npub|nprofile|note|nevent|naddr)[a-zA-Z0-9]+/g;
|
||||||
|
const hashtagRegex = /#(\w+)/g;
|
||||||
|
|
||||||
|
const addTagIfNew = (t: string[]) => {
|
||||||
|
if (!tags.find((t2) => t2[0] === t[0] && t2[1] === t[1])) {
|
||||||
|
tags.push(t);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
content = content.replace(tagRegex, (tag) => {
|
||||||
|
try {
|
||||||
|
const entity = tag.split(/(@|nostr:)/)[2];
|
||||||
|
const { type, data } = nip19.decode(entity);
|
||||||
|
let t: string[] | undefined;
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case "npub":
|
||||||
|
t = ["p", data as string];
|
||||||
|
break;
|
||||||
|
case "nprofile":
|
||||||
|
t = ["p", (data as ProfilePointer).pubkey as string];
|
||||||
|
break;
|
||||||
|
case "note":
|
||||||
|
promises.push(
|
||||||
|
new Promise(async (resolve) => {
|
||||||
|
addTagIfNew(["e", data, "", "mention"]);
|
||||||
|
resolve();
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case "nevent":
|
||||||
|
promises.push(
|
||||||
|
new Promise(async (resolve) => {
|
||||||
|
let { id, relays, author } = data as EventPointer;
|
||||||
|
|
||||||
|
// If the nevent doesn't have a relay specified, try to get one
|
||||||
|
if (!relays || relays.length === 0) {
|
||||||
|
relays = [""];
|
||||||
|
}
|
||||||
|
|
||||||
|
addTagIfNew(["e", id, relays[0], "mention"]);
|
||||||
|
if (author) addTagIfNew(["p", author]);
|
||||||
|
resolve();
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case "naddr":
|
||||||
|
promises.push(
|
||||||
|
new Promise(async (resolve) => {
|
||||||
|
const id = [data.kind, data.pubkey, data.identifier].join(":");
|
||||||
|
let relays = data.relays ?? [];
|
||||||
|
|
||||||
|
// If the naddr doesn't have a relay specified, try to get one
|
||||||
|
if (relays.length === 0) {
|
||||||
|
relays = [""];
|
||||||
|
}
|
||||||
|
|
||||||
|
addTagIfNew(["a", id, relays[0], "mention"]);
|
||||||
|
addTagIfNew(["p", data.pubkey]);
|
||||||
|
resolve();
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return tag;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (t) addTagIfNew(t);
|
||||||
|
|
||||||
|
return `nostr:${entity}`;
|
||||||
|
} catch (error) {
|
||||||
|
return tag;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await Promise.all(promises);
|
||||||
|
|
||||||
|
content = content.replace(hashtagRegex, (tag, word) => {
|
||||||
|
const t: string[] = ["t", word];
|
||||||
|
if (!tags.find((t2) => t2[0] === t[0] && t2[1] === t[1])) {
|
||||||
|
tags.push(t);
|
||||||
|
}
|
||||||
|
return tag; // keep the original tag in the content
|
||||||
|
});
|
||||||
|
|
||||||
|
return { content, tags };
|
||||||
|
}
|
107
pnpm-lock.yaml
107
pnpm-lock.yaml
@ -105,6 +105,12 @@ importers:
|
|||||||
react-i18next:
|
react-i18next:
|
||||||
specifier: ^14.0.5
|
specifier: ^14.0.5
|
||||||
version: 14.0.5(i18next@23.8.2)(react-dom@18.2.0)(react@18.2.0)
|
version: 14.0.5(i18next@23.8.2)(react-dom@18.2.0)(react@18.2.0)
|
||||||
|
slate:
|
||||||
|
specifier: ^0.101.5
|
||||||
|
version: 0.101.5
|
||||||
|
slate-react:
|
||||||
|
specifier: ^0.101.6
|
||||||
|
version: 0.101.6(react-dom@18.2.0)(react@18.2.0)(slate@0.101.5)
|
||||||
sonner:
|
sonner:
|
||||||
specifier: ^1.4.0
|
specifier: ^1.4.0
|
||||||
version: 1.4.0(react-dom@18.2.0)(react@18.2.0)
|
version: 1.4.0(react-dom@18.2.0)(react@18.2.0)
|
||||||
@ -200,9 +206,6 @@ importers:
|
|||||||
'@lume/icons':
|
'@lume/icons':
|
||||||
specifier: workspace:^
|
specifier: workspace:^
|
||||||
version: link:../icons
|
version: link:../icons
|
||||||
'@lume/storage':
|
|
||||||
specifier: workspace:^
|
|
||||||
version: link:../storage
|
|
||||||
'@lume/utils':
|
'@lume/utils':
|
||||||
specifier: workspace:^
|
specifier: workspace:^
|
||||||
version: link:../utils
|
version: link:../utils
|
||||||
@ -408,9 +411,6 @@ importers:
|
|||||||
'@lume/icons':
|
'@lume/icons':
|
||||||
specifier: workspace:^
|
specifier: workspace:^
|
||||||
version: link:../icons
|
version: link:../icons
|
||||||
'@lume/storage':
|
|
||||||
specifier: workspace:^
|
|
||||||
version: link:../storage
|
|
||||||
'@lume/ui':
|
'@lume/ui':
|
||||||
specifier: workspace:^
|
specifier: workspace:^
|
||||||
version: link:../ui
|
version: link:../ui
|
||||||
@ -811,43 +811,6 @@ importers:
|
|||||||
specifier: ^5.3.3
|
specifier: ^5.3.3
|
||||||
version: 5.3.3
|
version: 5.3.3
|
||||||
|
|
||||||
packages/storage:
|
|
||||||
dependencies:
|
|
||||||
'@tauri-apps/plugin-store':
|
|
||||||
specifier: 2.0.0-beta.0
|
|
||||||
version: 2.0.0-beta.0
|
|
||||||
react:
|
|
||||||
specifier: ^18.2.0
|
|
||||||
version: 18.2.0
|
|
||||||
scheduler:
|
|
||||||
specifier: ^0.23.0
|
|
||||||
version: 0.23.0
|
|
||||||
use-context-selector:
|
|
||||||
specifier: ^1.4.1
|
|
||||||
version: 1.4.1(react@18.2.0)(scheduler@0.23.0)
|
|
||||||
virtua:
|
|
||||||
specifier: ^0.27.0
|
|
||||||
version: 0.27.0(react-dom@18.2.0)(react@18.2.0)
|
|
||||||
zustand:
|
|
||||||
specifier: ^4.5.0
|
|
||||||
version: 4.5.0(@types/react@18.2.55)(react@18.2.0)
|
|
||||||
devDependencies:
|
|
||||||
'@lume/tsconfig':
|
|
||||||
specifier: workspace:*
|
|
||||||
version: link:../tsconfig
|
|
||||||
'@lume/types':
|
|
||||||
specifier: workspace:*
|
|
||||||
version: link:../types
|
|
||||||
'@lume/utils':
|
|
||||||
specifier: workspace:^
|
|
||||||
version: link:../utils
|
|
||||||
'@types/react':
|
|
||||||
specifier: ^18.2.55
|
|
||||||
version: 18.2.55
|
|
||||||
typescript:
|
|
||||||
specifier: ^5.3.3
|
|
||||||
version: 5.3.3
|
|
||||||
|
|
||||||
packages/tailwindcss:
|
packages/tailwindcss:
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@evilmartians/harmony':
|
'@evilmartians/harmony':
|
||||||
@ -885,9 +848,6 @@ importers:
|
|||||||
'@lume/icons':
|
'@lume/icons':
|
||||||
specifier: workspace:^
|
specifier: workspace:^
|
||||||
version: link:../icons
|
version: link:../icons
|
||||||
'@lume/storage':
|
|
||||||
specifier: workspace:^
|
|
||||||
version: link:../storage
|
|
||||||
'@lume/utils':
|
'@lume/utils':
|
||||||
specifier: workspace:^
|
specifier: workspace:^
|
||||||
version: link:../utils
|
version: link:../utils
|
||||||
@ -1036,6 +996,15 @@ importers:
|
|||||||
react:
|
react:
|
||||||
specifier: ^18.2.0
|
specifier: ^18.2.0
|
||||||
version: 18.2.0
|
version: 18.2.0
|
||||||
|
react-dom:
|
||||||
|
specifier: ^18.2.0
|
||||||
|
version: 18.2.0(react@18.2.0)
|
||||||
|
slate:
|
||||||
|
specifier: ^0.101.5
|
||||||
|
version: 0.101.5
|
||||||
|
slate-react:
|
||||||
|
specifier: ^0.101.6
|
||||||
|
version: 0.101.6(react-dom@18.2.0)(react@18.2.0)(slate@0.101.5)
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@lume/tsconfig':
|
'@lume/tsconfig':
|
||||||
specifier: workspace:^
|
specifier: workspace:^
|
||||||
@ -1046,6 +1015,9 @@ importers:
|
|||||||
'@types/react':
|
'@types/react':
|
||||||
specifier: ^18.2.55
|
specifier: ^18.2.55
|
||||||
version: 18.2.55
|
version: 18.2.55
|
||||||
|
'@types/react-dom':
|
||||||
|
specifier: ^18.2.19
|
||||||
|
version: 18.2.19
|
||||||
tailwind-merge:
|
tailwind-merge:
|
||||||
specifier: ^2.2.1
|
specifier: ^2.2.1
|
||||||
version: 2.2.1
|
version: 2.2.1
|
||||||
@ -3170,12 +3142,6 @@ packages:
|
|||||||
'@tauri-apps/api': 2.0.0-beta.0
|
'@tauri-apps/api': 2.0.0-beta.0
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/@tauri-apps/plugin-store@2.0.0-beta.0:
|
|
||||||
resolution: {integrity: sha512-DT3pzMyNcgO90hDgmnN7j5fYQIaaD54gbi0oKi7n4Nwa6y5GqHsgpnzot9IBSOTS6kYy6D8yrN43XN/xwG4vUg==}
|
|
||||||
dependencies:
|
|
||||||
'@tauri-apps/api': 2.0.0-beta.0
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/@tauri-apps/plugin-updater@2.0.0-beta.0:
|
/@tauri-apps/plugin-updater@2.0.0-beta.0:
|
||||||
resolution: {integrity: sha512-TkKzngrgg8dQOr869OcObLdN10yXNiT/ERQp7sRYvV0vMpRJhYSIwTkpF+UkZGuEXtSqqE0FJEnb+4WuCelMdw==}
|
resolution: {integrity: sha512-TkKzngrgg8dQOr869OcObLdN10yXNiT/ERQp7sRYvV0vMpRJhYSIwTkpF+UkZGuEXtSqqE0FJEnb+4WuCelMdw==}
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -7028,23 +6994,6 @@ packages:
|
|||||||
tslib: 2.6.2
|
tslib: 2.6.2
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/use-context-selector@1.4.1(react@18.2.0)(scheduler@0.23.0):
|
|
||||||
resolution: {integrity: sha512-Io2ArvcRO+6MWIhkdfMFt+WKQX+Vb++W8DS2l03z/Vw/rz3BclKpM0ynr4LYGyU85Eke+Yx5oIhTY++QR0ZDoA==}
|
|
||||||
peerDependencies:
|
|
||||||
react: '>=16.8.0'
|
|
||||||
react-dom: '*'
|
|
||||||
react-native: '*'
|
|
||||||
scheduler: '>=0.19.0'
|
|
||||||
peerDependenciesMeta:
|
|
||||||
react-dom:
|
|
||||||
optional: true
|
|
||||||
react-native:
|
|
||||||
optional: true
|
|
||||||
dependencies:
|
|
||||||
react: 18.2.0
|
|
||||||
scheduler: 0.23.0
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/use-debounce@10.0.0(react@18.2.0):
|
/use-debounce@10.0.0(react@18.2.0):
|
||||||
resolution: {integrity: sha512-XRjvlvCB46bah9IBXVnq/ACP2lxqXyZj0D9hj4K5OzNroMDpTEBg8Anuh1/UfRTRs7pLhQ+RiNxxwZu9+MVl1A==}
|
resolution: {integrity: sha512-XRjvlvCB46bah9IBXVnq/ACP2lxqXyZj0D9hj4K5OzNroMDpTEBg8Anuh1/UfRTRs7pLhQ+RiNxxwZu9+MVl1A==}
|
||||||
engines: {node: '>= 16.0.0'}
|
engines: {node: '>= 16.0.0'}
|
||||||
@ -7521,26 +7470,6 @@ packages:
|
|||||||
/zod@3.22.4:
|
/zod@3.22.4:
|
||||||
resolution: {integrity: sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==}
|
resolution: {integrity: sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==}
|
||||||
|
|
||||||
/zustand@4.5.0(@types/react@18.2.55)(react@18.2.0):
|
|
||||||
resolution: {integrity: sha512-zlVFqS5TQ21nwijjhJlx4f9iGrXSL0o/+Dpy4txAP22miJ8Ti6c1Ol1RLNN98BMib83lmDH/2KmLwaNXpjrO1A==}
|
|
||||||
engines: {node: '>=12.7.0'}
|
|
||||||
peerDependencies:
|
|
||||||
'@types/react': '>=16.8'
|
|
||||||
immer: '>=9.0.6'
|
|
||||||
react: '>=16.8'
|
|
||||||
peerDependenciesMeta:
|
|
||||||
'@types/react':
|
|
||||||
optional: true
|
|
||||||
immer:
|
|
||||||
optional: true
|
|
||||||
react:
|
|
||||||
optional: true
|
|
||||||
dependencies:
|
|
||||||
'@types/react': 18.2.55
|
|
||||||
react: 18.2.0
|
|
||||||
use-sync-external-store: 1.2.0(react@18.2.0)
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/zwitch@2.0.4:
|
/zwitch@2.0.4:
|
||||||
resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==}
|
resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==}
|
||||||
dev: false
|
dev: false
|
||||||
|
@ -3,7 +3,15 @@
|
|||||||
"identifier": "desktop-capability",
|
"identifier": "desktop-capability",
|
||||||
"description": "Capability for the desktop",
|
"description": "Capability for the desktop",
|
||||||
"platforms": ["linux", "macOS", "windows"],
|
"platforms": ["linux", "macOS", "windows"],
|
||||||
"windows": ["main", "splash", "settings", "event-*", "user-*", "column-*"],
|
"windows": [
|
||||||
|
"main",
|
||||||
|
"splash",
|
||||||
|
"editor",
|
||||||
|
"settings",
|
||||||
|
"event-*",
|
||||||
|
"user-*",
|
||||||
|
"column-*"
|
||||||
|
],
|
||||||
"permissions": [
|
"permissions": [
|
||||||
"path:default",
|
"path:default",
|
||||||
"event:default",
|
"event:default",
|
||||||
@ -27,6 +35,8 @@
|
|||||||
"clipboard-manager:allow-read",
|
"clipboard-manager:allow-read",
|
||||||
"webview:allow-create-webview-window",
|
"webview:allow-create-webview-window",
|
||||||
"webview:allow-create-webview",
|
"webview:allow-create-webview",
|
||||||
|
"dialog:allow-open",
|
||||||
|
"fs:allow-read-file",
|
||||||
{
|
{
|
||||||
"identifier": "http:default",
|
"identifier": "http:default",
|
||||||
"allow": [
|
"allow": [
|
||||||
|
@ -1 +1 @@
|
|||||||
{"desktop-capability":{"identifier":"desktop-capability","description":"Capability for the desktop","context":"local","windows":["main","splash","settings","event-*","user-*","column-*"],"permissions":["path:default","event:default","window:default","app:default","resources:default","menu:default","tray:default","theme:allow-set-theme","theme:allow-get-theme","notification:allow-is-permission-granted","notification:allow-request-permission","notification:default","os:allow-locale","os:allow-platform","updater:allow-check","updater:default","window:allow-start-dragging","store:allow-get","clipboard-manager:allow-write","clipboard-manager:allow-read","webview:allow-create-webview-window","webview:allow-create-webview",{"identifier":"http:default","allow":[{"url":"http://**/"},{"url":"https://**/"}]},{"identifier":"fs:allow-read-text-file","allow":[{"path":"$RESOURCE/locales/*"}]}],"platforms":["linux","macOS","windows"]}}
|
{"desktop-capability":{"identifier":"desktop-capability","description":"Capability for the desktop","context":"local","windows":["main","splash","editor","settings","event-*","user-*","column-*"],"permissions":["path:default","event:default","window:default","app:default","resources:default","menu:default","tray:default","theme:allow-set-theme","theme:allow-get-theme","notification:allow-is-permission-granted","notification:allow-request-permission","notification:default","os:allow-locale","os:allow-platform","updater:allow-check","updater:default","window:allow-start-dragging","store:allow-get","clipboard-manager:allow-write","clipboard-manager:allow-read","webview:allow-create-webview-window","webview:allow-create-webview","dialog:allow-open","fs:allow-read-file",{"identifier":"http:default","allow":[{"url":"http://**/"},{"url":"https://**/"}]},{"identifier":"fs:allow-read-text-file","allow":[{"path":"$RESOURCE/locales/*"}]}],"platforms":["linux","macOS","windows"]}}
|
@ -14,7 +14,7 @@ use tauri_plugin_autostart::MacosLauncher;
|
|||||||
use tokio::sync::Mutex;
|
use tokio::sync::Mutex;
|
||||||
|
|
||||||
pub struct Nostr {
|
pub struct Nostr {
|
||||||
client: Mutex<Client>,
|
client: Client,
|
||||||
contact_list: Mutex<Option<Vec<PublicKey>>>,
|
contact_list: Mutex<Option<Vec<PublicKey>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -66,7 +66,7 @@ fn main() {
|
|||||||
|
|
||||||
// Update global state
|
// Update global state
|
||||||
handle.manage(Nostr {
|
handle.manage(Nostr {
|
||||||
client: Mutex::new(client),
|
client: client.into(),
|
||||||
contact_list: Mutex::new(None),
|
contact_list: Mutex::new(None),
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
@ -5,7 +5,7 @@ use tauri::State;
|
|||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn get_event(id: &str, state: State<'_, Nostr>) -> Result<String, String> {
|
pub async fn get_event(id: &str, state: State<'_, Nostr>) -> Result<String, String> {
|
||||||
let client = state.client.lock().await;
|
let client = &state.client;
|
||||||
let event_id: Option<EventId> = match Nip19::from_bech32(id) {
|
let event_id: Option<EventId> = match Nip19::from_bech32(id) {
|
||||||
Ok(val) => match val {
|
Ok(val) => match val {
|
||||||
Nip19::EventId(id) => Some(id),
|
Nip19::EventId(id) => Some(id),
|
||||||
@ -44,7 +44,7 @@ pub async fn get_local_events(
|
|||||||
until: Option<&str>,
|
until: Option<&str>,
|
||||||
state: State<'_, Nostr>,
|
state: State<'_, Nostr>,
|
||||||
) -> Result<Vec<Event>, String> {
|
) -> Result<Vec<Event>, String> {
|
||||||
let client = state.client.lock().await;
|
let client = &state.client;
|
||||||
let f_until = match until {
|
let f_until = match until {
|
||||||
Some(until) => Timestamp::from_str(until).unwrap(),
|
Some(until) => Timestamp::from_str(until).unwrap(),
|
||||||
None => Timestamp::now(),
|
None => Timestamp::now(),
|
||||||
@ -78,7 +78,7 @@ pub async fn get_global_events(
|
|||||||
until: Option<&str>,
|
until: Option<&str>,
|
||||||
state: State<'_, Nostr>,
|
state: State<'_, Nostr>,
|
||||||
) -> Result<Vec<Event>, String> {
|
) -> Result<Vec<Event>, String> {
|
||||||
let client = state.client.lock().await;
|
let client = &state.client;
|
||||||
let f_until = match until {
|
let f_until = match until {
|
||||||
Some(until) => Timestamp::from_str(until).unwrap(),
|
Some(until) => Timestamp::from_str(until).unwrap(),
|
||||||
None => Timestamp::now(),
|
None => Timestamp::now(),
|
||||||
@ -101,7 +101,7 @@ pub async fn get_global_events(
|
|||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn get_event_thread(id: &str, state: State<'_, Nostr>) -> Result<Vec<Event>, ()> {
|
pub async fn get_event_thread(id: &str, state: State<'_, Nostr>) -> Result<Vec<Event>, ()> {
|
||||||
let client = state.client.lock().await;
|
let client = &state.client;
|
||||||
let event_id = EventId::from_hex(id).unwrap();
|
let event_id = EventId::from_hex(id).unwrap();
|
||||||
let filter = Filter::new().kinds(vec![Kind::TextNote]).event(event_id);
|
let filter = Filter::new().kinds(vec![Kind::TextNote]).event(event_id);
|
||||||
|
|
||||||
@ -116,14 +116,19 @@ pub async fn get_event_thread(id: &str, state: State<'_, Nostr>) -> Result<Vec<E
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn publish(content: &str, state: State<'_, Nostr>) -> Result<EventId, ()> {
|
pub async fn publish(
|
||||||
let client = state.client.lock().await;
|
content: &str,
|
||||||
let event = client
|
tags: Vec<Vec<String>>,
|
||||||
.publish_text_note(content, [])
|
state: State<'_, Nostr>,
|
||||||
.await
|
) -> Result<String, String> {
|
||||||
.expect("Publish new text note failed");
|
let client = &state.client;
|
||||||
|
let final_tags = tags.into_iter().map(|val| Tag::parse(val).unwrap());
|
||||||
|
|
||||||
Ok(event)
|
if let Ok(event_id) = client.publish_text_note(content, final_tags).await {
|
||||||
|
Ok(event_id.to_bech32().unwrap())
|
||||||
|
} else {
|
||||||
|
Err("Publish text note failed".into())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
@ -132,7 +137,7 @@ pub async fn reply_to(
|
|||||||
tags: Vec<String>,
|
tags: Vec<String>,
|
||||||
state: State<'_, Nostr>,
|
state: State<'_, Nostr>,
|
||||||
) -> Result<EventId, String> {
|
) -> Result<EventId, String> {
|
||||||
let client = state.client.lock().await;
|
let client = &state.client;
|
||||||
if let Ok(event_tags) = Tag::parse(tags) {
|
if let Ok(event_tags) = Tag::parse(tags) {
|
||||||
let event = client
|
let event = client
|
||||||
.publish_text_note(content, vec![event_tags])
|
.publish_text_note(content, vec![event_tags])
|
||||||
@ -147,7 +152,7 @@ pub async fn reply_to(
|
|||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn repost(id: &str, pubkey: &str, state: State<'_, Nostr>) -> Result<EventId, ()> {
|
pub async fn repost(id: &str, pubkey: &str, state: State<'_, Nostr>) -> Result<EventId, ()> {
|
||||||
let client = state.client.lock().await;
|
let client = &state.client;
|
||||||
let public_key = PublicKey::from_str(pubkey).unwrap();
|
let public_key = PublicKey::from_str(pubkey).unwrap();
|
||||||
let event_id = EventId::from_hex(id).unwrap();
|
let event_id = EventId::from_hex(id).unwrap();
|
||||||
|
|
||||||
@ -161,7 +166,7 @@ pub async fn repost(id: &str, pubkey: &str, state: State<'_, Nostr>) -> Result<E
|
|||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn upvote(id: &str, pubkey: &str, state: State<'_, Nostr>) -> Result<EventId, ()> {
|
pub async fn upvote(id: &str, pubkey: &str, state: State<'_, Nostr>) -> Result<EventId, ()> {
|
||||||
let client = state.client.lock().await;
|
let client = &state.client;
|
||||||
let public_key = PublicKey::from_str(pubkey).unwrap();
|
let public_key = PublicKey::from_str(pubkey).unwrap();
|
||||||
let event_id = EventId::from_hex(id).unwrap();
|
let event_id = EventId::from_hex(id).unwrap();
|
||||||
|
|
||||||
@ -175,7 +180,7 @@ pub async fn upvote(id: &str, pubkey: &str, state: State<'_, Nostr>) -> Result<E
|
|||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn downvote(id: &str, pubkey: &str, state: State<'_, Nostr>) -> Result<EventId, ()> {
|
pub async fn downvote(id: &str, pubkey: &str, state: State<'_, Nostr>) -> Result<EventId, ()> {
|
||||||
let client = state.client.lock().await;
|
let client = &state.client;
|
||||||
let public_key = PublicKey::from_str(pubkey).unwrap();
|
let public_key = PublicKey::from_str(pubkey).unwrap();
|
||||||
let event_id = EventId::from_hex(id).unwrap();
|
let event_id = EventId::from_hex(id).unwrap();
|
||||||
|
|
||||||
|
@ -39,7 +39,7 @@ pub async fn save_key(
|
|||||||
let signer = NostrSigner::Keys(nostr_keys);
|
let signer = NostrSigner::Keys(nostr_keys);
|
||||||
|
|
||||||
// Update client's signer
|
// Update client's signer
|
||||||
let client = state.client.lock().await;
|
let client = &state.client;
|
||||||
client.set_signer(Some(signer)).await;
|
client.set_signer(Some(signer)).await;
|
||||||
|
|
||||||
// Update contact list
|
// Update contact list
|
||||||
@ -91,7 +91,7 @@ pub fn get_public_key(nsec: &str) -> Result<String, ()> {
|
|||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn update_signer(nsec: &str, state: State<'_, Nostr>) -> Result<(), ()> {
|
pub async fn update_signer(nsec: &str, state: State<'_, Nostr>) -> Result<(), ()> {
|
||||||
let client = state.client.lock().await;
|
let client = &state.client;
|
||||||
let secret_key = SecretKey::from_bech32(nsec).unwrap();
|
let secret_key = SecretKey::from_bech32(nsec).unwrap();
|
||||||
let keys = Keys::new(secret_key);
|
let keys = Keys::new(secret_key);
|
||||||
let signer = NostrSigner::Keys(keys);
|
let signer = NostrSigner::Keys(keys);
|
||||||
@ -103,7 +103,7 @@ pub async fn update_signer(nsec: &str, state: State<'_, Nostr>) -> Result<(), ()
|
|||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn verify_signer(state: State<'_, Nostr>) -> Result<bool, ()> {
|
pub async fn verify_signer(state: State<'_, Nostr>) -> Result<bool, ()> {
|
||||||
let client = state.client.lock().await;
|
let client = &state.client;
|
||||||
|
|
||||||
if let Ok(_) = client.signer().await {
|
if let Ok(_) = client.signer().await {
|
||||||
Ok(true)
|
Ok(true)
|
||||||
@ -118,7 +118,7 @@ pub async fn load_selected_account(
|
|||||||
app_handle: tauri::AppHandle,
|
app_handle: tauri::AppHandle,
|
||||||
state: State<'_, Nostr>,
|
state: State<'_, Nostr>,
|
||||||
) -> Result<bool, String> {
|
) -> Result<bool, String> {
|
||||||
let client = state.client.lock().await;
|
let client = &state.client;
|
||||||
let config_dir = app_handle.path().app_config_dir().unwrap();
|
let config_dir = app_handle.path().app_config_dir().unwrap();
|
||||||
let keyring_entry = Entry::new("Lume Secret Storage", "AppKey").unwrap();
|
let keyring_entry = Entry::new("Lume Secret Storage", "AppKey").unwrap();
|
||||||
|
|
||||||
|
@ -5,7 +5,7 @@ use tauri::State;
|
|||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn get_profile(id: &str, state: State<'_, Nostr>) -> Result<Metadata, String> {
|
pub async fn get_profile(id: &str, state: State<'_, Nostr>) -> Result<Metadata, String> {
|
||||||
let client = state.client.lock().await;
|
let client = &state.client;
|
||||||
let public_key: Option<PublicKey> = match Nip19::from_bech32(id) {
|
let public_key: Option<PublicKey> = match Nip19::from_bech32(id) {
|
||||||
Ok(val) => match val {
|
Ok(val) => match val {
|
||||||
Nip19::Pubkey(pubkey) => Some(pubkey),
|
Nip19::Pubkey(pubkey) => Some(pubkey),
|
||||||
@ -42,7 +42,7 @@ pub async fn get_profile(id: &str, state: State<'_, Nostr>) -> Result<Metadata,
|
|||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn get_contact_list(state: State<'_, Nostr>) -> Result<Vec<String>, String> {
|
pub async fn get_contact_list(state: State<'_, Nostr>) -> Result<Vec<String>, String> {
|
||||||
let client = state.client.lock().await;
|
let client = &state.client;
|
||||||
let contact_list = client.get_contact_list(Some(Duration::from_secs(10))).await;
|
let contact_list = client.get_contact_list(Some(Duration::from_secs(10))).await;
|
||||||
|
|
||||||
if let Ok(list) = contact_list {
|
if let Ok(list) = contact_list {
|
||||||
@ -65,7 +65,7 @@ pub async fn create_profile(
|
|||||||
website: &str,
|
website: &str,
|
||||||
state: State<'_, Nostr>,
|
state: State<'_, Nostr>,
|
||||||
) -> Result<EventId, ()> {
|
) -> Result<EventId, ()> {
|
||||||
let client = state.client.lock().await;
|
let client = &state.client;
|
||||||
let metadata = Metadata::new()
|
let metadata = Metadata::new()
|
||||||
.name(name)
|
.name(name)
|
||||||
.display_name(display_name)
|
.display_name(display_name)
|
||||||
@ -89,7 +89,7 @@ pub async fn follow(
|
|||||||
alias: Option<&str>,
|
alias: Option<&str>,
|
||||||
state: State<'_, Nostr>,
|
state: State<'_, Nostr>,
|
||||||
) -> Result<EventId, String> {
|
) -> Result<EventId, String> {
|
||||||
let client = state.client.lock().await;
|
let client = &state.client;
|
||||||
let public_key = PublicKey::from_str(id).unwrap();
|
let public_key = PublicKey::from_str(id).unwrap();
|
||||||
let contact = Contact::new(public_key, None, alias);
|
let contact = Contact::new(public_key, None, alias);
|
||||||
let contact_list = client.get_contact_list(Some(Duration::from_secs(10))).await;
|
let contact_list = client.get_contact_list(Some(Duration::from_secs(10))).await;
|
||||||
@ -110,7 +110,7 @@ pub async fn follow(
|
|||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn unfollow(id: &str, state: State<'_, Nostr>) -> Result<EventId, String> {
|
pub async fn unfollow(id: &str, state: State<'_, Nostr>) -> Result<EventId, String> {
|
||||||
let client = state.client.lock().await;
|
let client = &state.client;
|
||||||
let public_key = PublicKey::from_str(id).unwrap();
|
let public_key = PublicKey::from_str(id).unwrap();
|
||||||
let contact_list = client.get_contact_list(Some(Duration::from_secs(10))).await;
|
let contact_list = client.get_contact_list(Some(Duration::from_secs(10))).await;
|
||||||
|
|
||||||
@ -135,7 +135,7 @@ pub async fn unfollow(id: &str, state: State<'_, Nostr>) -> Result<EventId, Stri
|
|||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn set_interest(content: &str, state: State<'_, Nostr>) -> Result<EventId, String> {
|
pub async fn set_interest(content: &str, state: State<'_, Nostr>) -> Result<EventId, String> {
|
||||||
let client = state.client.lock().await;
|
let client = &state.client;
|
||||||
let tag = Tag::Identifier("lume_user_interest".into());
|
let tag = Tag::Identifier("lume_user_interest".into());
|
||||||
let builder = EventBuilder::new(Kind::ApplicationSpecificData, content, vec![tag]);
|
let builder = EventBuilder::new(Kind::ApplicationSpecificData, content, vec![tag]);
|
||||||
|
|
||||||
@ -148,7 +148,7 @@ pub async fn set_interest(content: &str, state: State<'_, Nostr>) -> Result<Even
|
|||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn get_interest(id: &str, state: State<'_, Nostr>) -> Result<String, String> {
|
pub async fn get_interest(id: &str, state: State<'_, Nostr>) -> Result<String, String> {
|
||||||
let client = state.client.lock().await;
|
let client = &state.client;
|
||||||
let public_key: Option<PublicKey> = match Nip19::from_bech32(id) {
|
let public_key: Option<PublicKey> = match Nip19::from_bech32(id) {
|
||||||
Ok(val) => match val {
|
Ok(val) => match val {
|
||||||
Nip19::Pubkey(pubkey) => Some(pubkey),
|
Nip19::Pubkey(pubkey) => Some(pubkey),
|
||||||
@ -188,7 +188,7 @@ pub async fn get_interest(id: &str, state: State<'_, Nostr>) -> Result<String, S
|
|||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn set_settings(content: &str, state: State<'_, Nostr>) -> Result<EventId, String> {
|
pub async fn set_settings(content: &str, state: State<'_, Nostr>) -> Result<EventId, String> {
|
||||||
let client = state.client.lock().await;
|
let client = &state.client;
|
||||||
let tag = Tag::Identifier("lume_user_settings".into());
|
let tag = Tag::Identifier("lume_user_settings".into());
|
||||||
let builder = EventBuilder::new(Kind::ApplicationSpecificData, content, vec![tag]);
|
let builder = EventBuilder::new(Kind::ApplicationSpecificData, content, vec![tag]);
|
||||||
|
|
||||||
@ -201,7 +201,7 @@ pub async fn set_settings(content: &str, state: State<'_, Nostr>) -> Result<Even
|
|||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn get_settings(id: &str, state: State<'_, Nostr>) -> Result<String, String> {
|
pub async fn get_settings(id: &str, state: State<'_, Nostr>) -> Result<String, String> {
|
||||||
let client = state.client.lock().await;
|
let client = &state.client;
|
||||||
let public_key: Option<PublicKey> = match Nip19::from_bech32(id) {
|
let public_key: Option<PublicKey> = match Nip19::from_bech32(id) {
|
||||||
Ok(val) => match val {
|
Ok(val) => match val {
|
||||||
Nip19::Pubkey(pubkey) => Some(pubkey),
|
Nip19::Pubkey(pubkey) => Some(pubkey),
|
||||||
|
@ -4,7 +4,7 @@ use tauri::State;
|
|||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn list_connected_relays(state: State<'_, Nostr>) -> Result<Vec<Url>, ()> {
|
pub async fn list_connected_relays(state: State<'_, Nostr>) -> Result<Vec<Url>, ()> {
|
||||||
let client = state.client.lock().await;
|
let client = &state.client;
|
||||||
let relays = client.relays().await;
|
let relays = client.relays().await;
|
||||||
let list: Vec<Url> = relays.into_keys().collect();
|
let list: Vec<Url> = relays.into_keys().collect();
|
||||||
|
|
||||||
@ -13,7 +13,7 @@ pub async fn list_connected_relays(state: State<'_, Nostr>) -> Result<Vec<Url>,
|
|||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn connect_relay(relay: &str, state: State<'_, Nostr>) -> Result<bool, ()> {
|
pub async fn connect_relay(relay: &str, state: State<'_, Nostr>) -> Result<bool, ()> {
|
||||||
let client = state.client.lock().await;
|
let client = &state.client;
|
||||||
if let Ok(_) = client.add_relay(relay).await {
|
if let Ok(_) = client.add_relay(relay).await {
|
||||||
Ok(true)
|
Ok(true)
|
||||||
} else {
|
} else {
|
||||||
@ -23,7 +23,7 @@ pub async fn connect_relay(relay: &str, state: State<'_, Nostr>) -> Result<bool,
|
|||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn remove_relay(relay: &str, state: State<'_, Nostr>) -> Result<bool, ()> {
|
pub async fn remove_relay(relay: &str, state: State<'_, Nostr>) -> Result<bool, ()> {
|
||||||
let client = state.client.lock().await;
|
let client = &state.client;
|
||||||
if let Ok(_) = client.remove_relay(relay).await {
|
if let Ok(_) = client.remove_relay(relay).await {
|
||||||
Ok(true)
|
Ok(true)
|
||||||
} else {
|
} else {
|
||||||
|
Loading…
Reference in New Issue
Block a user