Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4b60b39119 | ||
|
|
d2e5122d5a | ||
|
|
32f3315344 | ||
|
|
5ca9444358 | ||
|
|
4dc13385a5 | ||
|
|
b90ad1421f | ||
|
|
bba324ea53 |
@@ -9,7 +9,7 @@
|
|||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@lume/ark": "workspace:^",
|
"@lume/system": "workspace:^",
|
||||||
"@lume/icons": "workspace:^",
|
"@lume/icons": "workspace:^",
|
||||||
"@lume/ui": "workspace:^",
|
"@lume/ui": "workspace:^",
|
||||||
"@lume/utils": "workspace:^",
|
"@lume/utils": "workspace:^",
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 72 KiB |
|
Before Width: | Height: | Size: 191 KiB |
|
Before Width: | Height: | Size: 118 KiB |
|
Before Width: | Height: | Size: 296 KiB |
|
Before Width: | Height: | Size: 72 KiB |
|
Before Width: | Height: | Size: 186 KiB |
|
After Width: | Height: | Size: 163 KiB |
|
Before Width: | Height: | Size: 310 KiB |
|
Before Width: | Height: | Size: 951 KiB |
|
Before Width: | Height: | Size: 80 KiB |
|
Before Width: | Height: | Size: 257 KiB |
@@ -1,20 +1,17 @@
|
|||||||
import { Ark } from "@lume/ark";
|
|
||||||
import { createSyncStoragePersister } from "@tanstack/query-sync-storage-persister";
|
import { createSyncStoragePersister } from "@tanstack/query-sync-storage-persister";
|
||||||
import { QueryClient } from "@tanstack/react-query";
|
import { QueryClient } from "@tanstack/react-query";
|
||||||
import { PersistQueryClientProvider } from "@tanstack/react-query-persist-client";
|
import { PersistQueryClientProvider } from "@tanstack/react-query-persist-client";
|
||||||
import { RouterProvider, createRouter } from "@tanstack/react-router";
|
import { RouterProvider, createRouter } from "@tanstack/react-router";
|
||||||
import { platform } from "@tauri-apps/plugin-os";
|
|
||||||
import React, { StrictMode } from "react";
|
import React, { StrictMode } from "react";
|
||||||
import ReactDOM from "react-dom/client";
|
import ReactDOM from "react-dom/client";
|
||||||
import { I18nextProvider } from "react-i18next";
|
import { I18nextProvider } from "react-i18next";
|
||||||
import "./app.css";
|
import "./app.css";
|
||||||
import i18n from "./locale";
|
import i18n from "./locale";
|
||||||
import { routeTree } from "./router.gen"; // auto generated file
|
import { routeTree } from "./router.gen"; // auto generated file
|
||||||
|
import { type } from "@tauri-apps/plugin-os";
|
||||||
|
|
||||||
const ark = new Ark();
|
const os = await type();
|
||||||
const queryClient = new QueryClient();
|
const queryClient = new QueryClient();
|
||||||
const platformName = await platform();
|
|
||||||
|
|
||||||
const persister = createSyncStoragePersister({
|
const persister = createSyncStoragePersister({
|
||||||
storage: window.localStorage,
|
storage: window.localStorage,
|
||||||
});
|
});
|
||||||
@@ -23,9 +20,8 @@ const persister = createSyncStoragePersister({
|
|||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
routeTree,
|
routeTree,
|
||||||
context: {
|
context: {
|
||||||
ark,
|
|
||||||
queryClient,
|
queryClient,
|
||||||
platform: platformName,
|
platform: os,
|
||||||
},
|
},
|
||||||
Wrap: ({ children }) => {
|
Wrap: ({ children }) => {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
|
import { NostrQuery } from "@lume/system";
|
||||||
import { Spinner } from "@lume/ui";
|
import { Spinner } from "@lume/ui";
|
||||||
import { cn } from "@lume/utils";
|
import { cn } from "@lume/utils";
|
||||||
import { useRouteContext } from "@tanstack/react-router";
|
|
||||||
import {
|
import {
|
||||||
type Dispatch,
|
type Dispatch,
|
||||||
type ReactNode,
|
type ReactNode,
|
||||||
@@ -18,21 +18,17 @@ export function AvatarUploader({
|
|||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
className?: string;
|
className?: string;
|
||||||
}) {
|
}) {
|
||||||
const { ark } = useRouteContext({ strict: false });
|
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
const uploadAvatar = async () => {
|
const uploadAvatar = async () => {
|
||||||
// start loading
|
|
||||||
setLoading(true);
|
|
||||||
try {
|
try {
|
||||||
const image = await ark.upload();
|
setLoading(true);
|
||||||
|
const image = await NostrQuery.upload();
|
||||||
setPicture(image);
|
setPicture(image);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
setLoading(false);
|
||||||
toast.error(String(e));
|
toast.error(String(e));
|
||||||
}
|
}
|
||||||
|
|
||||||
// stop loading
|
|
||||||
setLoading(false);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,16 +1,15 @@
|
|||||||
import { User } from "@/components/user";
|
import { User } from "@/components/user";
|
||||||
|
import { NostrAccount } from "@lume/system";
|
||||||
import { getBitcoinDisplayValues } from "@lume/utils";
|
import { getBitcoinDisplayValues } from "@lume/utils";
|
||||||
import { useRouteContext } from "@tanstack/react-router";
|
|
||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
|
|
||||||
export function Balance({ account }: { account: string }) {
|
export function Balance({ account }: { account: string }) {
|
||||||
const { ark } = useRouteContext({ strict: false });
|
|
||||||
const [balance, setBalance] = useState(0);
|
const [balance, setBalance] = useState(0);
|
||||||
const value = useMemo(() => getBitcoinDisplayValues(balance), [balance]);
|
const value = useMemo(() => getBitcoinDisplayValues(balance), [balance]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
async function getBalance() {
|
async function getBalance() {
|
||||||
const val = await ark.get_balance();
|
const val = await NostrAccount.getBalance();
|
||||||
setBalance(val);
|
setBalance(val);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,18 +1,23 @@
|
|||||||
import { ThreadIcon } from "@lume/icons";
|
import { ThreadIcon } from "@lume/icons";
|
||||||
import type { Event } from "@lume/types";
|
import type { NostrEvent } from "@lume/types";
|
||||||
import { Note } from "@/components/note";
|
import { Note } from "@/components/note";
|
||||||
import { cn } from "@lume/utils";
|
import { cn } from "@lume/utils";
|
||||||
import { useRouteContext } from "@tanstack/react-router";
|
import { LumeEvent } from "@lume/system";
|
||||||
|
import { useMemo } from "react";
|
||||||
|
|
||||||
export function Conversation({
|
export function Conversation({
|
||||||
event,
|
event,
|
||||||
|
gossip,
|
||||||
className,
|
className,
|
||||||
}: {
|
}: {
|
||||||
event: Event;
|
event: NostrEvent;
|
||||||
|
gossip?: boolean;
|
||||||
className?: string;
|
className?: string;
|
||||||
}) {
|
}) {
|
||||||
const { ark } = useRouteContext({ strict: false });
|
const thread = useMemo(
|
||||||
const thread = ark.get_thread(event.tags);
|
() => LumeEvent.getEventThread(event.tags, gossip),
|
||||||
|
[event],
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Note.Provider event={event}>
|
<Note.Provider event={event}>
|
||||||
|
|||||||
@@ -4,9 +4,7 @@ import { User } from "../user";
|
|||||||
|
|
||||||
export function NoteActivity({ className }: { className?: string }) {
|
export function NoteActivity({ className }: { className?: string }) {
|
||||||
const event = useNoteContext();
|
const event = useNoteContext();
|
||||||
const mentions = event.tags
|
const mentions = event.mentions;
|
||||||
.filter((tag) => tag[0] === "p")
|
|
||||||
.map((tag) => tag[1]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn("-mt-3 mb-2", className)}>
|
<div className={cn("-mt-3 mb-2", className)}>
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
import { VisitIcon } from "@lume/icons";
|
import { VisitIcon } from "@lume/icons";
|
||||||
import * as Tooltip from "@radix-ui/react-tooltip";
|
import * as Tooltip from "@radix-ui/react-tooltip";
|
||||||
import { useRouteContext } from "@tanstack/react-router";
|
|
||||||
import { useNoteContext } from "../provider";
|
import { useNoteContext } from "../provider";
|
||||||
|
import { LumeWindow } from "@lume/system";
|
||||||
|
|
||||||
export function NoteOpenThread() {
|
export function NoteOpenThread() {
|
||||||
const event = useNoteContext();
|
const event = useNoteContext();
|
||||||
const { ark } = useRouteContext({ strict: false });
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip.Provider>
|
<Tooltip.Provider>
|
||||||
@@ -13,7 +12,7 @@ export function NoteOpenThread() {
|
|||||||
<Tooltip.Trigger asChild>
|
<Tooltip.Trigger asChild>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => ark.open_event(event)}
|
onClick={() => LumeWindow.openEvent(event)}
|
||||||
className="group inline-flex h-7 w-14 bg-neutral-100 dark:bg-white/10 rounded-full items-center justify-center text-sm font-medium text-neutral-800 dark:text-neutral-200 hover:text-blue-500 hover:bg-neutral-200 dark:hover:bg-white/20"
|
className="group inline-flex h-7 w-14 bg-neutral-100 dark:bg-white/10 rounded-full items-center justify-center text-sm font-medium text-neutral-800 dark:text-neutral-200 hover:text-blue-500 hover:bg-neutral-200 dark:hover:bg-white/20"
|
||||||
>
|
>
|
||||||
<VisitIcon className="shrink-0 size-4" />
|
<VisitIcon className="shrink-0 size-4" />
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
import { ReplyIcon } from "@lume/icons";
|
import { ReplyIcon } from "@lume/icons";
|
||||||
import * as Tooltip from "@radix-ui/react-tooltip";
|
import * as Tooltip from "@radix-ui/react-tooltip";
|
||||||
import { useRouteContext } from "@tanstack/react-router";
|
|
||||||
import { useNoteContext } from "../provider";
|
import { useNoteContext } from "../provider";
|
||||||
import { cn } from "@lume/utils";
|
import { cn } from "@lume/utils";
|
||||||
|
import { LumeWindow } from "@lume/system";
|
||||||
|
|
||||||
export function NoteReply({ large = false }: { large?: boolean }) {
|
export function NoteReply({ large = false }: { large?: boolean }) {
|
||||||
const event = useNoteContext();
|
const event = useNoteContext();
|
||||||
const { ark } = useRouteContext({ strict: false });
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip.Provider>
|
<Tooltip.Provider>
|
||||||
@@ -14,7 +13,7 @@ export function NoteReply({ large = false }: { large?: boolean }) {
|
|||||||
<Tooltip.Trigger asChild>
|
<Tooltip.Trigger asChild>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => ark.open_editor(event.id)}
|
onClick={() => LumeWindow.openEditor(event.id)}
|
||||||
className={cn(
|
className={cn(
|
||||||
"inline-flex items-center justify-center text-neutral-800 dark:text-neutral-200",
|
"inline-flex items-center justify-center text-neutral-800 dark:text-neutral-200",
|
||||||
large
|
large
|
||||||
|
|||||||
@@ -2,15 +2,14 @@ import { QuoteIcon, RepostIcon } from "@lume/icons";
|
|||||||
import { cn } from "@lume/utils";
|
import { cn } from "@lume/utils";
|
||||||
import * as DropdownMenu from "@radix-ui/react-dropdown-menu";
|
import * as DropdownMenu from "@radix-ui/react-dropdown-menu";
|
||||||
import * as Tooltip from "@radix-ui/react-tooltip";
|
import * as Tooltip from "@radix-ui/react-tooltip";
|
||||||
import { useRouteContext } from "@tanstack/react-router";
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { Spinner } from "@lume/ui";
|
import { Spinner } from "@lume/ui";
|
||||||
import { useNoteContext } from "../provider";
|
import { useNoteContext } from "../provider";
|
||||||
|
import { LumeWindow } from "@lume/system";
|
||||||
|
|
||||||
export function NoteRepost({ large = false }: { large?: boolean }) {
|
export function NoteRepost({ large = false }: { large?: boolean }) {
|
||||||
const { ark } = useRouteContext({ strict: false });
|
|
||||||
const event = useNoteContext();
|
const event = useNoteContext();
|
||||||
|
|
||||||
const [t] = useTranslation();
|
const [t] = useTranslation();
|
||||||
@@ -23,7 +22,7 @@ export function NoteRepost({ large = false }: { large?: boolean }) {
|
|||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
||||||
// repost
|
// repost
|
||||||
await ark.repost(event.id, event.pubkey);
|
await event.repost();
|
||||||
|
|
||||||
// update state
|
// update state
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
@@ -86,7 +85,7 @@ export function NoteRepost({ large = false }: { large?: boolean }) {
|
|||||||
<DropdownMenu.Item asChild>
|
<DropdownMenu.Item asChild>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => ark.open_editor(event.id, true)}
|
onClick={() => LumeWindow.openEditor(event.id, true)}
|
||||||
className="inline-flex h-9 items-center gap-2 rounded-lg px-3 text-sm font-medium text-white hover:bg-neutral-900 focus:outline-none dark:text-black dark:hover:bg-neutral-100"
|
className="inline-flex h-9 items-center gap-2 rounded-lg px-3 text-sm font-medium text-white hover:bg-neutral-900 focus:outline-none dark:text-black dark:hover:bg-neutral-100"
|
||||||
>
|
>
|
||||||
<QuoteIcon className="size-4" />
|
<QuoteIcon className="size-4" />
|
||||||
|
|||||||
@@ -1,33 +1,19 @@
|
|||||||
import { ZapIcon } from "@lume/icons";
|
import { ZapIcon } from "@lume/icons";
|
||||||
import { useRouteContext, useSearch } from "@tanstack/react-router";
|
import { useRouteContext } from "@tanstack/react-router";
|
||||||
import { toast } from "sonner";
|
|
||||||
import { useNoteContext } from "../provider";
|
import { useNoteContext } from "../provider";
|
||||||
import { cn } from "@lume/utils";
|
import { cn } from "@lume/utils";
|
||||||
|
import { LumeWindow } from "@lume/system";
|
||||||
|
|
||||||
export function NoteZap({ large = false }: { large?: boolean }) {
|
export function NoteZap({ large = false }: { large?: boolean }) {
|
||||||
const event = useNoteContext();
|
const event = useNoteContext();
|
||||||
const { ark, settings } = useRouteContext({ strict: false });
|
const { settings } = useRouteContext({ strict: false });
|
||||||
const { account } = useSearch({ strict: false });
|
|
||||||
|
|
||||||
const zap = async () => {
|
|
||||||
try {
|
|
||||||
const nwc = await ark.load_nwc();
|
|
||||||
if (!nwc) {
|
|
||||||
ark.open_nwc();
|
|
||||||
} else {
|
|
||||||
ark.open_zap(event.id, event.pubkey, account);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
toast.error(String(e));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!settings.zap) return null;
|
if (!settings.zap) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => zap()}
|
onClick={() => LumeWindow.openZap(event.id, event.pubkey)}
|
||||||
className={cn(
|
className={cn(
|
||||||
"inline-flex items-center justify-center text-neutral-800 dark:text-neutral-200",
|
"inline-flex items-center justify-center text-neutral-800 dark:text-neutral-200",
|
||||||
large
|
large
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useEvent } from "@lume/ark";
|
import { useEvent } from "@lume/system";
|
||||||
import { cn } from "@lume/utils";
|
import { cn } from "@lume/utils";
|
||||||
import { Note } from ".";
|
import { Note } from ".";
|
||||||
import { InfoIcon } from "@lume/icons";
|
import { InfoIcon } from "@lume/icons";
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { useEvent } from "@lume/ark";
|
import { LumeWindow, useEvent } from "@lume/system";
|
||||||
import { LinkIcon } from "@lume/icons";
|
import { LinkIcon } from "@lume/icons";
|
||||||
import { useRouteContext } from "@tanstack/react-router";
|
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { cn } from "@lume/utils";
|
import { cn } from "@lume/utils";
|
||||||
import { User } from "@/components/user";
|
import { User } from "@/components/user";
|
||||||
@@ -13,7 +12,6 @@ export function MentionNote({
|
|||||||
eventId: string;
|
eventId: string;
|
||||||
openable?: boolean;
|
openable?: boolean;
|
||||||
}) {
|
}) {
|
||||||
const { ark } = useRouteContext({ strict: false });
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { isLoading, isError, data } = useEvent(eventId);
|
const { isLoading, isError, data } = useEvent(eventId);
|
||||||
|
|
||||||
@@ -62,9 +60,9 @@ export function MentionNote({
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
ark.open_event_id(data.id);
|
LumeWindow.openEvent(data);
|
||||||
}}
|
}}
|
||||||
className="z-10 h-7 w-28 inline-flex items-center justify-center gap-1 text-sm bg-neutral-100 dark:bg-white/10 rounded-full text-neutral-600 hover:text-blue-500 dark:text-neutral-400"
|
className="z-10 h-7 w-28 inline-flex items-center justify-center gap-1 text-sm bg-black/10 dark:bg-white/10 rounded-full text-neutral-600 hover:text-blue-500 dark:text-neutral-400"
|
||||||
>
|
>
|
||||||
View post
|
View post
|
||||||
<LinkIcon className="size-4" />
|
<LinkIcon className="size-4" />
|
||||||
|
|||||||
@@ -1,24 +1,20 @@
|
|||||||
import { useProfile } from "@lume/ark";
|
import { LumeWindow, useProfile } from "@lume/system";
|
||||||
import { displayNpub } from "@lume/utils";
|
import { displayNpub } from "@lume/utils";
|
||||||
import { useRouteContext } from "@tanstack/react-router";
|
|
||||||
|
|
||||||
export function MentionUser({ pubkey }: { pubkey: string }) {
|
export function MentionUser({ pubkey }: { pubkey: string }) {
|
||||||
const { ark } = useRouteContext({ strict: false });
|
|
||||||
const { isLoading, isError, profile } = useProfile(pubkey);
|
const { isLoading, isError, profile } = useProfile(pubkey);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => ark.open_profile(pubkey)}
|
onClick={() => LumeWindow.openProfile(pubkey)}
|
||||||
className="break-words text-start text-blue-500 hover:text-blue-600"
|
className="break-words text-start text-blue-500 hover:text-blue-600"
|
||||||
>
|
>
|
||||||
{isLoading
|
{isLoading
|
||||||
? "@anon"
|
? "@anon"
|
||||||
: isError
|
: isError
|
||||||
? displayNpub(pubkey, 16)
|
? displayNpub(pubkey, 16)
|
||||||
: `@${
|
: `@${profile?.name || profile?.display_name || "anon"}`}
|
||||||
profile?.name || profile?.display_name || profile?.name || "anon"
|
|
||||||
}`}
|
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,37 +1,28 @@
|
|||||||
import { HorizontalDotsIcon } from "@lume/icons";
|
import { HorizontalDotsIcon } from "@lume/icons";
|
||||||
import * as DropdownMenu from "@radix-ui/react-dropdown-menu";
|
import * as DropdownMenu from "@radix-ui/react-dropdown-menu";
|
||||||
import { useRouteContext } from "@tanstack/react-router";
|
|
||||||
import { writeText } from "@tauri-apps/plugin-clipboard-manager";
|
import { writeText } from "@tauri-apps/plugin-clipboard-manager";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { toast } from "sonner";
|
|
||||||
import { useNoteContext } from "./provider";
|
import { useNoteContext } from "./provider";
|
||||||
|
import { LumeWindow } from "@lume/system";
|
||||||
|
|
||||||
export function NoteMenu() {
|
export function NoteMenu() {
|
||||||
|
const { t } = useTranslation();
|
||||||
const event = useNoteContext();
|
const event = useNoteContext();
|
||||||
|
|
||||||
const { ark } = useRouteContext({ strict: false });
|
|
||||||
const { t } = useTranslation();
|
|
||||||
|
|
||||||
const copyID = async () => {
|
const copyID = async () => {
|
||||||
await writeText(await ark.event_to_bech32(event.id, [""]));
|
await writeText(await event.idAsBech32());
|
||||||
toast.success("Copied");
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const copyRaw = async () => {
|
const copyRaw = async () => {
|
||||||
await writeText(JSON.stringify(event));
|
await writeText(JSON.stringify(event));
|
||||||
toast.success("Copied");
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const copyNpub = async () => {
|
const copyNpub = async () => {
|
||||||
await writeText(await ark.user_to_bech32(event.pubkey, [""]));
|
await writeText(await event.pubkeyAsBech32());
|
||||||
toast.success("Copied");
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const copyLink = async () => {
|
const copyLink = async () => {
|
||||||
await writeText(
|
await writeText(`https://njump.me/${await event.idAsBech32()}`);
|
||||||
`https://njump.me/${await ark.event_to_bech32(event.id, [""])}`,
|
|
||||||
);
|
|
||||||
toast.success("Copied");
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -49,7 +40,7 @@ export function NoteMenu() {
|
|||||||
<DropdownMenu.Item asChild>
|
<DropdownMenu.Item asChild>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => ark.open_event(event)}
|
onClick={() => LumeWindow.openEvent(event)}
|
||||||
className="inline-flex h-9 items-center gap-2 rounded-lg px-3 text-sm font-medium text-white hover:bg-neutral-900 focus:outline-none dark:text-black dark:hover:bg-neutral-100"
|
className="inline-flex h-9 items-center gap-2 rounded-lg px-3 text-sm font-medium text-white hover:bg-neutral-900 focus:outline-none dark:text-black dark:hover:bg-neutral-100"
|
||||||
>
|
>
|
||||||
{t("note.menu.viewThread")}
|
{t("note.menu.viewThread")}
|
||||||
@@ -84,7 +75,7 @@ export function NoteMenu() {
|
|||||||
</DropdownMenu.Item>
|
</DropdownMenu.Item>
|
||||||
<DropdownMenu.Item asChild>
|
<DropdownMenu.Item asChild>
|
||||||
<button
|
<button
|
||||||
onClick={() => ark.open_profile(event.pubkey)}
|
onClick={() => LumeWindow.openProfile(event.pubkey)}
|
||||||
className="inline-flex h-9 items-center gap-2 rounded-lg px-3 text-sm font-medium text-white hover:bg-neutral-900 focus:outline-none dark:text-black dark:hover:bg-neutral-100"
|
className="inline-flex h-9 items-center gap-2 rounded-lg px-3 text-sm font-medium text-white hover:bg-neutral-900 focus:outline-none dark:text-black dark:hover:bg-neutral-100"
|
||||||
>
|
>
|
||||||
{t("note.menu.viewAuthor")}
|
{t("note.menu.viewAuthor")}
|
||||||
|
|||||||
@@ -1,19 +1,25 @@
|
|||||||
import type { Event } from "@lume/types";
|
import { LumeEvent } from "@lume/system";
|
||||||
|
import type { NostrEvent } from "@lume/types";
|
||||||
import { type ReactNode, createContext, useContext } from "react";
|
import { type ReactNode, createContext, useContext } from "react";
|
||||||
|
|
||||||
const EventContext = createContext<Event>(null);
|
const NoteContext = createContext<LumeEvent>(null);
|
||||||
|
|
||||||
export function NoteProvider({
|
export function NoteProvider({
|
||||||
event,
|
event,
|
||||||
children,
|
children,
|
||||||
}: { event: Event; children: ReactNode }) {
|
}: {
|
||||||
|
event: NostrEvent;
|
||||||
|
children: ReactNode;
|
||||||
|
}) {
|
||||||
|
const lumeEvent = new LumeEvent(event);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<EventContext.Provider value={event}>{children}</EventContext.Provider>
|
<NoteContext.Provider value={lumeEvent}>{children}</NoteContext.Provider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useNoteContext() {
|
export function useNoteContext() {
|
||||||
const context = useContext(EventContext);
|
const context = useContext(NoteContext);
|
||||||
if (!context) {
|
if (!context) {
|
||||||
throw new Error("Please import Note Provider to use useNoteContext() hook");
|
throw new Error("Please import Note Provider to use useNoteContext() hook");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
import { cn } from "@lume/utils";
|
import { cn } from "@lume/utils";
|
||||||
import * as HoverCard from "@radix-ui/react-hover-card";
|
import * as HoverCard from "@radix-ui/react-hover-card";
|
||||||
import { useRouteContext } from "@tanstack/react-router";
|
|
||||||
import { User } from "../user";
|
import { User } from "../user";
|
||||||
import { useNoteContext } from "./provider";
|
import { useNoteContext } from "./provider";
|
||||||
|
import { LumeWindow } from "@lume/system";
|
||||||
|
|
||||||
export function NoteUser({ className }: { className?: string }) {
|
export function NoteUser({ className }: { className?: string }) {
|
||||||
const { ark } = useRouteContext({ strict: false });
|
|
||||||
const event = useNoteContext();
|
const event = useNoteContext();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -46,7 +45,7 @@ export function NoteUser({ className }: { className?: string }) {
|
|||||||
</div>
|
</div>
|
||||||
<User.About className="line-clamp-3 text-sm text-white dark:text-neutral-900" />
|
<User.About className="line-clamp-3 text-sm text-white dark:text-neutral-900" />
|
||||||
<button
|
<button
|
||||||
onClick={() => ark.open_profile(event.pubkey)}
|
onClick={() => LumeWindow.openProfile(event.pubkey)}
|
||||||
className="mt-2 inline-flex h-9 w-full items-center justify-center rounded-lg bg-white text-sm font-medium hover:bg-neutral-200 dark:bg-neutral-100 dark:text-neutral-900 dark:hover:bg-neutral-200"
|
className="mt-2 inline-flex h-9 w-full items-center justify-center rounded-lg bg-white text-sm font-medium hover:bg-neutral-200 dark:bg-neutral-100 dark:text-neutral-900 dark:hover:bg-neutral-200"
|
||||||
>
|
>
|
||||||
View profile
|
View profile
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { Event } from "@lume/types";
|
import type { NostrEvent } from "@lume/types";
|
||||||
import { Note } from "@/components/note";
|
import { Note } from "@/components/note";
|
||||||
import { cn } from "@lume/utils";
|
import { cn } from "@lume/utils";
|
||||||
|
|
||||||
@@ -6,7 +6,7 @@ export function Notification({
|
|||||||
event,
|
event,
|
||||||
className,
|
className,
|
||||||
}: {
|
}: {
|
||||||
event: Event;
|
event: NostrEvent;
|
||||||
className?: string;
|
className?: string;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { QuoteIcon } from "@lume/icons";
|
import { QuoteIcon } from "@lume/icons";
|
||||||
import type { Event } from "@lume/types";
|
import type { NostrEvent } from "@lume/types";
|
||||||
import { Note } from "@/components/note";
|
import { Note } from "@/components/note";
|
||||||
import { cn } from "@lume/utils";
|
import { cn } from "@lume/utils";
|
||||||
|
|
||||||
@@ -7,7 +7,7 @@ export function Quote({
|
|||||||
event,
|
event,
|
||||||
className,
|
className,
|
||||||
}: {
|
}: {
|
||||||
event: Event;
|
event: NostrEvent;
|
||||||
className?: string;
|
className?: string;
|
||||||
}) {
|
}) {
|
||||||
const quoteEventId = event.tags.find(
|
const quoteEventId = event.tags.find(
|
||||||
|
|||||||
@@ -1,19 +1,18 @@
|
|||||||
import type { Event } from "@lume/types";
|
|
||||||
import { Spinner } from "@lume/ui";
|
import { Spinner } from "@lume/ui";
|
||||||
import { Note } from "@/components/note";
|
import { Note } from "@/components/note";
|
||||||
import { User } from "@/components/user";
|
import { User } from "@/components/user";
|
||||||
import { cn } from "@lume/utils";
|
import { cn } from "@lume/utils";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { useRouteContext } from "@tanstack/react-router";
|
import { NostrEvent } from "@lume/types";
|
||||||
|
import { NostrQuery } from "@lume/system";
|
||||||
|
|
||||||
export function RepostNote({
|
export function RepostNote({
|
||||||
event,
|
event,
|
||||||
className,
|
className,
|
||||||
}: {
|
}: {
|
||||||
event: Event;
|
event: NostrEvent;
|
||||||
className?: string;
|
className?: string;
|
||||||
}) {
|
}) {
|
||||||
const { ark } = useRouteContext({ strict: false });
|
|
||||||
const {
|
const {
|
||||||
isLoading,
|
isLoading,
|
||||||
isError,
|
isError,
|
||||||
@@ -23,12 +22,12 @@ export function RepostNote({
|
|||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
try {
|
try {
|
||||||
if (event.content.length > 50) {
|
if (event.content.length > 50) {
|
||||||
const embed: Event = JSON.parse(event.content);
|
const embed: NostrEvent = JSON.parse(event.content);
|
||||||
return embed;
|
return embed;
|
||||||
}
|
}
|
||||||
|
|
||||||
const id = event.tags.find((el) => el[0] === "e")?.[1];
|
const id = event.tags.find((el) => el[0] === "e")?.[1];
|
||||||
const repostEvent = await ark.get_event(id);
|
const repostEvent = await NostrQuery.getEvent(id);
|
||||||
|
|
||||||
return repostEvent;
|
return repostEvent;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { Event } from "@lume/types";
|
import type { NostrEvent } from "@lume/types";
|
||||||
import { cn } from "@lume/utils";
|
import { cn } from "@lume/utils";
|
||||||
import { Note } from "@/components/note";
|
import { Note } from "@/components/note";
|
||||||
|
|
||||||
@@ -6,7 +6,7 @@ export function TextNote({
|
|||||||
event,
|
event,
|
||||||
className,
|
className,
|
||||||
}: {
|
}: {
|
||||||
event: Event;
|
event: NostrEvent;
|
||||||
className?: string;
|
className?: string;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { cn } from "@lume/utils";
|
import { cn } from "@lume/utils";
|
||||||
import { useRouteContext } from "@tanstack/react-router";
|
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Spinner } from "@lume/ui";
|
import { Spinner } from "@lume/ui";
|
||||||
import { useUserContext } from "./provider";
|
import { useUserContext } from "./provider";
|
||||||
|
import { NostrAccount } from "@lume/system";
|
||||||
|
|
||||||
export function UserFollowButton({
|
export function UserFollowButton({
|
||||||
simple = false,
|
simple = false,
|
||||||
@@ -12,7 +12,6 @@ export function UserFollowButton({
|
|||||||
simple?: boolean;
|
simple?: boolean;
|
||||||
className?: string;
|
className?: string;
|
||||||
}) {
|
}) {
|
||||||
const { ark } = useRouteContext({ strict: false });
|
|
||||||
const user = useUserContext();
|
const user = useUserContext();
|
||||||
|
|
||||||
const [t] = useTranslation();
|
const [t] = useTranslation();
|
||||||
@@ -22,10 +21,10 @@ export function UserFollowButton({
|
|||||||
const toggleFollow = async () => {
|
const toggleFollow = async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
if (!followed) {
|
if (!followed) {
|
||||||
const add = await ark.follow(user.pubkey);
|
const add = await NostrAccount.follow(user.pubkey, user.profile?.name);
|
||||||
if (add) setFollowed(true);
|
if (add) setFollowed(true);
|
||||||
} else {
|
} else {
|
||||||
const remove = await ark.unfollow(user.pubkey);
|
const remove = await NostrAccount.unfollow(user.pubkey);
|
||||||
if (remove) setFollowed(false);
|
if (remove) setFollowed(false);
|
||||||
}
|
}
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
@@ -35,7 +34,7 @@ export function UserFollowButton({
|
|||||||
async function status() {
|
async function status() {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
||||||
const contacts = await ark.get_contact_list();
|
const contacts = await NostrAccount.getContactList();
|
||||||
if (contacts?.includes(user.pubkey)) {
|
if (contacts?.includes(user.pubkey)) {
|
||||||
setFollowed(true);
|
setFollowed(true);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,21 +2,21 @@ import { VerifiedIcon } from "@lume/icons";
|
|||||||
import { displayLongHandle, displayNpub } from "@lume/utils";
|
import { displayLongHandle, displayNpub } from "@lume/utils";
|
||||||
import * as Tooltip from "@radix-ui/react-tooltip";
|
import * as Tooltip from "@radix-ui/react-tooltip";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { useRouteContext } from "@tanstack/react-router";
|
|
||||||
import { useUserContext } from "./provider";
|
import { useUserContext } from "./provider";
|
||||||
|
import { NostrQuery } from "@lume/system";
|
||||||
|
|
||||||
export function UserNip05() {
|
export function UserNip05() {
|
||||||
const user = useUserContext();
|
const user = useUserContext();
|
||||||
|
|
||||||
const { ark } = useRouteContext({ strict: false });
|
|
||||||
const { isLoading, data: verified } = useQuery({
|
const { isLoading, data: verified } = useQuery({
|
||||||
queryKey: ["nip05", user?.pubkey],
|
queryKey: ["nip05", user?.pubkey],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
if (!user.profile?.nip05) return false;
|
const verify = await NostrQuery.verifyNip05(
|
||||||
const verify = await ark.verify_nip05(user.pubkey, user.profile?.nip05);
|
user.pubkey,
|
||||||
|
user.profile?.nip05,
|
||||||
|
);
|
||||||
return verify;
|
return verify;
|
||||||
},
|
},
|
||||||
enabled: !!user.profile,
|
enabled: !!user.profile?.nip05,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!user.profile?.nip05?.length) return;
|
if (!user.profile?.nip05?.length) return;
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import { useProfile } from "@lume/ark";
|
import { useProfile } from "@lume/system";
|
||||||
import type { Metadata } from "@lume/types";
|
import type { Metadata } from "@lume/types";
|
||||||
import { type ReactNode, createContext, useContext } from "react";
|
import { type ReactNode, createContext, useContext } from "react";
|
||||||
|
|
||||||
const UserContext = createContext<{
|
const UserContext = createContext<{
|
||||||
pubkey: string;
|
pubkey: string;
|
||||||
|
profile: Metadata;
|
||||||
isError: boolean;
|
isError: boolean;
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
profile: Metadata;
|
|
||||||
}>(null);
|
}>(null);
|
||||||
|
|
||||||
export function UserProvider({
|
export function UserProvider({
|
||||||
@@ -21,7 +21,7 @@ export function UserProvider({
|
|||||||
const { isLoading, isError, profile } = useProfile(pubkey, embedProfile);
|
const { isLoading, isError, profile } = useProfile(pubkey, embedProfile);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<UserContext.Provider value={{ pubkey, isError, isLoading, profile }}>
|
<UserContext.Provider value={{ pubkey, profile, isError, isLoading }}>
|
||||||
{children}
|
{children}
|
||||||
</UserContext.Provider>
|
</UserContext.Provider>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,44 +1,28 @@
|
|||||||
import { Column } from "@/components/column";
|
import { Column } from "@/components/column";
|
||||||
import { Toolbar } from "@/components/toolbar";
|
import { Toolbar } from "@/components/toolbar";
|
||||||
import { ArrowLeftIcon, ArrowRightIcon } from "@lume/icons";
|
import { ArrowLeftIcon, ArrowRightIcon } from "@lume/icons";
|
||||||
|
import { NostrQuery } from "@lume/system";
|
||||||
import type { EventColumns, LumeColumn } from "@lume/types";
|
import type { EventColumns, LumeColumn } from "@lume/types";
|
||||||
import { createFileRoute } from "@tanstack/react-router";
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
import { listen } from "@tauri-apps/api/event";
|
import { listen } from "@tauri-apps/api/event";
|
||||||
import { resolveResource } from "@tauri-apps/api/path";
|
|
||||||
import { getCurrent } from "@tauri-apps/api/window";
|
import { getCurrent } from "@tauri-apps/api/window";
|
||||||
import { readTextFile } from "@tauri-apps/plugin-fs";
|
|
||||||
import { nanoid } from "nanoid";
|
import { nanoid } from "nanoid";
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import { useDebouncedCallback } from "use-debounce";
|
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")({
|
||||||
loader: async ({ context }) => {
|
loader: async () => {
|
||||||
try {
|
const columns = NostrQuery.getColumns();
|
||||||
const userColumns = await context.ark.get_columns();
|
return columns;
|
||||||
if (userColumns.length > 0) {
|
|
||||||
return userColumns;
|
|
||||||
} else {
|
|
||||||
const systemPath = "resources/system_columns.json";
|
|
||||||
const resourcePath = await resolveResource(systemPath);
|
|
||||||
const resourceFile = await readTextFile(resourcePath);
|
|
||||||
const systemColumns: LumeColumn[] = JSON.parse(resourceFile);
|
|
||||||
|
|
||||||
return systemColumns;
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error(String(e));
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
component: Screen,
|
component: Screen,
|
||||||
});
|
});
|
||||||
|
|
||||||
function Screen() {
|
function Screen() {
|
||||||
const userSavedColumns = Route.useLoaderData();
|
|
||||||
const vlistRef = useRef<VListHandle>(null);
|
|
||||||
|
|
||||||
const { account } = Route.useParams();
|
const { account } = Route.useParams();
|
||||||
const { ark } = Route.useRouteContext();
|
const initialColumnList = Route.useLoaderData();
|
||||||
|
const vlistRef = useRef<VListHandle>(null);
|
||||||
|
|
||||||
const [selectedIndex, setSelectedIndex] = useState(-1);
|
const [selectedIndex, setSelectedIndex] = useState(-1);
|
||||||
const [columns, setColumns] = useState([]);
|
const [columns, setColumns] = useState([]);
|
||||||
@@ -115,12 +99,12 @@ function Screen() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setColumns(userSavedColumns);
|
setColumns(initialColumnList);
|
||||||
}, [userSavedColumns]);
|
}, [initialColumnList]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// save state
|
// save state
|
||||||
ark.set_columns(columns);
|
NostrQuery.setColumns(columns);
|
||||||
}, [columns]);
|
}, [columns]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -148,11 +132,10 @@ function Screen() {
|
|||||||
horizontal
|
horizontal
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
itemSize={440}
|
itemSize={440}
|
||||||
overscan={3}
|
overscan={5}
|
||||||
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"
|
||||||
cache={null}
|
|
||||||
>
|
>
|
||||||
{columns.map((column) => (
|
{columns.map((column) => (
|
||||||
<Column
|
<Column
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import {
|
|||||||
PlusIcon,
|
PlusIcon,
|
||||||
SearchIcon,
|
SearchIcon,
|
||||||
} from "@lume/icons";
|
} from "@lume/icons";
|
||||||
import { type Event, Kind } from "@lume/types";
|
import { type NostrEvent, Kind } from "@lume/types";
|
||||||
import { User } from "@/components/user";
|
import { User } from "@/components/user";
|
||||||
import {
|
import {
|
||||||
cn,
|
cn,
|
||||||
@@ -19,20 +19,24 @@ import { getCurrent } from "@tauri-apps/api/window";
|
|||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import * as Popover from "@radix-ui/react-popover";
|
import * as Popover from "@radix-ui/react-popover";
|
||||||
|
import { LumeWindow, NostrAccount, NostrQuery } from "@lume/system";
|
||||||
|
import { Link } from "@tanstack/react-router";
|
||||||
|
|
||||||
|
type AccountSearch = {
|
||||||
|
accounts?: string[];
|
||||||
|
};
|
||||||
|
|
||||||
export const Route = createFileRoute("/$account")({
|
export const Route = createFileRoute("/$account")({
|
||||||
beforeLoad: async ({ context }) => {
|
validateSearch: (search: Record<string, unknown>): AccountSearch => {
|
||||||
const ark = context.ark;
|
return {
|
||||||
const accounts = await ark.get_accounts();
|
accounts: (search?.accounts as string[]) || [],
|
||||||
|
};
|
||||||
return { accounts };
|
|
||||||
},
|
},
|
||||||
component: Screen,
|
component: Screen,
|
||||||
});
|
});
|
||||||
|
|
||||||
function Screen() {
|
function Screen() {
|
||||||
const { ark, platform } = Route.useRouteContext();
|
const { platform } = Route.useRouteContext();
|
||||||
const navigate = Route.useNavigate();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-screen w-screen flex-col">
|
<div className="flex h-screen w-screen flex-col">
|
||||||
@@ -45,18 +49,17 @@ function Screen() {
|
|||||||
>
|
>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<Accounts />
|
<Accounts />
|
||||||
<button
|
<Link
|
||||||
type="button"
|
to="/landing/"
|
||||||
onClick={() => navigate({ to: "/landing/" })}
|
|
||||||
className="inline-flex size-8 shrink-0 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 shrink-0 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>
|
</Link>
|
||||||
</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={() => LumeWindow.openEditor()}
|
||||||
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" />
|
||||||
@@ -65,7 +68,7 @@ function Screen() {
|
|||||||
<Bell />
|
<Bell />
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => ark.open_search()}
|
onClick={() => LumeWindow.openSearch()}
|
||||||
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" />
|
||||||
@@ -81,7 +84,7 @@ function Screen() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function Accounts() {
|
function Accounts() {
|
||||||
const { ark, accounts } = Route.useRouteContext();
|
const { accounts } = Route.useSearch();
|
||||||
const { account } = Route.useParams();
|
const { account } = Route.useParams();
|
||||||
|
|
||||||
const [windowWidth, setWindowWidth] = useState<number>(null);
|
const [windowWidth, setWindowWidth] = useState<number>(null);
|
||||||
@@ -102,11 +105,11 @@ function Accounts() {
|
|||||||
|
|
||||||
const changeAccount = async (npub: string) => {
|
const changeAccount = async (npub: string) => {
|
||||||
if (npub === account) {
|
if (npub === account) {
|
||||||
return await ark.open_profile(account);
|
return await LumeWindow.openProfile(account);
|
||||||
}
|
}
|
||||||
|
|
||||||
// change current account and update signer
|
// change current account and update signer
|
||||||
const select = await ark.load_account(npub);
|
const select = await NostrAccount.loadAccount(npub);
|
||||||
|
|
||||||
if (select) {
|
if (select) {
|
||||||
return navigate({ to: "/$account/home", params: { account: npub } });
|
return navigate({ to: "/$account/home", params: { account: npub } });
|
||||||
@@ -190,9 +193,7 @@ function Accounts() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function Bell() {
|
function Bell() {
|
||||||
const { ark } = Route.useRouteContext();
|
|
||||||
const { account } = Route.useParams();
|
const { account } = Route.useParams();
|
||||||
|
|
||||||
const [count, setCount] = useState(0);
|
const [count, setCount] = useState(0);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -202,8 +203,8 @@ function Bell() {
|
|||||||
setCount((prevCount) => prevCount + 1);
|
setCount((prevCount) => prevCount + 1);
|
||||||
await invoke("set_badge", { count });
|
await invoke("set_badge", { count });
|
||||||
|
|
||||||
const event: Event = JSON.parse(payload.payload);
|
const event: NostrEvent = JSON.parse(payload.payload);
|
||||||
const user = await ark.get_profile(event.pubkey);
|
const user = await NostrQuery.getProfile(event.pubkey);
|
||||||
const userName =
|
const userName =
|
||||||
user.display_name || user.name || displayNpub(event.pubkey, 16);
|
user.display_name || user.name || displayNpub(event.pubkey, 16);
|
||||||
|
|
||||||
@@ -240,7 +241,7 @@ function Bell() {
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setCount(0);
|
setCount(0);
|
||||||
ark.open_activity(account);
|
LumeWindow.openActivity(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"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,35 +1,21 @@
|
|||||||
import type { Ark } from "@lume/ark";
|
|
||||||
import { CheckCircleIcon, InfoCircleIcon, CancelCircleIcon } from "@lume/icons";
|
import { CheckCircleIcon, InfoCircleIcon, CancelCircleIcon } from "@lume/icons";
|
||||||
import type { Interests, Metadata, Settings } from "@lume/types";
|
import type { Settings } from "@lume/types";
|
||||||
import { Spinner } from "@lume/ui";
|
import { Spinner } from "@lume/ui";
|
||||||
import type { QueryClient } from "@tanstack/react-query";
|
import type { QueryClient } from "@tanstack/react-query";
|
||||||
import { Outlet, createRootRouteWithContext } from "@tanstack/react-router";
|
import { Outlet, createRootRouteWithContext } from "@tanstack/react-router";
|
||||||
import type { Platform } from "@tauri-apps/plugin-os";
|
import type { Platform } from "@tauri-apps/plugin-os";
|
||||||
import type { Descendant } from "slate";
|
|
||||||
import { Toaster } from "sonner";
|
import { Toaster } from "sonner";
|
||||||
|
|
||||||
type EditorElement = {
|
|
||||||
type: string;
|
|
||||||
children: Descendant[];
|
|
||||||
eventId?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
interface RouterContext {
|
interface RouterContext {
|
||||||
// System
|
// System
|
||||||
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;
|
// Accounts
|
||||||
// Profile
|
|
||||||
accounts?: string[];
|
accounts?: string[];
|
||||||
profile?: Metadata;
|
|
||||||
isNewUser?: boolean;
|
|
||||||
// Editor
|
|
||||||
initialValue?: EditorElement[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Route = createRootRouteWithContext<RouterContext>()({
|
export const Route = createRootRouteWithContext<RouterContext>()({
|
||||||
|
|||||||
@@ -3,11 +3,11 @@ import { Note } from "@/components/note";
|
|||||||
import { Await, createFileRoute, defer } from "@tanstack/react-router";
|
import { Await, createFileRoute, defer } from "@tanstack/react-router";
|
||||||
import { Suspense } from "react";
|
import { Suspense } from "react";
|
||||||
import { Virtualizer } from "virtua";
|
import { Virtualizer } from "virtua";
|
||||||
|
import { NostrQuery } from "@lume/system";
|
||||||
|
|
||||||
export const Route = createFileRoute("/activity/$account/texts")({
|
export const Route = createFileRoute("/activity/$account/texts")({
|
||||||
loader: async ({ context, params }) => {
|
loader: async ({ params }) => {
|
||||||
const ark = context.ark;
|
return { data: defer(NostrQuery.getUserActivities(params.account, "1")) };
|
||||||
return { data: defer(ark.get_activities(params.account, "1")) };
|
|
||||||
},
|
},
|
||||||
component: Screen,
|
component: Screen,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ function Screen() {
|
|||||||
const { account } = Route.useParams();
|
const { account } = Route.useParams();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container withDrag withNavigate={false}>
|
<Container withDrag>
|
||||||
<Box className="scrollbar-none shadow-none bg-black/5 dark:bg-white/5 backdrop-blur-sm flex flex-col overflow-y-auto">
|
<Box className="scrollbar-none shadow-none bg-black/5 dark:bg-white/5 backdrop-blur-sm flex flex-col overflow-y-auto">
|
||||||
<div className="h-14 shrink-0 flex w-full items-center gap-1 px-3">
|
<div className="h-14 shrink-0 flex w-full items-center gap-1 px-3">
|
||||||
<div className="inline-flex h-full w-full items-center gap-1">
|
<div className="inline-flex h-full w-full items-center gap-1">
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { User } from "@/components/user";
|
import { User } from "@/components/user";
|
||||||
|
import { NostrQuery } from "@lume/system";
|
||||||
import { Spinner } from "@lume/ui";
|
import { Spinner } from "@lume/ui";
|
||||||
import { decodeZapInvoice } from "@lume/utils";
|
import { decodeZapInvoice } from "@lume/utils";
|
||||||
import { Await, createFileRoute, defer } from "@tanstack/react-router";
|
import { Await, createFileRoute, defer } from "@tanstack/react-router";
|
||||||
@@ -6,9 +7,10 @@ import { Suspense } from "react";
|
|||||||
import { Virtualizer } from "virtua";
|
import { Virtualizer } from "virtua";
|
||||||
|
|
||||||
export const Route = createFileRoute("/activity/$account/zaps")({
|
export const Route = createFileRoute("/activity/$account/zaps")({
|
||||||
loader: async ({ context, params }) => {
|
loader: async ({ params }) => {
|
||||||
const ark = context.ark;
|
return {
|
||||||
return { data: defer(ark.get_activities(params.account, "9735")) };
|
data: defer(NostrQuery.getUserActivities(params.account, "9735")),
|
||||||
|
};
|
||||||
},
|
},
|
||||||
component: Screen,
|
component: Screen,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,43 +1,29 @@
|
|||||||
import { LaurelIcon } from "@lume/icons";
|
import { LaurelIcon } from "@lume/icons";
|
||||||
import type { AppRouteSearch, Settings } from "@lume/types";
|
import { NostrQuery } from "@lume/system";
|
||||||
import { Spinner } from "@lume/ui";
|
import { Spinner } from "@lume/ui";
|
||||||
import * as Switch from "@radix-ui/react-switch";
|
import * as Switch from "@radix-ui/react-switch";
|
||||||
import { createFileRoute, useNavigate } from "@tanstack/react-router";
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
import {
|
import { requestPermission } from "@tauri-apps/plugin-notification";
|
||||||
isPermissionGranted,
|
|
||||||
requestPermission,
|
|
||||||
} from "@tauri-apps/plugin-notification";
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
|
||||||
export const Route = createFileRoute("/auth/settings")({
|
export const Route = createFileRoute("/auth/$account/settings")({
|
||||||
validateSearch: (search: Record<string, string>): AppRouteSearch => {
|
beforeLoad: async () => {
|
||||||
return {
|
const settings = await NostrQuery.getSettings();
|
||||||
account: search.account,
|
return { settings };
|
||||||
};
|
|
||||||
},
|
|
||||||
beforeLoad: async ({ context }) => {
|
|
||||||
const permissionGranted = await isPermissionGranted(); // get notification permission
|
|
||||||
const ark = context.ark;
|
|
||||||
const settings = await ark.get_settings();
|
|
||||||
|
|
||||||
return {
|
|
||||||
settings: { ...settings, notification: permissionGranted },
|
|
||||||
};
|
|
||||||
},
|
},
|
||||||
component: Screen,
|
component: Screen,
|
||||||
pendingComponent: Pending,
|
pendingComponent: Pending,
|
||||||
});
|
});
|
||||||
|
|
||||||
function Screen() {
|
function Screen() {
|
||||||
const navigate = useNavigate();
|
const navigate = Route.useNavigate();
|
||||||
|
const { account } = Route.useParams();
|
||||||
const { account } = Route.useSearch();
|
const { settings } = Route.useRouteContext();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { ark, settings } = Route.useRouteContext();
|
|
||||||
|
|
||||||
const [newSettings, setNewSettings] = useState<Settings>(settings);
|
const [newSettings, setNewSettings] = useState(settings);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
const toggleNofitication = async () => {
|
const toggleNofitication = async () => {
|
||||||
@@ -82,7 +68,7 @@ function Screen() {
|
|||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
||||||
// publish settings
|
// publish settings
|
||||||
const eventId = await ark.set_settings(newSettings);
|
const eventId = await NostrQuery.setSettings(newSettings);
|
||||||
|
|
||||||
if (eventId) {
|
if (eventId) {
|
||||||
return navigate({
|
return navigate({
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
import { CheckIcon } from "@lume/icons";
|
import { CheckIcon } from "@lume/icons";
|
||||||
import type { AppRouteSearch } from "@lume/types";
|
|
||||||
import { Spinner } from "@lume/ui";
|
import { Spinner } from "@lume/ui";
|
||||||
import { displayNsec } from "@lume/utils";
|
import { displayNsec } from "@lume/utils";
|
||||||
import * as Checkbox from "@radix-ui/react-checkbox";
|
import * as Checkbox from "@radix-ui/react-checkbox";
|
||||||
@@ -10,17 +9,12 @@ import { useState } from "react";
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
|
||||||
export const Route = createFileRoute("/auth/new/backup")({
|
export const Route = createFileRoute("/auth/new/$account/backup")({
|
||||||
validateSearch: (search: Record<string, string>): AppRouteSearch => {
|
|
||||||
return {
|
|
||||||
account: search.account,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
component: Screen,
|
component: Screen,
|
||||||
});
|
});
|
||||||
|
|
||||||
function Screen() {
|
function Screen() {
|
||||||
const { account } = Route.useSearch();
|
const { account } = Route.useParams();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const [key, setKey] = useState(null);
|
const [key, setKey] = useState(null);
|
||||||
@@ -39,8 +33,8 @@ function Screen() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return navigate({
|
return navigate({
|
||||||
to: "/auth/settings",
|
to: "/auth/$account/settings",
|
||||||
search: { account },
|
params: { account },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { AvatarUploader } from "@/components/avatarUploader";
|
import { AvatarUploader } from "@/components/avatarUploader";
|
||||||
import { PlusIcon } from "@lume/icons";
|
import { PlusIcon } from "@lume/icons";
|
||||||
|
import { NostrAccount } from "@lume/system";
|
||||||
import type { Metadata } from "@lume/types";
|
import type { Metadata } from "@lume/types";
|
||||||
import { Spinner } from "@lume/ui";
|
import { Spinner } from "@lume/ui";
|
||||||
import { createFileRoute, useNavigate } from "@tanstack/react-router";
|
import { createFileRoute, useNavigate } from "@tanstack/react-router";
|
||||||
@@ -10,17 +11,17 @@ import { toast } from "sonner";
|
|||||||
|
|
||||||
export const Route = createFileRoute("/auth/new/profile")({
|
export const Route = createFileRoute("/auth/new/profile")({
|
||||||
component: Screen,
|
component: Screen,
|
||||||
loader: ({ context }) => {
|
loader: async () => {
|
||||||
return context.ark.create_keys();
|
const account = await NostrAccount.createAccount();
|
||||||
|
return account;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
function Screen() {
|
function Screen() {
|
||||||
const keys = Route.useLoaderData();
|
const account = Route.useLoaderData();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { ark } = Route.useRouteContext();
|
|
||||||
const { register, handleSubmit } = useForm();
|
const { register, handleSubmit } = useForm();
|
||||||
|
|
||||||
const [picture, setPicture] = useState<string>("");
|
const [picture, setPicture] = useState<string>("");
|
||||||
@@ -35,17 +36,17 @@ function Screen() {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// Save account keys
|
// Save account keys
|
||||||
const save = await ark.save_account(keys.nsec);
|
const save = await NostrAccount.saveAccount(account.nsec);
|
||||||
|
|
||||||
// Then create profile
|
// Then create profile
|
||||||
if (save) {
|
if (save) {
|
||||||
const profile: Metadata = { ...data, picture };
|
const profile: Metadata = { ...data, picture };
|
||||||
const eventId = await ark.create_profile(profile);
|
const eventId = await NostrAccount.createProfile(profile);
|
||||||
|
|
||||||
if (eventId) {
|
if (eventId) {
|
||||||
navigate({
|
navigate({
|
||||||
to: "/auth/new/backup",
|
to: "/auth/new/$account/backup",
|
||||||
search: { account: keys.npub },
|
params: { account: account.npub },
|
||||||
replace: true,
|
replace: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { NostrAccount } from "@lume/system";
|
||||||
import { Spinner } from "@lume/ui";
|
import { Spinner } from "@lume/ui";
|
||||||
import { createLazyFileRoute } from "@tanstack/react-router";
|
import { createLazyFileRoute } from "@tanstack/react-router";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
@@ -8,7 +9,6 @@ export const Route = createLazyFileRoute("/auth/privkey")({
|
|||||||
});
|
});
|
||||||
|
|
||||||
function Screen() {
|
function Screen() {
|
||||||
const { ark } = Route.useRouteContext();
|
|
||||||
const navigate = Route.useNavigate();
|
const navigate = Route.useNavigate();
|
||||||
|
|
||||||
const [key, setKey] = useState("");
|
const [key, setKey] = useState("");
|
||||||
@@ -24,12 +24,12 @@ function Screen() {
|
|||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
||||||
const npub = await ark.save_account(key, password);
|
const npub = await NostrAccount.saveAccount(key, password);
|
||||||
|
|
||||||
if (npub) {
|
if (npub) {
|
||||||
navigate({
|
navigate({
|
||||||
to: "/auth/settings",
|
to: "/auth/$account/settings",
|
||||||
search: { account: npub },
|
params: { account: npub },
|
||||||
replace: true,
|
replace: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { NostrAccount } from "@lume/system";
|
||||||
import { Spinner } from "@lume/ui";
|
import { Spinner } from "@lume/ui";
|
||||||
import { createLazyFileRoute } from "@tanstack/react-router";
|
import { createLazyFileRoute } from "@tanstack/react-router";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
@@ -8,7 +9,6 @@ export const Route = createLazyFileRoute("/auth/remote")({
|
|||||||
});
|
});
|
||||||
|
|
||||||
function Screen() {
|
function Screen() {
|
||||||
const { ark } = Route.useRouteContext();
|
|
||||||
const navigate = Route.useNavigate();
|
const navigate = Route.useNavigate();
|
||||||
|
|
||||||
const [uri, setUri] = useState("");
|
const [uri, setUri] = useState("");
|
||||||
@@ -23,12 +23,12 @@ function Screen() {
|
|||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
||||||
const npub = await ark.nostr_connect(uri);
|
const npub = await NostrAccount.connectRemoteAccount(uri);
|
||||||
|
|
||||||
if (npub) {
|
if (npub) {
|
||||||
navigate({
|
navigate({
|
||||||
to: "/auth/settings",
|
to: "/auth/$account/settings",
|
||||||
search: { account: npub },
|
params: { account: npub },
|
||||||
replace: true,
|
replace: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import { CancelIcon, CheckCircleIcon, PlusIcon } from "@lume/icons";
|
import { CancelIcon, PlusIcon } from "@lume/icons";
|
||||||
import type { ColumnRouteSearch } from "@lume/types";
|
import type { ColumnRouteSearch } from "@lume/types";
|
||||||
import { Spinner } from "@lume/ui";
|
import { Spinner } from "@lume/ui";
|
||||||
import { User } from "@/components/user";
|
import { User } from "@/components/user";
|
||||||
import { createFileRoute } from "@tanstack/react-router";
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
import { NostrAccount, NostrQuery } from "@lume/system";
|
||||||
|
|
||||||
export const Route = createFileRoute("/create-group")({
|
export const Route = createFileRoute("/create-group")({
|
||||||
validateSearch: (search: Record<string, string>): ColumnRouteSearch => {
|
validateSearch: (search: Record<string, string>): ColumnRouteSearch => {
|
||||||
@@ -14,17 +15,14 @@ export const Route = createFileRoute("/create-group")({
|
|||||||
name: search.name,
|
name: search.name,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
loader: async ({ context }) => {
|
loader: async () => {
|
||||||
const ark = context.ark;
|
const contacts = await NostrAccount.getContactList();
|
||||||
const contacts = await ark.get_contact_list();
|
|
||||||
return contacts;
|
return contacts;
|
||||||
},
|
},
|
||||||
component: Screen,
|
component: Screen,
|
||||||
});
|
});
|
||||||
|
|
||||||
function Screen() {
|
function Screen() {
|
||||||
const { ark } = Route.useRouteContext();
|
|
||||||
|
|
||||||
const [title, setTitle] = useState("");
|
const [title, setTitle] = useState("");
|
||||||
const [npub, setNpub] = useState("");
|
const [npub, setNpub] = useState("");
|
||||||
const [users, setUsers] = useState<string[]>([
|
const [users, setUsers] = useState<string[]>([
|
||||||
@@ -57,7 +55,10 @@ function Screen() {
|
|||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
|
|
||||||
const key = `lume_group_${search.label}`;
|
const key = `lume_group_${search.label}`;
|
||||||
const createGroup = await ark.set_nstore(key, JSON.stringify(users));
|
const createGroup = await NostrQuery.setNstore(
|
||||||
|
key,
|
||||||
|
JSON.stringify(users),
|
||||||
|
);
|
||||||
|
|
||||||
if (createGroup) {
|
if (createGroup) {
|
||||||
return navigate({ to: search.redirect, search: { ...search } });
|
return navigate({ to: search.redirect, search: { ...search } });
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
|
import { NostrAccount } from "@lume/system";
|
||||||
import type { ColumnRouteSearch } from "@lume/types";
|
import type { ColumnRouteSearch } from "@lume/types";
|
||||||
import { Spinner } from "@lume/ui";
|
import { Spinner } from "@lume/ui";
|
||||||
import { createFileRoute } from "@tanstack/react-router";
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
import { invoke } from "@tauri-apps/api/core";
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
|
||||||
@@ -30,7 +30,7 @@ function Screen() {
|
|||||||
try {
|
try {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
|
|
||||||
const sync: boolean = await invoke("friend_to_friend", { npub });
|
const sync = await NostrAccount.f2f(npub);
|
||||||
|
|
||||||
if (sync) {
|
if (sync) {
|
||||||
return navigate({ to: redirect });
|
return navigate({ to: redirect });
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { User } from "@/components/user";
|
|||||||
import { Spinner } from "@lume/ui";
|
import { Spinner } from "@lume/ui";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import type { ColumnRouteSearch } from "@lume/types";
|
import type { ColumnRouteSearch } from "@lume/types";
|
||||||
|
import { NostrAccount } from "@lume/system";
|
||||||
|
|
||||||
export const Route = createFileRoute("/create-newsfeed/users")({
|
export const Route = createFileRoute("/create-newsfeed/users")({
|
||||||
validateSearch: (search: Record<string, string>): ColumnRouteSearch => {
|
validateSearch: (search: Record<string, string>): ColumnRouteSearch => {
|
||||||
@@ -31,7 +32,6 @@ export const Route = createFileRoute("/create-newsfeed/users")({
|
|||||||
});
|
});
|
||||||
|
|
||||||
function Screen() {
|
function Screen() {
|
||||||
const { ark } = Route.useRouteContext();
|
|
||||||
const { data } = Route.useLoaderData();
|
const { data } = Route.useLoaderData();
|
||||||
const { redirect } = Route.useSearch();
|
const { redirect } = Route.useSearch();
|
||||||
|
|
||||||
@@ -52,7 +52,7 @@ function Screen() {
|
|||||||
try {
|
try {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
|
|
||||||
const newContactList = await ark.set_contact_list(follows);
|
const newContactList = await NostrAccount.setContactList(follows);
|
||||||
|
|
||||||
if (newContactList) {
|
if (newContactList) {
|
||||||
return navigate({ to: redirect });
|
return navigate({ to: redirect });
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { CheckCircleIcon } from "@lume/icons";
|
import { CheckCircleIcon } from "@lume/icons";
|
||||||
|
import { NostrQuery } from "@lume/system";
|
||||||
import type { ColumnRouteSearch, Topic } from "@lume/types";
|
import type { ColumnRouteSearch, Topic } from "@lume/types";
|
||||||
import { Spinner } from "@lume/ui";
|
import { Spinner } from "@lume/ui";
|
||||||
import { TOPICS } from "@lume/utils";
|
import { TOPICS } from "@lume/utils";
|
||||||
@@ -18,8 +19,6 @@ export const Route = createFileRoute("/create-topic")({
|
|||||||
});
|
});
|
||||||
|
|
||||||
function Screen() {
|
function Screen() {
|
||||||
const { ark } = Route.useRouteContext();
|
|
||||||
|
|
||||||
const [topics, setTopics] = useState<Topic[]>([]);
|
const [topics, setTopics] = useState<Topic[]>([]);
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
@@ -39,7 +38,10 @@ function Screen() {
|
|||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
|
|
||||||
const key = `lume_topic_${search.label}`;
|
const key = `lume_topic_${search.label}`;
|
||||||
const createTopic = await ark.set_nstore(key, JSON.stringify(topics));
|
const createTopic = await NostrQuery.setNstore(
|
||||||
|
key,
|
||||||
|
JSON.stringify(topics),
|
||||||
|
);
|
||||||
|
|
||||||
if (createTopic) {
|
if (createTopic) {
|
||||||
return navigate({ to: search.redirect, search: { ...search } });
|
return navigate({ to: search.redirect, search: { ...search } });
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { AddMediaIcon } from "@lume/icons";
|
import { AddMediaIcon } from "@lume/icons";
|
||||||
|
import { NostrQuery } from "@lume/system";
|
||||||
import { Spinner } from "@lume/ui";
|
import { Spinner } from "@lume/ui";
|
||||||
import { cn, insertImage, isImagePath } from "@lume/utils";
|
import { cn, insertImage, isImagePath } from "@lume/utils";
|
||||||
import * as Tooltip from "@radix-ui/react-tooltip";
|
import * as Tooltip from "@radix-ui/react-tooltip";
|
||||||
import { useRouteContext } from "@tanstack/react-router";
|
|
||||||
import type { UnlistenFn } from "@tauri-apps/api/event";
|
import type { UnlistenFn } from "@tauri-apps/api/event";
|
||||||
import { getCurrent } from "@tauri-apps/api/window";
|
import { getCurrent } from "@tauri-apps/api/window";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
@@ -11,15 +11,14 @@ import { toast } from "sonner";
|
|||||||
|
|
||||||
export function MediaButton({ className }: { className?: string }) {
|
export function MediaButton({ className }: { className?: string }) {
|
||||||
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 upload = async () => {
|
||||||
try {
|
try {
|
||||||
// start loading
|
// start loading
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
||||||
const image = await ark.upload();
|
const image = await NostrQuery.upload();
|
||||||
insertImage(editor, image);
|
insertImage(editor, image);
|
||||||
|
|
||||||
// reset loading
|
// reset loading
|
||||||
@@ -44,7 +43,7 @@ export function MediaButton({ className }: { className?: string }) {
|
|||||||
// upload all images
|
// upload all images
|
||||||
for (const item of items) {
|
for (const item of items) {
|
||||||
if (isImagePath(item)) {
|
if (isImagePath(item)) {
|
||||||
const image = await ark.upload(item);
|
const image = await NostrQuery.upload(item);
|
||||||
insertImage(editor, image);
|
insertImage(editor, image);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -67,7 +66,7 @@ export function MediaButton({ className }: { className?: string }) {
|
|||||||
<Tooltip.Trigger asChild>
|
<Tooltip.Trigger asChild>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => uploadToNostrBuild()}
|
onClick={() => upload()}
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
className={cn("inline-flex items-center justify-center", className)}
|
className={cn("inline-flex items-center justify-center", className)}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -3,20 +3,19 @@ import { cn, insertMention } from "@lume/utils";
|
|||||||
import * as Tooltip from "@radix-ui/react-tooltip";
|
import * as Tooltip from "@radix-ui/react-tooltip";
|
||||||
import * as DropdownMenu from "@radix-ui/react-dropdown-menu";
|
import * as DropdownMenu from "@radix-ui/react-dropdown-menu";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useRouteContext } from "@tanstack/react-router";
|
|
||||||
import { useSlateStatic } from "slate-react";
|
import { useSlateStatic } from "slate-react";
|
||||||
import type { Contact } from "@lume/types";
|
import type { Contact } from "@lume/types";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { User } from "@/components/user";
|
import { User } from "@/components/user";
|
||||||
|
import { NostrAccount, NostrQuery } from "@lume/system";
|
||||||
|
|
||||||
export function MentionButton({ className }: { className?: string }) {
|
export function MentionButton({ className }: { className?: string }) {
|
||||||
const editor = useSlateStatic();
|
const editor = useSlateStatic();
|
||||||
const { ark } = useRouteContext({ strict: false });
|
|
||||||
const [contacts, setContacts] = useState<string[]>([]);
|
const [contacts, setContacts] = useState<string[]>([]);
|
||||||
|
|
||||||
const select = async (user: string) => {
|
const select = async (user: string) => {
|
||||||
try {
|
try {
|
||||||
const metadata = await ark.get_profile(user);
|
const metadata = await NostrQuery.getProfile(user);
|
||||||
const contact: Contact = { pubkey: user, profile: metadata };
|
const contact: Contact = { pubkey: user, profile: metadata };
|
||||||
|
|
||||||
insertMention(editor, contact);
|
insertMention(editor, contact);
|
||||||
@@ -27,7 +26,7 @@ export function MentionButton({ className }: { className?: string }) {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
async function getContacts() {
|
async function getContacts() {
|
||||||
const data = await ark.get_contact_list();
|
const data = await NostrAccount.getContactList();
|
||||||
setContacts(data);
|
setContacts(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,20 +60,26 @@ export function MentionButton({ className }: { className?: string }) {
|
|||||||
</Tooltip.Provider>
|
</Tooltip.Provider>
|
||||||
<DropdownMenu.Portal>
|
<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">
|
<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) => (
|
{contacts.length < 1 ? (
|
||||||
<DropdownMenu.Item
|
<div className="w-full h-full flex items-center justify-center">
|
||||||
key={contact}
|
<p className="text-sm text-white">Contact List is empty.</p>
|
||||||
onClick={() => select(contact)}
|
</div>
|
||||||
className="shrink-0 h-11 flex items-center hover:bg-white/10 px-2"
|
) : (
|
||||||
>
|
contacts.map((contact) => (
|
||||||
<User.Provider pubkey={contact}>
|
<DropdownMenu.Item
|
||||||
<User.Root className="flex items-center gap-2">
|
key={contact}
|
||||||
<User.Avatar className="shrink-0 size-8 rounded-full" />
|
onClick={() => select(contact)}
|
||||||
<User.Name className="text-sm font-medium text-white dark:text-black" />
|
className="shrink-0 h-11 flex items-center hover:bg-white/10 px-2"
|
||||||
</User.Root>
|
>
|
||||||
</User.Provider>
|
<User.Provider pubkey={contact}>
|
||||||
</DropdownMenu.Item>
|
<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.Arrow className="fill-neutral-950 dark:fill-neutral-50" />
|
||||||
</DropdownMenu.Content>
|
</DropdownMenu.Content>
|
||||||
</DropdownMenu.Portal>
|
</DropdownMenu.Portal>
|
||||||
|
|||||||
@@ -3,13 +3,13 @@ import { cn } from "@lume/utils";
|
|||||||
import * as Tooltip from "@radix-ui/react-tooltip";
|
import * as Tooltip from "@radix-ui/react-tooltip";
|
||||||
import type { Dispatch, SetStateAction } from "react";
|
import type { Dispatch, SetStateAction } from "react";
|
||||||
|
|
||||||
export function NsfwToggle({
|
export function WarningToggle({
|
||||||
nsfw,
|
warning,
|
||||||
setNsfw,
|
setWarning,
|
||||||
className,
|
className,
|
||||||
}: {
|
}: {
|
||||||
nsfw: boolean;
|
warning: boolean;
|
||||||
setNsfw: Dispatch<SetStateAction<boolean>>;
|
setWarning: Dispatch<SetStateAction<boolean>>;
|
||||||
className?: string;
|
className?: string;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
@@ -18,11 +18,11 @@ export function NsfwToggle({
|
|||||||
<Tooltip.Trigger asChild>
|
<Tooltip.Trigger asChild>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setNsfw((prev) => !prev)}
|
onClick={() => setWarning((prev) => !prev)}
|
||||||
className={cn(
|
className={cn(
|
||||||
"inline-flex items-center justify-center",
|
"inline-flex items-center justify-center",
|
||||||
className,
|
className,
|
||||||
nsfw ? "bg-blue-500 text-white" : "",
|
warning ? "bg-blue-500 text-white" : "",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<NsfwIcon className="size-4" />
|
<NsfwIcon className="size-4" />
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { ComposeFilledIcon, TrashIcon } from "@lume/icons";
|
import { ComposeFilledIcon } from "@lume/icons";
|
||||||
import { Spinner } from "@lume/ui";
|
import { Spinner } from "@lume/ui";
|
||||||
import {
|
import {
|
||||||
cn,
|
cn,
|
||||||
@@ -8,7 +8,6 @@ import {
|
|||||||
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 { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { type Descendant, Node, Transforms, createEditor } from "slate";
|
import { type Descendant, Node, Transforms, createEditor } from "slate";
|
||||||
@@ -22,16 +21,22 @@ import {
|
|||||||
withReact,
|
withReact,
|
||||||
} from "slate-react";
|
} from "slate-react";
|
||||||
import { MediaButton } from "./-components/media";
|
import { MediaButton } from "./-components/media";
|
||||||
import { NsfwToggle } from "./-components/nsfw";
|
|
||||||
import { MentionButton } from "./-components/mention";
|
import { MentionButton } from "./-components/mention";
|
||||||
|
import { LumeEvent } from "@lume/system";
|
||||||
|
import { WarningToggle } from "./-components/warning";
|
||||||
import { MentionNote } from "@/components/note/mentions/note";
|
import { MentionNote } from "@/components/note/mentions/note";
|
||||||
import { toast } from "sonner";
|
|
||||||
|
|
||||||
type EditorSearch = {
|
type EditorSearch = {
|
||||||
reply_to: string;
|
reply_to: string;
|
||||||
quote: boolean;
|
quote: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type EditorElement = {
|
||||||
|
type: string;
|
||||||
|
children: Descendant[];
|
||||||
|
eventId?: string;
|
||||||
|
};
|
||||||
|
|
||||||
export const Route = createFileRoute("/editor/")({
|
export const Route = createFileRoute("/editor/")({
|
||||||
validateSearch: (search: Record<string, string>): EditorSearch => {
|
validateSearch: (search: Record<string, string>): EditorSearch => {
|
||||||
return {
|
return {
|
||||||
@@ -39,43 +44,23 @@ export const Route = createFileRoute("/editor/")({
|
|||||||
quote: search.quote === "true" || false,
|
quote: search.quote === "true" || false,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
beforeLoad: async ({ search }) => {
|
|
||||||
return {
|
|
||||||
initialValue: search.quote
|
|
||||||
? [
|
|
||||||
{
|
|
||||||
type: "paragraph",
|
|
||||||
children: [{ text: "" }],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: "event",
|
|
||||||
eventId: `nostr:${nip19.noteEncode(search.reply_to)}`,
|
|
||||||
children: [{ text: "" }],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: "paragraph",
|
|
||||||
children: [{ text: "" }],
|
|
||||||
},
|
|
||||||
]
|
|
||||||
: [
|
|
||||||
{
|
|
||||||
type: "paragraph",
|
|
||||||
children: [{ text: "" }],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
},
|
|
||||||
component: Screen,
|
component: Screen,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const initialValue: EditorElement[] = [
|
||||||
|
{
|
||||||
|
type: "paragraph",
|
||||||
|
children: [{ text: "" }],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
function Screen() {
|
function Screen() {
|
||||||
const { reply_to, quote } = Route.useSearch();
|
const search = Route.useSearch();
|
||||||
const { ark, initialValue } = Route.useRouteContext();
|
|
||||||
|
|
||||||
const [t] = useTranslation();
|
const [t] = useTranslation();
|
||||||
const [editorValue, setEditorValue] = useState(initialValue);
|
const [editorValue, setEditorValue] = useState(initialValue);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [nsfw, setNsfw] = useState(false);
|
const [warning, setWarning] = useState(false);
|
||||||
const [editor] = useState(() =>
|
const [editor] = useState(() =>
|
||||||
withMentions(withNostrEvent(withImages(withReact(createEditor())))),
|
withMentions(withNostrEvent(withImages(withReact(createEditor())))),
|
||||||
);
|
);
|
||||||
@@ -116,7 +101,12 @@ function Screen() {
|
|||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
||||||
const content = serialize(editor.children);
|
const content = serialize(editor.children);
|
||||||
const eventId = await ark.publish(content, reply_to, quote);
|
const eventId = await LumeEvent.publish(
|
||||||
|
content,
|
||||||
|
search.reply_to,
|
||||||
|
search.quote,
|
||||||
|
warning,
|
||||||
|
);
|
||||||
|
|
||||||
if (eventId) {
|
if (eventId) {
|
||||||
await sendNativeNotification(
|
await sendNativeNotification(
|
||||||
@@ -137,15 +127,15 @@ function Screen() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full h-full">
|
<div className="w-full h-full flex flex-col">
|
||||||
<Slate editor={editor} initialValue={editorValue}>
|
<Slate editor={editor} initialValue={editorValue}>
|
||||||
<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 border-b border-black/10 dark:border-white/10"
|
className="shrink-0 flex h-14 w-full items-center justify-end gap-2 px-2 border-b border-black/10 dark:border-white/10"
|
||||||
>
|
>
|
||||||
<NsfwToggle
|
<WarningToggle
|
||||||
nsfw={nsfw}
|
warning={warning}
|
||||||
setNsfw={setNsfw}
|
setWarning={setWarning}
|
||||||
className="size-8 rounded-full bg-black/10 hover:bg-black/20 dark:bg-white/10 dark:hover:bg-white/20"
|
className="size-8 rounded-full bg-black/10 hover:bg-black/20 dark:bg-white/10 dark:hover:bg-white/20"
|
||||||
/>
|
/>
|
||||||
<MentionButton className="size-8 rounded-full bg-black/10 hover:bg-black/20 dark:bg-white/10 dark:hover:bg-white/20" />
|
<MentionButton className="size-8 rounded-full bg-black/10 hover:bg-black/20 dark:bg-white/10 dark:hover:bg-white/20" />
|
||||||
@@ -163,13 +153,13 @@ function Screen() {
|
|||||||
{t("global.post")}
|
{t("global.post")}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex h-full w-full flex-1 flex-col">
|
<div className="flex-1 overflow-y-auto flex flex-col">
|
||||||
{reply_to && !quote ? (
|
{search.reply_to ? (
|
||||||
<div className="px-4 py-2">
|
<div className="px-4 py-2">
|
||||||
<MentionNote eventId={reply_to} />
|
<MentionNote eventId={search.reply_to} />
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
<div className="overflow-y-auto p-4">
|
<div className="overflow-y-auto scrollbar-none p-4">
|
||||||
<Editable
|
<Editable
|
||||||
key={JSON.stringify(editorValue)}
|
key={JSON.stringify(editorValue)}
|
||||||
autoFocus={true}
|
autoFocus={true}
|
||||||
@@ -178,7 +168,7 @@ function Screen() {
|
|||||||
spellCheck={false}
|
spellCheck={false}
|
||||||
renderElement={(props) => <Element {...props} />}
|
renderElement={(props) => <Element {...props} />}
|
||||||
placeholder={
|
placeholder={
|
||||||
reply_to ? "Type your reply..." : t("editor.placeholder")
|
search.reply_to ? "Type your reply..." : t("editor.placeholder")
|
||||||
}
|
}
|
||||||
className="focus:outline-none"
|
className="focus:outline-none"
|
||||||
/>
|
/>
|
||||||
@@ -252,35 +242,24 @@ const withImages = (editor: ReactEditor) => {
|
|||||||
return editor;
|
return editor;
|
||||||
};
|
};
|
||||||
|
|
||||||
const Image = ({ attributes, children, element }) => {
|
const Image = ({ attributes, element, children }) => {
|
||||||
const editor = useSlateStatic();
|
const editor = useSlateStatic();
|
||||||
const path = ReactEditor.findPath(editor as ReactEditor, element);
|
|
||||||
|
|
||||||
const selected = useSelected();
|
const selected = useSelected();
|
||||||
const focused = useFocused();
|
const focused = useFocused();
|
||||||
|
const path = ReactEditor.findPath(editor as ReactEditor, element);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div {...attributes}>
|
<div {...attributes}>
|
||||||
{children}
|
{children}
|
||||||
<div contentEditable={false} className="relative my-2">
|
<img
|
||||||
<img
|
src={element.url}
|
||||||
src={element.url}
|
alt={element.url}
|
||||||
alt={element.url}
|
className={cn(
|
||||||
className={cn(
|
"my-2 h-auto w-1/2 rounded-lg object-cover ring-2 outline outline-1 -outline-offset-1 outline-black/15",
|
||||||
"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",
|
||||||
selected && focused ? "ring-blue-500" : "ring-transparent",
|
)}
|
||||||
)}
|
onClick={() => Transforms.removeNodes(editor, { at: path })}
|
||||||
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>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useEvent } from "@lume/ark";
|
import { NostrQuery, useEvent } from "@lume/system";
|
||||||
import type { Event } from "@lume/types";
|
import type { NostrEvent } from "@lume/types";
|
||||||
import { Box, Container, Spinner } from "@lume/ui";
|
import { Box, Container, Spinner } from "@lume/ui";
|
||||||
import { Note } from "@/components/note";
|
import { Note } from "@/components/note";
|
||||||
import { createFileRoute } from "@tanstack/react-router";
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
@@ -7,10 +7,8 @@ import { WindowVirtualizer } from "virtua";
|
|||||||
import { ReplyList } from "./-components/replyList";
|
import { ReplyList } from "./-components/replyList";
|
||||||
|
|
||||||
export const Route = createFileRoute("/events/$eventId")({
|
export const Route = createFileRoute("/events/$eventId")({
|
||||||
beforeLoad: async ({ context }) => {
|
beforeLoad: async () => {
|
||||||
const ark = context.ark;
|
const settings = await NostrQuery.getSettings();
|
||||||
const settings = await ark.get_settings();
|
|
||||||
|
|
||||||
return { settings };
|
return { settings };
|
||||||
},
|
},
|
||||||
component: Screen,
|
component: Screen,
|
||||||
@@ -52,7 +50,7 @@ function Screen() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function MainNote({ data }: { data: Event }) {
|
function MainNote({ data }: { data: NostrEvent }) {
|
||||||
return (
|
return (
|
||||||
<Note.Provider event={data}>
|
<Note.Provider event={data}>
|
||||||
<Note.Root>
|
<Note.Root>
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import type { EventWithReplies } from "@lume/types";
|
import type { EventWithReplies } from "@lume/types";
|
||||||
import { Spinner } from "@lume/ui";
|
import { Spinner } from "@lume/ui";
|
||||||
import { cn } from "@lume/utils";
|
import { cn } from "@lume/utils";
|
||||||
import { useRouteContext } from "@tanstack/react-router";
|
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Reply } from "./reply";
|
import { Reply } from "./reply";
|
||||||
|
import { LumeEvent } from "@lume/system";
|
||||||
|
|
||||||
export function ReplyList({
|
export function ReplyList({
|
||||||
eventId,
|
eventId,
|
||||||
@@ -13,13 +13,12 @@ export function ReplyList({
|
|||||||
eventId: string;
|
eventId: string;
|
||||||
className?: string;
|
className?: string;
|
||||||
}) {
|
}) {
|
||||||
const { ark } = useRouteContext({ strict: false });
|
|
||||||
const [t] = useTranslation();
|
const [t] = useTranslation();
|
||||||
const [data, setData] = useState<null | EventWithReplies[]>(null);
|
const [data, setData] = useState<null | EventWithReplies[]>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
async function getReplies() {
|
async function getReplies() {
|
||||||
const events = await ark.get_event_thread(eventId);
|
const events = await LumeEvent.getReplies(eventId);
|
||||||
setData(events);
|
setData(events);
|
||||||
}
|
}
|
||||||
getReplies();
|
getReplies();
|
||||||
|
|||||||
@@ -1,7 +1,12 @@
|
|||||||
import type { Event } from "@lume/types";
|
import type { NostrEvent } from "@lume/types";
|
||||||
import { Note } from "@/components/note";
|
import { Note } from "@/components/note";
|
||||||
|
|
||||||
export function SubReply({ event }: { event: Event; rootEventId?: string }) {
|
export function SubReply({
|
||||||
|
event,
|
||||||
|
}: {
|
||||||
|
event: NostrEvent;
|
||||||
|
rootEventId?: string;
|
||||||
|
}) {
|
||||||
return (
|
return (
|
||||||
<Note.Provider event={event}>
|
<Note.Provider event={event}>
|
||||||
<Note.Root>
|
<Note.Root>
|
||||||
|
|||||||
@@ -3,7 +3,8 @@ import { Quote } from "@/components/quote";
|
|||||||
import { RepostNote } from "@/components/repost";
|
import { RepostNote } from "@/components/repost";
|
||||||
import { TextNote } from "@/components/text";
|
import { TextNote } from "@/components/text";
|
||||||
import { ArrowRightCircleIcon, ArrowRightIcon } from "@lume/icons";
|
import { ArrowRightCircleIcon, ArrowRightIcon } from "@lume/icons";
|
||||||
import { type ColumnRouteSearch, type Event, Kind } from "@lume/types";
|
import { NostrQuery } from "@lume/system";
|
||||||
|
import { type ColumnRouteSearch, type NostrEvent, Kind } from "@lume/types";
|
||||||
import { Spinner } from "@lume/ui";
|
import { Spinner } from "@lume/ui";
|
||||||
import { useInfiniteQuery } from "@tanstack/react-query";
|
import { useInfiniteQuery } from "@tanstack/react-query";
|
||||||
import { Link, createFileRoute } from "@tanstack/react-router";
|
import { Link, createFileRoute } from "@tanstack/react-router";
|
||||||
@@ -17,10 +18,8 @@ export const Route = createFileRoute("/global")({
|
|||||||
name: search.name,
|
name: search.name,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
beforeLoad: async ({ context }) => {
|
beforeLoad: async () => {
|
||||||
const ark = context.ark;
|
const settings = await NostrQuery.getSettings();
|
||||||
const settings = await ark.get_settings();
|
|
||||||
|
|
||||||
return { settings };
|
return { settings };
|
||||||
},
|
},
|
||||||
component: Screen,
|
component: Screen,
|
||||||
@@ -28,7 +27,6 @@ export const Route = createFileRoute("/global")({
|
|||||||
|
|
||||||
export function Screen() {
|
export function Screen() {
|
||||||
const { label, account } = Route.useSearch();
|
const { label, account } = Route.useSearch();
|
||||||
const { ark } = Route.useRouteContext();
|
|
||||||
const {
|
const {
|
||||||
data,
|
data,
|
||||||
isLoading,
|
isLoading,
|
||||||
@@ -40,7 +38,7 @@ export function Screen() {
|
|||||||
queryKey: [label, account],
|
queryKey: [label, account],
|
||||||
initialPageParam: 0,
|
initialPageParam: 0,
|
||||||
queryFn: async ({ pageParam }: { pageParam: number }) => {
|
queryFn: async ({ pageParam }: { pageParam: number }) => {
|
||||||
const events = await ark.get_global_events(20, pageParam);
|
const events = await NostrQuery.getGlobalEvents(pageParam);
|
||||||
return events;
|
return events;
|
||||||
},
|
},
|
||||||
getNextPageParam: (lastPage) => lastPage?.at(-1)?.created_at - 1,
|
getNextPageParam: (lastPage) => lastPage?.at(-1)?.created_at - 1,
|
||||||
@@ -48,7 +46,7 @@ export function Screen() {
|
|||||||
refetchOnWindowFocus: false,
|
refetchOnWindowFocus: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
const renderItem = (event: Event) => {
|
const renderItem = (event: NostrEvent) => {
|
||||||
if (!event) return;
|
if (!event) return;
|
||||||
switch (event.kind) {
|
switch (event.kind) {
|
||||||
case Kind.Repost:
|
case Kind.Repost:
|
||||||
|
|||||||
@@ -3,7 +3,8 @@ import { Quote } from "@/components/quote";
|
|||||||
import { RepostNote } from "@/components/repost";
|
import { RepostNote } from "@/components/repost";
|
||||||
import { TextNote } from "@/components/text";
|
import { TextNote } from "@/components/text";
|
||||||
import { ArrowRightCircleIcon, ArrowRightIcon } from "@lume/icons";
|
import { ArrowRightCircleIcon, ArrowRightIcon } from "@lume/icons";
|
||||||
import { type ColumnRouteSearch, type Event, Kind } from "@lume/types";
|
import { NostrAccount, NostrQuery } from "@lume/system";
|
||||||
|
import { type ColumnRouteSearch, type NostrEvent, Kind } from "@lume/types";
|
||||||
import { Spinner } from "@lume/ui";
|
import { Spinner } from "@lume/ui";
|
||||||
import { useInfiniteQuery } from "@tanstack/react-query";
|
import { useInfiniteQuery } from "@tanstack/react-query";
|
||||||
import { Link, createFileRoute, redirect } from "@tanstack/react-router";
|
import { Link, createFileRoute, redirect } from "@tanstack/react-router";
|
||||||
@@ -17,11 +18,10 @@ export const Route = createFileRoute("/group")({
|
|||||||
name: search.name,
|
name: search.name,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
beforeLoad: async ({ search, context }) => {
|
beforeLoad: async ({ search }) => {
|
||||||
const ark = context.ark;
|
|
||||||
const key = `lume_group_${search.label}`;
|
const key = `lume_group_${search.label}`;
|
||||||
const groups = (await ark.get_nstore(key)) as string[];
|
const groups = (await NostrQuery.getNstore(key)) as string[];
|
||||||
const settings = await ark.get_settings();
|
const settings = await NostrQuery.getSettings();
|
||||||
|
|
||||||
if (!groups?.length) {
|
if (!groups?.length) {
|
||||||
throw redirect({
|
throw redirect({
|
||||||
@@ -43,7 +43,7 @@ export const Route = createFileRoute("/group")({
|
|||||||
|
|
||||||
export function Screen() {
|
export function Screen() {
|
||||||
const { label, account } = Route.useSearch();
|
const { label, account } = Route.useSearch();
|
||||||
const { ark, groups } = Route.useRouteContext();
|
const { groups } = Route.useRouteContext();
|
||||||
const {
|
const {
|
||||||
data,
|
data,
|
||||||
isLoading,
|
isLoading,
|
||||||
@@ -55,7 +55,7 @@ export function Screen() {
|
|||||||
queryKey: [label, account],
|
queryKey: [label, account],
|
||||||
initialPageParam: 0,
|
initialPageParam: 0,
|
||||||
queryFn: async ({ pageParam }: { pageParam: number }) => {
|
queryFn: async ({ pageParam }: { pageParam: number }) => {
|
||||||
const events = await ark.get_local_events(groups, 20, pageParam);
|
const events = await NostrQuery.getLocalEvents(groups, pageParam);
|
||||||
return events;
|
return events;
|
||||||
},
|
},
|
||||||
getNextPageParam: (lastPage) => lastPage?.at(-1)?.created_at - 1,
|
getNextPageParam: (lastPage) => lastPage?.at(-1)?.created_at - 1,
|
||||||
@@ -64,7 +64,7 @@ export function Screen() {
|
|||||||
refetchOnWindowFocus: false,
|
refetchOnWindowFocus: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
const renderItem = (event: Event) => {
|
const renderItem = (event: NostrEvent) => {
|
||||||
if (!event) return;
|
if (!event) return;
|
||||||
switch (event.kind) {
|
switch (event.kind) {
|
||||||
case Kind.Repost:
|
case Kind.Repost:
|
||||||
|
|||||||
@@ -4,28 +4,22 @@ import { User } from "@/components/user";
|
|||||||
import { checkForAppUpdates } from "@lume/utils";
|
import { checkForAppUpdates } from "@lume/utils";
|
||||||
import { Link } from "@tanstack/react-router";
|
import { Link } from "@tanstack/react-router";
|
||||||
import { createFileRoute, redirect } from "@tanstack/react-router";
|
import { createFileRoute, redirect } from "@tanstack/react-router";
|
||||||
import { invoke } from "@tauri-apps/api/core";
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
import { NostrAccount } from "@lume/system";
|
||||||
|
|
||||||
export const Route = createFileRoute("/")({
|
export const Route = createFileRoute("/")({
|
||||||
beforeLoad: async ({ context }) => {
|
beforeLoad: async () => {
|
||||||
// check for app updates
|
await checkForAppUpdates(true); // check for app updates
|
||||||
await checkForAppUpdates(true);
|
const accounts = await NostrAccount.getAccounts();
|
||||||
|
|
||||||
const ark = context.ark;
|
if (accounts.length < 1) {
|
||||||
const accounts = await ark.get_accounts();
|
|
||||||
|
|
||||||
if (!accounts.length) {
|
|
||||||
throw redirect({
|
throw redirect({
|
||||||
to: "/landing/",
|
to: "/landing",
|
||||||
replace: true,
|
replace: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Run notification service
|
|
||||||
await invoke("run_notification", { accounts });
|
|
||||||
|
|
||||||
return { accounts };
|
return { accounts };
|
||||||
},
|
},
|
||||||
component: Screen,
|
component: Screen,
|
||||||
@@ -33,7 +27,7 @@ export const Route = createFileRoute("/")({
|
|||||||
|
|
||||||
function Screen() {
|
function Screen() {
|
||||||
const navigate = Route.useNavigate();
|
const navigate = Route.useNavigate();
|
||||||
const { ark, accounts } = Route.useRouteContext();
|
const context = Route.useRouteContext();
|
||||||
|
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
@@ -41,11 +35,15 @@ function Screen() {
|
|||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
||||||
const loadAccount = await ark.load_account(npub);
|
const status = await NostrAccount.loadAccount(npub);
|
||||||
if (loadAccount) {
|
|
||||||
|
if (status) {
|
||||||
return navigate({
|
return navigate({
|
||||||
to: "/$account/home",
|
to: "/$account/home",
|
||||||
params: { account: npub },
|
params: { account: npub },
|
||||||
|
search: {
|
||||||
|
accounts: context.accounts,
|
||||||
|
},
|
||||||
replace: true,
|
replace: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -64,60 +62,45 @@ function Screen() {
|
|||||||
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">
|
||||||
<h2 className="mb-1 text-2xl">{currentDate}</h2>
|
<h2 className="text-xl text-neutral-700 dark:text-neutral-300">
|
||||||
|
{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 flex-wrap px-3 items-center justify-center gap-6">
|
<div className="flex flex-wrap px-3 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 text-white" />
|
<Spinner className="size-6" />
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{accounts.map((account) => (
|
{context.accounts.map((account) => (
|
||||||
<button
|
<button
|
||||||
type="button"
|
|
||||||
key={account}
|
key={account}
|
||||||
|
type="button"
|
||||||
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-3 rounded-2xl p-2 hover:bg-black/10 dark:hover:bg-white/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-[6rem] truncate font-medium leading-tight" />
|
||||||
</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-3 rounded-2xl p-2 hover:bg-black/10 dark:hover:bg-white/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-black/5 dark:bg-white/5">
|
||||||
<PlusIcon className="size-8" />
|
<PlusIcon className="size-8" />
|
||||||
</div>
|
</div>
|
||||||
<p className="text-lg font-medium leading-tight">Add</p>
|
<p className="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 inset-0 h-full w-full">
|
|
||||||
<img
|
|
||||||
src="/lock-screen.jpg"
|
|
||||||
srcSet="/lock-screen@2x.jpg 2x"
|
|
||||||
alt="Lock Screen Background"
|
|
||||||
className="h-full w-full object-cover"
|
|
||||||
/>
|
|
||||||
<a
|
|
||||||
href="https://njump.me/nprofile1qqs9tuz9jpn57djg7nxunhyvuvk69g5zqaxdpvpqt9hwqv7395u9rpg6zq5uw"
|
|
||||||
target="_blank"
|
|
||||||
className="absolute bottom-3 right-3 z-50 rounded-md bg-white/20 px-2 py-1 text-xs font-medium text-white dark:bg-black/20"
|
|
||||||
rel="noreferrer"
|
|
||||||
>
|
|
||||||
Design by NoGood
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
import { KeyIcon, RemoteIcon } from "@lume/icons";
|
||||||
|
import { Link, createLazyFileRoute } from "@tanstack/react-router";
|
||||||
|
|
||||||
|
export const Route = createLazyFileRoute("/landing")({
|
||||||
|
component: Screen,
|
||||||
|
});
|
||||||
|
|
||||||
|
function Screen() {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-tauri-drag-region
|
||||||
|
className="flex flex-col justify-center items-center h-screen w-screen"
|
||||||
|
>
|
||||||
|
<div className="mx-auto max-w-xs lg:max-w-md w-full">
|
||||||
|
<div className="flex w-full flex-col gap-2 bg-white rounded-xl shadow-primary backdrop-blur-lg dark:bg-white/20 dark:ring-1 ring-neutral-800/50 px-2">
|
||||||
|
<div className="h-20 flex items-center border-b border-neutral-100 dark:border-white/5">
|
||||||
|
<Link
|
||||||
|
to="/auth/new/profile"
|
||||||
|
className="h-14 w-full flex items-center justify-center gap-2 hover:bg-neutral-100 dark:hover:bg-white/10 rounded-lg px-2"
|
||||||
|
>
|
||||||
|
<div className="size-9 shrink-0 rounded-full inline-flex items-center justify-center">
|
||||||
|
<img
|
||||||
|
src="/icon.jpeg"
|
||||||
|
alt="App Icon"
|
||||||
|
className="size-9 object-cover rounded-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 inline-flex flex-col">
|
||||||
|
<span className="leading-tight font-semibold">
|
||||||
|
Create new account
|
||||||
|
</span>
|
||||||
|
<span className="leading-tight text-sm text-neutral-500">
|
||||||
|
Use everywhere
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-1 pb-2.5">
|
||||||
|
<Link
|
||||||
|
to="/auth/privkey"
|
||||||
|
className="inline-flex h-11 w-full items-center gap-2 rounded-lg px-2 hover:bg-neutral-100 dark:hover:bg-white/10"
|
||||||
|
>
|
||||||
|
<div className="size-9 inline-flex items-center justify-center">
|
||||||
|
<KeyIcon className="size-5 text-neutral-600 dark:text-neutral-400" />
|
||||||
|
</div>
|
||||||
|
Login with Private Key
|
||||||
|
</Link>
|
||||||
|
<Link
|
||||||
|
to="/auth/remote"
|
||||||
|
className="inline-flex h-11 w-full items-center gap-2 rounded-lg px-2 hover:bg-neutral-100 dark:hover:bg-white/10"
|
||||||
|
>
|
||||||
|
<div className="size-9 inline-flex items-center justify-center">
|
||||||
|
<RemoteIcon className="size-5 text-neutral-600 dark:text-neutral-400" />
|
||||||
|
</div>
|
||||||
|
Nostr Connect
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,85 +0,0 @@
|
|||||||
import { KeyIcon, RemoteIcon } from "@lume/icons";
|
|
||||||
import { Link, createFileRoute } from "@tanstack/react-router";
|
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
|
|
||||||
export const Route = createFileRoute("/landing/")({
|
|
||||||
component: Screen,
|
|
||||||
});
|
|
||||||
|
|
||||||
function Screen() {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="relative flex h-screen w-screen bg-black">
|
|
||||||
<div
|
|
||||||
data-tauri-drag-region
|
|
||||||
className="absolute left-0 top-0 z-50 h-16 w-full"
|
|
||||||
/>
|
|
||||||
<div className="z-20 flex h-full w-full flex-col items-center justify-between">
|
|
||||||
<div />
|
|
||||||
<div className="mx-auto flex w-full max-w-4xl flex-col items-center gap-10">
|
|
||||||
<div className="flex flex-col items-center text-center">
|
|
||||||
<img
|
|
||||||
src="/heading-en.png"
|
|
||||||
srcSet="/heading-en@2x.png 2x"
|
|
||||||
alt="lume"
|
|
||||||
className="xl:w-2/3"
|
|
||||||
/>
|
|
||||||
<p className="mt-4 whitespace-pre-line text-lg font-medium leading-snug text-white/70">
|
|
||||||
{t("welcome.title")}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="mx-auto flex w-full max-w-sm flex-col gap-4">
|
|
||||||
<Link
|
|
||||||
to="/auth/new/profile"
|
|
||||||
className="inline-flex h-11 w-full items-center justify-center rounded-lg bg-white font-medium text-blue-500 backdrop-blur-lg hover:bg-white/90"
|
|
||||||
>
|
|
||||||
{t("welcome.signup")}
|
|
||||||
</Link>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<div className="h-px flex-1 bg-white/20" />
|
|
||||||
<div className="text-white/70">{t("login.or")}</div>
|
|
||||||
<div className="h-px flex-1 bg-white/20" />
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col gap-2">
|
|
||||||
<Link
|
|
||||||
to="/auth/remote"
|
|
||||||
className="group inline-flex h-11 w-full items-center justify-between gap-2 rounded-lg bg-white/20 px-3 font-medium text-white backdrop-blur-md hover:bg-white/40"
|
|
||||||
>
|
|
||||||
<RemoteIcon className="size-5 text-neutral-600 dark:text-neutral-400 group-hover:text-neutral-400 dark:group-hover:text-neutral-600" />
|
|
||||||
Nostr Connect
|
|
||||||
<div className="size-5" />
|
|
||||||
</Link>
|
|
||||||
<Link
|
|
||||||
to="/auth/privkey"
|
|
||||||
className="group inline-flex h-11 w-full items-center justify-between gap-2 rounded-lg bg-white/20 px-3 font-medium text-white backdrop-blur-md hover:bg-white/40"
|
|
||||||
>
|
|
||||||
<KeyIcon className="size-5 text-neutral-600 dark:text-neutral-400 group-hover:text-neutral-400 dark:group-hover:text-neutral-600" />
|
|
||||||
Private Key
|
|
||||||
<div className="size-5" />
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex h-11 items-center justify-center" />
|
|
||||||
</div>
|
|
||||||
<div className="absolute z-10 h-full w-full bg-black/5 backdrop-blur-sm" />
|
|
||||||
<div className="absolute inset-0 h-full w-full">
|
|
||||||
<img
|
|
||||||
src="/lock-screen.jpg"
|
|
||||||
srcSet="/lock-screen@2x.jpg 2x"
|
|
||||||
alt="Lock Screen Background"
|
|
||||||
className="h-full w-full object-cover"
|
|
||||||
/>
|
|
||||||
<a
|
|
||||||
href="https://njump.me/nprofile1qqs9tuz9jpn57djg7nxunhyvuvk69g5zqaxdpvpqt9hwqv7395u9rpg6zq5uw"
|
|
||||||
target="_blank"
|
|
||||||
className="absolute bottom-3 right-3 z-50 rounded-md bg-white/20 px-2 py-1 text-xs font-medium text-white backdrop-blur-md dark:bg-black/20"
|
|
||||||
rel="noreferrer"
|
|
||||||
>
|
|
||||||
Design by NoGood
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -2,11 +2,12 @@ import { Conversation } from "@/components/conversation";
|
|||||||
import { Quote } from "@/components/quote";
|
import { Quote } from "@/components/quote";
|
||||||
import { RepostNote } from "@/components/repost";
|
import { RepostNote } from "@/components/repost";
|
||||||
import { TextNote } from "@/components/text";
|
import { TextNote } from "@/components/text";
|
||||||
import { ArrowRightCircleIcon, ArrowRightIcon } from "@lume/icons";
|
import { ArrowRightCircleIcon } from "@lume/icons";
|
||||||
import { type ColumnRouteSearch, type Event, Kind } from "@lume/types";
|
import { NostrAccount, NostrQuery } from "@lume/system";
|
||||||
|
import { type ColumnRouteSearch, type NostrEvent, Kind } from "@lume/types";
|
||||||
import { Spinner } from "@lume/ui";
|
import { Spinner } from "@lume/ui";
|
||||||
import { useInfiniteQuery } from "@tanstack/react-query";
|
import { useInfiniteQuery } from "@tanstack/react-query";
|
||||||
import { Link, redirect } from "@tanstack/react-router";
|
import { redirect } from "@tanstack/react-router";
|
||||||
import { createFileRoute } from "@tanstack/react-router";
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
import { Virtualizer } from "virtua";
|
import { Virtualizer } from "virtua";
|
||||||
|
|
||||||
@@ -18,10 +19,9 @@ export const Route = createFileRoute("/newsfeed")({
|
|||||||
name: search.name,
|
name: search.name,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
beforeLoad: async ({ search, context }) => {
|
beforeLoad: async ({ search }) => {
|
||||||
const ark = context.ark;
|
const settings = await NostrQuery.getSettings();
|
||||||
const settings = await ark.get_settings();
|
const contacts = await NostrAccount.getContactList();
|
||||||
const contacts = await ark.get_contact_list();
|
|
||||||
|
|
||||||
if (!contacts.length) {
|
if (!contacts.length) {
|
||||||
throw redirect({
|
throw redirect({
|
||||||
@@ -40,7 +40,7 @@ export const Route = createFileRoute("/newsfeed")({
|
|||||||
|
|
||||||
export function Screen() {
|
export function Screen() {
|
||||||
const { label, account } = Route.useSearch();
|
const { label, account } = Route.useSearch();
|
||||||
const { ark, contacts } = Route.useRouteContext();
|
const { contacts, settings } = Route.useRouteContext();
|
||||||
const {
|
const {
|
||||||
data,
|
data,
|
||||||
isLoading,
|
isLoading,
|
||||||
@@ -52,7 +52,7 @@ export function Screen() {
|
|||||||
queryKey: [label, account],
|
queryKey: [label, account],
|
||||||
initialPageParam: 0,
|
initialPageParam: 0,
|
||||||
queryFn: async ({ pageParam }: { pageParam: number }) => {
|
queryFn: async ({ pageParam }: { pageParam: number }) => {
|
||||||
const events = await ark.get_local_events(contacts, 20, pageParam);
|
const events = await NostrQuery.getLocalEvents(contacts, pageParam);
|
||||||
return events;
|
return events;
|
||||||
},
|
},
|
||||||
getNextPageParam: (lastPage) => lastPage?.at(-1)?.created_at - 1,
|
getNextPageParam: (lastPage) => lastPage?.at(-1)?.created_at - 1,
|
||||||
@@ -60,7 +60,7 @@ export function Screen() {
|
|||||||
refetchOnWindowFocus: false,
|
refetchOnWindowFocus: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
const renderItem = (event: Event) => {
|
const renderItem = (event: NostrEvent) => {
|
||||||
if (!event) return;
|
if (!event) return;
|
||||||
switch (event.kind) {
|
switch (event.kind) {
|
||||||
case Kind.Repost:
|
case Kind.Repost:
|
||||||
@@ -72,7 +72,14 @@ export function Screen() {
|
|||||||
const isQuote = event.tags.filter((tag) => tag[0] === "q").length > 0;
|
const isQuote = event.tags.filter((tag) => tag[0] === "q").length > 0;
|
||||||
|
|
||||||
if (isConversation) {
|
if (isConversation) {
|
||||||
return <Conversation key={event.id} event={event} className="mb-3" />;
|
return (
|
||||||
|
<Conversation
|
||||||
|
key={event.id}
|
||||||
|
className="mb-3"
|
||||||
|
event={event}
|
||||||
|
gossip={settings?.gossip}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isQuote) {
|
if (isQuote) {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { ZapIcon } from "@lume/icons";
|
import { ZapIcon } from "@lume/icons";
|
||||||
|
import { NostrAccount } from "@lume/system";
|
||||||
import { Container } from "@lume/ui";
|
import { Container } from "@lume/ui";
|
||||||
import { createLazyFileRoute } from "@tanstack/react-router";
|
import { createLazyFileRoute } from "@tanstack/react-router";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
@@ -8,18 +9,16 @@ export const Route = createLazyFileRoute("/nwc")({
|
|||||||
});
|
});
|
||||||
|
|
||||||
function Screen() {
|
function Screen() {
|
||||||
const { ark } = Route.useRouteContext();
|
|
||||||
|
|
||||||
const [uri, setUri] = useState("");
|
const [uri, setUri] = useState("");
|
||||||
const [isDone, setIsDone] = useState(false);
|
const [isDone, setIsDone] = useState(false);
|
||||||
|
|
||||||
const save = async () => {
|
const save = async () => {
|
||||||
const nwc = await ark.set_nwc(uri);
|
const nwc = await NostrAccount.setWallet(uri);
|
||||||
setIsDone(nwc);
|
setIsDone(nwc);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container withDrag withNavigate={false}>
|
<Container withDrag>
|
||||||
<div className="h-full w-full flex-1 px-5">
|
<div className="h-full w-full flex-1 px-5">
|
||||||
{!isDone ? (
|
{!isDone ? (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { SearchIcon } from "@lume/icons";
|
import { SearchIcon } from "@lume/icons";
|
||||||
import { type Event, Kind } from "@lume/types";
|
import { type NostrEvent, Kind } from "@lume/types";
|
||||||
import { Spinner } from "@lume/ui";
|
import { Spinner } from "@lume/ui";
|
||||||
import { Note } from "@/components/note";
|
import { Note } from "@/components/note";
|
||||||
import { User } from "@/components/user";
|
import { User } from "@/components/user";
|
||||||
@@ -7,6 +7,7 @@ import { createFileRoute } from "@tanstack/react-router";
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { useDebounce } from "use-debounce";
|
import { useDebounce } from "use-debounce";
|
||||||
|
import { LumeWindow } from "@lume/system";
|
||||||
|
|
||||||
export const Route = createFileRoute("/search")({
|
export const Route = createFileRoute("/search")({
|
||||||
component: Screen,
|
component: Screen,
|
||||||
@@ -14,7 +15,7 @@ export const Route = createFileRoute("/search")({
|
|||||||
|
|
||||||
function Screen() {
|
function Screen() {
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [events, setEvents] = useState<Event[]>([]);
|
const [events, setEvents] = useState<NostrEvent[]>([]);
|
||||||
const [search, setSearch] = useState("");
|
const [search, setSearch] = useState("");
|
||||||
const [searchValue] = useDebounce(search, 500);
|
const [searchValue] = useDebounce(search, 500);
|
||||||
|
|
||||||
@@ -25,7 +26,7 @@ function Screen() {
|
|||||||
const query = `https://api.nostr.wine/search?query=${searchValue}&kind=0,1`;
|
const query = `https://api.nostr.wine/search?query=${searchValue}&kind=0,1`;
|
||||||
const res = await fetch(query);
|
const res = await fetch(query);
|
||||||
const content = await res.json();
|
const content = await res.json();
|
||||||
const events = content.data as Event[];
|
const events = content.data as NostrEvent[];
|
||||||
const sorted = events.sort((a, b) => b.created_at - a.created_at);
|
const sorted = events.sort((a, b) => b.created_at - a.created_at);
|
||||||
|
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
@@ -102,14 +103,12 @@ function Screen() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function SearchUser({ event }: { event: Event }) {
|
function SearchUser({ event }: { event: NostrEvent }) {
|
||||||
const { ark } = Route.useRouteContext();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
key={event.id}
|
key={event.id}
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => ark.open_profile(event.pubkey)}
|
onClick={() => LumeWindow.openProfile(event.pubkey)}
|
||||||
className="col-span-1 p-2 hover:bg-black/10 dark:hover:bg-white/10 rounded-lg"
|
className="col-span-1 p-2 hover:bg-black/10 dark:hover:bg-white/10 rounded-lg"
|
||||||
>
|
>
|
||||||
<User.Provider pubkey={event.pubkey} embedProfile={event.content}>
|
<User.Provider pubkey={event.pubkey} embedProfile={event.content}>
|
||||||
@@ -125,7 +124,7 @@ function SearchUser({ event }: { event: Event }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function SearchNote({ event }: { event: Event }) {
|
function SearchNote({ event }: { event: NostrEvent }) {
|
||||||
return (
|
return (
|
||||||
<div className="bg-white dark:bg-black/20 backdrop-blur-lg rounded-xl shadow-primary dark:ring-1 ring-neutral-800/50">
|
<div className="bg-white dark:bg-black/20 backdrop-blur-lg rounded-xl shadow-primary dark:ring-1 ring-neutral-800/50">
|
||||||
<Note.Provider event={event}>
|
<Note.Provider event={event}>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { User } from "@/components/user";
|
import { User } from "@/components/user";
|
||||||
import type { Account } from "@lume/types";
|
import { NostrAccount } from "@lume/system";
|
||||||
import { displayNsec } from "@lume/utils";
|
import { displayNsec } from "@lume/utils";
|
||||||
import { createFileRoute } from "@tanstack/react-router";
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
import { invoke } from "@tauri-apps/api/core";
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
@@ -7,19 +7,20 @@ import { writeText } from "@tauri-apps/plugin-clipboard-manager";
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
|
||||||
|
interface Account {
|
||||||
|
npub: string;
|
||||||
|
nsec: string;
|
||||||
|
}
|
||||||
|
|
||||||
export const Route = createFileRoute("/settings/backup")({
|
export const Route = createFileRoute("/settings/backup")({
|
||||||
component: Screen,
|
component: Screen,
|
||||||
loader: async ({ context }) => {
|
loader: async () => {
|
||||||
const ark = context.ark;
|
const npubs = await NostrAccount.getAccounts();
|
||||||
const npubs = await ark.get_accounts();
|
|
||||||
|
|
||||||
const accounts: Account[] = [];
|
const accounts: Account[] = [];
|
||||||
|
|
||||||
for (const account of npubs) {
|
for (const npub of npubs) {
|
||||||
const nsec: string = await invoke("get_stored_nsec", {
|
const nsec: string = await invoke("get_stored_nsec", { npub });
|
||||||
npub: account.npub,
|
accounts.push({ npub, nsec });
|
||||||
});
|
|
||||||
accounts.push({ ...account, nsec });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return accounts;
|
return accounts;
|
||||||
@@ -33,14 +34,14 @@ function Screen() {
|
|||||||
<div className="mx-auto w-full max-w-xl">
|
<div className="mx-auto w-full max-w-xl">
|
||||||
<div className="flex flex-col gap-3 divide-y divide-neutral-300 dark:divide-neutral-700">
|
<div className="flex flex-col gap-3 divide-y divide-neutral-300 dark:divide-neutral-700">
|
||||||
{accounts.map((account) => (
|
{accounts.map((account) => (
|
||||||
<NostrAccount key={account.npub} account={account} />
|
<List key={account.npub} account={account} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function NostrAccount({ account }: { account: Account }) {
|
function List({ account }: { account: Account }) {
|
||||||
const [key, setKey] = useState(account.nsec);
|
const [key, setKey] = useState(account.nsec);
|
||||||
const [copied, setCopied] = useState(false);
|
const [copied, setCopied] = useState(false);
|
||||||
const [passphase, setPassphase] = useState("");
|
const [passphase, setPassphase] = useState("");
|
||||||
|
|||||||
@@ -1,28 +1,22 @@
|
|||||||
|
import { NostrQuery } from "@lume/system";
|
||||||
import type { Settings } from "@lume/types";
|
import type { Settings } from "@lume/types";
|
||||||
import * as Switch from "@radix-ui/react-switch";
|
import * as Switch from "@radix-ui/react-switch";
|
||||||
import { createFileRoute } from "@tanstack/react-router";
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
import {
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
isPermissionGranted,
|
import { requestPermission } from "@tauri-apps/plugin-notification";
|
||||||
requestPermission,
|
|
||||||
} from "@tauri-apps/plugin-notification";
|
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useDebouncedCallback } from "use-debounce";
|
import { useDebouncedCallback } from "use-debounce";
|
||||||
|
|
||||||
export const Route = createFileRoute("/settings/general")({
|
export const Route = createFileRoute("/settings/general")({
|
||||||
beforeLoad: async ({ context }) => {
|
beforeLoad: async () => {
|
||||||
const permissionGranted = await isPermissionGranted(); // get notification permission
|
const settings = await NostrQuery.getSettings();
|
||||||
const ark = context.ark;
|
return { settings };
|
||||||
const settings = await ark.get_settings();
|
|
||||||
|
|
||||||
return {
|
|
||||||
settings: { ...settings, notification: permissionGranted },
|
|
||||||
};
|
|
||||||
},
|
},
|
||||||
component: Screen,
|
component: Screen,
|
||||||
});
|
});
|
||||||
|
|
||||||
function Screen() {
|
function Screen() {
|
||||||
const { ark, settings } = Route.useRouteContext();
|
const { settings } = Route.useRouteContext();
|
||||||
const [newSettings, setNewSettings] = useState<Settings>(settings);
|
const [newSettings, setNewSettings] = useState<Settings>(settings);
|
||||||
|
|
||||||
const toggleNofitication = async () => {
|
const toggleNofitication = async () => {
|
||||||
@@ -33,6 +27,13 @@ function Screen() {
|
|||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const toggleGossip = async () => {
|
||||||
|
setNewSettings((prev) => ({
|
||||||
|
...prev,
|
||||||
|
gossip: !newSettings.gossip,
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
const toggleAutoUpdate = () => {
|
const toggleAutoUpdate = () => {
|
||||||
setNewSettings((prev) => ({
|
setNewSettings((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
@@ -61,8 +62,21 @@ function Screen() {
|
|||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const changeTheme = (theme: string) => {
|
||||||
|
if (theme === "auto" || theme === "light" || theme === "dark") {
|
||||||
|
invoke("plugin:theme|set_theme", {
|
||||||
|
theme: theme,
|
||||||
|
}).then(() =>
|
||||||
|
setNewSettings((prev) => ({
|
||||||
|
...prev,
|
||||||
|
theme,
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const updateSettings = useDebouncedCallback(() => {
|
const updateSettings = useDebouncedCallback(() => {
|
||||||
ark.set_settings(newSettings);
|
NostrQuery.setSettings(newSettings);
|
||||||
}, 200);
|
}, 200);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -95,6 +109,24 @@ function Screen() {
|
|||||||
</Switch.Root>
|
</Switch.Root>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex w-full items-start justify-between gap-4 py-3">
|
||||||
|
<div className="flex-1">
|
||||||
|
<h3 className="font-medium">Relay Hint</h3>
|
||||||
|
<p className="text-sm text-neutral-700 dark:text-neutral-300">
|
||||||
|
Automatically connect to the necessary relay suggested by
|
||||||
|
Relay Hint when fetching a new event.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="w-36 flex justify-end shrink-0">
|
||||||
|
<Switch.Root
|
||||||
|
checked={newSettings.gossip}
|
||||||
|
onClick={() => toggleGossip()}
|
||||||
|
className="relative h-7 w-12 shrink-0 cursor-default rounded-full bg-black/10 outline-none data-[state=checked]:bg-blue-500 dark:bg-white/10"
|
||||||
|
>
|
||||||
|
<Switch.Thumb className="block size-6 translate-x-0.5 rounded-full bg-white transition-transform duration-100 will-change-transform data-[state=checked]:translate-x-[19px]" />
|
||||||
|
</Switch.Root>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div className="flex w-full items-start justify-between gap-4 py-3">
|
<div className="flex w-full items-start justify-between gap-4 py-3">
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<h3 className="font-medium">Enhanced Privacy</h3>
|
<h3 className="font-medium">Enhanced Privacy</h3>
|
||||||
@@ -155,24 +187,42 @@ function Screen() {
|
|||||||
Interface
|
Interface
|
||||||
</h2>
|
</h2>
|
||||||
<div className="flex flex-col divide-y divide-black/10 dark:divide-white/10 bg-black/5 dark:bg-white/5 rounded-xl px-3">
|
<div className="flex flex-col divide-y divide-black/10 dark:divide-white/10 bg-black/5 dark:bg-white/5 rounded-xl px-3">
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex w-full items-start justify-between gap-4 py-3">
|
||||||
<div className="flex w-full items-start justify-between gap-4 py-3">
|
<div className="flex-1">
|
||||||
<div className="flex-1">
|
<h3 className="font-semibold">Zap</h3>
|
||||||
<h3 className="font-semibold">Zap</h3>
|
<p className="text-sm text-neutral-700 dark:text-neutral-300">
|
||||||
<p className="text-sm text-neutral-700 dark:text-neutral-300">
|
Show the Zap button in each note and user's profile screen,
|
||||||
Show the Zap button in each note and user's profile screen,
|
use for send bitcoin tip to other users.
|
||||||
use for send bitcoin tip to other users.
|
</p>
|
||||||
</p>
|
</div>
|
||||||
</div>
|
<div className="w-36 flex justify-end shrink-0">
|
||||||
<div className="w-36 flex justify-end shrink-0">
|
<Switch.Root
|
||||||
<Switch.Root
|
checked={newSettings.zap}
|
||||||
checked={newSettings.zap}
|
onClick={() => toggleZap()}
|
||||||
onClick={() => toggleZap()}
|
className="relative h-7 w-12 shrink-0 cursor-default rounded-full bg-black/10 outline-none data-[state=checked]:bg-blue-500 dark:bg-white/10"
|
||||||
className="relative h-7 w-12 shrink-0 cursor-default rounded-full bg-black/10 outline-none data-[state=checked]:bg-blue-500 dark:bg-white/10"
|
>
|
||||||
>
|
<Switch.Thumb className="block size-6 translate-x-0.5 rounded-full bg-white transition-transform duration-100 will-change-transform data-[state=checked]:translate-x-[19px]" />
|
||||||
<Switch.Thumb className="block size-6 translate-x-0.5 rounded-full bg-white transition-transform duration-100 will-change-transform data-[state=checked]:translate-x-[19px]" />
|
</Switch.Root>
|
||||||
</Switch.Root>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex w-full items-start justify-between gap-4 py-3">
|
||||||
|
<div className="flex-1">
|
||||||
|
<h3 className="font-semibold">Appearance</h3>
|
||||||
|
<p className="text-sm text-neutral-700 dark:text-neutral-300">
|
||||||
|
* Require restarting the app to take effect.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="w-36 flex justify-end shrink-0">
|
||||||
|
<select
|
||||||
|
name="theme"
|
||||||
|
className="bg-transparent shadow-none outline-none rounded-lg border-1 border-black/10 dark:border-white/10 py-1 w-24"
|
||||||
|
defaultValue={settings.theme}
|
||||||
|
onChange={(e) => changeTheme(e.target.value)}
|
||||||
|
>
|
||||||
|
<option value="auto">Auto</option>
|
||||||
|
<option value="light">Light</option>
|
||||||
|
<option value="dark">Dark</option>
|
||||||
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,14 +1,13 @@
|
|||||||
import { CancelIcon, PlusIcon } from "@lume/icons";
|
import { CancelIcon, PlusIcon } from "@lume/icons";
|
||||||
|
import { NostrQuery } from "@lume/system";
|
||||||
import { createFileRoute } from "@tanstack/react-router";
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
|
||||||
export const Route = createFileRoute("/settings/relay")({
|
export const Route = createFileRoute("/settings/relay")({
|
||||||
loader: async ({ context }) => {
|
loader: async () => {
|
||||||
const ark = context.ark;
|
const relays = await NostrQuery.getRelays();
|
||||||
const relays = await ark.get_relays();
|
|
||||||
|
|
||||||
return relays;
|
return relays;
|
||||||
},
|
},
|
||||||
component: Screen,
|
component: Screen,
|
||||||
@@ -18,12 +17,11 @@ function Screen() {
|
|||||||
const relayList = Route.useLoaderData();
|
const relayList = Route.useLoaderData();
|
||||||
const [relays, setRelays] = useState(relayList.connected);
|
const [relays, setRelays] = useState(relayList.connected);
|
||||||
|
|
||||||
const { ark } = Route.useRouteContext();
|
|
||||||
const { register, reset, handleSubmit } = useForm();
|
const { register, reset, handleSubmit } = useForm();
|
||||||
|
|
||||||
const onSubmit = async (data: { url: string }) => {
|
const onSubmit = async (data: { url: string }) => {
|
||||||
try {
|
try {
|
||||||
const add = await ark.add_relay(data.url);
|
const add = await NostrQuery.connectRelay(data.url);
|
||||||
if (add) {
|
if (add) {
|
||||||
setRelays((prev) => [...prev, data.url]);
|
setRelays((prev) => [...prev, data.url]);
|
||||||
reset();
|
reset();
|
||||||
@@ -56,6 +54,7 @@ function Screen() {
|
|||||||
<div>
|
<div>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
onClick={() => NostrQuery.removeRelay(relay)}
|
||||||
className="inline-flex items-center justify-center size-7 rounded-md hover:bg-black/10 dark:hover:bg-white/10"
|
className="inline-flex items-center justify-center size-7 rounded-md hover:bg-black/10 dark:hover:bg-white/10"
|
||||||
>
|
>
|
||||||
<CancelIcon className="size-4" />
|
<CancelIcon className="size-4" />
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { AvatarUploader } from "@/components/avatarUploader";
|
import { AvatarUploader } from "@/components/avatarUploader";
|
||||||
import { PlusIcon } from "@lume/icons";
|
import { PlusIcon } from "@lume/icons";
|
||||||
|
import { NostrAccount } from "@lume/system";
|
||||||
import type { Metadata } from "@lume/types";
|
import type { Metadata } from "@lume/types";
|
||||||
import { Spinner } from "@lume/ui";
|
import { Spinner } from "@lume/ui";
|
||||||
import { Link } from "@tanstack/react-router";
|
import { Link } from "@tanstack/react-router";
|
||||||
@@ -9,16 +10,15 @@ import { useForm } from "react-hook-form";
|
|||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
|
||||||
export const Route = createFileRoute("/settings/user")({
|
export const Route = createFileRoute("/settings/user")({
|
||||||
beforeLoad: async ({ context }) => {
|
beforeLoad: async () => {
|
||||||
const ark = context.ark;
|
const profile = await NostrAccount.getProfile();
|
||||||
const profile = await ark.get_current_user_profile();
|
|
||||||
return { profile };
|
return { profile };
|
||||||
},
|
},
|
||||||
component: Screen,
|
component: Screen,
|
||||||
});
|
});
|
||||||
|
|
||||||
function Screen() {
|
function Screen() {
|
||||||
const { ark, profile } = Route.useRouteContext();
|
const { profile } = Route.useRouteContext();
|
||||||
const { register, handleSubmit } = useForm({ defaultValues: profile });
|
const { register, handleSubmit } = useForm({ defaultValues: profile });
|
||||||
|
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
@@ -29,7 +29,7 @@ function Screen() {
|
|||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
||||||
const newProfile: Metadata = { ...profile, ...data, picture };
|
const newProfile: Metadata = { ...profile, ...data, picture };
|
||||||
await ark.create_profile(newProfile);
|
await NostrAccount.createProfile(newProfile);
|
||||||
|
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
@@ -3,7 +3,13 @@ import { Quote } from "@/components/quote";
|
|||||||
import { RepostNote } from "@/components/repost";
|
import { RepostNote } from "@/components/repost";
|
||||||
import { TextNote } from "@/components/text";
|
import { TextNote } from "@/components/text";
|
||||||
import { ArrowRightCircleIcon, ArrowRightIcon } from "@lume/icons";
|
import { ArrowRightCircleIcon, ArrowRightIcon } from "@lume/icons";
|
||||||
import { type ColumnRouteSearch, type Event, Kind, Topic } from "@lume/types";
|
import { NostrQuery } from "@lume/system";
|
||||||
|
import {
|
||||||
|
type ColumnRouteSearch,
|
||||||
|
type NostrEvent,
|
||||||
|
Kind,
|
||||||
|
Topic,
|
||||||
|
} from "@lume/types";
|
||||||
import { Spinner } from "@lume/ui";
|
import { Spinner } from "@lume/ui";
|
||||||
import { useInfiniteQuery } from "@tanstack/react-query";
|
import { useInfiniteQuery } from "@tanstack/react-query";
|
||||||
import { Link, createFileRoute, redirect } from "@tanstack/react-router";
|
import { Link, createFileRoute, redirect } from "@tanstack/react-router";
|
||||||
@@ -17,11 +23,10 @@ export const Route = createFileRoute("/topic")({
|
|||||||
name: search.name,
|
name: search.name,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
beforeLoad: async ({ search, context }) => {
|
beforeLoad: async ({ search }) => {
|
||||||
const ark = context.ark;
|
|
||||||
const key = `lume_topic_${search.label}`;
|
const key = `lume_topic_${search.label}`;
|
||||||
const topics = (await ark.get_nstore(key)) as unknown as Topic[];
|
const topics = (await NostrQuery.getNstore(key)) as unknown as Topic[];
|
||||||
const settings = await ark.get_settings();
|
const settings = await NostrQuery.getSettings();
|
||||||
|
|
||||||
if (!topics?.length) {
|
if (!topics?.length) {
|
||||||
throw redirect({
|
throw redirect({
|
||||||
@@ -49,7 +54,7 @@ export const Route = createFileRoute("/topic")({
|
|||||||
|
|
||||||
export function Screen() {
|
export function Screen() {
|
||||||
const { label, account } = Route.useSearch();
|
const { label, account } = Route.useSearch();
|
||||||
const { ark, hashtags } = Route.useRouteContext();
|
const { hashtags } = Route.useRouteContext();
|
||||||
const {
|
const {
|
||||||
data,
|
data,
|
||||||
isLoading,
|
isLoading,
|
||||||
@@ -61,7 +66,7 @@ export function Screen() {
|
|||||||
queryKey: [label, account],
|
queryKey: [label, account],
|
||||||
initialPageParam: 0,
|
initialPageParam: 0,
|
||||||
queryFn: async ({ pageParam }: { pageParam: number }) => {
|
queryFn: async ({ pageParam }: { pageParam: number }) => {
|
||||||
const events = ark.get_hashtag_events(hashtags, 20, pageParam);
|
const events = NostrQuery.getHashtagEvents(hashtags, pageParam);
|
||||||
return events;
|
return events;
|
||||||
},
|
},
|
||||||
getNextPageParam: (lastPage) => lastPage?.at(-1)?.created_at - 1,
|
getNextPageParam: (lastPage) => lastPage?.at(-1)?.created_at - 1,
|
||||||
@@ -69,7 +74,7 @@ export function Screen() {
|
|||||||
refetchOnWindowFocus: false,
|
refetchOnWindowFocus: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
const renderItem = (event: Event) => {
|
const renderItem = (event: NostrEvent) => {
|
||||||
if (!event) return;
|
if (!event) return;
|
||||||
switch (event.kind) {
|
switch (event.kind) {
|
||||||
case Kind.Repost:
|
case Kind.Repost:
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { TextNote } from "@/components/text";
|
import { TextNote } from "@/components/text";
|
||||||
import type { Event } from "@lume/types";
|
import type { NostrEvent } from "@lume/types";
|
||||||
import { Spinner } from "@lume/ui";
|
import { Spinner } from "@lume/ui";
|
||||||
import { Await, createFileRoute } from "@tanstack/react-router";
|
import { Await, createFileRoute } from "@tanstack/react-router";
|
||||||
import { defer } from "@tanstack/react-router";
|
import { defer } from "@tanstack/react-router";
|
||||||
@@ -15,7 +15,7 @@ export const Route = createFileRoute("/trending/notes")({
|
|||||||
signal: abortController.signal,
|
signal: abortController.signal,
|
||||||
})
|
})
|
||||||
.then((res) => res.json())
|
.then((res) => res.json())
|
||||||
.then((res) => res.notes.map((item) => item.event) as Event[]),
|
.then((res) => res.notes.map((item) => item.event) as NostrEvent[]),
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { ArticleIcon, GroupFeedsIcon } from "@lume/icons";
|
import { ArticleIcon, GroupFeedsIcon } from "@lume/icons";
|
||||||
|
import { NostrQuery } from "@lume/system";
|
||||||
import type { ColumnRouteSearch } from "@lume/types";
|
import type { ColumnRouteSearch } from "@lume/types";
|
||||||
import { cn } from "@lume/utils";
|
import { cn } from "@lume/utils";
|
||||||
import { Link, Outlet } from "@tanstack/react-router";
|
import { Link, Outlet } from "@tanstack/react-router";
|
||||||
@@ -12,10 +13,8 @@ export const Route = createFileRoute("/trending")({
|
|||||||
name: search.name,
|
name: search.name,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
beforeLoad: async ({ context }) => {
|
beforeLoad: async () => {
|
||||||
const ark = context.ark;
|
const settings = await NostrQuery.getSettings();
|
||||||
const settings = await ark.get_settings();
|
|
||||||
|
|
||||||
return { settings };
|
return { settings };
|
||||||
},
|
},
|
||||||
component: Screen,
|
component: Screen,
|
||||||
|
|||||||
@@ -6,20 +6,18 @@ import { Conversation } from "@/components/conversation";
|
|||||||
import { Quote } from "@/components/quote";
|
import { Quote } from "@/components/quote";
|
||||||
import { RepostNote } from "@/components/repost";
|
import { RepostNote } from "@/components/repost";
|
||||||
import { TextNote } from "@/components/text";
|
import { TextNote } from "@/components/text";
|
||||||
import { type Event, Kind } from "@lume/types";
|
import { type NostrEvent, Kind } from "@lume/types";
|
||||||
import { Suspense } from "react";
|
import { Suspense } from "react";
|
||||||
import { Await } from "@tanstack/react-router";
|
import { Await } from "@tanstack/react-router";
|
||||||
|
import { NostrQuery } from "@lume/system";
|
||||||
|
|
||||||
export const Route = createFileRoute("/users/$pubkey")({
|
export const Route = createFileRoute("/users/$pubkey")({
|
||||||
beforeLoad: async ({ context }) => {
|
beforeLoad: async () => {
|
||||||
const ark = context.ark;
|
const settings = await NostrQuery.getSettings();
|
||||||
const settings = await ark.get_settings();
|
|
||||||
|
|
||||||
return { settings };
|
return { settings };
|
||||||
},
|
},
|
||||||
loader: async ({ params, context }) => {
|
loader: async ({ params }) => {
|
||||||
const ark = context.ark;
|
return { data: defer(NostrQuery.getUserEvents(params.pubkey)) };
|
||||||
return { data: defer(ark.get_events_by(params.pubkey, 50)) };
|
|
||||||
},
|
},
|
||||||
component: Screen,
|
component: Screen,
|
||||||
});
|
});
|
||||||
@@ -28,7 +26,7 @@ function Screen() {
|
|||||||
const { pubkey } = Route.useParams();
|
const { pubkey } = Route.useParams();
|
||||||
const { data } = Route.useLoaderData();
|
const { data } = Route.useLoaderData();
|
||||||
|
|
||||||
const renderItem = (event: Event) => {
|
const renderItem = (event: NostrEvent) => {
|
||||||
if (!event) return;
|
if (!event) return;
|
||||||
switch (event.kind) {
|
switch (event.kind) {
|
||||||
case Kind.Repost:
|
case Kind.Repost:
|
||||||
|
|||||||
@@ -1,71 +0,0 @@
|
|||||||
import { RepostNote } from "@/components/repost";
|
|
||||||
import { TextNote } from "@/components/text";
|
|
||||||
import { ArrowRightCircleIcon, InfoIcon } from "@lume/icons";
|
|
||||||
import { type Event, Kind } from "@lume/types";
|
|
||||||
import { Spinner } from "@lume/ui";
|
|
||||||
import { useInfiniteQuery } from "@tanstack/react-query";
|
|
||||||
import { useRouteContext } from "@tanstack/react-router";
|
|
||||||
|
|
||||||
export function EventList({ id }: { id: string }) {
|
|
||||||
const { ark } = useRouteContext({ strict: false });
|
|
||||||
const { data, hasNextPage, isLoading, isFetchingNextPage, fetchNextPage } =
|
|
||||||
useInfiniteQuery({
|
|
||||||
queryKey: ["events", id],
|
|
||||||
initialPageParam: 0,
|
|
||||||
queryFn: async ({ pageParam }: { pageParam: number }) => {
|
|
||||||
const events = await ark.get_events_by(id, 20, pageParam);
|
|
||||||
return events;
|
|
||||||
},
|
|
||||||
getNextPageParam: (lastPage) => {
|
|
||||||
const lastEvent = lastPage?.at(-1);
|
|
||||||
return lastEvent ? lastEvent.created_at - 1 : null;
|
|
||||||
},
|
|
||||||
refetchOnWindowFocus: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
const renderItem = (event: Event) => {
|
|
||||||
if (!event) return;
|
|
||||||
switch (event.kind) {
|
|
||||||
case Kind.Repost:
|
|
||||||
return <RepostNote key={event.id} event={event} />;
|
|
||||||
default:
|
|
||||||
return <TextNote key={event.id} event={event} />;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
{isLoading ? (
|
|
||||||
<div className="flex h-20 w-full flex-col items-center justify-center gap-1">
|
|
||||||
<Spinner className="size-5" />
|
|
||||||
</div>
|
|
||||||
) : !data.length ? (
|
|
||||||
<div className="flex items-center gap-2 rounded-xl bg-neutral-50 p-5 dark:bg-neutral-950">
|
|
||||||
<InfoIcon className="size-6" />
|
|
||||||
<p>Empty newsfeed.</p>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
data.map((item) => renderItem(item))
|
|
||||||
)}
|
|
||||||
<div className="flex h-20 items-center justify-center">
|
|
||||||
{hasNextPage ? (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => fetchNextPage()}
|
|
||||||
disabled={!hasNextPage || isFetchingNextPage}
|
|
||||||
className="inline-flex h-12 w-36 items-center justify-center gap-2 rounded-full bg-neutral-100 px-3 font-medium hover:bg-neutral-200 focus:outline-none dark:bg-neutral-900 dark:hover:bg-neutral-800"
|
|
||||||
>
|
|
||||||
{isFetchingNextPage ? (
|
|
||||||
<Spinner className="size-5" />
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<ArrowRightCircleIcon className="size-5" />
|
|
||||||
Load more
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -7,6 +7,7 @@ import { useState } from "react";
|
|||||||
import CurrencyInput from "react-currency-input-field";
|
import CurrencyInput from "react-currency-input-field";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
import { LumeEvent } from "@lume/system";
|
||||||
|
|
||||||
const DEFAULT_VALUES = [69, 100, 200, 500];
|
const DEFAULT_VALUES = [69, 100, 200, 500];
|
||||||
|
|
||||||
@@ -16,7 +17,6 @@ export const Route = createLazyFileRoute("/zap/$id")({
|
|||||||
|
|
||||||
function Screen() {
|
function Screen() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { ark } = Route.useRouteContext();
|
|
||||||
const { id } = Route.useParams();
|
const { id } = Route.useParams();
|
||||||
// @ts-ignore, magic !!!
|
// @ts-ignore, magic !!!
|
||||||
const { pubkey, account } = Route.useSearch();
|
const { pubkey, account } = Route.useSearch();
|
||||||
@@ -31,7 +31,7 @@ function Screen() {
|
|||||||
// start loading
|
// start loading
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
|
|
||||||
const val = await ark.zap_event(id, amount, message);
|
const val = await LumeEvent.zap(id, amount, message);
|
||||||
|
|
||||||
if (val) {
|
if (val) {
|
||||||
setIsCompleted(true);
|
setIsCompleted(true);
|
||||||
|
|||||||
@@ -1,902 +0,0 @@
|
|||||||
import {
|
|
||||||
type Event,
|
|
||||||
type EventWithReplies,
|
|
||||||
type Interests,
|
|
||||||
type Keys,
|
|
||||||
type LumeColumn,
|
|
||||||
type Metadata,
|
|
||||||
type Settings,
|
|
||||||
Relays,
|
|
||||||
} from "@lume/types";
|
|
||||||
import { generateContentTags } from "@lume/utils";
|
|
||||||
import { invoke } from "@tauri-apps/api/core";
|
|
||||||
import type { WebviewWindow } from "@tauri-apps/api/webviewWindow";
|
|
||||||
import { open } from "@tauri-apps/plugin-dialog";
|
|
||||||
import { readFile } from "@tauri-apps/plugin-fs";
|
|
||||||
|
|
||||||
enum NSTORE_KEYS {
|
|
||||||
settings = "lume_user_settings",
|
|
||||||
columns = "lume_user_columns",
|
|
||||||
}
|
|
||||||
|
|
||||||
export class Ark {
|
|
||||||
public windows: WebviewWindow[];
|
|
||||||
public settings: Settings;
|
|
||||||
public accounts: string[];
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
this.windows = [];
|
|
||||||
this.settings = undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async get_accounts() {
|
|
||||||
try {
|
|
||||||
const cmd: string = await invoke("get_accounts");
|
|
||||||
const parse = cmd.split(/\s+/).filter((v) => v.startsWith("npub1"));
|
|
||||||
const accounts = [...new Set(parse)];
|
|
||||||
|
|
||||||
if (!this.accounts) {
|
|
||||||
this.accounts = accounts;
|
|
||||||
}
|
|
||||||
|
|
||||||
return accounts;
|
|
||||||
} catch (e) {
|
|
||||||
console.info(String(e));
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async load_account(npub: string) {
|
|
||||||
try {
|
|
||||||
const cmd: boolean = await invoke("load_account", {
|
|
||||||
npub,
|
|
||||||
});
|
|
||||||
return cmd;
|
|
||||||
} catch (e) {
|
|
||||||
throw new Error(String(e));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async nostr_connect(uri: string) {
|
|
||||||
try {
|
|
||||||
const remoteKey = uri.replace("bunker://", "").split("?")[0];
|
|
||||||
const npub: string = await invoke("to_npub", { hex: remoteKey });
|
|
||||||
|
|
||||||
if (npub) {
|
|
||||||
const connect: string = await invoke("nostr_connect", {
|
|
||||||
npub,
|
|
||||||
uri,
|
|
||||||
});
|
|
||||||
|
|
||||||
return connect;
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
throw new Error(String(e));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async create_keys() {
|
|
||||||
try {
|
|
||||||
const cmd: Keys = await invoke("create_account");
|
|
||||||
return cmd;
|
|
||||||
} catch (e) {
|
|
||||||
console.error(String(e));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async save_account(nsec: string, password = "") {
|
|
||||||
try {
|
|
||||||
const cmd: string = await invoke("save_account", {
|
|
||||||
nsec,
|
|
||||||
password,
|
|
||||||
});
|
|
||||||
|
|
||||||
return cmd;
|
|
||||||
} catch (e) {
|
|
||||||
throw new Error(String(e));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async event_to_bech32(id: string, relays: string[]) {
|
|
||||||
try {
|
|
||||||
const cmd: string = await invoke("event_to_bech32", {
|
|
||||||
id,
|
|
||||||
relays,
|
|
||||||
});
|
|
||||||
return cmd;
|
|
||||||
} catch (e) {
|
|
||||||
throw new Error(String(e));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async get_relays() {
|
|
||||||
try {
|
|
||||||
const cmd: Relays = await invoke("get_relays");
|
|
||||||
return cmd;
|
|
||||||
} catch (e) {
|
|
||||||
console.error(String(e));
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async add_relay(url: string) {
|
|
||||||
try {
|
|
||||||
const relayUrl = new URL(url);
|
|
||||||
|
|
||||||
if (relayUrl.protocol === "wss:" || relayUrl.protocol === "ws:") {
|
|
||||||
const cmd: boolean = await invoke("connect_relay", { relay: relayUrl });
|
|
||||||
return cmd;
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
throw new Error(String(e));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async remove_relay(url: string) {
|
|
||||||
try {
|
|
||||||
const relayUrl = new URL(url);
|
|
||||||
|
|
||||||
if (relayUrl.protocol === "wss:" || relayUrl.protocol === "ws:") {
|
|
||||||
const cmd: boolean = await invoke("remove_relay", { relay: relayUrl });
|
|
||||||
return cmd;
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
throw new Error(String(e));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async get_activities(account: string, kind: "1" | "6" | "9735" = "1") {
|
|
||||||
try {
|
|
||||||
const events: Event[] = await invoke("get_activities", { account, kind });
|
|
||||||
return events;
|
|
||||||
} catch (e) {
|
|
||||||
console.error(String(e));
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async get_event(id: string) {
|
|
||||||
try {
|
|
||||||
const eventId: string = id.replace("nostr:", "").replace(/[^\w\s]/gi, "");
|
|
||||||
const cmd: string = await invoke("get_event", { id: eventId });
|
|
||||||
const event: Event = JSON.parse(cmd);
|
|
||||||
return event;
|
|
||||||
} catch (e) {
|
|
||||||
console.error(id, String(e));
|
|
||||||
throw new Error(String(e));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async search(content: string, limit: number) {
|
|
||||||
try {
|
|
||||||
if (content.length < 1) return [];
|
|
||||||
|
|
||||||
const events: Event[] = await invoke("search", {
|
|
||||||
content: content.trim(),
|
|
||||||
limit,
|
|
||||||
});
|
|
||||||
|
|
||||||
return events;
|
|
||||||
} catch (e) {
|
|
||||||
console.info(String(e));
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private dedup_events(nostrEvents: Event[]) {
|
|
||||||
const seens = new Set<string>();
|
|
||||||
const events = nostrEvents.filter((event) => {
|
|
||||||
const eTags = event.tags.filter((el) => el[0] === "e");
|
|
||||||
const ids = eTags.map((item) => item[1]);
|
|
||||||
const isDup = ids.some((id) => seens.has(id));
|
|
||||||
|
|
||||||
// Add found ids to seen list
|
|
||||||
for (const id of ids) {
|
|
||||||
seens.add(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Filter NSFW event
|
|
||||||
if (this.settings?.nsfw) {
|
|
||||||
const wTags = event.tags.filter((t) => t[0] === "content-warning");
|
|
||||||
const isLewd = wTags.length > 0;
|
|
||||||
|
|
||||||
return !isDup && !isLewd;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Filter duplicate event
|
|
||||||
return !isDup;
|
|
||||||
});
|
|
||||||
|
|
||||||
return events;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async get_local_events(
|
|
||||||
pubkeys: string[],
|
|
||||||
limit: number,
|
|
||||||
asOf?: number,
|
|
||||||
) {
|
|
||||||
try {
|
|
||||||
const until: string = asOf && asOf > 0 ? asOf.toString() : undefined;
|
|
||||||
const nostrEvents: Event[] = await invoke("get_local_events", {
|
|
||||||
pubkeys,
|
|
||||||
limit,
|
|
||||||
until,
|
|
||||||
});
|
|
||||||
const events = this.dedup_events(nostrEvents);
|
|
||||||
|
|
||||||
return events;
|
|
||||||
} catch (e) {
|
|
||||||
console.error("[get_local_events] failed", String(e));
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async get_global_events(limit: number, asOf?: number) {
|
|
||||||
try {
|
|
||||||
const until: string = asOf && asOf > 0 ? asOf.toString() : undefined;
|
|
||||||
const nostrEvents: Event[] = await invoke("get_global_events", {
|
|
||||||
limit,
|
|
||||||
until,
|
|
||||||
});
|
|
||||||
const events = this.dedup_events(nostrEvents);
|
|
||||||
|
|
||||||
return events;
|
|
||||||
} catch (e) {
|
|
||||||
console.error("[get_global_events] failed", String(e));
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async get_hashtag_events(
|
|
||||||
hashtags: string[],
|
|
||||||
limit: number,
|
|
||||||
asOf?: number,
|
|
||||||
) {
|
|
||||||
try {
|
|
||||||
const until: string = asOf && asOf > 0 ? asOf.toString() : undefined;
|
|
||||||
const nostrTags = hashtags.map((tag) => tag.replace("#", ""));
|
|
||||||
const nostrEvents: Event[] = await invoke("get_hashtag_events", {
|
|
||||||
hashtags: nostrTags,
|
|
||||||
limit,
|
|
||||||
until,
|
|
||||||
});
|
|
||||||
const events = this.dedup_events(nostrEvents);
|
|
||||||
|
|
||||||
return events;
|
|
||||||
} catch (e) {
|
|
||||||
console.error("[get_hashtag_events] failed", String(e));
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async get_group_events(
|
|
||||||
contacts: string[],
|
|
||||||
limit: number,
|
|
||||||
asOf?: number,
|
|
||||||
) {
|
|
||||||
try {
|
|
||||||
const until: string = asOf && asOf > 0 ? asOf.toString() : undefined;
|
|
||||||
const nostrEvents: Event[] = await invoke("get_group_events", {
|
|
||||||
list: contacts,
|
|
||||||
limit,
|
|
||||||
until,
|
|
||||||
});
|
|
||||||
const events = this.dedup_events(nostrEvents);
|
|
||||||
|
|
||||||
return events;
|
|
||||||
} catch (e) {
|
|
||||||
console.error("[get_group_events] failed", String(e));
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async get_events_by(pubkey: string, limit: number, asOf?: number) {
|
|
||||||
try {
|
|
||||||
const until: string = asOf && asOf > 0 ? asOf.toString() : undefined;
|
|
||||||
const nostrEvents: Event[] = await invoke("get_events_by", {
|
|
||||||
publicKey: pubkey,
|
|
||||||
limit,
|
|
||||||
as_of: until,
|
|
||||||
});
|
|
||||||
|
|
||||||
return nostrEvents.sort((a, b) => b.created_at - a.created_at);
|
|
||||||
} catch (e) {
|
|
||||||
console.error("[get_events_by] failed", String(e));
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async publish(
|
|
||||||
content: string,
|
|
||||||
reply_to?: string,
|
|
||||||
quote?: boolean,
|
|
||||||
nsfw?: boolean,
|
|
||||||
) {
|
|
||||||
try {
|
|
||||||
const g = await generateContentTags(content);
|
|
||||||
|
|
||||||
const eventContent = g.content;
|
|
||||||
const eventTags = g.tags;
|
|
||||||
|
|
||||||
if (reply_to) {
|
|
||||||
const replyEvent = await this.get_event(reply_to);
|
|
||||||
const relayHint =
|
|
||||||
replyEvent.tags.find((ev) => ev[0] === "e")?.[0][2] ?? "";
|
|
||||||
|
|
||||||
if (quote) {
|
|
||||||
eventTags.push(["e", replyEvent.id, relayHint, "mention"]);
|
|
||||||
eventTags.push(["q", replyEvent.id]);
|
|
||||||
} else {
|
|
||||||
const rootEvent = replyEvent.tags.find((ev) => ev[3] === "root");
|
|
||||||
|
|
||||||
if (rootEvent) {
|
|
||||||
eventTags.push([
|
|
||||||
"e",
|
|
||||||
rootEvent[1],
|
|
||||||
rootEvent[2] || relayHint,
|
|
||||||
"root",
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
eventTags.push(["e", replyEvent.id, relayHint, "reply"]);
|
|
||||||
eventTags.push(["p", replyEvent.pubkey]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (nsfw) {
|
|
||||||
eventTags.push(["L", "content-warning"]);
|
|
||||||
eventTags.push(["l", "reason", "content-warning"]);
|
|
||||||
eventTags.push(["content-warning", "nsfw"]);
|
|
||||||
}
|
|
||||||
|
|
||||||
const cmd: string = await invoke("publish", {
|
|
||||||
content: eventContent,
|
|
||||||
tags: eventTags,
|
|
||||||
});
|
|
||||||
|
|
||||||
return cmd;
|
|
||||||
} catch (e) {
|
|
||||||
throw new Error(String(e));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async reply_to(content: string, tags: string[]) {
|
|
||||||
try {
|
|
||||||
const cmd: string = await invoke("reply_to", { content, tags });
|
|
||||||
return cmd;
|
|
||||||
} catch (e) {
|
|
||||||
throw new Error(String(e));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async repost(id: string, author: string) {
|
|
||||||
try {
|
|
||||||
const cmd: string = await invoke("repost", { id, pubkey: author });
|
|
||||||
return cmd;
|
|
||||||
} catch (e) {
|
|
||||||
throw new Error(String(e));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async get_event_thread(id: string) {
|
|
||||||
try {
|
|
||||||
const events: EventWithReplies[] = await invoke("get_thread", {
|
|
||||||
id,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (events.length > 0) {
|
|
||||||
const replies = new Set();
|
|
||||||
for (const event of events) {
|
|
||||||
const tags = event.tags.filter(
|
|
||||||
(el) => el[0] === "e" && el[1] !== id && el[3] !== "mention",
|
|
||||||
);
|
|
||||||
if (tags.length > 0) {
|
|
||||||
for (const tag of tags) {
|
|
||||||
const rootIndex = events.findIndex((el) => el.id === tag[1]);
|
|
||||||
if (rootIndex !== -1) {
|
|
||||||
const rootEvent = events[rootIndex];
|
|
||||||
if (rootEvent?.replies) {
|
|
||||||
rootEvent.replies.push(event);
|
|
||||||
} else {
|
|
||||||
rootEvent.replies = [event];
|
|
||||||
}
|
|
||||||
replies.add(event.id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const cleanEvents = events.filter((ev) => !replies.has(ev.id));
|
|
||||||
return cleanEvents;
|
|
||||||
}
|
|
||||||
|
|
||||||
return events;
|
|
||||||
} catch (e) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public get_thread(tags: string[][], gossip: boolean = false) {
|
|
||||||
let root: string = null;
|
|
||||||
let reply: string = null;
|
|
||||||
|
|
||||||
// Get all event references from tags, ignore mention
|
|
||||||
const events = tags.filter((el) => el[0] === "e" && el[3] !== "mention");
|
|
||||||
|
|
||||||
if (gossip) {
|
|
||||||
const relays = tags.filter((el) => el[0] === "e" && el[2]?.length);
|
|
||||||
|
|
||||||
if (relays.length >= 1) {
|
|
||||||
for (const relay of relays) {
|
|
||||||
if (relay[2]?.length) this.add_relay(relay[2]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (events.length === 1) {
|
|
||||||
root = events[0][1];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (events.length > 1) {
|
|
||||||
root = events.find((el) => el[3] === "root")?.[1] ?? events[0][1];
|
|
||||||
reply = events.find((el) => el[3] === "reply")?.[1] ?? events[1][1];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fix some rare case when root === reply
|
|
||||||
if (root && reply && root === reply) {
|
|
||||||
reply = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
root,
|
|
||||||
reply,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public async get_profile(pubkey: string) {
|
|
||||||
try {
|
|
||||||
const id = pubkey.replace("nostr:", "").replace(/[^\w\s]/gi, "");
|
|
||||||
const cmd: Metadata = await invoke("get_profile", { id });
|
|
||||||
|
|
||||||
return cmd;
|
|
||||||
} catch (e) {
|
|
||||||
console.error(pubkey, String(e));
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async get_current_user_profile() {
|
|
||||||
try {
|
|
||||||
const cmd: Metadata = await invoke("get_current_user_profile");
|
|
||||||
return cmd;
|
|
||||||
} catch {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async create_profile(profile: Metadata) {
|
|
||||||
try {
|
|
||||||
const event: string = await invoke("create_profile", {
|
|
||||||
name: profile.name || "",
|
|
||||||
display_name: profile.display_name || "",
|
|
||||||
displayName: profile.display_name || "",
|
|
||||||
about: profile.about || "",
|
|
||||||
picture: profile.picture || "",
|
|
||||||
banner: profile.banner || "",
|
|
||||||
nip05: profile.nip05 || "",
|
|
||||||
lud16: profile.lud16 || "",
|
|
||||||
website: profile.website || "",
|
|
||||||
});
|
|
||||||
return event;
|
|
||||||
} catch (e) {
|
|
||||||
throw new Error(String(e));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async set_contact_list(pubkeys: string[]) {
|
|
||||||
try {
|
|
||||||
const cmd: boolean = await invoke("set_contact_list", { pubkeys });
|
|
||||||
return cmd;
|
|
||||||
} catch (e) {
|
|
||||||
throw new Error(String(e));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async get_contact_list() {
|
|
||||||
try {
|
|
||||||
const cmd: string[] = await invoke("get_contact_list");
|
|
||||||
return cmd;
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async follow(id: string, alias?: string) {
|
|
||||||
try {
|
|
||||||
const cmd: string = await invoke("follow", { id, alias });
|
|
||||||
return cmd;
|
|
||||||
} catch (e) {
|
|
||||||
throw new Error(String(e));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async unfollow(id: string) {
|
|
||||||
try {
|
|
||||||
const cmd: string = await invoke("unfollow", { id });
|
|
||||||
return cmd;
|
|
||||||
} catch (e) {
|
|
||||||
throw new Error(String(e));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async user_to_bech32(key: string, relays: string[]) {
|
|
||||||
try {
|
|
||||||
const cmd: string = await invoke("user_to_bech32", {
|
|
||||||
key,
|
|
||||||
relays,
|
|
||||||
});
|
|
||||||
return cmd;
|
|
||||||
} catch (e) {
|
|
||||||
throw new Error(String(e));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async verify_nip05(pubkey: string, nip05: string) {
|
|
||||||
try {
|
|
||||||
const cmd: boolean = await invoke("verify_nip05", {
|
|
||||||
key: pubkey,
|
|
||||||
nip05,
|
|
||||||
});
|
|
||||||
return cmd;
|
|
||||||
} catch {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async set_nwc(uri: string) {
|
|
||||||
try {
|
|
||||||
const cmd: boolean = await invoke("set_nwc", { uri });
|
|
||||||
return cmd;
|
|
||||||
} catch (e) {
|
|
||||||
throw new Error(String(e));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async load_nwc() {
|
|
||||||
try {
|
|
||||||
const cmd: boolean = await invoke("load_nwc");
|
|
||||||
return cmd;
|
|
||||||
} catch (e) {
|
|
||||||
throw new Error(String(e));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async get_balance() {
|
|
||||||
try {
|
|
||||||
const cmd: number = await invoke("get_balance");
|
|
||||||
return cmd;
|
|
||||||
} catch (e) {
|
|
||||||
throw new Error(String(e));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async zap_profile(id: string, amount: number, message: string) {
|
|
||||||
try {
|
|
||||||
const cmd: boolean = await invoke("zap_profile", { id, amount, message });
|
|
||||||
return cmd;
|
|
||||||
} catch (e) {
|
|
||||||
throw new Error(String(e));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async zap_event(id: string, amount: number, message: string) {
|
|
||||||
try {
|
|
||||||
const cmd: boolean = await invoke("zap_event", { id, amount, message });
|
|
||||||
return cmd;
|
|
||||||
} catch (e) {
|
|
||||||
throw new Error(String(e));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async upload(filePath?: string) {
|
|
||||||
const allowExts = [
|
|
||||||
"png",
|
|
||||||
"jpeg",
|
|
||||||
"jpg",
|
|
||||||
"gif",
|
|
||||||
"mp4",
|
|
||||||
"mp3",
|
|
||||||
"webm",
|
|
||||||
"mkv",
|
|
||||||
"avi",
|
|
||||||
"mov",
|
|
||||||
];
|
|
||||||
|
|
||||||
const selected =
|
|
||||||
filePath ||
|
|
||||||
(
|
|
||||||
await open({
|
|
||||||
multiple: false,
|
|
||||||
filters: [
|
|
||||||
{
|
|
||||||
name: "Media",
|
|
||||||
extensions: allowExts,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
})
|
|
||||||
).path;
|
|
||||||
|
|
||||||
// User cancelled action
|
|
||||||
if (!selected) return null;
|
|
||||||
|
|
||||||
try {
|
|
||||||
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) {
|
|
||||||
throw new Error(String(e));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async get_columns() {
|
|
||||||
try {
|
|
||||||
const cmd: string = await invoke("get_nstore", {
|
|
||||||
key: NSTORE_KEYS.columns,
|
|
||||||
});
|
|
||||||
const columns: LumeColumn[] = cmd ? JSON.parse(cmd) : [];
|
|
||||||
return columns;
|
|
||||||
} catch {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async set_columns(columns: LumeColumn[]) {
|
|
||||||
try {
|
|
||||||
const cmd: string = await invoke("set_nstore", {
|
|
||||||
key: NSTORE_KEYS.columns,
|
|
||||||
content: JSON.stringify(columns),
|
|
||||||
});
|
|
||||||
return cmd;
|
|
||||||
} catch (e) {
|
|
||||||
throw new Error(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async get_settings() {
|
|
||||||
try {
|
|
||||||
if (this.settings) return this.settings;
|
|
||||||
|
|
||||||
const cmd: string = await invoke("get_nstore", {
|
|
||||||
key: NSTORE_KEYS.settings,
|
|
||||||
});
|
|
||||||
const settings: Settings = cmd ? JSON.parse(cmd) : null;
|
|
||||||
|
|
||||||
this.settings = settings;
|
|
||||||
|
|
||||||
return settings;
|
|
||||||
} catch {
|
|
||||||
const defaultSettings: Settings = {
|
|
||||||
autoUpdate: false,
|
|
||||||
enhancedPrivacy: false,
|
|
||||||
notification: false,
|
|
||||||
zap: false,
|
|
||||||
nsfw: false,
|
|
||||||
};
|
|
||||||
this.settings = defaultSettings;
|
|
||||||
return defaultSettings;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async set_settings(settings: Settings) {
|
|
||||||
try {
|
|
||||||
const cmd: string = await invoke("set_nstore", {
|
|
||||||
key: NSTORE_KEYS.settings,
|
|
||||||
content: JSON.stringify(settings),
|
|
||||||
});
|
|
||||||
return cmd;
|
|
||||||
} catch (e) {
|
|
||||||
throw new Error(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async get_nstore(key: string) {
|
|
||||||
try {
|
|
||||||
const cmd: string = await invoke("get_nstore", {
|
|
||||||
key,
|
|
||||||
});
|
|
||||||
const parse: string | string[] = cmd ? JSON.parse(cmd) : null;
|
|
||||||
if (!parse.length) return null;
|
|
||||||
return parse;
|
|
||||||
} catch {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async set_nstore(key: string, content: string) {
|
|
||||||
try {
|
|
||||||
const cmd: string = await invoke("set_nstore", {
|
|
||||||
key,
|
|
||||||
content,
|
|
||||||
});
|
|
||||||
return cmd;
|
|
||||||
} catch (e) {
|
|
||||||
throw new Error(String(e));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async open_event_id(id: string) {
|
|
||||||
try {
|
|
||||||
const label = `event-${id}`;
|
|
||||||
const url = `/events/${id}`;
|
|
||||||
|
|
||||||
await invoke("open_window", {
|
|
||||||
label,
|
|
||||||
title: "Thread",
|
|
||||||
url,
|
|
||||||
width: 500,
|
|
||||||
height: 800,
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
throw new Error(String(e));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async open_event(event: Event) {
|
|
||||||
try {
|
|
||||||
let root: string = undefined;
|
|
||||||
let reply: string = undefined;
|
|
||||||
|
|
||||||
const eTags = event.tags.filter(
|
|
||||||
(tag) => tag[0] === "e" || tag[0] === "q",
|
|
||||||
);
|
|
||||||
|
|
||||||
root = eTags.find((el) => el[3] === "root")?.[1];
|
|
||||||
reply = eTags.find((el) => el[3] === "reply")?.[1];
|
|
||||||
|
|
||||||
if (!root) root = eTags[0]?.[1];
|
|
||||||
if (!reply) reply = eTags[1]?.[1];
|
|
||||||
|
|
||||||
const label = `event-${event.id}`;
|
|
||||||
const url = `/events/${root ?? reply ?? event.id}`;
|
|
||||||
|
|
||||||
await invoke("open_window", {
|
|
||||||
label,
|
|
||||||
title: "Thread",
|
|
||||||
url,
|
|
||||||
width: 500,
|
|
||||||
height: 800,
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
throw new Error(String(e));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async open_profile(pubkey: string) {
|
|
||||||
try {
|
|
||||||
const label = `user-${pubkey}`;
|
|
||||||
await invoke("open_window", {
|
|
||||||
label,
|
|
||||||
title: "Profile",
|
|
||||||
url: `/users/${pubkey}`,
|
|
||||||
width: 500,
|
|
||||||
height: 800,
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
throw new Error(String(e));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async open_editor(reply_to?: string, quote = false) {
|
|
||||||
try {
|
|
||||||
let url: string;
|
|
||||||
|
|
||||||
if (reply_to) {
|
|
||||||
url = `/editor?reply_to=${reply_to}"e=${quote}`;
|
|
||||||
} else {
|
|
||||||
url = "/editor";
|
|
||||||
}
|
|
||||||
|
|
||||||
const label = `editor-${reply_to ? reply_to : 0}`;
|
|
||||||
|
|
||||||
await invoke("open_window", {
|
|
||||||
label,
|
|
||||||
title: "Editor",
|
|
||||||
url,
|
|
||||||
width: 560,
|
|
||||||
height: 340,
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
throw new Error(String(e));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async open_nwc() {
|
|
||||||
try {
|
|
||||||
const label = "nwc";
|
|
||||||
await invoke("open_window", {
|
|
||||||
label,
|
|
||||||
title: "Nostr Wallet Connect",
|
|
||||||
url: "/nwc",
|
|
||||||
width: 400,
|
|
||||||
height: 600,
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
throw new Error(String(e));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async open_zap(id: string, pubkey: string, account: string) {
|
|
||||||
try {
|
|
||||||
const label = `zap-${id}`;
|
|
||||||
await invoke("open_window", {
|
|
||||||
label,
|
|
||||||
title: "Zap",
|
|
||||||
url: `/zap/${id}?pubkey=${pubkey}&account=${account}`,
|
|
||||||
width: 400,
|
|
||||||
height: 500,
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
throw new Error(String(e));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async open_settings() {
|
|
||||||
try {
|
|
||||||
const label = "settings";
|
|
||||||
await invoke("open_window", {
|
|
||||||
label,
|
|
||||||
title: "Settings",
|
|
||||||
url: "/settings",
|
|
||||||
width: 800,
|
|
||||||
height: 500,
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
throw new Error(String(e));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async open_search() {
|
|
||||||
try {
|
|
||||||
const label = "search";
|
|
||||||
await invoke("open_window", {
|
|
||||||
label,
|
|
||||||
title: "Search",
|
|
||||||
url: "/search",
|
|
||||||
width: 400,
|
|
||||||
height: 600,
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
throw new Error(String(e));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async open_activity(account: string) {
|
|
||||||
try {
|
|
||||||
const label = "activity";
|
|
||||||
await invoke("open_window", {
|
|
||||||
label,
|
|
||||||
title: "Activity",
|
|
||||||
url: `/activity/${account}/texts`,
|
|
||||||
width: 400,
|
|
||||||
height: 600,
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
throw new Error(String(e));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
import { useQuery } from "@tanstack/react-query";
|
|
||||||
import { invoke } from "@tauri-apps/api/core";
|
|
||||||
|
|
||||||
export function usePreview(url: string) {
|
|
||||||
const { isLoading, isError, data } = useQuery({
|
|
||||||
queryKey: ["url", url],
|
|
||||||
queryFn: async () => {
|
|
||||||
try {
|
|
||||||
const cmd = await invoke("fetch_opg", { url });
|
|
||||||
console.log(cmd);
|
|
||||||
return cmd;
|
|
||||||
} catch (e) {
|
|
||||||
throw new Error(e);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
refetchOnWindowFocus: false,
|
|
||||||
refetchOnMount: false,
|
|
||||||
refetchOnReconnect: false,
|
|
||||||
staleTime: Number.POSITIVE_INFINITY,
|
|
||||||
retry: 2,
|
|
||||||
});
|
|
||||||
|
|
||||||
return { isLoading, isError, data };
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
export * from "./ark";
|
|
||||||
export * from "./hooks/useEvent";
|
|
||||||
export * from "./hooks/useProfile";
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"name": "@lume/ark",
|
"name": "@lume/system",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"main": "./src/index.ts",
|
"main": "./src/index.ts",
|
||||||
@@ -0,0 +1,160 @@
|
|||||||
|
import { Metadata } from "@lume/types";
|
||||||
|
import { commands } from "./commands";
|
||||||
|
|
||||||
|
export class NostrAccount {
|
||||||
|
static async getAccounts() {
|
||||||
|
const query = await commands.getAccounts();
|
||||||
|
|
||||||
|
if (query.status === "ok") {
|
||||||
|
return query.data;
|
||||||
|
} else {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static async loadAccount(npub: string) {
|
||||||
|
const query = await commands.loadAccount(npub);
|
||||||
|
|
||||||
|
if (query.status === "ok") {
|
||||||
|
return query.data;
|
||||||
|
} else {
|
||||||
|
throw new Error(query.error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static async createAccount() {
|
||||||
|
const query = await commands.createAccount();
|
||||||
|
|
||||||
|
if (query.status === "ok") {
|
||||||
|
return query.data;
|
||||||
|
} else {
|
||||||
|
throw new Error(query.error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static async createProfile(profile: Metadata) {
|
||||||
|
const query = await commands.createProfile(
|
||||||
|
profile.name || "",
|
||||||
|
profile.display_name || "",
|
||||||
|
profile.about || "",
|
||||||
|
profile.picture || "",
|
||||||
|
profile.banner || "",
|
||||||
|
profile.nip05 || "",
|
||||||
|
profile.lud16 || "",
|
||||||
|
profile.website || "",
|
||||||
|
);
|
||||||
|
|
||||||
|
if (query.status === "ok") {
|
||||||
|
return query.data;
|
||||||
|
} else {
|
||||||
|
throw new Error(query.error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static async saveAccount(nsec: string, password = "") {
|
||||||
|
const query = await commands.saveAccount(nsec, password);
|
||||||
|
|
||||||
|
if (query.status === "ok") {
|
||||||
|
return query.data;
|
||||||
|
} else {
|
||||||
|
throw new Error(query.error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static async connectRemoteAccount(uri: string) {
|
||||||
|
const remoteKey = uri.replace("bunker://", "").split("?")[0];
|
||||||
|
const npub = await commands.toNpub(remoteKey);
|
||||||
|
|
||||||
|
if (npub.status === "ok") {
|
||||||
|
const connect = await commands.nostrConnect(npub.data, uri);
|
||||||
|
|
||||||
|
if (connect.status === "ok") {
|
||||||
|
return connect.data;
|
||||||
|
} else {
|
||||||
|
throw new Error(connect.error);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new Error(npub.error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static async setContactList(pubkeys: string[]) {
|
||||||
|
const query = await commands.setContactList(pubkeys);
|
||||||
|
|
||||||
|
if (query.status === "ok") {
|
||||||
|
return query.data;
|
||||||
|
} else {
|
||||||
|
throw new Error(query.error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static async setWallet(uri: string) {
|
||||||
|
const query = await commands.setNwc(uri);
|
||||||
|
|
||||||
|
if (query.status === "ok") {
|
||||||
|
return query.data;
|
||||||
|
} else {
|
||||||
|
throw new Error(query.error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static async getProfile() {
|
||||||
|
const query = await commands.getCurrentUserProfile();
|
||||||
|
|
||||||
|
if (query.status === "ok") {
|
||||||
|
return JSON.parse(query.data) as Metadata;
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static async getBalance() {
|
||||||
|
const query = await commands.getBalance();
|
||||||
|
|
||||||
|
if (query.status === "ok") {
|
||||||
|
return parseInt(query.data);
|
||||||
|
} else {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static async getContactList() {
|
||||||
|
const query = await commands.getContactList();
|
||||||
|
|
||||||
|
if (query.status === "ok") {
|
||||||
|
return query.data;
|
||||||
|
} else {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static async follow(pubkey: string, alias?: string) {
|
||||||
|
const query = await commands.follow(pubkey, alias);
|
||||||
|
|
||||||
|
if (query.status === "ok") {
|
||||||
|
return query.data;
|
||||||
|
} else {
|
||||||
|
throw new Error(query.error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static async unfollow(pubkey: string) {
|
||||||
|
const query = await commands.unfollow(pubkey);
|
||||||
|
|
||||||
|
if (query.status === "ok") {
|
||||||
|
return query.data;
|
||||||
|
} else {
|
||||||
|
throw new Error(query.error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static async f2f(npub: string) {
|
||||||
|
const query = await commands.friendToFriend(npub);
|
||||||
|
|
||||||
|
if (query.status === "ok") {
|
||||||
|
return query.data;
|
||||||
|
} else {
|
||||||
|
throw new Error(query.error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,598 @@
|
|||||||
|
// This file was generated by [tauri-specta](https://github.com/oscartbeaumont/tauri-specta). Do not edit this file manually.
|
||||||
|
|
||||||
|
export const commands = {
|
||||||
|
async getRelays(): Promise<Result<Relays, null>> {
|
||||||
|
try {
|
||||||
|
return { status: "ok", data: await TAURI_INVOKE("get_relays") };
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof Error) throw e;
|
||||||
|
else return { status: "error", error: e as any };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async connectRelay(relay: string): Promise<Result<boolean, null>> {
|
||||||
|
try {
|
||||||
|
return {
|
||||||
|
status: "ok",
|
||||||
|
data: await TAURI_INVOKE("connect_relay", { relay }),
|
||||||
|
};
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof Error) throw e;
|
||||||
|
else return { status: "error", error: e as any };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async removeRelay(relay: string): Promise<Result<boolean, null>> {
|
||||||
|
try {
|
||||||
|
return {
|
||||||
|
status: "ok",
|
||||||
|
data: await TAURI_INVOKE("remove_relay", { relay }),
|
||||||
|
};
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof Error) throw e;
|
||||||
|
else return { status: "error", error: e as any };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async getAccounts(): Promise<Result<string[], string>> {
|
||||||
|
try {
|
||||||
|
return { status: "ok", data: await TAURI_INVOKE("get_accounts") };
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof Error) throw e;
|
||||||
|
else return { status: "error", error: e as any };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async createAccount(): Promise<Result<Account, null>> {
|
||||||
|
try {
|
||||||
|
return { status: "ok", data: await TAURI_INVOKE("create_account") };
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof Error) throw e;
|
||||||
|
else return { status: "error", error: e as any };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async saveAccount(
|
||||||
|
nsec: string,
|
||||||
|
password: string,
|
||||||
|
): Promise<Result<string, string>> {
|
||||||
|
try {
|
||||||
|
return {
|
||||||
|
status: "ok",
|
||||||
|
data: await TAURI_INVOKE("save_account", { nsec, password }),
|
||||||
|
};
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof Error) throw e;
|
||||||
|
else return { status: "error", error: e as any };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async getEncryptedKey(
|
||||||
|
npub: string,
|
||||||
|
password: string,
|
||||||
|
): Promise<Result<string, string>> {
|
||||||
|
try {
|
||||||
|
return {
|
||||||
|
status: "ok",
|
||||||
|
data: await TAURI_INVOKE("get_encrypted_key", { npub, password }),
|
||||||
|
};
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof Error) throw e;
|
||||||
|
else return { status: "error", error: e as any };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async nostrConnect(
|
||||||
|
npub: string,
|
||||||
|
uri: string,
|
||||||
|
): Promise<Result<string, string>> {
|
||||||
|
try {
|
||||||
|
return {
|
||||||
|
status: "ok",
|
||||||
|
data: await TAURI_INVOKE("nostr_connect", { npub, uri }),
|
||||||
|
};
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof Error) throw e;
|
||||||
|
else return { status: "error", error: e as any };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async loadAccount(npub: string): Promise<Result<boolean, string>> {
|
||||||
|
try {
|
||||||
|
return {
|
||||||
|
status: "ok",
|
||||||
|
data: await TAURI_INVOKE("load_account", { npub }),
|
||||||
|
};
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof Error) throw e;
|
||||||
|
else return { status: "error", error: e as any };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async eventToBech32(
|
||||||
|
id: string,
|
||||||
|
relays: string[],
|
||||||
|
): Promise<Result<string, null>> {
|
||||||
|
try {
|
||||||
|
return {
|
||||||
|
status: "ok",
|
||||||
|
data: await TAURI_INVOKE("event_to_bech32", { id, relays }),
|
||||||
|
};
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof Error) throw e;
|
||||||
|
else return { status: "error", error: e as any };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async userToBech32(
|
||||||
|
key: string,
|
||||||
|
relays: string[],
|
||||||
|
): Promise<Result<string, null>> {
|
||||||
|
try {
|
||||||
|
return {
|
||||||
|
status: "ok",
|
||||||
|
data: await TAURI_INVOKE("user_to_bech32", { key, relays }),
|
||||||
|
};
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof Error) throw e;
|
||||||
|
else return { status: "error", error: e as any };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async toNpub(hex: string): Promise<Result<string, null>> {
|
||||||
|
try {
|
||||||
|
return { status: "ok", data: await TAURI_INVOKE("to_npub", { hex }) };
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof Error) throw e;
|
||||||
|
else return { status: "error", error: e as any };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async verifyNip05(
|
||||||
|
key: string,
|
||||||
|
nip05: string,
|
||||||
|
): Promise<Result<boolean, string>> {
|
||||||
|
try {
|
||||||
|
return {
|
||||||
|
status: "ok",
|
||||||
|
data: await TAURI_INVOKE("verify_nip05", { key, nip05 }),
|
||||||
|
};
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof Error) throw e;
|
||||||
|
else return { status: "error", error: e as any };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async getActivities(
|
||||||
|
account: string,
|
||||||
|
kind: string,
|
||||||
|
): Promise<Result<string[], string>> {
|
||||||
|
try {
|
||||||
|
return {
|
||||||
|
status: "ok",
|
||||||
|
data: await TAURI_INVOKE("get_activities", { account, kind }),
|
||||||
|
};
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof Error) throw e;
|
||||||
|
else return { status: "error", error: e as any };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async getCurrentUserProfile(): Promise<Result<string, string>> {
|
||||||
|
try {
|
||||||
|
return {
|
||||||
|
status: "ok",
|
||||||
|
data: await TAURI_INVOKE("get_current_user_profile"),
|
||||||
|
};
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof Error) throw e;
|
||||||
|
else return { status: "error", error: e as any };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async getProfile(id: string): Promise<Result<string, string>> {
|
||||||
|
try {
|
||||||
|
return { status: "ok", data: await TAURI_INVOKE("get_profile", { id }) };
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof Error) throw e;
|
||||||
|
else return { status: "error", error: e as any };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async getContactList(): Promise<Result<string[], string>> {
|
||||||
|
try {
|
||||||
|
return { status: "ok", data: await TAURI_INVOKE("get_contact_list") };
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof Error) throw e;
|
||||||
|
else return { status: "error", error: e as any };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async setContactList(pubkeys: string[]): Promise<Result<boolean, string>> {
|
||||||
|
try {
|
||||||
|
return {
|
||||||
|
status: "ok",
|
||||||
|
data: await TAURI_INVOKE("set_contact_list", { pubkeys }),
|
||||||
|
};
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof Error) throw e;
|
||||||
|
else return { status: "error", error: e as any };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async createProfile(
|
||||||
|
name: string,
|
||||||
|
displayName: string,
|
||||||
|
about: string,
|
||||||
|
picture: string,
|
||||||
|
banner: string,
|
||||||
|
nip05: string,
|
||||||
|
lud16: string,
|
||||||
|
website: string,
|
||||||
|
): Promise<Result<string, string>> {
|
||||||
|
try {
|
||||||
|
return {
|
||||||
|
status: "ok",
|
||||||
|
data: await TAURI_INVOKE("create_profile", {
|
||||||
|
name,
|
||||||
|
displayName,
|
||||||
|
about,
|
||||||
|
picture,
|
||||||
|
banner,
|
||||||
|
nip05,
|
||||||
|
lud16,
|
||||||
|
website,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof Error) throw e;
|
||||||
|
else return { status: "error", error: e as any };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async follow(
|
||||||
|
id: string,
|
||||||
|
alias: string | null,
|
||||||
|
): Promise<Result<string, string>> {
|
||||||
|
try {
|
||||||
|
return {
|
||||||
|
status: "ok",
|
||||||
|
data: await TAURI_INVOKE("follow", { id, alias }),
|
||||||
|
};
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof Error) throw e;
|
||||||
|
else return { status: "error", error: e as any };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async unfollow(id: string): Promise<Result<string, string>> {
|
||||||
|
try {
|
||||||
|
return { status: "ok", data: await TAURI_INVOKE("unfollow", { id }) };
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof Error) throw e;
|
||||||
|
else return { status: "error", error: e as any };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async getNstore(key: string): Promise<Result<string, string>> {
|
||||||
|
try {
|
||||||
|
return { status: "ok", data: await TAURI_INVOKE("get_nstore", { key }) };
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof Error) throw e;
|
||||||
|
else return { status: "error", error: e as any };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async setNstore(
|
||||||
|
key: string,
|
||||||
|
content: string,
|
||||||
|
): Promise<Result<string, string>> {
|
||||||
|
try {
|
||||||
|
return {
|
||||||
|
status: "ok",
|
||||||
|
data: await TAURI_INVOKE("set_nstore", { key, content }),
|
||||||
|
};
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof Error) throw e;
|
||||||
|
else return { status: "error", error: e as any };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async setNwc(uri: string): Promise<Result<boolean, string>> {
|
||||||
|
try {
|
||||||
|
return { status: "ok", data: await TAURI_INVOKE("set_nwc", { uri }) };
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof Error) throw e;
|
||||||
|
else return { status: "error", error: e as any };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async loadNwc(): Promise<Result<boolean, string>> {
|
||||||
|
try {
|
||||||
|
return { status: "ok", data: await TAURI_INVOKE("load_nwc") };
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof Error) throw e;
|
||||||
|
else return { status: "error", error: e as any };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async getBalance(): Promise<Result<string, string>> {
|
||||||
|
try {
|
||||||
|
return { status: "ok", data: await TAURI_INVOKE("get_balance") };
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof Error) throw e;
|
||||||
|
else return { status: "error", error: e as any };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async zapProfile(
|
||||||
|
id: string,
|
||||||
|
amount: string,
|
||||||
|
message: string,
|
||||||
|
): Promise<Result<boolean, string>> {
|
||||||
|
try {
|
||||||
|
return {
|
||||||
|
status: "ok",
|
||||||
|
data: await TAURI_INVOKE("zap_profile", { id, amount, message }),
|
||||||
|
};
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof Error) throw e;
|
||||||
|
else return { status: "error", error: e as any };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async zapEvent(
|
||||||
|
id: string,
|
||||||
|
amount: string,
|
||||||
|
message: string,
|
||||||
|
): Promise<Result<boolean, string>> {
|
||||||
|
try {
|
||||||
|
return {
|
||||||
|
status: "ok",
|
||||||
|
data: await TAURI_INVOKE("zap_event", { id, amount, message }),
|
||||||
|
};
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof Error) throw e;
|
||||||
|
else return { status: "error", error: e as any };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async friendToFriend(npub: string): Promise<Result<boolean, string>> {
|
||||||
|
try {
|
||||||
|
return {
|
||||||
|
status: "ok",
|
||||||
|
data: await TAURI_INVOKE("friend_to_friend", { npub }),
|
||||||
|
};
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof Error) throw e;
|
||||||
|
else return { status: "error", error: e as any };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async getEvent(id: string): Promise<Result<string, string>> {
|
||||||
|
try {
|
||||||
|
return { status: "ok", data: await TAURI_INVOKE("get_event", { id }) };
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof Error) throw e;
|
||||||
|
else return { status: "error", error: e as any };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async getReplies(id: string): Promise<Result<string[], string>> {
|
||||||
|
try {
|
||||||
|
return { status: "ok", data: await TAURI_INVOKE("get_replies", { id }) };
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof Error) throw e;
|
||||||
|
else return { status: "error", error: e as any };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async getEventsBy(
|
||||||
|
publicKey: string,
|
||||||
|
asOf: string | null,
|
||||||
|
): Promise<Result<string[], string>> {
|
||||||
|
try {
|
||||||
|
return {
|
||||||
|
status: "ok",
|
||||||
|
data: await TAURI_INVOKE("get_events_by", { publicKey, asOf }),
|
||||||
|
};
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof Error) throw e;
|
||||||
|
else return { status: "error", error: e as any };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async getLocalEvents(
|
||||||
|
pubkeys: string[],
|
||||||
|
until: string | null,
|
||||||
|
): Promise<Result<string[], string>> {
|
||||||
|
try {
|
||||||
|
return {
|
||||||
|
status: "ok",
|
||||||
|
data: await TAURI_INVOKE("get_local_events", { pubkeys, until }),
|
||||||
|
};
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof Error) throw e;
|
||||||
|
else return { status: "error", error: e as any };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async getGlobalEvents(
|
||||||
|
until: string | null,
|
||||||
|
): Promise<Result<string[], string>> {
|
||||||
|
try {
|
||||||
|
return {
|
||||||
|
status: "ok",
|
||||||
|
data: await TAURI_INVOKE("get_global_events", { until }),
|
||||||
|
};
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof Error) throw e;
|
||||||
|
else return { status: "error", error: e as any };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async getHashtagEvents(
|
||||||
|
hashtags: string[],
|
||||||
|
until: string | null,
|
||||||
|
): Promise<Result<string[], string>> {
|
||||||
|
try {
|
||||||
|
return {
|
||||||
|
status: "ok",
|
||||||
|
data: await TAURI_INVOKE("get_hashtag_events", { hashtags, until }),
|
||||||
|
};
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof Error) throw e;
|
||||||
|
else return { status: "error", error: e as any };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async publish(
|
||||||
|
content: string,
|
||||||
|
tags: string[][],
|
||||||
|
): Promise<Result<string, string>> {
|
||||||
|
try {
|
||||||
|
return {
|
||||||
|
status: "ok",
|
||||||
|
data: await TAURI_INVOKE("publish", { content, tags }),
|
||||||
|
};
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof Error) throw e;
|
||||||
|
else return { status: "error", error: e as any };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async repost(raw: string): Promise<Result<string, string>> {
|
||||||
|
try {
|
||||||
|
return { status: "ok", data: await TAURI_INVOKE("repost", { raw }) };
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof Error) throw e;
|
||||||
|
else return { status: "error", error: e as any };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async showInFolder(path: string): Promise<void> {
|
||||||
|
await TAURI_INVOKE("show_in_folder", { path });
|
||||||
|
},
|
||||||
|
async createColumn(
|
||||||
|
label: string,
|
||||||
|
x: number,
|
||||||
|
y: number,
|
||||||
|
width: number,
|
||||||
|
height: number,
|
||||||
|
url: string,
|
||||||
|
): Promise<Result<string, string>> {
|
||||||
|
try {
|
||||||
|
return {
|
||||||
|
status: "ok",
|
||||||
|
data: await TAURI_INVOKE("create_column", {
|
||||||
|
label,
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
url,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof Error) throw e;
|
||||||
|
else return { status: "error", error: e as any };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async closeColumn(label: string): Promise<Result<boolean, null>> {
|
||||||
|
try {
|
||||||
|
return {
|
||||||
|
status: "ok",
|
||||||
|
data: await TAURI_INVOKE("close_column", { label }),
|
||||||
|
};
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof Error) throw e;
|
||||||
|
else return { status: "error", error: e as any };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async repositionColumn(
|
||||||
|
label: string,
|
||||||
|
x: number,
|
||||||
|
y: number,
|
||||||
|
): Promise<Result<null, string>> {
|
||||||
|
try {
|
||||||
|
return {
|
||||||
|
status: "ok",
|
||||||
|
data: await TAURI_INVOKE("reposition_column", { label, x, y }),
|
||||||
|
};
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof Error) throw e;
|
||||||
|
else return { status: "error", error: e as any };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async resizeColumn(
|
||||||
|
label: string,
|
||||||
|
width: number,
|
||||||
|
height: number,
|
||||||
|
): Promise<Result<null, string>> {
|
||||||
|
try {
|
||||||
|
return {
|
||||||
|
status: "ok",
|
||||||
|
data: await TAURI_INVOKE("resize_column", { label, width, height }),
|
||||||
|
};
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof Error) throw e;
|
||||||
|
else return { status: "error", error: e as any };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async openWindow(
|
||||||
|
label: string,
|
||||||
|
title: string,
|
||||||
|
url: string,
|
||||||
|
width: number,
|
||||||
|
height: number,
|
||||||
|
): Promise<Result<null, string>> {
|
||||||
|
try {
|
||||||
|
return {
|
||||||
|
status: "ok",
|
||||||
|
data: await TAURI_INVOKE("open_window", {
|
||||||
|
label,
|
||||||
|
title,
|
||||||
|
url,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof Error) throw e;
|
||||||
|
else return { status: "error", error: e as any };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async setBadge(count: number): Promise<void> {
|
||||||
|
await TAURI_INVOKE("set_badge", { count });
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/** user-defined types **/
|
||||||
|
|
||||||
|
export type Account = { npub: string; nsec: string };
|
||||||
|
export type Relays = {
|
||||||
|
connected: string[];
|
||||||
|
read: string[] | null;
|
||||||
|
write: string[] | null;
|
||||||
|
both: string[] | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** tauri-specta globals **/
|
||||||
|
|
||||||
|
import { invoke as TAURI_INVOKE } from "@tauri-apps/api/core";
|
||||||
|
import * as TAURI_API_EVENT from "@tauri-apps/api/event";
|
||||||
|
import { type WebviewWindow as __WebviewWindow__ } from "@tauri-apps/api/webviewWindow";
|
||||||
|
|
||||||
|
type __EventObj__<T> = {
|
||||||
|
listen: (
|
||||||
|
cb: TAURI_API_EVENT.EventCallback<T>,
|
||||||
|
) => ReturnType<typeof TAURI_API_EVENT.listen<T>>;
|
||||||
|
once: (
|
||||||
|
cb: TAURI_API_EVENT.EventCallback<T>,
|
||||||
|
) => ReturnType<typeof TAURI_API_EVENT.once<T>>;
|
||||||
|
emit: T extends null
|
||||||
|
? (payload?: T) => ReturnType<typeof TAURI_API_EVENT.emit>
|
||||||
|
: (payload: T) => ReturnType<typeof TAURI_API_EVENT.emit>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Result<T, E> =
|
||||||
|
| { status: "ok"; data: T }
|
||||||
|
| { status: "error"; error: E };
|
||||||
|
|
||||||
|
function __makeEvents__<T extends Record<string, any>>(
|
||||||
|
mappings: Record<keyof T, string>,
|
||||||
|
) {
|
||||||
|
return new Proxy(
|
||||||
|
{} as unknown as {
|
||||||
|
[K in keyof T]: __EventObj__<T[K]> & {
|
||||||
|
(handle: __WebviewWindow__): __EventObj__<T[K]>;
|
||||||
|
};
|
||||||
|
},
|
||||||
|
{
|
||||||
|
get: (_, event) => {
|
||||||
|
const name = mappings[event as keyof T];
|
||||||
|
|
||||||
|
return new Proxy((() => {}) as any, {
|
||||||
|
apply: (_, __, [window]: [__WebviewWindow__]) => ({
|
||||||
|
listen: (arg: any) => window.listen(name, arg),
|
||||||
|
once: (arg: any) => window.once(name, arg),
|
||||||
|
emit: (arg: any) => window.emit(name, arg),
|
||||||
|
}),
|
||||||
|
get: (_, command: keyof __EventObj__<any>) => {
|
||||||
|
switch (command) {
|
||||||
|
case "listen":
|
||||||
|
return (arg: any) => TAURI_API_EVENT.listen(name, arg);
|
||||||
|
case "once":
|
||||||
|
return (arg: any) => TAURI_API_EVENT.once(name, arg);
|
||||||
|
case "emit":
|
||||||
|
return (arg: any) => TAURI_API_EVENT.emit(name, arg);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import { NostrEvent } from "@lume/types";
|
||||||
|
|
||||||
|
export function dedupEvents(nostrEvents: NostrEvent[], nsfw: boolean = false) {
|
||||||
|
const seens = new Set<string>();
|
||||||
|
const events = nostrEvents.filter((event) => {
|
||||||
|
const eTags = event.tags.filter((el) => el[0] === "e");
|
||||||
|
const ids = eTags.map((item) => item[1]);
|
||||||
|
const isDup = ids.some((id) => seens.has(id));
|
||||||
|
|
||||||
|
// Add found ids to seen list
|
||||||
|
for (const id of ids) {
|
||||||
|
seens.add(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter NSFW event
|
||||||
|
if (nsfw) {
|
||||||
|
const wTags = event.tags.filter((t) => t[0] === "content-warning");
|
||||||
|
const isLewd = wTags.length > 0;
|
||||||
|
|
||||||
|
return !isDup && !isLewd;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter duplicate event
|
||||||
|
return !isDup;
|
||||||
|
});
|
||||||
|
|
||||||
|
return events;
|
||||||
|
}
|
||||||
@@ -0,0 +1,201 @@
|
|||||||
|
import { EventWithReplies, Kind, NostrEvent } from "@lume/types";
|
||||||
|
import { commands } from "./commands";
|
||||||
|
import { generateContentTags } from "@lume/utils";
|
||||||
|
|
||||||
|
export class LumeEvent {
|
||||||
|
public id: string;
|
||||||
|
public pubkey: string;
|
||||||
|
public created_at: number;
|
||||||
|
public kind: Kind;
|
||||||
|
public tags: string[][];
|
||||||
|
public content: string;
|
||||||
|
public sig: string;
|
||||||
|
public relay?: string;
|
||||||
|
#raw: NostrEvent;
|
||||||
|
|
||||||
|
constructor(event: NostrEvent) {
|
||||||
|
this.#raw = event;
|
||||||
|
Object.assign(this, event);
|
||||||
|
}
|
||||||
|
|
||||||
|
get mentions() {
|
||||||
|
return this.tags.filter((tag) => tag[0] === "p").map((tag) => tag[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
static getEventThread(tags: string[][], gossip?: boolean) {
|
||||||
|
let root: string = null;
|
||||||
|
let reply: string = null;
|
||||||
|
|
||||||
|
// Get all event references from tags, ignore mention
|
||||||
|
const events = tags.filter((el) => el[0] === "e" && el[3] !== "mention");
|
||||||
|
|
||||||
|
if (gossip) {
|
||||||
|
const relays = tags.filter((el) => el[0] === "e" && el[2]?.length);
|
||||||
|
|
||||||
|
if (relays.length >= 1) {
|
||||||
|
for (const relay of relays) {
|
||||||
|
if (relay[2]?.length)
|
||||||
|
commands
|
||||||
|
.connectRelay(relay[2])
|
||||||
|
.then(() => console.log("[gossip]: ", relay[2]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (events.length === 1) {
|
||||||
|
root = events[0][1];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (events.length > 1) {
|
||||||
|
root = events.find((el) => el[3] === "root")?.[1] ?? events[0][1];
|
||||||
|
reply = events.find((el) => el[3] === "reply")?.[1] ?? events[1][1];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fix some rare case when root === reply
|
||||||
|
if (root && reply && root === reply) {
|
||||||
|
reply = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
root,
|
||||||
|
reply,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static async getReplies(id: string) {
|
||||||
|
const query = await commands.getReplies(id);
|
||||||
|
|
||||||
|
if (query.status === "ok") {
|
||||||
|
const events = query.data.map(
|
||||||
|
(item) => JSON.parse(item) as EventWithReplies,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (events.length > 0) {
|
||||||
|
const replies = new Set();
|
||||||
|
|
||||||
|
for (const event of events) {
|
||||||
|
const tags = event.tags.filter(
|
||||||
|
(el) => el[0] === "e" && el[1] !== id && el[3] !== "mention",
|
||||||
|
);
|
||||||
|
|
||||||
|
if (tags.length > 0) {
|
||||||
|
for (const tag of tags) {
|
||||||
|
const rootIndex = events.findIndex((el) => el.id === tag[1]);
|
||||||
|
|
||||||
|
if (rootIndex !== -1) {
|
||||||
|
const rootEvent = events[rootIndex];
|
||||||
|
|
||||||
|
if (rootEvent?.replies) {
|
||||||
|
rootEvent.replies.push(event);
|
||||||
|
} else {
|
||||||
|
rootEvent.replies = [event];
|
||||||
|
}
|
||||||
|
|
||||||
|
replies.add(event.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return events.filter((ev) => !replies.has(ev.id));
|
||||||
|
}
|
||||||
|
|
||||||
|
return events;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static async publish(
|
||||||
|
content: string,
|
||||||
|
reply_to?: string,
|
||||||
|
quote?: boolean,
|
||||||
|
nsfw?: boolean,
|
||||||
|
) {
|
||||||
|
const g = await generateContentTags(content);
|
||||||
|
|
||||||
|
const eventContent = g.content;
|
||||||
|
const eventTags = g.tags;
|
||||||
|
|
||||||
|
if (reply_to) {
|
||||||
|
const queryReply = await commands.getEvent(reply_to);
|
||||||
|
|
||||||
|
if (queryReply.status === "ok") {
|
||||||
|
const replyEvent = JSON.parse(queryReply.data) as NostrEvent;
|
||||||
|
const relayHint =
|
||||||
|
replyEvent.tags.find((ev) => ev[0] === "e")?.[0][2] ?? "";
|
||||||
|
|
||||||
|
if (quote) {
|
||||||
|
eventTags.push(["e", replyEvent.id, relayHint, "mention"]);
|
||||||
|
eventTags.push(["q", replyEvent.id]);
|
||||||
|
} else {
|
||||||
|
const rootEvent = replyEvent.tags.find((ev) => ev[3] === "root");
|
||||||
|
|
||||||
|
if (rootEvent) {
|
||||||
|
eventTags.push([
|
||||||
|
"e",
|
||||||
|
rootEvent[1],
|
||||||
|
rootEvent[2] || relayHint,
|
||||||
|
"root",
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
eventTags.push(["e", replyEvent.id, relayHint, "reply"]);
|
||||||
|
eventTags.push(["p", replyEvent.pubkey]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nsfw) {
|
||||||
|
eventTags.push(["L", "content-warning"]);
|
||||||
|
eventTags.push(["l", "reason", "content-warning"]);
|
||||||
|
eventTags.push(["content-warning", "nsfw"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
const query = await commands.publish(eventContent, eventTags);
|
||||||
|
|
||||||
|
if (query.status === "ok") {
|
||||||
|
return query.data;
|
||||||
|
} else {
|
||||||
|
throw new Error(query.error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static async zap(id: string, amount: number, message: string) {
|
||||||
|
const query = await commands.zapEvent(id, amount.toString(), message);
|
||||||
|
|
||||||
|
if (query.status === "ok") {
|
||||||
|
return query.data;
|
||||||
|
} else {
|
||||||
|
throw new Error(query.error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async idAsBech32() {
|
||||||
|
const query = await commands.eventToBech32(this.id, []);
|
||||||
|
|
||||||
|
if (query.status === "ok") {
|
||||||
|
return query.data;
|
||||||
|
} else {
|
||||||
|
throw new Error(query.error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async pubkeyAsBech32() {
|
||||||
|
const query = await commands.userToBech32(this.pubkey, []);
|
||||||
|
|
||||||
|
if (query.status === "ok") {
|
||||||
|
return query.data;
|
||||||
|
} else {
|
||||||
|
throw new Error(query.error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async repost() {
|
||||||
|
const query = await commands.repost(JSON.stringify(this.#raw));
|
||||||
|
|
||||||
|
if (query.status === "ok") {
|
||||||
|
return query.data;
|
||||||
|
} else {
|
||||||
|
throw new Error(query.error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { Event } from "@lume/types";
|
import type { Event, NostrEvent } from "@lume/types";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { invoke } from "@tauri-apps/api/core";
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
|
|
||||||
@@ -12,7 +12,7 @@ export function useEvent(id: string) {
|
|||||||
.split("'")[0]
|
.split("'")[0]
|
||||||
.split(".")[0];
|
.split(".")[0];
|
||||||
const cmd: string = await invoke("get_event", { id: eventId });
|
const cmd: string = await invoke("get_event", { id: eventId });
|
||||||
const event: Event = JSON.parse(cmd);
|
const event: NostrEvent = JSON.parse(cmd);
|
||||||
return event;
|
return event;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw new Error(e);
|
throw new Error(e);
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
import { useInfiniteQuery } from "@tanstack/react-query";
|
||||||
|
import { commands } from "../commands";
|
||||||
|
import { dedupEvents } from "../dedup";
|
||||||
|
import { NostrEvent } from "@lume/types";
|
||||||
|
|
||||||
|
export function useInfiniteEvents(
|
||||||
|
contacts: string[],
|
||||||
|
label: string,
|
||||||
|
account: string,
|
||||||
|
nsfw?: boolean,
|
||||||
|
) {
|
||||||
|
const pubkeys = contacts;
|
||||||
|
const {
|
||||||
|
data,
|
||||||
|
isLoading,
|
||||||
|
isFetching,
|
||||||
|
isFetchingNextPage,
|
||||||
|
hasNextPage,
|
||||||
|
fetchNextPage,
|
||||||
|
} = useInfiniteQuery({
|
||||||
|
queryKey: [label, account],
|
||||||
|
initialPageParam: 0,
|
||||||
|
queryFn: async ({ pageParam }: { pageParam: number }) => {
|
||||||
|
try {
|
||||||
|
const until: string = pageParam > 0 ? pageParam.toString() : undefined;
|
||||||
|
const query = await commands.getLocalEvents(pubkeys, until);
|
||||||
|
|
||||||
|
if (query.status === "ok") {
|
||||||
|
const nostrEvents = query.data as unknown as NostrEvent[];
|
||||||
|
const events = dedupEvents(nostrEvents, nsfw);
|
||||||
|
|
||||||
|
return events;
|
||||||
|
} else {
|
||||||
|
throw new Error(query.error);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
throw new Error(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getNextPageParam: (lastPage) => lastPage?.at(-1)?.created_at - 1,
|
||||||
|
select: (data) => data?.pages.flatMap((page) => page),
|
||||||
|
refetchOnWindowFocus: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
data,
|
||||||
|
isLoading,
|
||||||
|
isFetching,
|
||||||
|
isFetchingNextPage,
|
||||||
|
hasNextPage,
|
||||||
|
fetchNextPage,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { Metadata } from "@lume/types";
|
import type { Metadata } from "@lume/types";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { invoke } from "@tauri-apps/api/core";
|
import { commands } from "../commands";
|
||||||
|
|
||||||
export function useProfile(pubkey: string, embed?: string) {
|
export function useProfile(pubkey: string, embed?: string) {
|
||||||
const {
|
const {
|
||||||
@@ -11,15 +11,16 @@ export function useProfile(pubkey: string, embed?: string) {
|
|||||||
queryKey: ["user", pubkey],
|
queryKey: ["user", pubkey],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
try {
|
try {
|
||||||
if (embed) {
|
if (embed) return JSON.parse(embed) as Metadata;
|
||||||
const profile: Metadata = JSON.parse(embed);
|
|
||||||
return profile;
|
const normalize = pubkey.replace("nostr:", "").replace(/[^\w\s]/gi, "");
|
||||||
|
const query = await commands.getProfile(normalize);
|
||||||
|
|
||||||
|
if (query.status === "ok") {
|
||||||
|
return JSON.parse(query.data) as Metadata;
|
||||||
|
} else {
|
||||||
|
throw new Error(query.error);
|
||||||
}
|
}
|
||||||
|
|
||||||
const id = pubkey.replace("nostr:", "").replace(/[^\w\s]/gi, "");
|
|
||||||
const cmd: Metadata = await invoke("get_profile", { id });
|
|
||||||
|
|
||||||
return cmd;
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw new Error(e);
|
throw new Error(e);
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
export * from "./event";
|
||||||
|
export * from "./account";
|
||||||
|
export * from "./query";
|
||||||
|
export * from "./window";
|
||||||
|
export * from "./commands";
|
||||||
|
export * from "./hooks/useEvent";
|
||||||
|
export * from "./hooks/useInfiniteEvents";
|
||||||
|
export * from "./hooks/useProfile";
|
||||||
@@ -0,0 +1,309 @@
|
|||||||
|
import { LumeColumn, Metadata, NostrEvent, Settings } from "@lume/types";
|
||||||
|
import { commands } from "./commands";
|
||||||
|
import { resolveResource } from "@tauri-apps/api/path";
|
||||||
|
import { readFile, readTextFile } from "@tauri-apps/plugin-fs";
|
||||||
|
import { isPermissionGranted } from "@tauri-apps/plugin-notification";
|
||||||
|
import { open } from "@tauri-apps/plugin-dialog";
|
||||||
|
import { dedupEvents } from "./dedup";
|
||||||
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
|
|
||||||
|
enum NSTORE_KEYS {
|
||||||
|
settings = "lume_user_settings",
|
||||||
|
columns = "lume_user_columns",
|
||||||
|
}
|
||||||
|
|
||||||
|
export class NostrQuery {
|
||||||
|
static async upload(filePath?: string) {
|
||||||
|
const allowExts = [
|
||||||
|
"png",
|
||||||
|
"jpeg",
|
||||||
|
"jpg",
|
||||||
|
"gif",
|
||||||
|
"mp4",
|
||||||
|
"mp3",
|
||||||
|
"webm",
|
||||||
|
"mkv",
|
||||||
|
"avi",
|
||||||
|
"mov",
|
||||||
|
];
|
||||||
|
|
||||||
|
const selected =
|
||||||
|
filePath ||
|
||||||
|
(
|
||||||
|
await open({
|
||||||
|
multiple: false,
|
||||||
|
filters: [
|
||||||
|
{
|
||||||
|
name: "Media",
|
||||||
|
extensions: allowExts,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
).path;
|
||||||
|
|
||||||
|
// User cancelled action
|
||||||
|
if (!selected) return null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
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) {
|
||||||
|
throw new Error(String(e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static async getProfile(pubkey: string) {
|
||||||
|
const normalize = pubkey.replace("nostr:", "").replace(/[^\w\s]/gi, "");
|
||||||
|
const query = await commands.getProfile(normalize);
|
||||||
|
|
||||||
|
if (query.status === "ok") {
|
||||||
|
const profile: Metadata = JSON.parse(query.data);
|
||||||
|
return profile;
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static async getEvent(id: string) {
|
||||||
|
const normalize: string = id.replace("nostr:", "").replace(/[^\w\s]/gi, "");
|
||||||
|
const query = await commands.getEvent(normalize);
|
||||||
|
|
||||||
|
if (query.status === "ok") {
|
||||||
|
const event: NostrEvent = JSON.parse(query.data);
|
||||||
|
return event;
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static async getUserEvents(pubkey: string, asOf?: number) {
|
||||||
|
const until: string = asOf && asOf > 0 ? asOf.toString() : undefined;
|
||||||
|
const query = await commands.getEventsBy(pubkey, until);
|
||||||
|
|
||||||
|
if (query.status === "ok") {
|
||||||
|
const events = query.data.map((item) => JSON.parse(item) as NostrEvent);
|
||||||
|
return events;
|
||||||
|
} else {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static async getUserActivities(
|
||||||
|
account: string,
|
||||||
|
kind: "1" | "6" | "9735" = "1",
|
||||||
|
) {
|
||||||
|
const query = await commands.getActivities(account, kind);
|
||||||
|
|
||||||
|
if (query.status === "ok") {
|
||||||
|
const events = query.data.map((item) => JSON.parse(item) as NostrEvent);
|
||||||
|
return events;
|
||||||
|
} else {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static async getLocalEvents(pubkeys: string[], asOf?: number) {
|
||||||
|
const until: string = asOf && asOf > 0 ? asOf.toString() : undefined;
|
||||||
|
const query = await commands.getLocalEvents(pubkeys, until);
|
||||||
|
|
||||||
|
if (query.status === "ok") {
|
||||||
|
const events = query.data.map((item) => JSON.parse(item) as NostrEvent);
|
||||||
|
const dedup = dedupEvents(events);
|
||||||
|
|
||||||
|
return dedup;
|
||||||
|
} else {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static async getGlobalEvents(asOf?: number) {
|
||||||
|
const until: string = asOf && asOf > 0 ? asOf.toString() : undefined;
|
||||||
|
const query = await commands.getGlobalEvents(until);
|
||||||
|
|
||||||
|
if (query.status === "ok") {
|
||||||
|
const events = query.data.map((item) => JSON.parse(item) as NostrEvent);
|
||||||
|
const dedup = dedupEvents(events);
|
||||||
|
|
||||||
|
return dedup;
|
||||||
|
} else {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static async getHashtagEvents(hashtags: string[], asOf?: number) {
|
||||||
|
const until: string = asOf && asOf > 0 ? asOf.toString() : undefined;
|
||||||
|
const nostrTags = hashtags.map((tag) => tag.replace("#", ""));
|
||||||
|
const query = await commands.getHashtagEvents(nostrTags, until);
|
||||||
|
|
||||||
|
if (query.status === "ok") {
|
||||||
|
const events = query.data.map((item) => JSON.parse(item) as NostrEvent);
|
||||||
|
const dedup = dedupEvents(events);
|
||||||
|
|
||||||
|
return dedup;
|
||||||
|
} else {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static async verifyNip05(pubkey: string, nip05?: string) {
|
||||||
|
if (!nip05) return false;
|
||||||
|
|
||||||
|
const query = await commands.verifyNip05(pubkey, nip05);
|
||||||
|
|
||||||
|
if (query.status === "ok") {
|
||||||
|
return query.data;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static async getNstore(key: string) {
|
||||||
|
const query = await commands.getNstore(key);
|
||||||
|
|
||||||
|
if (query.status === "ok") {
|
||||||
|
const data: string | string[] = query.data
|
||||||
|
? JSON.parse(query.data)
|
||||||
|
: null;
|
||||||
|
return data;
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static async setNstore(key: string, value: string) {
|
||||||
|
const query = await commands.setNstore(key, value);
|
||||||
|
|
||||||
|
if (query.status === "ok") {
|
||||||
|
return query.data;
|
||||||
|
} else {
|
||||||
|
throw new Error(query.error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static async getSettings() {
|
||||||
|
const query = await commands.getNstore(NSTORE_KEYS.settings);
|
||||||
|
|
||||||
|
if (query.status === "ok") {
|
||||||
|
const settings: Settings = query.data ? JSON.parse(query.data) : null;
|
||||||
|
const isGranted = await isPermissionGranted();
|
||||||
|
const theme: "auto" | "light" | "dark" = await invoke(
|
||||||
|
"plugin:theme|get_theme",
|
||||||
|
);
|
||||||
|
|
||||||
|
return { ...settings, theme, notification: isGranted };
|
||||||
|
} else {
|
||||||
|
const initial: Settings = {
|
||||||
|
autoUpdate: false,
|
||||||
|
enhancedPrivacy: false,
|
||||||
|
notification: false,
|
||||||
|
zap: false,
|
||||||
|
nsfw: false,
|
||||||
|
gossip: false,
|
||||||
|
theme: "auto",
|
||||||
|
};
|
||||||
|
|
||||||
|
return initial;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static async setSettings(settings: Settings) {
|
||||||
|
const query = await commands.setNstore(
|
||||||
|
NSTORE_KEYS.settings,
|
||||||
|
JSON.stringify(settings),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (query.status === "ok") {
|
||||||
|
return query.data;
|
||||||
|
} else {
|
||||||
|
throw new Error(query.error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static async getColumns() {
|
||||||
|
const query = await commands.getNstore(NSTORE_KEYS.columns);
|
||||||
|
|
||||||
|
if (query.status === "ok") {
|
||||||
|
const columns: LumeColumn[] = query.data ? JSON.parse(query.data) : [];
|
||||||
|
|
||||||
|
if (columns.length < 1) {
|
||||||
|
const systemPath = "resources/system_columns.json";
|
||||||
|
const resourcePath = await resolveResource(systemPath);
|
||||||
|
const resourceFile = await readTextFile(resourcePath);
|
||||||
|
const systemColumns: LumeColumn[] = JSON.parse(resourceFile);
|
||||||
|
|
||||||
|
return systemColumns;
|
||||||
|
}
|
||||||
|
|
||||||
|
return columns;
|
||||||
|
} else {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static async setColumns(columns: LumeColumn[]) {
|
||||||
|
const query = await commands.setNstore(
|
||||||
|
NSTORE_KEYS.columns,
|
||||||
|
JSON.stringify(columns),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (query.status === "ok") {
|
||||||
|
return query.data;
|
||||||
|
} else {
|
||||||
|
throw new Error(query.error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static async getRelays() {
|
||||||
|
const query = await commands.getRelays();
|
||||||
|
|
||||||
|
if (query.status === "ok") {
|
||||||
|
return query.data;
|
||||||
|
} else {
|
||||||
|
throw new Error(query.error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static async connectRelay(url: string) {
|
||||||
|
const relayUrl = new URL(url);
|
||||||
|
|
||||||
|
if (relayUrl.protocol === "wss:" || relayUrl.protocol === "ws:") {
|
||||||
|
const query = await commands.connectRelay(relayUrl.toString());
|
||||||
|
|
||||||
|
if (query.status === "ok") {
|
||||||
|
return query.data;
|
||||||
|
} else {
|
||||||
|
throw new Error(query.error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static async removeRelay(url: string) {
|
||||||
|
const relayUrl = new URL(url);
|
||||||
|
|
||||||
|
if (relayUrl.protocol === "wss:" || relayUrl.protocol === "ws:") {
|
||||||
|
const query = await commands.removeRelay(relayUrl.toString());
|
||||||
|
|
||||||
|
if (query.status === "ok") {
|
||||||
|
return query.data;
|
||||||
|
} else {
|
||||||
|
throw new Error(query.error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,140 @@
|
|||||||
|
import { NostrEvent } from "@lume/types";
|
||||||
|
import { commands } from "./commands";
|
||||||
|
|
||||||
|
export class LumeWindow {
|
||||||
|
static async openEvent(event: NostrEvent) {
|
||||||
|
const eTags = event.tags.filter((tag) => tag[0] === "e" || tag[0] === "q");
|
||||||
|
const root: string =
|
||||||
|
eTags.find((el) => el[3] === "root")?.[1] ?? eTags[0]?.[1];
|
||||||
|
const reply: string =
|
||||||
|
eTags.find((el) => el[3] === "reply")?.[1] ?? eTags[1]?.[1];
|
||||||
|
|
||||||
|
const label = `event-${event.id}`;
|
||||||
|
const url = `/events/${root ?? reply ?? event.id}`;
|
||||||
|
|
||||||
|
const query = await commands.openWindow(label, "Thread", url, 500, 800);
|
||||||
|
|
||||||
|
if (query.status === "ok") {
|
||||||
|
return query.data;
|
||||||
|
} else {
|
||||||
|
throw new Error(query.error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static async openProfile(pubkey: string) {
|
||||||
|
const label = `user-${pubkey}`;
|
||||||
|
const query = await commands.openWindow(
|
||||||
|
label,
|
||||||
|
"Profile",
|
||||||
|
`/users/${pubkey}`,
|
||||||
|
500,
|
||||||
|
800,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (query.status === "ok") {
|
||||||
|
return query.data;
|
||||||
|
} else {
|
||||||
|
throw new Error(query.error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static async openEditor(reply_to?: string, quote = false) {
|
||||||
|
let url: string;
|
||||||
|
|
||||||
|
if (reply_to) {
|
||||||
|
url = `/editor?reply_to=${reply_to}"e=${quote}`;
|
||||||
|
} else {
|
||||||
|
url = "/editor";
|
||||||
|
}
|
||||||
|
|
||||||
|
const label = `editor-${reply_to ? reply_to : 0}`;
|
||||||
|
const query = await commands.openWindow(label, "Editor", url, 560, 340);
|
||||||
|
|
||||||
|
if (query.status === "ok") {
|
||||||
|
return query.data;
|
||||||
|
} else {
|
||||||
|
throw new Error(query.error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static async openZap(id: string, pubkey: string) {
|
||||||
|
const nwc = await commands.loadNwc();
|
||||||
|
|
||||||
|
if (nwc.status === "ok") {
|
||||||
|
const status = nwc.data;
|
||||||
|
|
||||||
|
if (!status) {
|
||||||
|
const label = "nwc";
|
||||||
|
await commands.openWindow(
|
||||||
|
label,
|
||||||
|
"Nostr Wallet Connect",
|
||||||
|
"/nwc",
|
||||||
|
400,
|
||||||
|
600,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
const label = `zap-${id}`;
|
||||||
|
await commands.openWindow(
|
||||||
|
label,
|
||||||
|
"Zap",
|
||||||
|
`/zap/${id}?pubkey=${pubkey}`,
|
||||||
|
400,
|
||||||
|
500,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new Error(nwc.error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static async openSettings() {
|
||||||
|
const label = "settings";
|
||||||
|
const query = await commands.openWindow(
|
||||||
|
label,
|
||||||
|
"Settings",
|
||||||
|
"/settings",
|
||||||
|
800,
|
||||||
|
500,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (query.status === "ok") {
|
||||||
|
return query.data;
|
||||||
|
} else {
|
||||||
|
throw new Error(query.error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static async openSearch() {
|
||||||
|
const label = "search";
|
||||||
|
const query = await commands.openWindow(
|
||||||
|
label,
|
||||||
|
"Search",
|
||||||
|
"/search",
|
||||||
|
400,
|
||||||
|
600,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (query.status === "ok") {
|
||||||
|
return query.data;
|
||||||
|
} else {
|
||||||
|
throw new Error(query.error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static async openActivity(account: string) {
|
||||||
|
const label = "activity";
|
||||||
|
const query = await commands.openWindow(
|
||||||
|
label,
|
||||||
|
"Activity",
|
||||||
|
`/activity/${account}/texts`,
|
||||||
|
400,
|
||||||
|
600,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (query.status === "ok") {
|
||||||
|
return query.data;
|
||||||
|
} else {
|
||||||
|
throw new Error(query.error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,6 +4,8 @@ export interface Settings {
|
|||||||
autoUpdate: boolean;
|
autoUpdate: boolean;
|
||||||
zap: boolean;
|
zap: boolean;
|
||||||
nsfw: boolean;
|
nsfw: boolean;
|
||||||
|
gossip: boolean;
|
||||||
|
theme: "auto" | "light" | "dark";
|
||||||
[key: string]: string | number | boolean;
|
[key: string]: string | number | boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -26,7 +28,7 @@ export enum Kind {
|
|||||||
// #TODO: Add all nostr kinds
|
// #TODO: Add all nostr kinds
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Event {
|
export interface NostrEvent {
|
||||||
id: string;
|
id: string;
|
||||||
pubkey: string;
|
pubkey: string;
|
||||||
created_at: number;
|
created_at: number;
|
||||||
@@ -34,11 +36,10 @@ export interface Event {
|
|||||||
tags: string[][];
|
tags: string[][];
|
||||||
content: string;
|
content: string;
|
||||||
sig: string;
|
sig: string;
|
||||||
relay?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface EventWithReplies extends Event {
|
export interface EventWithReplies extends NostrEvent {
|
||||||
replies: Array<Event>;
|
replies: Array<NostrEvent>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Metadata {
|
export interface Metadata {
|
||||||
|
|||||||
@@ -4,43 +4,11 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"main": "./src/index.ts",
|
"main": "./src/index.ts",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@getalby/sdk": "^3.5.1",
|
|
||||||
"@lume/ark": "workspace:^",
|
|
||||||
"@lume/icons": "workspace:^",
|
"@lume/icons": "workspace:^",
|
||||||
"@lume/utils": "workspace:^",
|
"@lume/utils": "workspace:^",
|
||||||
"@radix-ui/react-accordion": "^1.1.2",
|
|
||||||
"@radix-ui/react-alert-dialog": "^1.0.5",
|
|
||||||
"@radix-ui/react-avatar": "^1.0.4",
|
|
||||||
"@radix-ui/react-collapsible": "^1.0.3",
|
|
||||||
"@radix-ui/react-dialog": "^1.0.5",
|
|
||||||
"@radix-ui/react-dropdown-menu": "^2.0.6",
|
|
||||||
"@radix-ui/react-hover-card": "^1.0.7",
|
|
||||||
"@radix-ui/react-popover": "^1.0.7",
|
|
||||||
"@radix-ui/react-tooltip": "^1.0.7",
|
|
||||||
"@tanstack/react-query": "^5.36.0",
|
|
||||||
"@tanstack/react-router": "^1.32.5",
|
|
||||||
"framer-motion": "^11.2.0",
|
|
||||||
"get-urls": "^12.1.0",
|
|
||||||
"media-chrome": "^3.2.2",
|
|
||||||
"minidenticons": "^4.2.1",
|
|
||||||
"nanoid": "^5.0.7",
|
|
||||||
"qrcode.react": "^3.1.0",
|
|
||||||
"re-resizable": "^6.9.16",
|
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-currency-input-field": "^3.8.0",
|
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"react-hook-form": "^7.51.4",
|
"react-snap-carousel": "^0.4.0"
|
||||||
"react-hotkeys-hook": "^4.5.0",
|
|
||||||
"react-i18next": "^14.1.1",
|
|
||||||
"react-snap-carousel": "^0.4.0",
|
|
||||||
"react-string-replace": "^1.1.1",
|
|
||||||
"slate": "^0.103.0",
|
|
||||||
"slate-react": "^0.102.0",
|
|
||||||
"sonner": "^1.4.41",
|
|
||||||
"string-strip-html": "^13.4.8",
|
|
||||||
"uqr": "^0.1.2",
|
|
||||||
"use-debounce": "^10.0.0",
|
|
||||||
"virtua": "^0.31.0"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@lume/tailwindcss": "workspace:^",
|
"@lume/tailwindcss": "workspace:^",
|
||||||
|
|||||||
@@ -60,17 +60,10 @@ export const insertMention = (
|
|||||||
name: contact.profile.name || contact.profile.display_name || "anon",
|
name: contact.profile.name || contact.profile.display_name || "anon",
|
||||||
children: [text],
|
children: [text],
|
||||||
};
|
};
|
||||||
const extraText = [
|
|
||||||
{
|
|
||||||
type: "paragraph",
|
|
||||||
children: [text],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
// @ts-ignore, idk
|
// @ts-ignore, idk
|
||||||
ReactEditor.focus(editor);
|
ReactEditor.focus(editor);
|
||||||
Transforms.insertNodes(editor, mention);
|
Transforms.insertNodes(editor, mention);
|
||||||
Transforms.insertNodes(editor, extraText);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const insertNostrEvent = (
|
export const insertNostrEvent = (
|
||||||
|
|||||||
@@ -57,12 +57,12 @@ importers:
|
|||||||
|
|
||||||
apps/desktop2:
|
apps/desktop2:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@lume/ark':
|
|
||||||
specifier: workspace:^
|
|
||||||
version: link:../../packages/ark
|
|
||||||
'@lume/icons':
|
'@lume/icons':
|
||||||
specifier: workspace:^
|
specifier: workspace:^
|
||||||
version: link:../../packages/icons
|
version: link:../../packages/icons
|
||||||
|
'@lume/system':
|
||||||
|
specifier: workspace:^
|
||||||
|
version: link:../../packages/system
|
||||||
'@lume/ui':
|
'@lume/ui':
|
||||||
specifier: workspace:^
|
specifier: workspace:^
|
||||||
version: link:../../packages/ui
|
version: link:../../packages/ui
|
||||||
@@ -240,7 +240,23 @@ importers:
|
|||||||
specifier: ^0.5.13
|
specifier: ^0.5.13
|
||||||
version: 0.5.13(tailwindcss@3.4.3)
|
version: 0.5.13(tailwindcss@3.4.3)
|
||||||
|
|
||||||
packages/ark:
|
packages/icons:
|
||||||
|
dependencies:
|
||||||
|
react:
|
||||||
|
specifier: ^18.3.1
|
||||||
|
version: 18.3.1
|
||||||
|
devDependencies:
|
||||||
|
'@lume/tsconfig':
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../tsconfig
|
||||||
|
'@types/react':
|
||||||
|
specifier: ^18.3.2
|
||||||
|
version: 18.3.2
|
||||||
|
typescript:
|
||||||
|
specifier: ^5.4.5
|
||||||
|
version: 5.4.5
|
||||||
|
|
||||||
|
packages/system:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@lume/utils':
|
'@lume/utils':
|
||||||
specifier: workspace:^
|
specifier: workspace:^
|
||||||
@@ -265,22 +281,6 @@ importers:
|
|||||||
specifier: ^5.4.5
|
specifier: ^5.4.5
|
||||||
version: 5.4.5
|
version: 5.4.5
|
||||||
|
|
||||||
packages/icons:
|
|
||||||
dependencies:
|
|
||||||
react:
|
|
||||||
specifier: ^18.3.1
|
|
||||||
version: 18.3.1
|
|
||||||
devDependencies:
|
|
||||||
'@lume/tsconfig':
|
|
||||||
specifier: workspace:*
|
|
||||||
version: link:../tsconfig
|
|
||||||
'@types/react':
|
|
||||||
specifier: ^18.3.2
|
|
||||||
version: 18.3.2
|
|
||||||
typescript:
|
|
||||||
specifier: ^5.4.5
|
|
||||||
version: 5.4.5
|
|
||||||
|
|
||||||
packages/tailwindcss:
|
packages/tailwindcss:
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@evilmartians/harmony':
|
'@evilmartians/harmony':
|
||||||
@@ -312,117 +312,21 @@ importers:
|
|||||||
|
|
||||||
packages/ui:
|
packages/ui:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@getalby/sdk':
|
|
||||||
specifier: ^3.5.1
|
|
||||||
version: 3.5.1(typescript@5.4.5)
|
|
||||||
'@lume/ark':
|
|
||||||
specifier: workspace:^
|
|
||||||
version: link:../ark
|
|
||||||
'@lume/icons':
|
'@lume/icons':
|
||||||
specifier: workspace:^
|
specifier: workspace:^
|
||||||
version: link:../icons
|
version: link:../icons
|
||||||
'@lume/utils':
|
'@lume/utils':
|
||||||
specifier: workspace:^
|
specifier: workspace:^
|
||||||
version: link:../utils
|
version: link:../utils
|
||||||
'@radix-ui/react-accordion':
|
|
||||||
specifier: ^1.1.2
|
|
||||||
version: 1.1.2(@types/react@18.3.2)(react-dom@18.3.1)(react@18.3.1)
|
|
||||||
'@radix-ui/react-alert-dialog':
|
|
||||||
specifier: ^1.0.5
|
|
||||||
version: 1.0.5(@types/react@18.3.2)(react-dom@18.3.1)(react@18.3.1)
|
|
||||||
'@radix-ui/react-avatar':
|
|
||||||
specifier: ^1.0.4
|
|
||||||
version: 1.0.4(@types/react-dom@18.3.0)(@types/react@18.3.2)(react-dom@18.3.1)(react@18.3.1)
|
|
||||||
'@radix-ui/react-collapsible':
|
|
||||||
specifier: ^1.0.3
|
|
||||||
version: 1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.2)(react-dom@18.3.1)(react@18.3.1)
|
|
||||||
'@radix-ui/react-dialog':
|
|
||||||
specifier: ^1.0.5
|
|
||||||
version: 1.0.5(@types/react-dom@18.3.0)(@types/react@18.3.2)(react-dom@18.3.1)(react@18.3.1)
|
|
||||||
'@radix-ui/react-dropdown-menu':
|
|
||||||
specifier: ^2.0.6
|
|
||||||
version: 2.0.6(@types/react-dom@18.3.0)(@types/react@18.3.2)(react-dom@18.3.1)(react@18.3.1)
|
|
||||||
'@radix-ui/react-hover-card':
|
|
||||||
specifier: ^1.0.7
|
|
||||||
version: 1.0.7(@types/react-dom@18.3.0)(@types/react@18.3.2)(react-dom@18.3.1)(react@18.3.1)
|
|
||||||
'@radix-ui/react-popover':
|
|
||||||
specifier: ^1.0.7
|
|
||||||
version: 1.0.7(@types/react-dom@18.3.0)(@types/react@18.3.2)(react-dom@18.3.1)(react@18.3.1)
|
|
||||||
'@radix-ui/react-tooltip':
|
|
||||||
specifier: ^1.0.7
|
|
||||||
version: 1.0.7(@types/react-dom@18.3.0)(@types/react@18.3.2)(react-dom@18.3.1)(react@18.3.1)
|
|
||||||
'@tanstack/react-query':
|
|
||||||
specifier: ^5.36.0
|
|
||||||
version: 5.36.0(react@18.3.1)
|
|
||||||
'@tanstack/react-router':
|
|
||||||
specifier: ^1.32.5
|
|
||||||
version: 1.32.5(react-dom@18.3.1)(react@18.3.1)
|
|
||||||
framer-motion:
|
|
||||||
specifier: ^11.2.0
|
|
||||||
version: 11.2.0(react-dom@18.3.1)(react@18.3.1)
|
|
||||||
get-urls:
|
|
||||||
specifier: ^12.1.0
|
|
||||||
version: 12.1.0
|
|
||||||
media-chrome:
|
|
||||||
specifier: ^3.2.2
|
|
||||||
version: 3.2.2
|
|
||||||
minidenticons:
|
|
||||||
specifier: ^4.2.1
|
|
||||||
version: 4.2.1
|
|
||||||
nanoid:
|
|
||||||
specifier: ^5.0.7
|
|
||||||
version: 5.0.7
|
|
||||||
qrcode.react:
|
|
||||||
specifier: ^3.1.0
|
|
||||||
version: 3.1.0(react@18.3.1)
|
|
||||||
re-resizable:
|
|
||||||
specifier: ^6.9.16
|
|
||||||
version: 6.9.16(react-dom@18.3.1)(react@18.3.1)
|
|
||||||
react:
|
react:
|
||||||
specifier: ^18.3.1
|
specifier: ^18.3.1
|
||||||
version: 18.3.1
|
version: 18.3.1
|
||||||
react-currency-input-field:
|
|
||||||
specifier: ^3.8.0
|
|
||||||
version: 3.8.0(react@18.3.1)
|
|
||||||
react-dom:
|
react-dom:
|
||||||
specifier: ^18.3.1
|
specifier: ^18.3.1
|
||||||
version: 18.3.1(react@18.3.1)
|
version: 18.3.1(react@18.3.1)
|
||||||
react-hook-form:
|
|
||||||
specifier: ^7.51.4
|
|
||||||
version: 7.51.4(react@18.3.1)
|
|
||||||
react-hotkeys-hook:
|
|
||||||
specifier: ^4.5.0
|
|
||||||
version: 4.5.0(react-dom@18.3.1)(react@18.3.1)
|
|
||||||
react-i18next:
|
|
||||||
specifier: ^14.1.1
|
|
||||||
version: 14.1.1(i18next@23.11.4)(react-dom@18.3.1)(react@18.3.1)
|
|
||||||
react-snap-carousel:
|
react-snap-carousel:
|
||||||
specifier: ^0.4.0
|
specifier: ^0.4.0
|
||||||
version: 0.4.0(react@18.3.1)
|
version: 0.4.0(react@18.3.1)
|
||||||
react-string-replace:
|
|
||||||
specifier: ^1.1.1
|
|
||||||
version: 1.1.1
|
|
||||||
slate:
|
|
||||||
specifier: ^0.103.0
|
|
||||||
version: 0.103.0
|
|
||||||
slate-react:
|
|
||||||
specifier: ^0.102.0
|
|
||||||
version: 0.102.0(react-dom@18.3.1)(react@18.3.1)(slate@0.103.0)
|
|
||||||
sonner:
|
|
||||||
specifier: ^1.4.41
|
|
||||||
version: 1.4.41(react-dom@18.3.1)(react@18.3.1)
|
|
||||||
string-strip-html:
|
|
||||||
specifier: ^13.4.8
|
|
||||||
version: 13.4.8
|
|
||||||
uqr:
|
|
||||||
specifier: ^0.1.2
|
|
||||||
version: 0.1.2
|
|
||||||
use-debounce:
|
|
||||||
specifier: ^10.0.0
|
|
||||||
version: 10.0.0(react@18.3.1)
|
|
||||||
virtua:
|
|
||||||
specifier: ^0.31.0
|
|
||||||
version: 0.31.0(react-dom@18.3.1)(react@18.3.1)
|
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@lume/tailwindcss':
|
'@lume/tailwindcss':
|
||||||
specifier: workspace:^
|
specifier: workspace:^
|
||||||
@@ -1469,16 +1373,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-7ncjjSpRSRKvjJEoru092iFiEoC89lz4oG4+SGg9hh7DI/5SXf+kE+dg+6Fv/bwiK/WJCo4Q2gvPZGRlCE5mcA==}
|
resolution: {integrity: sha512-7ncjjSpRSRKvjJEoru092iFiEoC89lz4oG4+SGg9hh7DI/5SXf+kE+dg+6Fv/bwiK/WJCo4Q2gvPZGRlCE5mcA==}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/@getalby/sdk@3.5.1(typescript@5.4.5):
|
|
||||||
resolution: {integrity: sha512-Qz9GgXMoVpupDLqbzA2CHpru+9yqijQrxeRN7CDfV6l39js/BGwin93MFTh7eFj2TsMo+i8JeM3BVn+SJn/iRg==}
|
|
||||||
engines: {node: '>=14'}
|
|
||||||
dependencies:
|
|
||||||
eventemitter3: 5.0.1
|
|
||||||
nostr-tools: 1.17.0(typescript@5.4.5)
|
|
||||||
transitivePeerDependencies:
|
|
||||||
- typescript
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/@img/sharp-darwin-arm64@0.33.3:
|
/@img/sharp-darwin-arm64@0.33.3:
|
||||||
resolution: {integrity: sha512-FaNiGX1MrOuJ3hxuNzWgsT/mg5OHG/Izh59WW2mk1UwYHUwtfbhk5QNKYZgxf0pLOhx9ctGiGa2OykD71vOnSw==}
|
resolution: {integrity: sha512-FaNiGX1MrOuJ3hxuNzWgsT/mg5OHG/Izh59WW2mk1UwYHUwtfbhk5QNKYZgxf0pLOhx9ctGiGa2OykD71vOnSw==}
|
||||||
engines: {glibc: '>=2.26', node: ^18.17.0 || ^20.3.0 || >=21.0.0, npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'}
|
engines: {glibc: '>=2.26', node: ^18.17.0 || ^20.3.0 || >=21.0.0, npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'}
|
||||||
@@ -1716,10 +1610,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA==}
|
resolution: {integrity: sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA==}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/@noble/ciphers@0.2.0:
|
|
||||||
resolution: {integrity: sha512-6YBxJDAapHSdd3bLDv6x2wRPwq4QFMUaB3HvljNBUTThDd12eSm7/3F+2lnfzx2jvM+S6Nsy0jEt9QbPqSwqRw==}
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/@noble/ciphers@0.5.3:
|
/@noble/ciphers@0.5.3:
|
||||||
resolution: {integrity: sha512-B0+6IIHiqEs3BPMT0hcRmHvEj2QHOLu+uwt+tqDDeVd0oyVzh7BPrDcPjRnV1PV/5LaknXJJQvOuRGR0zQJz+w==}
|
resolution: {integrity: sha512-B0+6IIHiqEs3BPMT0hcRmHvEj2QHOLu+uwt+tqDDeVd0oyVzh7BPrDcPjRnV1PV/5LaknXJJQvOuRGR0zQJz+w==}
|
||||||
dev: false
|
dev: false
|
||||||
@@ -1776,59 +1666,6 @@ packages:
|
|||||||
'@babel/runtime': 7.24.5
|
'@babel/runtime': 7.24.5
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/@radix-ui/react-accordion@1.1.2(@types/react@18.3.2)(react-dom@18.3.1)(react@18.3.1):
|
|
||||||
resolution: {integrity: sha512-fDG7jcoNKVjSK6yfmuAs0EnPDro0WMXIhMtXdTBWqEioVW206ku+4Lw07e+13lUkFkpoEQ2PdeMIAGpdqEAmDg==}
|
|
||||||
peerDependencies:
|
|
||||||
'@types/react': '*'
|
|
||||||
'@types/react-dom': '*'
|
|
||||||
react: ^16.8 || ^17.0 || ^18.0
|
|
||||||
react-dom: ^16.8 || ^17.0 || ^18.0
|
|
||||||
peerDependenciesMeta:
|
|
||||||
'@types/react':
|
|
||||||
optional: true
|
|
||||||
'@types/react-dom':
|
|
||||||
optional: true
|
|
||||||
dependencies:
|
|
||||||
'@babel/runtime': 7.24.5
|
|
||||||
'@radix-ui/primitive': 1.0.1
|
|
||||||
'@radix-ui/react-collapsible': 1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.2)(react-dom@18.3.1)(react@18.3.1)
|
|
||||||
'@radix-ui/react-collection': 1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.2)(react-dom@18.3.1)(react@18.3.1)
|
|
||||||
'@radix-ui/react-compose-refs': 1.0.1(@types/react@18.3.2)(react@18.3.1)
|
|
||||||
'@radix-ui/react-context': 1.0.1(@types/react@18.3.2)(react@18.3.1)
|
|
||||||
'@radix-ui/react-direction': 1.0.1(@types/react@18.3.2)(react@18.3.1)
|
|
||||||
'@radix-ui/react-id': 1.0.1(@types/react@18.3.2)(react@18.3.1)
|
|
||||||
'@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.2)(react-dom@18.3.1)(react@18.3.1)
|
|
||||||
'@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.3.2)(react@18.3.1)
|
|
||||||
'@types/react': 18.3.2
|
|
||||||
react: 18.3.1
|
|
||||||
react-dom: 18.3.1(react@18.3.1)
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/@radix-ui/react-alert-dialog@1.0.5(@types/react@18.3.2)(react-dom@18.3.1)(react@18.3.1):
|
|
||||||
resolution: {integrity: sha512-OrVIOcZL0tl6xibeuGt5/+UxoT2N27KCFOPjFyfXMnchxSHZ/OW7cCX2nGlIYJrbHK/fczPcFzAwvNBB6XBNMA==}
|
|
||||||
peerDependencies:
|
|
||||||
'@types/react': '*'
|
|
||||||
'@types/react-dom': '*'
|
|
||||||
react: ^16.8 || ^17.0 || ^18.0
|
|
||||||
react-dom: ^16.8 || ^17.0 || ^18.0
|
|
||||||
peerDependenciesMeta:
|
|
||||||
'@types/react':
|
|
||||||
optional: true
|
|
||||||
'@types/react-dom':
|
|
||||||
optional: true
|
|
||||||
dependencies:
|
|
||||||
'@babel/runtime': 7.24.5
|
|
||||||
'@radix-ui/primitive': 1.0.1
|
|
||||||
'@radix-ui/react-compose-refs': 1.0.1(@types/react@18.3.2)(react@18.3.1)
|
|
||||||
'@radix-ui/react-context': 1.0.1(@types/react@18.3.2)(react@18.3.1)
|
|
||||||
'@radix-ui/react-dialog': 1.0.5(@types/react-dom@18.3.0)(@types/react@18.3.2)(react-dom@18.3.1)(react@18.3.1)
|
|
||||||
'@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.2)(react-dom@18.3.1)(react@18.3.1)
|
|
||||||
'@radix-ui/react-slot': 1.0.2(@types/react@18.3.2)(react@18.3.1)
|
|
||||||
'@types/react': 18.3.2
|
|
||||||
react: 18.3.1
|
|
||||||
react-dom: 18.3.1(react@18.3.1)
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/@radix-ui/react-arrow@1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.2)(react-dom@18.3.1)(react@18.3.1):
|
/@radix-ui/react-arrow@1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.2)(react-dom@18.3.1)(react@18.3.1):
|
||||||
resolution: {integrity: sha512-wSP+pHsB/jQRaL6voubsQ/ZlrGBHHrOjmBnr19hxYgtS0WvAFwZhK2WP/YY5yF9uKECCEEDGxuLxq1NBK51wFA==}
|
resolution: {integrity: sha512-wSP+pHsB/jQRaL6voubsQ/ZlrGBHHrOjmBnr19hxYgtS0WvAFwZhK2WP/YY5yF9uKECCEEDGxuLxq1NBK51wFA==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -3203,12 +3040,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-RvC8KMw5BCac1NvRRyaHgMMEtBaZ6wh0pyPTBu7izn4Sj/AX9Y4aXU5c7rX8PnM/knsuUpC1IeoBkANtxBypsQ==}
|
resolution: {integrity: sha512-RvC8KMw5BCac1NvRRyaHgMMEtBaZ6wh0pyPTBu7izn4Sj/AX9Y4aXU5c7rX8PnM/knsuUpC1IeoBkANtxBypsQ==}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/@types/lodash-es@4.17.12:
|
|
||||||
resolution: {integrity: sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==}
|
|
||||||
dependencies:
|
|
||||||
'@types/lodash': 4.17.1
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/@types/lodash@4.17.1:
|
/@types/lodash@4.17.1:
|
||||||
resolution: {integrity: sha512-X+2qazGS3jxLAIz5JDXDzglAF3KpijdhFxlf/V1+hEsOUc+HnWi81L/uv/EvGuV90WY+7mPGFCUDGfQC3Gj95Q==}
|
resolution: {integrity: sha512-X+2qazGS3jxLAIz5JDXDzglAF3KpijdhFxlf/V1+hEsOUc+HnWi81L/uv/EvGuV90WY+7mPGFCUDGfQC3Gj95Q==}
|
||||||
dev: false
|
dev: false
|
||||||
@@ -3707,24 +3538,10 @@ packages:
|
|||||||
wrap-ansi: 7.0.0
|
wrap-ansi: 7.0.0
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/clone-regexp@3.0.0:
|
|
||||||
resolution: {integrity: sha512-ujdnoq2Kxb8s3ItNBtnYeXdm07FcU0u8ARAT1lQ2YdMwQC+cdiXX8KoqMVuglztILivceTtp4ivqGSmEmhBUJw==}
|
|
||||||
engines: {node: '>=12'}
|
|
||||||
dependencies:
|
|
||||||
is-regexp: 3.1.0
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/clsx@2.1.1:
|
/clsx@2.1.1:
|
||||||
resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
|
resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
|
|
||||||
/codsen-utils@1.6.4:
|
|
||||||
resolution: {integrity: sha512-PDyvQ5f2PValmqZZIJATimcokDt4JjIev8cKbZgEOoZm+U1IJDYuLeTcxZPQdep99R/X0RIlQ6ReQgPOVnPbNw==}
|
|
||||||
engines: {node: '>=14.18.0'}
|
|
||||||
dependencies:
|
|
||||||
rfdc: 1.3.1
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/color-convert@1.9.3:
|
/color-convert@1.9.3:
|
||||||
resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==}
|
resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==}
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -3778,11 +3595,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-rj8l8pD4bJ1nx+dAkMhV1xB5RuZEyVysfxJqB1pRchh1KVvwOv9b7CGB8ZfjTImVv2oF+sYMUkMZq6Na5Ftmbg==}
|
resolution: {integrity: sha512-rj8l8pD4bJ1nx+dAkMhV1xB5RuZEyVysfxJqB1pRchh1KVvwOv9b7CGB8ZfjTImVv2oF+sYMUkMZq6Na5Ftmbg==}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/convert-hrtime@5.0.0:
|
|
||||||
resolution: {integrity: sha512-lOETlkIeYSJWcbbcvjRKGxVMXJR+8+OQb/mTPbA4ObPMytYIsUbuOE0Jzy60hjARYszq1id0j8KgVhC+WGZVTg==}
|
|
||||||
engines: {node: '>=12'}
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/convert-source-map@2.0.0:
|
/convert-source-map@2.0.0:
|
||||||
resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
|
resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
|
||||||
|
|
||||||
@@ -4096,25 +3908,6 @@ packages:
|
|||||||
/fraction.js@4.3.7:
|
/fraction.js@4.3.7:
|
||||||
resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==}
|
resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==}
|
||||||
|
|
||||||
/framer-motion@11.2.0(react-dom@18.3.1)(react@18.3.1):
|
|
||||||
resolution: {integrity: sha512-LRfLVPEwtO9IXJCAsWvtj3XZxrdZDcTxNNkZEq30aQ8p7/wimfUkDy67TDWdtzPiyKDkqOHDhaQC6XVrQ4Fh7A==}
|
|
||||||
peerDependencies:
|
|
||||||
'@emotion/is-prop-valid': '*'
|
|
||||||
react: ^18.0.0
|
|
||||||
react-dom: ^18.0.0
|
|
||||||
peerDependenciesMeta:
|
|
||||||
'@emotion/is-prop-valid':
|
|
||||||
optional: true
|
|
||||||
react:
|
|
||||||
optional: true
|
|
||||||
react-dom:
|
|
||||||
optional: true
|
|
||||||
dependencies:
|
|
||||||
react: 18.3.1
|
|
||||||
react-dom: 18.3.1(react@18.3.1)
|
|
||||||
tslib: 2.6.2
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/fsevents@2.3.3:
|
/fsevents@2.3.3:
|
||||||
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
|
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
|
||||||
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
|
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
|
||||||
@@ -4125,11 +3918,6 @@ packages:
|
|||||||
/function-bind@1.1.2:
|
/function-bind@1.1.2:
|
||||||
resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==}
|
resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==}
|
||||||
|
|
||||||
/function-timeout@0.1.1:
|
|
||||||
resolution: {integrity: sha512-0NVVC0TaP7dSTvn1yMiy6d6Q8gifzbvQafO46RtLG/kHJUBNd+pVRGOBoK44wNBvtSPUJRfdVvkFdD3p0xvyZg==}
|
|
||||||
engines: {node: '>=14.16'}
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/gensync@1.0.0-beta.2:
|
/gensync@1.0.0-beta.2:
|
||||||
resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==}
|
resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==}
|
||||||
engines: {node: '>=6.9.0'}
|
engines: {node: '>=6.9.0'}
|
||||||
@@ -4154,17 +3942,6 @@ packages:
|
|||||||
engines: {node: '>=16'}
|
engines: {node: '>=16'}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/get-urls@12.1.0:
|
|
||||||
resolution: {integrity: sha512-qHO+QmPiI1bEw0Y/m+WMAAx/UoEEXLZwEx0DVaKMtlHNrKbMeV960LryIpd+E2Ykb9XkVHmVtpbCsmul3GhR0g==}
|
|
||||||
engines: {node: '>=16'}
|
|
||||||
dependencies:
|
|
||||||
normalize-url: 8.0.1
|
|
||||||
super-regex: 0.2.0
|
|
||||||
url-regex-safe: 4.0.0
|
|
||||||
transitivePeerDependencies:
|
|
||||||
- re2
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/github-slugger@2.0.0:
|
/github-slugger@2.0.0:
|
||||||
resolution: {integrity: sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw==}
|
resolution: {integrity: sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw==}
|
||||||
dev: false
|
dev: false
|
||||||
@@ -4340,10 +4117,6 @@ packages:
|
|||||||
space-separated-tokens: 2.0.2
|
space-separated-tokens: 2.0.2
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/html-entities@2.5.2:
|
|
||||||
resolution: {integrity: sha512-K//PSRMQk4FZ78Kyau+mZurHn3FH0Vwr+H36eE0rPbeYkRRi9YxceYPhuN60UwWorxyKHhqoAJl2OFKa4BVtaA==}
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/html-escaper@3.0.3:
|
/html-escaper@3.0.3:
|
||||||
resolution: {integrity: sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ==}
|
resolution: {integrity: sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ==}
|
||||||
dev: false
|
dev: false
|
||||||
@@ -4393,11 +4166,6 @@ packages:
|
|||||||
loose-envify: 1.4.0
|
loose-envify: 1.4.0
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/ip-regex@4.3.0:
|
|
||||||
resolution: {integrity: sha512-B9ZWJxHHOHUhUjCPrMpLD4xEq35bUTClHM1S6CBU5ixQnkZmwipwgc96vAd7AAGM9TGHvJR+Uss+/Ak6UphK+Q==}
|
|
||||||
engines: {node: '>=8'}
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/is-arrayish@0.3.2:
|
/is-arrayish@0.3.2:
|
||||||
resolution: {integrity: sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==}
|
resolution: {integrity: sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==}
|
||||||
requiresBuild: true
|
requiresBuild: true
|
||||||
@@ -4476,11 +4244,6 @@ packages:
|
|||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/is-regexp@3.1.0:
|
|
||||||
resolution: {integrity: sha512-rbku49cWloU5bSMI+zaRaXdQHXnthP6DZ/vLnfdSKyL4zUzuWnomtOEiZZOd+ioQ+avFo/qau3KPTc7Fjy1uPA==}
|
|
||||||
engines: {node: '>=12'}
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/is-stream@3.0.0:
|
/is-stream@3.0.0:
|
||||||
resolution: {integrity: sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==}
|
resolution: {integrity: sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==}
|
||||||
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
|
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
|
||||||
@@ -4606,10 +4369,6 @@ packages:
|
|||||||
p-locate: 5.0.0
|
p-locate: 5.0.0
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/lodash-es@4.17.21:
|
|
||||||
resolution: {integrity: sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==}
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/lodash.castarray@4.4.0:
|
/lodash.castarray@4.4.0:
|
||||||
resolution: {integrity: sha512-aVx8ztPv7/2ULbArGJ2Y42bG1mEQ5mGjpdvrbJcJFU3TbYybe+QlLS4pst9zV52ymy2in1KpFPiZnAOATxD4+Q==}
|
resolution: {integrity: sha512-aVx8ztPv7/2ULbArGJ2Y42bG1mEQ5mGjpdvrbJcJFU3TbYybe+QlLS4pst9zV52ymy2in1KpFPiZnAOATxD4+Q==}
|
||||||
dev: true
|
dev: true
|
||||||
@@ -4808,10 +4567,6 @@ packages:
|
|||||||
'@types/mdast': 4.0.4
|
'@types/mdast': 4.0.4
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/media-chrome@3.2.2:
|
|
||||||
resolution: {integrity: sha512-Fjf1rNxlZqVR5nj7a9ay+XpzeKVPMMBltK2XiMOTc5bomxAvyo3vJZ9JKzmAYVoiZ6sbDNWJJVSVf6b3laFwew==}
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/merge-stream@2.0.0:
|
/merge-stream@2.0.0:
|
||||||
resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==}
|
resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==}
|
||||||
dev: false
|
dev: false
|
||||||
@@ -5151,28 +4906,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==}
|
resolution: {integrity: sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
/normalize-url@8.0.1:
|
|
||||||
resolution: {integrity: sha512-IO9QvjUMWxPQQhs60oOu10CRkWCiZzSUkzbXGGV9pviYl1fXYcvkzQ5jV9z8Y6un8ARoVRl4EtC6v6jNqbaJ/w==}
|
|
||||||
engines: {node: '>=14.16'}
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/nostr-tools@1.17.0(typescript@5.4.5):
|
|
||||||
resolution: {integrity: sha512-LZmR8GEWKZeElbFV5Xte75dOeE9EFUW/QLI1Ncn3JKn0kFddDKEfBbFN8Mu4TMs+L4HR/WTPha2l+PPuRnJcMw==}
|
|
||||||
peerDependencies:
|
|
||||||
typescript: '>=5.0.0'
|
|
||||||
peerDependenciesMeta:
|
|
||||||
typescript:
|
|
||||||
optional: true
|
|
||||||
dependencies:
|
|
||||||
'@noble/ciphers': 0.2.0
|
|
||||||
'@noble/curves': 1.1.0
|
|
||||||
'@noble/hashes': 1.3.1
|
|
||||||
'@scure/base': 1.1.1
|
|
||||||
'@scure/bip32': 1.3.1
|
|
||||||
'@scure/bip39': 1.2.1
|
|
||||||
typescript: 5.4.5
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/nostr-tools@2.5.2(typescript@5.4.5):
|
/nostr-tools@2.5.2(typescript@5.4.5):
|
||||||
resolution: {integrity: sha512-Ls2FKh694eudBye6q89yJ5JhXjQle1MWp1yD2sBZ5j9M3IOBEW8ia9IED5W6daSAjlT/Z/pV77yTkdF45c1Rbg==}
|
resolution: {integrity: sha512-Ls2FKh694eudBye6q89yJ5JhXjQle1MWp1yD2sBZ5j9M3IOBEW8ia9IED5W6daSAjlT/Z/pV77yTkdF45c1Rbg==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -5472,58 +5205,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==}
|
resolution: {integrity: sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/qrcode.react@3.1.0(react@18.3.1):
|
|
||||||
resolution: {integrity: sha512-oyF+Urr3oAMUG/OiOuONL3HXM+53wvuH3mtIWQrYmsXoAq0DkvZp2RYUWFSMFtbdOpuS++9v+WAkzNVkMlNW6Q==}
|
|
||||||
peerDependencies:
|
|
||||||
react: ^16.8.0 || ^17.0.0 || ^18.0.0
|
|
||||||
dependencies:
|
|
||||||
react: 18.3.1
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/queue-microtask@1.2.3:
|
/queue-microtask@1.2.3:
|
||||||
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
|
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
|
||||||
|
|
||||||
/ranges-apply@7.0.16:
|
|
||||||
resolution: {integrity: sha512-4rGJHOyA7qatiMDg3vcETkc/TVBPU86/xZRTXff6o7a2neYLmj0EXUUAlhLVuiWAzTPHDPHOQxtk8EDrIF4ohg==}
|
|
||||||
engines: {node: '>=14.18.0'}
|
|
||||||
dependencies:
|
|
||||||
ranges-merge: 9.0.15
|
|
||||||
tiny-invariant: 1.3.3
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/ranges-merge@9.0.15:
|
|
||||||
resolution: {integrity: sha512-hvt4hx0FKIaVfjd1oKx0poL57ljxdL2KHC6bXBrAdsx2iCsH+x7nO/5J0k2veM/isnOcFZKp0ZKkiCjCtzy74Q==}
|
|
||||||
engines: {node: '>=14.18.0'}
|
|
||||||
dependencies:
|
|
||||||
ranges-push: 7.0.15
|
|
||||||
ranges-sort: 6.0.11
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/ranges-push@7.0.15:
|
|
||||||
resolution: {integrity: sha512-gXpBYQ5Umf3uG6jkJnw5ddok2Xfo5p22rAJBLrqzNKa7qkj3q5AOCoxfRPXEHUVaJutfXc9K9eGXdIzdyQKPkw==}
|
|
||||||
engines: {node: '>=14.18.0'}
|
|
||||||
dependencies:
|
|
||||||
codsen-utils: 1.6.4
|
|
||||||
ranges-sort: 6.0.11
|
|
||||||
string-collapse-leading-whitespace: 7.0.7
|
|
||||||
string-trim-spaces-only: 5.0.10
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/ranges-sort@6.0.11:
|
|
||||||
resolution: {integrity: sha512-fhNEG0vGi7bESitNNqNBAfYPdl2efB+1paFlI8BQDCNkruERKuuhG8LkQClDIVqUJLkrmKuOSPQ3xZHqVnVo3Q==}
|
|
||||||
engines: {node: '>=14.18.0'}
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/re-resizable@6.9.16(react-dom@18.3.1)(react@18.3.1):
|
|
||||||
resolution: {integrity: sha512-D9+ofwgPQRC6PL6cwavCZO9MUR8TKKxV1nHjbutSdNaFHK9v5k8m6DcESMXrw1+mRJn7fBHJRhZpa7EQ1ZWEEA==}
|
|
||||||
peerDependencies:
|
|
||||||
react: ^16.13.1 || ^17.0.0 || ^18.0.0
|
|
||||||
react-dom: ^16.13.1 || ^17.0.0 || ^18.0.0
|
|
||||||
dependencies:
|
|
||||||
react: 18.3.1
|
|
||||||
react-dom: 18.3.1(react@18.3.1)
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/react-currency-input-field@3.8.0(react@18.3.1):
|
/react-currency-input-field@3.8.0(react@18.3.1):
|
||||||
resolution: {integrity: sha512-DKSIjacrvgUDOpuB16b+OVDvp5pbCt+s+RHJgpRZCHNhzg1yBpRUoy4fbnXpeOj0kdbwf5BaXCr2mAtxEujfhg==}
|
resolution: {integrity: sha512-DKSIjacrvgUDOpuB16b+OVDvp5pbCt+s+RHJgpRZCHNhzg1yBpRUoy4fbnXpeOj0kdbwf5BaXCr2mAtxEujfhg==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -5818,10 +5502,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==}
|
resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==}
|
||||||
engines: {iojs: '>=1.0.0', node: '>=0.10.0'}
|
engines: {iojs: '>=1.0.0', node: '>=0.10.0'}
|
||||||
|
|
||||||
/rfdc@1.3.1:
|
|
||||||
resolution: {integrity: sha512-r5a3l5HzYlIC68TpmYKlxWjmOP6wiPJ1vWv2HeLhNsRZMrCkxeqxiHlQ21oXmQ4F3SiryXBHhAD7JZqvOJjFmg==}
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/rollup@4.17.2:
|
/rollup@4.17.2:
|
||||||
resolution: {integrity: sha512-/9ClTJPByC0U4zNLowV1tMBe8yMEAxewtR3cUNX5BoEpGH3dQEWpJLr6CLp0fPdYRF/fzVOgvDb1zXuakwF5kQ==}
|
resolution: {integrity: sha512-/9ClTJPByC0U4zNLowV1tMBe8yMEAxewtR3cUNX5BoEpGH3dQEWpJLr6CLp0fPdYRF/fzVOgvDb1zXuakwF5kQ==}
|
||||||
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
|
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
|
||||||
@@ -6012,37 +5692,6 @@ packages:
|
|||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/string-collapse-leading-whitespace@7.0.7:
|
|
||||||
resolution: {integrity: sha512-jF9eynJoE6ezTCdYI8Qb02/ij/DlU9ItG93Dty4SWfJeLFrotOr+wH9IRiWHTqO3mjCyqBWEiU3uSTIbxYbAEQ==}
|
|
||||||
engines: {node: '>=14.18.0'}
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/string-left-right@6.0.17:
|
|
||||||
resolution: {integrity: sha512-nuyIV4D4ivnwT64E0TudmCRg52NfkumuEUilyoOrHb/Z2wEOF5I+9SI6P+veFKqWKZfGpAs6OqKe4nAjujARyw==}
|
|
||||||
engines: {node: '>=14.18.0'}
|
|
||||||
dependencies:
|
|
||||||
codsen-utils: 1.6.4
|
|
||||||
rfdc: 1.3.1
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/string-strip-html@13.4.8:
|
|
||||||
resolution: {integrity: sha512-vlcRAtx5DN6zXGUx3EYGFg0/JOQWM65mqLgDaBHviQPP+ovUFzqZ30iQ+674JHWr9wNgnzFGxx9TGipPZMnZXg==}
|
|
||||||
engines: {node: '>=14.18.0'}
|
|
||||||
dependencies:
|
|
||||||
'@types/lodash-es': 4.17.12
|
|
||||||
codsen-utils: 1.6.4
|
|
||||||
html-entities: 2.5.2
|
|
||||||
lodash-es: 4.17.21
|
|
||||||
ranges-apply: 7.0.16
|
|
||||||
ranges-push: 7.0.15
|
|
||||||
string-left-right: 6.0.17
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/string-trim-spaces-only@5.0.10:
|
|
||||||
resolution: {integrity: sha512-MhmjE5jNqb1Ylo+BARPRlsdChGLrnPpAUWrT1VOxo9WhWwKVUU6CbZTfjwKaQPYTGS/wsX/4Zek88FM2rEb5iA==}
|
|
||||||
engines: {node: '>=14.18.0'}
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/string-width@4.2.3:
|
/string-width@4.2.3:
|
||||||
resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==}
|
resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
@@ -6115,15 +5764,6 @@ packages:
|
|||||||
pirates: 4.0.6
|
pirates: 4.0.6
|
||||||
ts-interface-checker: 0.1.13
|
ts-interface-checker: 0.1.13
|
||||||
|
|
||||||
/super-regex@0.2.0:
|
|
||||||
resolution: {integrity: sha512-WZzIx3rC1CvbMDloLsVw0lkZVKJWbrkJ0k1ghKFmcnPrW1+jWbgTkTEWVtD9lMdmI4jZEz40+naBxl1dCUhXXw==}
|
|
||||||
engines: {node: '>=14.16'}
|
|
||||||
dependencies:
|
|
||||||
clone-regexp: 3.0.0
|
|
||||||
function-timeout: 0.1.1
|
|
||||||
time-span: 5.1.0
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/supports-color@5.5.0:
|
/supports-color@5.5.0:
|
||||||
resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==}
|
resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==}
|
||||||
engines: {node: '>=4'}
|
engines: {node: '>=4'}
|
||||||
@@ -6194,13 +5834,6 @@ packages:
|
|||||||
dependencies:
|
dependencies:
|
||||||
any-promise: 1.3.0
|
any-promise: 1.3.0
|
||||||
|
|
||||||
/time-span@5.1.0:
|
|
||||||
resolution: {integrity: sha512-75voc/9G4rDIJleOo4jPvN4/YC4GRZrY8yy1uU4lwrB3XEQbWve8zXoO5No4eFrGcTAMYyoY67p8jRQdtA1HbA==}
|
|
||||||
engines: {node: '>=12'}
|
|
||||||
dependencies:
|
|
||||||
convert-hrtime: 5.0.0
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/tiny-invariant@1.3.1:
|
/tiny-invariant@1.3.1:
|
||||||
resolution: {integrity: sha512-AD5ih2NlSssTCwsMznbvwMZpJ1cbhkGd2uueNxzv2jDlEeZdU04JQfRnggJQ8DrcVBGjAsCKwFBbDlVNtEMlzw==}
|
resolution: {integrity: sha512-AD5ih2NlSssTCwsMznbvwMZpJ1cbhkGd2uueNxzv2jDlEeZdU04JQfRnggJQ8DrcVBGjAsCKwFBbDlVNtEMlzw==}
|
||||||
dev: false
|
dev: false
|
||||||
@@ -6211,11 +5844,6 @@ packages:
|
|||||||
/tiny-warning@1.0.3:
|
/tiny-warning@1.0.3:
|
||||||
resolution: {integrity: sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==}
|
resolution: {integrity: sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==}
|
||||||
|
|
||||||
/tlds@1.252.0:
|
|
||||||
resolution: {integrity: sha512-GA16+8HXvqtfEnw/DTcwB0UU354QE1n3+wh08oFjr6Znl7ZLAeUgYzCcK+/CCrOyE0vnHR8/pu3XXG3vDijXpQ==}
|
|
||||||
hasBin: true
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/to-fast-properties@2.0.0:
|
/to-fast-properties@2.0.0:
|
||||||
resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==}
|
resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==}
|
||||||
engines: {node: '>=4'}
|
engines: {node: '>=4'}
|
||||||
@@ -6459,23 +6087,6 @@ packages:
|
|||||||
escalade: 3.1.2
|
escalade: 3.1.2
|
||||||
picocolors: 1.0.1
|
picocolors: 1.0.1
|
||||||
|
|
||||||
/uqr@0.1.2:
|
|
||||||
resolution: {integrity: sha512-MJu7ypHq6QasgF5YRTjqscSzQp/W11zoUk6kvmlH+fmWEs63Y0Eib13hYFwAzagRJcVY8WVnlV+eBDUGMJ5IbA==}
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/url-regex-safe@4.0.0:
|
|
||||||
resolution: {integrity: sha512-BrnFCWKNFrFnRzKD66NtJqQepfJrUHNPvPxE5y5NSAhXBb4OlobQjt7907Jm4ItPiXaeX+dDWMkcnOd4jR9N8A==}
|
|
||||||
engines: {node: '>= 14'}
|
|
||||||
peerDependencies:
|
|
||||||
re2: ^1.20.1
|
|
||||||
peerDependenciesMeta:
|
|
||||||
re2:
|
|
||||||
optional: true
|
|
||||||
dependencies:
|
|
||||||
ip-regex: 4.3.0
|
|
||||||
tlds: 1.252.0
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/use-callback-ref@1.3.2(@types/react@18.3.2)(react@18.3.1):
|
/use-callback-ref@1.3.2(@types/react@18.3.2)(react@18.3.1):
|
||||||
resolution: {integrity: sha512-elOQwe6Q8gqZgDA8mrh44qRTQqpIHDcZ3hXTLjBe1i4ph8XpNJnO+aQf3NaG+lriLopI4HMx9VjQLfPQ6vhnoA==}
|
resolution: {integrity: sha512-elOQwe6Q8gqZgDA8mrh44qRTQqpIHDcZ3hXTLjBe1i4ph8XpNJnO+aQf3NaG+lriLopI4HMx9VjQLfPQ6vhnoA==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
|
|||||||
@@ -2,6 +2,12 @@
|
|||||||
# It is not intended for manual editing.
|
# It is not intended for manual editing.
|
||||||
version = 3
|
version = 3
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "Inflector"
|
||||||
|
version = "0.11.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "addr2line"
|
name = "addr2line"
|
||||||
version = "0.21.0"
|
version = "0.21.0"
|
||||||
@@ -2481,6 +2487,12 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "indoc"
|
||||||
|
version = "2.0.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b248f5224d1d606005e02c97f5aa4e88eeb230488bcc03bc9ca4d7991399f2b5"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "infer"
|
name = "infer"
|
||||||
version = "0.15.0"
|
version = "0.15.0"
|
||||||
@@ -2883,6 +2895,7 @@ dependencies = [
|
|||||||
"rand 0.8.5",
|
"rand 0.8.5",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
"specta",
|
||||||
"tauri",
|
"tauri",
|
||||||
"tauri-build",
|
"tauri-build",
|
||||||
"tauri-plugin-clipboard-manager",
|
"tauri-plugin-clipboard-manager",
|
||||||
@@ -2894,9 +2907,11 @@ dependencies = [
|
|||||||
"tauri-plugin-os",
|
"tauri-plugin-os",
|
||||||
"tauri-plugin-process",
|
"tauri-plugin-process",
|
||||||
"tauri-plugin-shell",
|
"tauri-plugin-shell",
|
||||||
|
"tauri-plugin-theme",
|
||||||
"tauri-plugin-updater",
|
"tauri-plugin-updater",
|
||||||
"tauri-plugin-upload",
|
"tauri-plugin-upload",
|
||||||
"tauri-plugin-window-state",
|
"tauri-plugin-window-state",
|
||||||
|
"tauri-specta",
|
||||||
"tokio",
|
"tokio",
|
||||||
"webpage",
|
"webpage",
|
||||||
]
|
]
|
||||||
@@ -3698,6 +3713,12 @@ dependencies = [
|
|||||||
"subtle",
|
"subtle",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "paste"
|
||||||
|
version = "1.0.15"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pathdiff"
|
name = "pathdiff"
|
||||||
version = "0.2.1"
|
version = "0.2.1"
|
||||||
@@ -4960,6 +4981,31 @@ dependencies = [
|
|||||||
"system-deps",
|
"system-deps",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "specta"
|
||||||
|
version = "2.0.0-rc.12"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3624a07cbde326fdf1ec37cbd39d06a224660fa0199b7db7316f2349583df981"
|
||||||
|
dependencies = [
|
||||||
|
"once_cell",
|
||||||
|
"paste",
|
||||||
|
"serde",
|
||||||
|
"specta-macros",
|
||||||
|
"thiserror",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "specta-macros"
|
||||||
|
version = "2.0.0-rc.10"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ef33e9678ae36993fcbfc46aa29568ef10d32fd54428808759c6a450998c43ec"
|
||||||
|
dependencies = [
|
||||||
|
"Inflector",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 1.0.109",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "spin"
|
name = "spin"
|
||||||
version = "0.9.8"
|
version = "0.9.8"
|
||||||
@@ -5209,6 +5255,7 @@ dependencies = [
|
|||||||
"serde_json",
|
"serde_json",
|
||||||
"serde_repr",
|
"serde_repr",
|
||||||
"serialize-to-javascript",
|
"serialize-to-javascript",
|
||||||
|
"specta",
|
||||||
"state",
|
"state",
|
||||||
"swift-rs",
|
"swift-rs",
|
||||||
"tauri-build",
|
"tauri-build",
|
||||||
@@ -5468,6 +5515,24 @@ dependencies = [
|
|||||||
"tokio",
|
"tokio",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tauri-plugin-theme"
|
||||||
|
version = "0.4.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c23856e93f56cd8d1868e7dd8a4b529b836fd5da315cefd7702b252a850cdf5a"
|
||||||
|
dependencies = [
|
||||||
|
"cocoa",
|
||||||
|
"dirs-next",
|
||||||
|
"futures-lite 2.3.0",
|
||||||
|
"gtk",
|
||||||
|
"once_cell",
|
||||||
|
"serde",
|
||||||
|
"tauri",
|
||||||
|
"tauri-plugin",
|
||||||
|
"tintanum",
|
||||||
|
"tokio",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri-plugin-updater"
|
name = "tauri-plugin-updater"
|
||||||
version = "2.0.0-beta.5"
|
version = "2.0.0-beta.5"
|
||||||
@@ -5574,6 +5639,34 @@ dependencies = [
|
|||||||
"wry",
|
"wry",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tauri-specta"
|
||||||
|
version = "2.0.0-rc.10"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "856a2bbbbd4d39ae2c1e6d22aec50623596708ff8f7e4c598123dbc5165f5f76"
|
||||||
|
dependencies = [
|
||||||
|
"heck 0.5.0",
|
||||||
|
"indoc",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"specta",
|
||||||
|
"tauri",
|
||||||
|
"tauri-specta-macros",
|
||||||
|
"thiserror",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tauri-specta-macros"
|
||||||
|
version = "2.0.0-rc.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e6f9e90bf2012877e2c4029a1bf756277183e9c7c77b850ef965711553998012"
|
||||||
|
dependencies = [
|
||||||
|
"heck 0.5.0",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.63",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri-utils"
|
name = "tauri-utils"
|
||||||
version = "2.0.0-beta.15"
|
version = "2.0.0-beta.15"
|
||||||
@@ -5731,6 +5824,16 @@ dependencies = [
|
|||||||
"time-core",
|
"time-core",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tintanum"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7abbcf9173afc80733c20b7e27a30bc9284d6535bdbde2a70904032de63e16e8"
|
||||||
|
dependencies = [
|
||||||
|
"futures-lite 1.13.0",
|
||||||
|
"zbus 3.15.1",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tinyvec"
|
name = "tinyvec"
|
||||||
version = "1.6.0"
|
version = "1.6.0"
|
||||||
|
|||||||
@@ -37,6 +37,9 @@ tauri-plugin-decorum = "0.1.0"
|
|||||||
webpage = { version = "2.0", features = ["serde"] }
|
webpage = { version = "2.0", features = ["serde"] }
|
||||||
keyring = "2"
|
keyring = "2"
|
||||||
keyring-search = "0.2.0"
|
keyring-search = "0.2.0"
|
||||||
|
specta = "=2.0.0-rc.12"
|
||||||
|
tauri-specta = { version = "=2.0.0-rc.10", features = ["typescript"] }
|
||||||
|
tauri-plugin-theme = "0.4.1"
|
||||||
|
|
||||||
[target.'cfg(target_os = "macos")'.dependencies]
|
[target.'cfg(target_os = "macos")'.dependencies]
|
||||||
cocoa = "0.25.0"
|
cocoa = "0.25.0"
|
||||||
|
|||||||
@@ -1,82 +1,85 @@
|
|||||||
{
|
{
|
||||||
"$schema": "../gen/schemas/desktop-schema.json",
|
"$schema": "../gen/schemas/desktop-schema.json",
|
||||||
"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": [
|
"windows": [
|
||||||
"main",
|
"main",
|
||||||
"splash",
|
"splash",
|
||||||
"settings",
|
"settings",
|
||||||
"search",
|
"search",
|
||||||
"nwc",
|
"nwc",
|
||||||
"activity",
|
"activity",
|
||||||
"zap-*",
|
"zap-*",
|
||||||
"event-*",
|
"event-*",
|
||||||
"user-*",
|
"user-*",
|
||||||
"editor-*",
|
"editor-*",
|
||||||
"column-*"
|
"column-*"
|
||||||
],
|
],
|
||||||
"permissions": [
|
"permissions": [
|
||||||
"path:default",
|
"path:default",
|
||||||
"event:default",
|
"event:default",
|
||||||
"window:default",
|
"window:default",
|
||||||
"app:default",
|
"app:default",
|
||||||
"resources:default",
|
"resources:default",
|
||||||
"menu:default",
|
"menu:default",
|
||||||
"tray:default",
|
"tray:default",
|
||||||
"notification:allow-is-permission-granted",
|
"notification:allow-is-permission-granted",
|
||||||
"notification:allow-request-permission",
|
"notification:allow-request-permission",
|
||||||
"notification:default",
|
"notification:default",
|
||||||
"os:allow-locale",
|
"os:allow-locale",
|
||||||
"os:allow-platform",
|
"os:allow-platform",
|
||||||
"updater:default",
|
"os:allow-os-type",
|
||||||
"updater:allow-check",
|
"updater:default",
|
||||||
"updater:allow-download-and-install",
|
"updater:allow-check",
|
||||||
"window:allow-start-dragging",
|
"updater:allow-download-and-install",
|
||||||
"window:allow-create",
|
"window:allow-start-dragging",
|
||||||
"window:allow-close",
|
"window:allow-create",
|
||||||
"window:allow-set-focus",
|
"window:allow-close",
|
||||||
"window:allow-center",
|
"window:allow-set-focus",
|
||||||
"window:allow-minimize",
|
"window:allow-center",
|
||||||
"window:allow-maximize",
|
"window:allow-minimize",
|
||||||
"window:allow-set-size",
|
"window:allow-maximize",
|
||||||
"window:allow-set-focus",
|
"window:allow-set-size",
|
||||||
"window:allow-start-dragging",
|
"window:allow-set-focus",
|
||||||
"decorum:allow-show-snap-overlay",
|
"window:allow-start-dragging",
|
||||||
"clipboard-manager:allow-write-text",
|
"decorum:allow-show-snap-overlay",
|
||||||
"clipboard-manager:allow-read-text",
|
"clipboard-manager:allow-write-text",
|
||||||
"webview:allow-create-webview-window",
|
"clipboard-manager:allow-read-text",
|
||||||
"webview:allow-create-webview",
|
"webview:allow-create-webview-window",
|
||||||
"webview:allow-set-webview-size",
|
"webview:allow-create-webview",
|
||||||
"webview:allow-set-webview-position",
|
"webview:allow-set-webview-size",
|
||||||
"webview:allow-webview-close",
|
"webview:allow-set-webview-position",
|
||||||
"dialog:allow-open",
|
"webview:allow-webview-close",
|
||||||
"dialog:allow-ask",
|
"dialog:allow-open",
|
||||||
"dialog:allow-message",
|
"dialog:allow-ask",
|
||||||
"process:allow-restart",
|
"dialog:allow-message",
|
||||||
"fs:allow-read-file",
|
"process:allow-restart",
|
||||||
"shell:allow-open",
|
"fs:allow-read-file",
|
||||||
{
|
"theme:allow-set-theme",
|
||||||
"identifier": "http:default",
|
"theme:allow-get-theme",
|
||||||
"allow": [
|
"shell:allow-open",
|
||||||
{
|
{
|
||||||
"url": "http://**/"
|
"identifier": "http:default",
|
||||||
},
|
"allow": [
|
||||||
{
|
{
|
||||||
"url": "https://**/"
|
"url": "http://**/"
|
||||||
}
|
},
|
||||||
]
|
{
|
||||||
},
|
"url": "https://**/"
|
||||||
{
|
}
|
||||||
"identifier": "fs:allow-read-text-file",
|
]
|
||||||
"allow": [
|
},
|
||||||
{
|
{
|
||||||
"path": "$RESOURCE/locales/*"
|
"identifier": "fs:allow-read-text-file",
|
||||||
},
|
"allow": [
|
||||||
{
|
{
|
||||||
"path": "$RESOURCE/resources/*"
|
"path": "$RESOURCE/locales/*"
|
||||||
}
|
},
|
||||||
]
|
{
|
||||||
}
|
"path": "$RESOURCE/resources/*"
|
||||||
]
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
{"desktop-capability":{"identifier":"desktop-capability","description":"Capability for the desktop","local":true,"windows":["main","splash","settings","search","nwc","activity","zap-*","event-*","user-*","editor-*","column-*"],"permissions":["path:default","event:default","window:default","app:default","resources:default","menu:default","tray:default","notification:allow-is-permission-granted","notification:allow-request-permission","notification:default","os:allow-locale","os:allow-platform","updater:default","updater:allow-check","updater:allow-download-and-install","window:allow-start-dragging","window:allow-create","window:allow-close","window:allow-set-focus","window:allow-center","window:allow-minimize","window:allow-maximize","window:allow-set-size","window:allow-set-focus","window:allow-start-dragging","decorum:allow-show-snap-overlay","clipboard-manager:allow-write-text","clipboard-manager:allow-read-text","webview:allow-create-webview-window","webview:allow-create-webview","webview:allow-set-webview-size","webview:allow-set-webview-position","webview:allow-webview-close","dialog:allow-open","dialog:allow-ask","dialog:allow-message","process:allow-restart","fs:allow-read-file","shell:allow-open",{"identifier":"http:default","allow":[{"url":"http://**/"},{"url":"https://**/"}]},{"identifier":"fs:allow-read-text-file","allow":[{"path":"$RESOURCE/locales/*"},{"path":"$RESOURCE/resources/*"}]}],"platforms":["linux","macOS","windows"]}}
|
{"desktop-capability":{"identifier":"desktop-capability","description":"Capability for the desktop","local":true,"windows":["main","splash","settings","search","nwc","activity","zap-*","event-*","user-*","editor-*","column-*"],"permissions":["path:default","event:default","window:default","app:default","resources:default","menu:default","tray:default","notification:allow-is-permission-granted","notification:allow-request-permission","notification:default","os:allow-locale","os:allow-platform","os:allow-os-type","updater:default","updater:allow-check","updater:allow-download-and-install","window:allow-start-dragging","window:allow-create","window:allow-close","window:allow-set-focus","window:allow-center","window:allow-minimize","window:allow-maximize","window:allow-set-size","window:allow-set-focus","window:allow-start-dragging","decorum:allow-show-snap-overlay","clipboard-manager:allow-write-text","clipboard-manager:allow-read-text","webview:allow-create-webview-window","webview:allow-create-webview","webview:allow-set-webview-size","webview:allow-set-webview-position","webview:allow-webview-close","dialog:allow-open","dialog:allow-ask","dialog:allow-message","process:allow-restart","fs:allow-read-file","theme:allow-set-theme","theme:allow-get-theme","shell:allow-open",{"identifier":"http:default","allow":[{"url":"http://**/"},{"url":"https://**/"}]},{"identifier":"fs:allow-read-text-file","allow":[{"path":"$RESOURCE/locales/*"},{"path":"$RESOURCE/resources/*"}]}],"platforms":["linux","macOS","windows"]}}
|
||||||
@@ -5654,6 +5654,40 @@
|
|||||||
"shell:deny-stdin-write"
|
"shell:deny-stdin-write"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"theme:default"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "theme:allow-get-theme -> Enables the get_theme command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"theme:allow-get-theme"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "theme:allow-set-theme -> Enables the set_theme command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"theme:allow-set-theme"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "theme:deny-get-theme -> Denies the get_theme command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"theme:deny-get-theme"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "theme:deny-set-theme -> Denies the set_theme command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"theme:deny-set-theme"
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"description": "tray:default -> Default permissions for the plugin.",
|
"description": "tray:default -> Default permissions for the plugin.",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
|
|||||||