Compare commits

...

6 Commits

Author SHA1 Message Date
reya
6e5d0f0e76 chore: bump version 2024-06-24 10:06:12 +07:00
XIAO YU
f0712e5961 refactor: improve error handling (#216) 2024-06-24 07:19:36 +07:00
雨宮蓮
3fbd66dece Add bitcoin connect (#215)
* feat: add bitcoin connect

* feat: improve zap screen
2024-06-21 14:56:10 +07:00
reya
1283432632 feat: use native feature instead of react 2024-06-21 10:24:09 +07:00
reya
59eaaec903 feat: support content warning 2024-06-21 08:57:49 +07:00
雨宮蓮
4f0f210076 Add lazy carousel (#214)
* refactor: carousel

* feat: improve image carousel
2024-06-21 07:51:11 +07:00
51 changed files with 1022 additions and 1018 deletions

2
.gitignore vendored
View File

@@ -35,4 +35,4 @@ dist/
.DS_Store .DS_Store
*.pem *.pem
.vscode/ .vscode/
ndb/ .idea/

5
.idea/.gitignore generated vendored
View File

@@ -1,5 +0,0 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/

View File

@@ -1,5 +0,0 @@
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="PREFERRED_PROJECT_CODE_STYLE" value="Default" />
</state>
</component>

View File

@@ -1,46 +0,0 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="CheckEmptyScriptTag" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="CheckValidXmlInScriptTagBody" enabled="false" level="ERROR" enabled_by_default="false" />
<inspection_tool class="GithubFunctionSignatureValidation" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="GrazieInspection" enabled="false" level="GRAMMAR_ERROR" enabled_by_default="false" />
<inspection_tool class="HtmlExtraClosingTag" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="HtmlMissingClosingTag" enabled="false" level="INFORMATION" enabled_by_default="false" />
<inspection_tool class="HtmlUnknownAnchorTarget" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="HtmlUnknownAttribute" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="HtmlUnknownBooleanAttribute" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="HtmlUnknownTag" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="HtmlUnknownTarget" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="HtmlWrongAttributeValue" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="HttpClientInappropriateProtocolUsageInspection" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
<inspection_tool class="HttpClientUnresolvedAuthId" enabled="false" level="ERROR" enabled_by_default="false" />
<inspection_tool class="HttpClientUnresolvedVariable" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="HttpRequestContentLengthIsIgnored" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="HttpRequestEnvironmentAuthConfigurationValidationInspection" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="HttpRequestPlaceholder" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="HttpRequestRequestSeparatorJsonBodyInspection" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
<inspection_tool class="HttpRequestRequestSeparatorXmlBodyInspection" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
<inspection_tool class="HttpRequestRequestSeparatorYamlBodyInspection" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
<inspection_tool class="HttpRequestWhitespaceInsideRequestTargetPath" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
<inspection_tool class="IncorrectHttpHeaderInspection" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="LanguageDetectionInspection" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="MandatoryParamsAbsent" enabled="false" level="ERROR" enabled_by_default="false" />
<inspection_tool class="MarkdownIncorrectTableFormatting" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
<inspection_tool class="MarkdownIncorrectlyNumberedListItem" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="MarkdownLinkDestinationWithSpaces" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="MarkdownNoTableBorders" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="MarkdownOutdatedTableOfContents" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="MarkdownUnresolvedFileReference" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="MarkdownUnresolvedHeaderReference" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="MarkdownUnresolvedLinkLabel" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="RequiredAttributes" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="SpellCheckingInspection" enabled="false" level="TYPO" enabled_by_default="false">
<option name="processCode" value="true" />
<option name="processLiterals" value="true" />
<option name="processComments" value="true" />
</inspection_tool>
<inspection_tool class="UndefinedAction" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="UndefinedParamsPresent" enabled="false" level="WARNING" enabled_by_default="false" />
</profile>
</component>

17
.idea/lume.iml generated
View File

@@ -1,17 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="EMPTY_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/src-tauri/src" isTestSource="false" />
<excludeFolder url="file://$MODULE_DIR$/.github" />
<excludeFolder url="file://$MODULE_DIR$/.turbo" />
<excludeFolder url="file://$MODULE_DIR$/apps" />
<excludeFolder url="file://$MODULE_DIR$/flatpak" />
<excludeFolder url="file://$MODULE_DIR$/node_modules" />
<excludeFolder url="file://$MODULE_DIR$/packages" />
<excludeFolder url="file://$MODULE_DIR$/src-tauri/target" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

8
.idea/modules.xml generated
View File

@@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/lume.iml" filepath="$PROJECT_DIR$/.idea/lume.iml" />
</modules>
</component>
</project>

6
.idea/vcs.xml generated
View File

@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

View File

@@ -9,16 +9,13 @@
"preview": "vite preview" "preview": "vite preview"
}, },
"dependencies": { "dependencies": {
"@getalby/bitcoin-connect-react": "^3.5.3",
"@lume/icons": "workspace:^", "@lume/icons": "workspace:^",
"@lume/system": "workspace:^", "@lume/system": "workspace:^",
"@lume/ui": "workspace:^", "@lume/ui": "workspace:^",
"@lume/utils": "workspace:^", "@lume/utils": "workspace:^",
"@radix-ui/react-avatar": "^1.0.4", "@radix-ui/react-avatar": "^1.0.4",
"@radix-ui/react-checkbox": "^1.0.4", "@radix-ui/react-checkbox": "^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-popover": "^1.0.7",
"@radix-ui/react-scroll-area": "^1.1.0", "@radix-ui/react-scroll-area": "^1.1.0",
"@radix-ui/react-switch": "^1.0.3", "@radix-ui/react-switch": "^1.0.3",
@@ -37,12 +34,10 @@
"react-currency-input-field": "^3.8.0", "react-currency-input-field": "^3.8.0",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
"react-hook-form": "^7.52.0", "react-hook-form": "^7.52.0",
"react-hotkeys-hook": "^4.5.0",
"react-i18next": "^14.1.2", "react-i18next": "^14.1.2",
"react-string-replace": "^1.1.1", "react-string-replace": "^1.1.1",
"slate": "^0.103.0", "slate": "^0.103.0",
"slate-react": "^0.105.0", "slate-react": "^0.105.0",
"sonner": "^1.5.0",
"use-debounce": "^10.0.1", "use-debounce": "^10.0.1",
"virtua": "^0.31.0" "virtua": "^0.31.0"
}, },

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 17 KiB

View File

@@ -1,13 +1,13 @@
import { NostrQuery } from "@lume/system"; 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 { message } from "@tauri-apps/plugin-dialog";
import { import {
type Dispatch, type Dispatch,
type ReactNode, type ReactNode,
type SetStateAction, type SetStateAction,
useState, useState,
} from "react"; } from "react";
import { toast } from "sonner";
export function AvatarUploader({ export function AvatarUploader({
setPicture, setPicture,
@@ -27,7 +27,7 @@ export function AvatarUploader({
setPicture(image); setPicture(image);
} catch (e) { } catch (e) {
setLoading(false); setLoading(false);
toast.error(String(e)); await message(String(e), { title: "Lume", kind: "error" });
} }
}; };

View File

@@ -4,8 +4,8 @@ import { Spinner } from "@lume/ui";
import { cn } from "@lume/utils"; import { cn } from "@lume/utils";
import { useRouteContext } from "@tanstack/react-router"; import { useRouteContext } from "@tanstack/react-router";
import { Menu, MenuItem } from "@tauri-apps/api/menu"; import { Menu, MenuItem } from "@tauri-apps/api/menu";
import { message } from "@tauri-apps/plugin-dialog";
import { useCallback, useState } from "react"; import { useCallback, useState } from "react";
import { toast } from "sonner";
import { useNoteContext } from "../provider"; import { useNoteContext } from "../provider";
export function NoteRepost({ large = false }: { large?: boolean }) { export function NoteRepost({ large = false }: { large?: boolean }) {
@@ -27,12 +27,12 @@ export function NoteRepost({ large = false }: { large?: boolean }) {
// update state // update state
setLoading(false); setLoading(false);
setIsRepost(true); setIsRepost(true);
// notify
toast.success("You've reposted this post successfully");
} catch { } catch {
setLoading(false); setLoading(false);
toast.error("Repost failed, try again later"); await message("Repost failed, try again later", {
title: "Lume",
kind: "info",
});
} }
}; };

View File

@@ -1,7 +1,7 @@
import { cn } from "@lume/utils"; import { cn } from "@lume/utils";
import { useRouteContext } from "@tanstack/react-router"; import { useRouteContext } from "@tanstack/react-router";
import { nanoid } from "nanoid"; import { nanoid } from "nanoid";
import { type ReactNode, useMemo } from "react"; import { type ReactNode, useMemo, useState } from "react";
import reactStringReplace from "react-string-replace"; import reactStringReplace from "react-string-replace";
import { Hashtag } from "./mentions/hashtag"; import { Hashtag } from "./mentions/hashtag";
import { MentionNote } from "./mentions/note"; import { MentionNote } from "./mentions/note";
@@ -23,6 +23,8 @@ export function NoteContent({
}) { }) {
const { settings } = useRouteContext({ strict: false }); const { settings } = useRouteContext({ strict: false });
const event = useNoteContext(); const event = useNoteContext();
const warning = useMemo(() => event.warning, [event]);
const content = useMemo(() => { const content = useMemo(() => {
try { try {
// Get parsed meta // Get parsed meta
@@ -85,14 +87,34 @@ export function NoteContent({
)); ));
return richContent; return richContent;
} catch (e) { } catch {
console.log("[parser]: ", e);
return event.content; return event.content;
} }
}, [event.content]); }, [event.content]);
const [blurred, setBlurred] = useState(() => warning?.length > 0);
return ( return (
<div className="flex flex-col gap-2"> <div className="relative flex flex-col gap-2">
{blurred ? (
<div className="absolute inset-0 z-10 flex items-center justify-center w-full h-full bg-black/80 backdrop-blur-xl">
<div className="flex flex-col items-center justify-center gap-2 text-center">
<p className="text-sm text-white/60">
The content is hidden because the author
<br />
marked it with a warning for a reason:
</p>
<p className="text-sm font-medium text-white">{warning}</p>
<button
type="button"
onClick={() => setBlurred(false)}
className="inline-flex items-center justify-center px-2 mt-4 text-sm font-medium border rounded-lg text-white/70 h-9 w-max bg-white/20 hover:bg-white/30 border-white/5"
>
View anyway
</button>
</div>
</div>
) : null}
<div <div
className={cn( className={cn(
"select-text text-pretty content-break overflow-hidden", "select-text text-pretty content-break overflow-hidden",
@@ -104,11 +126,15 @@ export function NoteContent({
> >
{content} {content}
</div> </div>
{settings.display_media && event.meta?.images.length ? ( {settings.display_media ? (
<Images urls={event.meta.images} /> <>
) : null} {event.meta?.images.length ? (
{settings.display_media && event.meta?.videos.length ? ( <Images urls={event.meta.images} />
<Videos urls={event.meta.videos} /> ) : null}
{event.meta?.videos.length ? (
<Videos urls={event.meta.videos} />
) : null}
</>
) : null} ) : null}
</div> </div>
); );

View File

@@ -1,23 +1,79 @@
import { Carousel, CarouselItem } from "@lume/ui"; import { ArrowLeftIcon, ArrowRightIcon } from "@lume/icons";
import { Spinner } from "@lume/ui";
import { cn } from "@lume/utils";
import { useRouteContext } from "@tanstack/react-router"; import { useRouteContext } from "@tanstack/react-router";
import { open } from "@tauri-apps/plugin-shell"; import { open } from "@tauri-apps/plugin-shell";
import { useMemo } from "react"; import useEmblaCarousel from "embla-carousel-react";
import { useCallback, useEffect, useMemo, useState } from "react";
export function Images({ urls }: { urls: string[] }) { export function Images({ urls }: { urls: string[] }) {
const { settings } = useRouteContext({ strict: false }); const { settings } = useRouteContext({ strict: false });
const [slidesInView, setSlidesInView] = useState([]);
const [emblaRef, emblaApi] = useEmblaCarousel({
dragFree: true,
align: "start",
watchSlides: false,
});
const imageUrls = useMemo(() => { const imageUrls = useMemo(() => {
if (settings.image_resize_service.length) { if (settings.image_resize_service.length) {
const newUrls = urls.map( let newUrls: string[];
(url) =>
`${settings.image_resize_service}?url=${url}&ll&af&default=1&n=-1`, if (urls.length === 1) {
); newUrls = urls.map(
(url) =>
`${settings.image_resize_service}?url=${url}&ll&af&default=1&n=-1`,
);
} else {
newUrls = urls.map(
(url) =>
`${settings.image_resize_service}?url=${url}&w=480&h=640&ll&af&default=1&n=-1`,
);
}
return newUrls; return newUrls;
} else { } else {
return urls; return urls;
} }
}, [settings.image_resize_service]); }, [settings.image_resize_service]);
const scrollPrev = useCallback(() => {
if (emblaApi) emblaApi.scrollPrev();
}, [emblaApi]);
const scrollNext = useCallback(() => {
if (emblaApi) emblaApi.scrollNext();
}, [emblaApi]);
const updateSlidesInView = useCallback((emblaApi) => {
setSlidesInView((slidesInView) => {
if (slidesInView.length === emblaApi.slideNodes().length) {
emblaApi.off("slidesInView", updateSlidesInView);
}
const inView = emblaApi
.slidesInView()
.filter((index) => !slidesInView.includes(index));
return slidesInView.concat(inView);
});
}, []);
useEffect(() => {
if (emblaApi && urls.length > 1) {
updateSlidesInView(emblaApi);
emblaApi.on("slidesInView", updateSlidesInView);
emblaApi.on("reInit", updateSlidesInView);
}
return () => {
emblaApi?.off("slidesInView", updateSlidesInView);
emblaApi?.off("reInit", updateSlidesInView);
};
}, [emblaApi, updateSlidesInView]);
if (urls.length === 1) { if (urls.length === 1) {
return ( return (
<div className="px-3 group"> <div className="px-3 group">
@@ -40,26 +96,84 @@ export function Images({ urls }: { urls: string[] }) {
} }
return ( return (
<Carousel <div className="relative pl-2 overflow-hidden group">
items={imageUrls} <div ref={emblaRef} className="w-full">
renderItem={({ item, index, isSnapPoint }) => ( <div className="flex w-full gap-2 scrollbar-none">
<CarouselItem key={item + index} isSnapPoint={isSnapPoint}> {imageUrls.map((url, index) => (
<img <LazyImage
src={item} /* biome-ignore lint/suspicious/noArrayIndexKey: url can be duplicated */
alt={item} key={url + index}
loading="lazy" url={url}
decoding="async" inView={slidesInView.indexOf(index) > -1}
style={{ contentVisibility: "auto" }} />
className="object-cover w-full h-full rounded-lg outline outline-1 -outline-offset-1 outline-black/15" ))}
onClick={() => open(item)} </div>
onKeyDown={() => open(item)} </div>
onError={({ currentTarget }) => { <div
currentTarget.onerror = null; aria-hidden
currentTarget.src = "/404.jpg"; className="absolute z-10 items-center justify-between hidden w-full px-5 transform -translate-x-1/2 -translate-y-1/2 group-hover:flex left-1/2 top-1/2"
}} >
/> <button
</CarouselItem> type="button"
)} disabled={!emblaApi?.canScrollPrev}
/> className={cn(
"size-11 rounded-full bg-black/30 backdrop-blur-sm flex items-center justify-center text-white",
!emblaApi?.canScrollPrev ? "opacity-50" : "",
)}
onClick={() => scrollPrev()}
>
<ArrowLeftIcon className="size-6" />
</button>
<button
type="button"
disabled={!emblaApi?.canScrollNext}
className={cn(
"size-11 rounded-full bg-black/30 backdrop-blur-sm flex items-center justify-center text-white",
!emblaApi?.canScrollNext ? "opacity-50" : "",
)}
onClick={() => scrollNext()}
>
<ArrowRightIcon className="size-6" />
</button>
</div>
</div>
);
}
function LazyImage({ url, inView }: { url: string; inView: boolean }) {
const [hasLoaded, setHasLoaded] = useState(false);
const setLoaded = useCallback(() => {
if (inView) setHasLoaded(true);
}, [inView, setHasLoaded]);
return (
<div className="w-[240px] h-[320px] shrink-0 relative rounded-lg overflow-hidden">
{!hasLoaded ? (
<div className="flex items-center justify-center size-full bg-black/5 dark:bg-white/5">
<Spinner className="size-4" />
</div>
) : null}
<img
src={
inView
? url
: "data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs="
}
data-src={url}
alt={url}
loading="lazy"
decoding="async"
style={{ contentVisibility: "auto" }}
className="object-cover w-full h-full rounded-lg outline outline-1 -outline-offset-1 outline-black/15"
onClick={() => open(url)}
onKeyDown={() => open(url)}
onLoad={setLoaded}
onError={({ currentTarget }) => {
currentTarget.onerror = null;
currentTarget.src = "/404.jpg";
}}
/>
</div>
); );
} }

View File

@@ -1,62 +1,62 @@
import { LumeWindow } from "@lume/system";
import { cn } from "@lume/utils"; import { cn } from "@lume/utils";
import * as HoverCard from "@radix-ui/react-hover-card"; import { Menu, MenuItem } from "@tauri-apps/api/menu";
import { writeText } from "@tauri-apps/plugin-clipboard-manager";
import { useCallback } from "react";
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 event = useNoteContext(); const event = useNoteContext();
const showContextMenu = useCallback(async (e: React.MouseEvent) => {
e.preventDefault();
const menuItems = await Promise.all([
MenuItem.new({
text: "View Profile",
action: () => LumeWindow.openProfile(event.pubkey),
}),
MenuItem.new({
text: "Copy Public Key",
action: async () => {
const pubkey = await event.pubkeyAsBech32();
await writeText(pubkey);
},
}),
]);
const menu = await Menu.new({
items: menuItems,
});
await menu.popup().catch((e) => console.error(e));
}, []);
return ( return (
<User.Provider pubkey={event.pubkey}> <User.Provider pubkey={event.pubkey}>
<HoverCard.Root> <User.Root className={cn("flex items-start justify-between", className)}>
<User.Root <div className="flex w-full gap-2">
className={cn("flex items-start justify-between", className)} <button
> type="button"
<div className="flex w-full gap-2"> onClick={(e) => showContextMenu(e)}
<HoverCard.Trigger className="shrink-0"> className="shrink-0"
<User.Avatar className="object-cover rounded-full size-8 outline outline-1 -outline-offset-1 outline-black/15" />
</HoverCard.Trigger>
<div className="flex items-center w-full gap-3">
<div className="flex items-center gap-1">
<User.Name className="font-semibold text-neutral-950 dark:text-neutral-50" />
<User.NIP05 />
</div>
<div className="text-neutral-600 dark:text-neutral-400">·</div>
<User.Time
time={event.created_at}
className="text-neutral-600 dark:text-neutral-400"
/>
</div>
</div>
</User.Root>
<HoverCard.Portal>
<HoverCard.Content
className="w-[300px] rounded-xl bg-black p-3 data-[side=bottom]:animate-slideUpAndFade data-[state=open]:transition-all dark:bg-white dark:shadow-none"
sideOffset={5}
side="right"
> >
<div className="flex flex-col gap-2"> <User.Avatar className="object-cover rounded-full size-8 outline outline-1 -outline-offset-1 outline-black/15" />
<User.Avatar className="object-cover rounded-lg size-11" /> </button>
<div className="flex flex-col gap-2"> <div className="flex items-center w-full gap-3">
<div className="inline-flex items-center gap-1"> <div className="flex items-center gap-1">
<User.Name className="font-semibold leading-tight text-white dark:text-neutral-900" /> <User.Name className="font-semibold text-neutral-950 dark:text-neutral-50" />
<User.NIP05 /> <User.NIP05 />
</div>
<User.About className="text-sm text-white line-clamp-3 dark:text-neutral-900" />
<button
type="button"
onClick={() => LumeWindow.openProfile(event.pubkey)}
className="inline-flex items-center justify-center w-full mt-2 text-sm font-medium bg-white rounded-lg h-9 hover:bg-neutral-200 dark:bg-neutral-100 dark:text-neutral-900 dark:hover:bg-neutral-200"
>
View profile
</button>
</div>
</div> </div>
<HoverCard.Arrow className="fill-black dark:fill-white" /> <div className="text-neutral-600 dark:text-neutral-400">·</div>
</HoverCard.Content> <User.Time
</HoverCard.Portal> time={event.created_at}
</HoverCard.Root> className="text-neutral-600 dark:text-neutral-400"
/>
</div>
</div>
</User.Root>
</User.Provider> </User.Provider>
); );
} }

View File

@@ -1,18 +1,14 @@
import { User } from "@/components/user"; import { User } from "@/components/user";
import { import { ComposeFilledIcon, HorizontalDotsIcon, PlusIcon } from "@lume/icons";
ComposeFilledIcon,
HorizontalDotsIcon,
PlusIcon,
SearchIcon,
} from "@lume/icons";
import { LumeWindow, NostrAccount } from "@lume/system"; import { LumeWindow, NostrAccount } from "@lume/system";
import { cn } from "@lume/utils"; import { cn } from "@lume/utils";
import * as Popover from "@radix-ui/react-popover"; import * as Popover from "@radix-ui/react-popover";
import { Outlet, createFileRoute } from "@tanstack/react-router"; import { Outlet, createFileRoute } from "@tanstack/react-router";
import { Link } from "@tanstack/react-router"; import { Link } from "@tanstack/react-router";
import { Menu, MenuItem } from "@tauri-apps/api/menu";
import { getCurrent } from "@tauri-apps/api/window"; import { getCurrent } from "@tauri-apps/api/window";
import { useEffect, useMemo, useState } from "react"; import { message } from "@tauri-apps/plugin-dialog";
import { toast } from "sonner"; import { useCallback, useEffect, useMemo, useState } from "react";
export const Route = createFileRoute("/$account")({ export const Route = createFileRoute("/$account")({
beforeLoad: async () => { beforeLoad: async () => {
@@ -63,12 +59,12 @@ function Screen() {
} }
function Accounts() { function Accounts() {
const navigate = Route.useNavigate();
const { accounts } = Route.useRouteContext(); const { accounts } = Route.useRouteContext();
const { account } = Route.useParams(); const { account } = Route.useParams();
const [windowWidth, setWindowWidth] = useState<number>(null); const [windowWidth, setWindowWidth] = useState<number>(null);
const navigate = Route.useNavigate();
const sortedList = useMemo(() => { const sortedList = useMemo(() => {
const list = accounts; const list = accounts;
@@ -82,9 +78,33 @@ function Accounts() {
return list; return list;
}, [accounts]); }, [accounts]);
const changeAccount = async (npub: string) => { const showContextMenu = useCallback(
async (e: React.MouseEvent, npub: string) => {
e.preventDefault();
const menuItems = await Promise.all([
MenuItem.new({
text: "View Profile",
action: () => LumeWindow.openProfile(npub),
}),
MenuItem.new({
text: "Open Settings",
action: () => LumeWindow.openSettings(),
}),
]);
const menu = await Menu.new({
items: menuItems,
});
await menu.popup().catch((e) => console.error(e));
},
[],
);
const changeAccount = async (e: React.MouseEvent, npub: string) => {
if (npub === account) { if (npub === account) {
return await LumeWindow.openProfile(account); return showContextMenu(e, npub);
} }
// Change current account and update signer // Change current account and update signer
@@ -102,7 +122,7 @@ function Accounts() {
replace: true, replace: true,
}); });
} else { } else {
toast.warning("Something wrong."); await message("Something wrong.", { title: "Accounts", kind: "error" });
} }
}; };
@@ -135,7 +155,11 @@ function Accounts() {
{sortedList {sortedList
.slice(0, windowWidth > 500 ? account.length : 2) .slice(0, windowWidth > 500 ? account.length : 2)
.map((user) => ( .map((user) => (
<button key={user} type="button" onClick={() => changeAccount(user)}> <button
key={user}
type="button"
onClick={(e) => changeAccount(e, user)}
>
<User.Provider pubkey={user}> <User.Provider pubkey={user}>
<User.Root <User.Root
className={cn( className={cn(
@@ -161,12 +185,12 @@ function Accounts() {
<HorizontalDotsIcon className="size-5" /> <HorizontalDotsIcon className="size-5" />
</Popover.Trigger> </Popover.Trigger>
<Popover.Portal> <Popover.Portal>
<Popover.Content className="flex h-11 select-none items-center justify-center rounded-md bg-neutral-950 p-1 text-sm text-neutral-50 will-change-[transform,opacity] data-[state=delayed-open]:data-[side=bottom]:animate-slideUpAndFade data-[state=delayed-open]:data-[side=left]:animate-slideRightAndFade data-[state=delayed-open]:data-[side=right]:animate-slideLeftAndFade data-[state=delayed-open]:data-[side=top]:animate-slideDownAndFade dark:bg-neutral-50 dark:text-neutral-950"> <Popover.Content className="flex h-11 select-none items-center justify-center rounded-md bg-black/20 p-1 will-change-[transform,opacity] data-[state=delayed-open]:data-[side=bottom]:animate-slideUpAndFade data-[state=delayed-open]:data-[side=left]:animate-slideRightAndFade data-[state=delayed-open]:data-[side=right]:animate-slideLeftAndFade data-[state=delayed-open]:data-[side=top]:animate-slideDownAndFade">
{sortedList.slice(2).map((user) => ( {sortedList.slice(2).map((user) => (
<button <button
key={user} key={user}
type="button" type="button"
onClick={() => changeAccount(user)} onClick={(e) => changeAccount(e, user)}
className="inline-flex items-center justify-center rounded-md size-9 hover:bg-white/10" className="inline-flex items-center justify-center rounded-md size-9 hover:bg-white/10"
> >
<User.Provider pubkey={user}> <User.Provider pubkey={user}>

View File

@@ -1,10 +1,8 @@
import { CancelCircleIcon, CheckCircleIcon, InfoCircleIcon } from "@lume/icons";
import type { Settings } from "@lume/system"; import type { Settings } from "@lume/system";
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 { Toaster } from "sonner";
interface RouterContext { interface RouterContext {
// System // System
@@ -19,21 +17,7 @@ interface RouterContext {
} }
export const Route = createRootRouteWithContext<RouterContext>()({ export const Route = createRootRouteWithContext<RouterContext>()({
component: () => ( component: () => <Outlet />,
<>
<Toaster
position="bottom-right"
icons={{
success: <CheckCircleIcon className="size-5" />,
info: <InfoCircleIcon className="size-5" />,
error: <CancelCircleIcon className="size-5" />,
}}
closeButton
theme="system"
/>
<Outlet />
</>
),
pendingComponent: Pending, pendingComponent: Pending,
wrapInSuspense: true, wrapInSuspense: true,
}); });

View File

@@ -5,9 +5,9 @@ import * as Checkbox from "@radix-ui/react-checkbox";
import { createFileRoute, useNavigate } from "@tanstack/react-router"; import { createFileRoute, useNavigate } from "@tanstack/react-router";
import { invoke } from "@tauri-apps/api/core"; import { invoke } from "@tauri-apps/api/core";
import { writeText } from "@tauri-apps/plugin-clipboard-manager"; import { writeText } from "@tauri-apps/plugin-clipboard-manager";
import { message } from "@tauri-apps/plugin-dialog";
import { useState } from "react"; import { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { toast } from "sonner";
export const Route = createFileRoute("/auth/$account/backup")({ export const Route = createFileRoute("/auth/$account/backup")({
component: Screen, component: Screen,
@@ -29,7 +29,10 @@ function Screen() {
try { try {
if (key) { if (key) {
if (!confirm.c1 || !confirm.c2 || !confirm.c3) { if (!confirm.c1 || !confirm.c2 || !confirm.c3) {
return toast.warning("You need to confirm before continue"); return await message("You need to confirm before continue", {
title: "Backup",
kind: "info",
});
} }
navigate({ to: "/", replace: true }); navigate({ to: "/", replace: true });
@@ -48,7 +51,10 @@ function Screen() {
}); });
} catch (e) { } catch (e) {
setLoading(false); setLoading(false);
toast.error(String(e)); await message(String(e), {
title: "Backup",
kind: "error",
});
} }
}; };
@@ -57,7 +63,10 @@ function Screen() {
await writeText(key); await writeText(key);
setCopied(true); setCopied(true);
} catch (e) { } catch (e) {
toast.error(e); await message(String(e), {
title: "Backup",
kind: "error",
});
} }
}; };

View File

@@ -4,10 +4,10 @@ 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";
import { message } from "@tauri-apps/plugin-dialog";
import { useState } from "react"; import { useState } from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { toast } from "sonner";
export const Route = createFileRoute("/auth/create-profile")({ export const Route = createFileRoute("/auth/create-profile")({
component: Screen, component: Screen,
@@ -53,7 +53,7 @@ function Screen() {
} }
} catch (e) { } catch (e) {
setLoading(false); setLoading(false);
toast.error(String(e)); await message(String(e), { title: "Create Profile", kind: "error" });
} }
}; };

View File

@@ -1,8 +1,8 @@
import { NostrAccount } from "@lume/system"; 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 { message } from "@tauri-apps/plugin-dialog";
import { useState } from "react"; import { useState } from "react";
import { toast } from "sonner";
export const Route = createLazyFileRoute("/auth/import")({ export const Route = createLazyFileRoute("/auth/import")({
component: Screen, component: Screen,
@@ -16,10 +16,12 @@ function Screen() {
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const submit = async () => { const submit = async () => {
if (!key.startsWith("nsec1")) if (!key.startsWith("nsec1")) {
return toast.warning( return await message(
"You need to enter a valid private key starts with nsec or ncryptsec", "You need to enter a valid private key starts with nsec or ncryptsec",
{ title: "Import Key", kind: "info" },
); );
}
try { try {
setLoading(true); setLoading(true);
@@ -31,7 +33,7 @@ function Screen() {
} }
} catch (e) { } catch (e) {
setLoading(false); setLoading(false);
toast.error(e); await message(String(e), { title: "Import Key", kind: "error" });
} }
}; };

View File

@@ -1,8 +1,8 @@
import { NostrAccount } from "@lume/system"; 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 { message } from "@tauri-apps/plugin-dialog";
import { useState } from "react"; import { useState } from "react";
import { toast } from "sonner";
export const Route = createLazyFileRoute("/auth/remote")({ export const Route = createLazyFileRoute("/auth/remote")({
component: Screen, component: Screen,
@@ -15,10 +15,12 @@ function Screen() {
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const submit = async () => { const submit = async () => {
if (!uri.startsWith("bunker://")) if (!uri.startsWith("bunker://")) {
return toast.warning( return await message(
"You need to enter a valid Connect URI starts with bunker://", "You need to enter a valid Connect URI starts with bunker://",
{ title: "Nostr Connect", kind: "info" },
); );
}
try { try {
setLoading(true); setLoading(true);
@@ -30,7 +32,7 @@ function Screen() {
} }
} catch (e) { } catch (e) {
setLoading(false); setLoading(false);
toast.error(e); await message(String(e), { title: "Nostr Connect", kind: "error" });
} }
}; };

View File

@@ -3,9 +3,9 @@ import { NostrQuery } from "@lume/system";
import type { Relay } from "@lume/types"; import type { Relay } 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 { message } from "@tauri-apps/plugin-dialog";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { toast } from "sonner";
export const Route = createFileRoute("/bootstrap-relays")({ export const Route = createFileRoute("/bootstrap-relays")({
loader: async () => { loader: async () => {
@@ -32,7 +32,7 @@ function Screen() {
setRelays((prev) => [...prev, relay]); setRelays((prev) => [...prev, relay]);
reset(); reset();
} catch (e) { } catch (e) {
toast.error(String(e)); await message(String(e), { title: "Bootstrap Relays", kind: "error" });
} }
}; };
@@ -41,8 +41,7 @@ function Screen() {
setIsLoading(true); setIsLoading(true);
await NostrQuery.saveBootstrapRelays(relays); await NostrQuery.saveBootstrapRelays(relays);
} catch (e) { } catch (e) {
setIsLoading(false); await message(String(e), { title: "Bootstrap Relays", kind: "error" });
toast.error(String(e));
} }
}; };

View File

@@ -1,11 +1,11 @@
import { User } from "@/components/user";
import { CancelIcon, PlusIcon } from "@lume/icons"; import { CancelIcon, PlusIcon } from "@lume/icons";
import { NostrAccount, NostrQuery } 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 { User } from "@/components/user";
import { createFileRoute } from "@tanstack/react-router"; import { createFileRoute } from "@tanstack/react-router";
import { message } from "@tauri-apps/plugin-dialog";
import { useState } from "react"; import { useState } from "react";
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 => {
@@ -65,25 +65,25 @@ function Screen() {
} }
} catch (e) { } catch (e) {
setIsLoading(false); setIsLoading(false);
toast.error(e); await message(String(e), { title: "Create Group", kind: "error" });
} }
}; };
return ( return (
<div className="w-full h-full flex flex-col items-center justify-center gap-4"> <div className="flex flex-col items-center justify-center w-full h-full gap-4">
<div className="text-center flex flex-col items-center justify-center"> <div className="flex flex-col items-center justify-center text-center">
<h1 className="text-2xl font-serif font-medium"> <h1 className="font-serif text-2xl font-medium">
Focus feeds for people you like Focus feeds for people you like
</h1> </h1>
<p className="leading-tight text-neutral-700 dark:text-neutral-300"> <p className="leading-tight text-neutral-700 dark:text-neutral-300">
Add some people for custom feeds. Add some people for custom feeds.
</p> </p>
</div> </div>
<div className="w-4/5 max-w-full flex flex-col gap-3"> <div className="flex flex-col w-4/5 max-w-full gap-3">
<div className="w-full h-9 shrink-0 flex items-center bg-black/5 dark:bg-white/5 rounded-lg"> <div className="flex items-center w-full rounded-lg h-9 shrink-0 bg-black/5 dark:bg-white/5">
<label <label
htmlFor="name" htmlFor="name"
className="w-16 border-r border-black/10 dark:border-white/10 shrink-0 text-center text-sm font-semibold" className="w-16 text-sm font-semibold text-center border-r border-black/10 dark:border-white/10 shrink-0"
> >
Name Name
</label> </label>
@@ -92,10 +92,10 @@ function Screen() {
value={title} value={title}
onChange={(e) => setTitle(e.target.value)} onChange={(e) => setTitle(e.target.value)}
placeholder="Enter a name for this group" placeholder="Enter a name for this group"
className="h-full bg-transparent border-none text-sm px-3 placeholder:text-neutral-600 focus:border-neutral-500 focus:ring-0 dark:placeholder:text-neutral-400" className="h-full px-3 text-sm bg-transparent border-none placeholder:text-neutral-600 focus:border-neutral-500 focus:ring-0 dark:placeholder:text-neutral-400"
/> />
</div> </div>
<div className="w-full flex flex-col items-center gap-3"> <div className="flex flex-col items-center w-full gap-3">
<div className="overflow-y-auto scrollbar-none p-2 w-full h-[450px] flex flex-col gap-3 bg-black/5 dark:bg-white/5 backdrop-blur-lg rounded-xl"> <div className="overflow-y-auto scrollbar-none p-2 w-full h-[450px] flex flex-col gap-3 bg-black/5 dark:bg-white/5 backdrop-blur-lg rounded-xl">
<div className="flex gap-2"> <div className="flex gap-2">
<input <input
@@ -103,12 +103,12 @@ function Screen() {
value={npub} value={npub}
onChange={(e) => setNpub(e.target.value)} onChange={(e) => setNpub(e.target.value)}
placeholder="npub1..." placeholder="npub1..."
className="h-9 w-full rounded-lg bg-black/10 dark:bg-white/10 border-none text-sm px-3 placeholder:text-neutral-600 focus:border-neutral-500 focus:ring-0 dark:placeholder:text-neutral-400" className="w-full px-3 text-sm border-none rounded-lg h-9 bg-black/10 dark:bg-white/10 placeholder:text-neutral-600 focus:border-neutral-500 focus:ring-0 dark:placeholder:text-neutral-400"
/> />
<button <button
type="button" type="button"
onClick={() => addUser()} onClick={() => addUser()}
className="inline-flex size-9 rounded-lg items-center justify-center bg-black/20 dark:bg-white/20 shrink-0 text-white hover:bg-blue-500" className="inline-flex items-center justify-center text-white rounded-lg size-9 bg-black/20 dark:bg-white/20 shrink-0 hover:bg-blue-500"
> >
<PlusIcon className="size-6" /> <PlusIcon className="size-6" />
</button> </button>
@@ -122,11 +122,11 @@ function Screen() {
key={item} key={item}
type="button" type="button"
onClick={() => toggleUser(item)} onClick={() => toggleUser(item)}
className="inline-flex items-center justify-between px-3 py-2 rounded-lg bg-white dark:bg-black/20 backdrop-blur-lg shadow-primary dark:ring-1 ring-neutral-800/50" className="inline-flex items-center justify-between px-3 py-2 bg-white rounded-lg dark:bg-black/20 backdrop-blur-lg shadow-primary dark:ring-1 ring-neutral-800/50"
> >
<User.Provider pubkey={item}> <User.Provider pubkey={item}>
<User.Root className="flex items-center gap-2.5"> <User.Root className="flex items-center gap-2.5">
<User.Avatar className="size-8 rounded-full object-cover" /> <User.Avatar className="object-cover rounded-full size-8" />
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<User.Name className="text-sm font-medium" /> <User.Name className="text-sm font-medium" />
</div> </div>
@@ -138,7 +138,7 @@ function Screen() {
</button> </button>
)) ))
) : ( ) : (
<div className="bg-black/5 dark:bg-white/5 text-sm flex items-center justify-center h-14 rounded-lg"> <div className="flex items-center justify-center text-sm rounded-lg bg-black/5 dark:bg-white/5 h-14">
Empty. Empty.
</div> </div>
)} )}
@@ -153,11 +153,11 @@ function Screen() {
key={item} key={item}
type="button" type="button"
onClick={() => toggleUser(item)} onClick={() => toggleUser(item)}
className="inline-flex items-center justify-between px-3 py-2 rounded-lg bg-white dark:bg-black/20 backdrop-blur-lg shadow-primary dark:ring-1 ring-neutral-800/50" className="inline-flex items-center justify-between px-3 py-2 bg-white rounded-lg dark:bg-black/20 backdrop-blur-lg shadow-primary dark:ring-1 ring-neutral-800/50"
> >
<User.Provider pubkey={item}> <User.Provider pubkey={item}>
<User.Root className="flex items-center gap-2.5"> <User.Root className="flex items-center gap-2.5">
<User.Avatar className="size-8 rounded-full object-cover" /> <User.Avatar className="object-cover rounded-full size-8" />
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<User.Name className="text-sm font-medium" /> <User.Name className="text-sm font-medium" />
</div> </div>
@@ -166,7 +166,7 @@ function Screen() {
</button> </button>
)) ))
) : ( ) : (
<div className="bg-black/5 dark:bg-white/5 text-sm flex items-center justify-center h-14 rounded-lg"> <div className="flex items-center justify-center text-sm rounded-lg bg-black/5 dark:bg-white/5 h-14">
<p> <p>
Find more user at{" "} Find more user at{" "}
<a <a
@@ -187,7 +187,7 @@ function Screen() {
type="button" type="button"
onClick={() => submit()} onClick={() => submit()}
disabled={isLoading || users.length < 1} disabled={isLoading || users.length < 1}
className="inline-flex items-center justify-center w-36 rounded-full h-9 bg-blue-500 text-white text-sm font-medium hover:bg-blue-600 disabled:opacity-50" className="inline-flex items-center justify-center text-sm font-medium text-white bg-blue-500 rounded-full w-36 h-9 hover:bg-blue-600 disabled:opacity-50"
> >
{isLoading ? <Spinner /> : "Confirm"} {isLoading ? <Spinner /> : "Confirm"}
</button> </button>

View File

@@ -2,8 +2,8 @@ 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 { message } from "@tauri-apps/plugin-dialog";
import { useState } from "react"; import { useState } from "react";
import { toast } from "sonner";
export const Route = createFileRoute("/create-newsfeed/f2f")({ export const Route = createFileRoute("/create-newsfeed/f2f")({
validateSearch: (search: Record<string, string>): ColumnRouteSearch => { validateSearch: (search: Record<string, string>): ColumnRouteSearch => {
@@ -24,8 +24,12 @@ function Screen() {
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const submit = async () => { const submit = async () => {
if (!npub.startsWith("npub1")) if (!npub.startsWith("npub1")) {
return toast.warning("You must enter a valid npub."); return await message("You must enter a valid npub.", {
title: "Create Newsfeed",
kind: "info",
});
}
try { try {
setIsLoading(true); setIsLoading(true);
@@ -37,13 +41,16 @@ function Screen() {
} }
} catch (e) { } catch (e) {
setIsLoading(false); setIsLoading(false);
toast.error(String(e)); await message(String(e), {
title: "Create Newsfeed",
kind: "error",
});
} }
}; };
return ( return (
<div className="overflow-y-auto scrollbar-none p-2 shrink-0 h-[450px] bg-white dark:bg-white/20 backdrop-blur-lg rounded-xl shadow-primary dark:ring-1 ring-neutral-800/50"> <div className="overflow-y-auto scrollbar-none p-2 shrink-0 h-[450px] bg-white dark:bg-white/20 backdrop-blur-lg rounded-xl shadow-primary dark:ring-1 ring-neutral-800/50">
<div className="h-full flex flex-col justify-between"> <div className="flex flex-col justify-between h-full">
<div className="flex-1 flex flex-col gap-1.5 justify-center px-5"> <div className="flex-1 flex flex-col gap-1.5 justify-center px-5">
<p className="font-semibold text-neutral-500"> <p className="font-semibold text-neutral-500">
You already have a friend on Nostr? You already have a friend on Nostr?
@@ -60,7 +67,7 @@ function Screen() {
</div> </div>
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1">
<label htmlFor="npub" className="font-medium text-sm"> <label htmlFor="npub" className="text-sm font-medium">
NPUB NPUB
</label> </label>
<input <input
@@ -69,13 +76,13 @@ function Screen() {
value={npub} value={npub}
onChange={(e) => setNpub(e.target.value)} onChange={(e) => setNpub(e.target.value)}
spellCheck={false} spellCheck={false}
className="h-11 rounded-lg bg-transparent border border-neutral-200 dark:border-neutral-800 px-3 placeholder:text-neutral-600 focus:border-blue-500 focus:ring-0 dark:placeholder:text-neutral-400" className="px-3 bg-transparent border rounded-lg h-11 border-neutral-200 dark:border-neutral-800 placeholder:text-neutral-600 focus:border-blue-500 focus:ring-0 dark:placeholder:text-neutral-400"
/> />
</div> </div>
<button <button
type="button" type="button"
onClick={() => submit()} onClick={() => submit()}
className="inline-flex items-center justify-center w-full rounded-lg h-9 bg-blue-500 text-white text-sm font-medium hover:bg-blue-600" className="inline-flex items-center justify-center w-full text-sm font-medium text-white bg-blue-500 rounded-lg h-9 hover:bg-blue-600"
> >
{isLoading ? <Spinner /> : "Confirm"} {isLoading ? <Spinner /> : "Confirm"}
</button> </button>

View File

@@ -1,11 +1,11 @@
import { createFileRoute } from "@tanstack/react-router";
import { Suspense, useState } from "react";
import { Await, defer } from "@tanstack/react-router";
import { User } from "@/components/user"; import { User } from "@/components/user";
import { Spinner } from "@lume/ui";
import { toast } from "sonner";
import type { ColumnRouteSearch } from "@lume/types";
import { NostrAccount } from "@lume/system"; import { NostrAccount } from "@lume/system";
import type { ColumnRouteSearch } from "@lume/types";
import { Spinner } from "@lume/ui";
import { createFileRoute } from "@tanstack/react-router";
import { Await, defer } from "@tanstack/react-router";
import { message } from "@tauri-apps/plugin-dialog";
import { Suspense, useState } from "react";
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 => {
@@ -59,16 +59,19 @@ function Screen() {
} }
} catch (e) { } catch (e) {
setIsLoading(false); setIsLoading(false);
toast.error(String(e)); await message(String(e), {
title: "Create Group",
kind: "error",
});
} }
}; };
return ( return (
<div className="w-full flex flex-col items-center gap-3"> <div className="flex flex-col items-center w-full gap-3">
<div className="overflow-y-auto scrollbar-none p-2 w-full h-[450px] bg-black/5 dark:bg-white/5 backdrop-blur-lg rounded-xl"> <div className="overflow-y-auto scrollbar-none p-2 w-full h-[450px] bg-black/5 dark:bg-white/5 backdrop-blur-lg rounded-xl">
<Suspense <Suspense
fallback={ fallback={
<div className="flex h-20 w-full flex-col items-center justify-center gap-1"> <div className="flex flex-col items-center justify-center w-full h-20 gap-1">
<button <button
type="button" type="button"
className="inline-flex items-center gap-2 text-sm font-medium" className="inline-flex items-center gap-2 text-sm font-medium"
@@ -85,27 +88,27 @@ function Screen() {
users.profiles.map((item: { pubkey: string }) => ( users.profiles.map((item: { pubkey: string }) => (
<div <div
key={item.pubkey} key={item.pubkey}
className="h-max w-full overflow-hidden mb-2 p-2 bg-white dark:bg-black/20 backdrop-blur-lg rounded-lg shadow-primary dark:ring-1 ring-neutral-800/50" className="w-full p-2 mb-2 overflow-hidden bg-white rounded-lg h-max dark:bg-black/20 backdrop-blur-lg shadow-primary dark:ring-1 ring-neutral-800/50"
> >
<User.Provider pubkey={item.pubkey}> <User.Provider pubkey={item.pubkey}>
<User.Root> <User.Root>
<div className="flex h-full w-full flex-col gap-2"> <div className="flex flex-col w-full h-full gap-2">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<User.Avatar className="size-7 shrink-0 rounded-full object-cover" /> <User.Avatar className="object-cover rounded-full size-7 shrink-0" />
<User.Name className="text-sm leadning-tight max-w-[15rem] truncate font-semibold" /> <User.Name className="text-sm leadning-tight max-w-[15rem] truncate font-semibold" />
</div> </div>
<button <button
type="button" type="button"
onClick={() => toggleFollow(item.pubkey)} onClick={() => toggleFollow(item.pubkey)}
className="inline-flex h-7 w-20 items-center justify-center rounded-lg bg-black/10 text-sm font-medium hover:bg-black/20 dark:bg-white/10 dark:hover:bg-white/20" className="inline-flex items-center justify-center w-20 text-sm font-medium rounded-lg h-7 bg-black/10 hover:bg-black/20 dark:bg-white/10 dark:hover:bg-white/20"
> >
{follows.includes(item.pubkey) {follows.includes(item.pubkey)
? "Unfollow" ? "Unfollow"
: "Follow"} : "Follow"}
</button> </button>
</div> </div>
<User.About className="line-clamp-3 max-w-none select-text text-neutral-800 dark:text-neutral-400" /> <User.About className="select-text line-clamp-3 max-w-none text-neutral-800 dark:text-neutral-400" />
</div> </div>
</User.Root> </User.Root>
</User.Provider> </User.Provider>
@@ -119,7 +122,7 @@ function Screen() {
type="button" type="button"
onClick={() => submit()} onClick={() => submit()}
disabled={isLoading || follows.length < 1} disabled={isLoading || follows.length < 1}
className="inline-flex items-center justify-center w-36 rounded-full h-9 bg-blue-500 text-white text-sm font-medium hover:bg-blue-600 disabled:opacity-50" className="inline-flex items-center justify-center text-sm font-medium text-white bg-blue-500 rounded-full w-36 h-9 hover:bg-blue-600 disabled:opacity-50"
> >
{isLoading ? <Spinner /> : "Confirm"} {isLoading ? <Spinner /> : "Confirm"}
</button> </button>

View File

@@ -4,8 +4,8 @@ import type { ColumnRouteSearch } from "@lume/types";
import { Spinner } from "@lume/ui"; import { Spinner } from "@lume/ui";
import { TOPICS } from "@lume/utils"; import { TOPICS } from "@lume/utils";
import { createFileRoute } from "@tanstack/react-router"; import { createFileRoute } from "@tanstack/react-router";
import { message } from "@tauri-apps/plugin-dialog";
import { useState } from "react"; import { useState } from "react";
import { toast } from "sonner";
type Topic = { type Topic = {
title: string; title: string;
@@ -53,7 +53,10 @@ function Screen() {
} }
} catch (e) { } catch (e) {
setIsLoading(false); setIsLoading(false);
toast.error(String(e)); await message(String(e), {
title: "Create Topic",
kind: "error",
});
} }
}; };

View File

@@ -4,9 +4,9 @@ import { Spinner } from "@lume/ui";
import { insertImage, isImagePath } from "@lume/utils"; import { insertImage, isImagePath } from "@lume/utils";
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 { message } from "@tauri-apps/plugin-dialog";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { useSlateStatic } from "slate-react"; import { useSlateStatic } from "slate-react";
import { toast } from "sonner";
export function MediaButton() { export function MediaButton() {
const editor = useSlateStatic(); const editor = useSlateStatic();
@@ -24,7 +24,7 @@ export function MediaButton() {
setLoading(false); setLoading(false);
} catch (e) { } catch (e) {
setLoading(false); setLoading(false);
toast.error(`Upload failed, error: ${e}`); await message(String(e), { title: "Upload", kind: "error" });
} }
}; };

View File

@@ -1,12 +1,12 @@
import { PlusIcon, RelayIcon } from "@lume/icons";
import { Spinner } from "@lume/ui";
import { User } from "@/components/user"; import { User } from "@/components/user";
import { PlusIcon, RelayIcon } from "@lume/icons";
import { NostrAccount } from "@lume/system";
import { Spinner } from "@lume/ui";
import { checkForAppUpdates, displayNpub } from "@lume/utils"; import { checkForAppUpdates, displayNpub } 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 { message } from "@tauri-apps/plugin-dialog";
import { useState } from "react"; import { useState } from "react";
import { toast } from "sonner";
import { NostrAccount } from "@lume/system";
export const Route = createFileRoute("/")({ export const Route = createFileRoute("/")({
beforeLoad: async () => { beforeLoad: async () => {
@@ -51,7 +51,10 @@ function Screen() {
} }
} catch (e) { } catch (e) {
setLoading({ npub: "", status: false }); setLoading({ npub: "", status: false });
toast.error(String(e)); await message(String(e), {
title: "Account",
kind: "error",
});
} }
}; };

View File

@@ -19,42 +19,33 @@ function Screen() {
return ( return (
<Container withDrag> <Container withDrag>
<div className="h-full w-full flex-1 px-5"> <div className="flex-1 w-full h-full px-5">
{!isDone ? ( <div className="flex flex-col gap-2">
<> <div>
<div className="flex flex-col gap-2"> <h3 className="text-2xl font-light">
<div className="inline-flex size-14 items-center justify-center rounded-xl bg-black text-white shadow-md"> Connect <span className="font-semibold">bitcoin wallet</span> to
<ZapIcon className="size-5" /> start zapping to your favorite content and creator.
</div> </h3>
<div> </div>
<h3 className="text-2xl font-light"> </div>
Connect <span className="font-semibold">bitcoin wallet</span>{" "} <div className="flex flex-col gap-2 mt-10">
to start zapping to your favorite content and creator. <div className="flex flex-col gap-1.5">
</h3> <label>Paste a Nostr Wallet Connect connection string</label>
</div> <textarea
</div> value={uri}
<div className="mt-10 flex flex-col gap-2"> onChange={(e) => setUri(e.target.value)}
<div className="flex flex-col gap-1.5"> placeholder="nostrconnect://"
<label>Paste a Nostr Wallet Connect connection string</label> className="w-full h-24 px-3 bg-transparent rounded-lg border-neutral-300 placeholder:text-neutral-500 focus:border-blue-500 focus:ring focus:ring-blue-200 dark:border-neutral-700 dark:placeholder:text-neutral-400 dark:focus:ring-blue-800"
<textarea />
value={uri} </div>
onChange={(e) => setUri(e.target.value)} <button
placeholder="nostrconnect://" type="button"
className="h-24 w-full rounded-lg border-neutral-300 bg-transparent px-3 placeholder:text-neutral-500 focus:border-blue-500 focus:ring focus:ring-blue-200 dark:border-neutral-700 dark:placeholder:text-neutral-400 dark:focus:ring-blue-800" onClick={save}
/> className="inline-flex h-11 w-full items-center justify-center gap-1.5 rounded-lg bg-blue-500 px-5 font-medium text-white hover:bg-blue-600"
</div> >
<button Save & Connect
type="button" </button>
onClick={save} </div>
className="inline-flex h-11 w-full items-center justify-center gap-1.5 rounded-lg bg-blue-500 px-5 font-medium text-white hover:bg-blue-600"
>
Save & Connect
</button>
</div>
</>
) : (
<div>Done</div>
)}
</div> </div>
</Container> </Container>
); );

View File

@@ -1,13 +1,13 @@
import { SearchIcon } from "@lume/icons";
import { type NostrEvent, Kind } from "@lume/types";
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 { createFileRoute } from "@tanstack/react-router"; import { SearchIcon } from "@lume/icons";
import { useEffect, useState } from "react";
import { toast } from "sonner";
import { useDebounce } from "use-debounce";
import { LumeEvent, LumeWindow } from "@lume/system"; import { LumeEvent, LumeWindow } from "@lume/system";
import { Kind, type NostrEvent } from "@lume/types";
import { Spinner } from "@lume/ui";
import { createFileRoute } from "@tanstack/react-router";
import { message } from "@tauri-apps/plugin-dialog";
import { useEffect, useState } from "react";
import { useDebounce } from "use-debounce";
export const Route = createFileRoute("/search")({ export const Route = createFileRoute("/search")({
component: Screen, component: Screen,
@@ -34,7 +34,10 @@ function Screen() {
setEvents(sorted); setEvents(sorted);
} catch (e) { } catch (e) {
setLoading(false); setLoading(false);
toast.error(String(e)); await message(String(e), {
title: "Search",
kind: "error",
});
} }
}; };

View File

@@ -79,7 +79,7 @@ function Screen() {
); );
}} }}
</Link> </Link>
<Link to="/settings/zap"> <Link to="/settings/wallet">
{({ isActive }) => { {({ isActive }) => {
return ( return (
<div <div
@@ -91,9 +91,7 @@ function Screen() {
)} )}
> >
<ZapIcon className="size-5 shrink-0" /> <ZapIcon className="size-5 shrink-0" />
<p className="text-sm font-medium"> <p className="text-sm font-medium">Wallet</p>
{t("settings.zap.title")}
</p>
</div> </div>
); );
}} }}

View File

@@ -1,11 +1,11 @@
import { User } from "@/components/user"; import { User } from "@/components/user";
import { NostrAccount } from "@lume/system"; import { NostrAccount } from "@lume/system";
import { displayNpub, displayNsec } from "@lume/utils"; import { displayNpub } 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";
import { writeText } from "@tauri-apps/plugin-clipboard-manager"; import { writeText } from "@tauri-apps/plugin-clipboard-manager";
import { message } from "@tauri-apps/plugin-dialog";
import { useState } from "react"; import { useState } from "react";
import { toast } from "sonner";
interface Account { interface Account {
npub: string; npub: string;
@@ -43,7 +43,7 @@ function Account({ account }: { account: string }) {
await writeText(data); await writeText(data);
setCopied(true); setCopied(true);
} catch (e) { } catch (e) {
toast.error(e); await message(String(e), { title: "Backup", kind: "error" });
} }
}; };

View File

@@ -0,0 +1,39 @@
import { Button, init } from "@getalby/bitcoin-connect-react";
import { NostrAccount } from "@lume/system";
import { createFileRoute } from "@tanstack/react-router";
import { getCurrent } from "@tauri-apps/api/webviewWindow";
export const Route = createFileRoute("/settings/bitcoin-connect")({
beforeLoad: () => {
init({
appName: "Lume",
filters: ["nwc"],
showBalance: true,
});
},
component: Screen,
});
function Screen() {
const setNwcUri = async (uri: string) => {
const cmd = await NostrAccount.setWallet(uri);
if (cmd) getCurrent().close();
};
return (
<div className="flex items-center justify-center size-full">
<div className="flex flex-col items-center justify-center gap-3 text-center">
<div>
<p className="text-sm text-black/70 dark:text-white/70">
Click to the button below to connect with your Bitcoin wallet.
</p>
</div>
<Button
onConnected={(provider) =>
setNwcUri(provider.client.nostrWalletConnectUrl)
}
/>
</div>
</div>
);
}

View File

@@ -51,7 +51,7 @@ function Screen() {
return ( return (
<div className="w-full max-w-xl mx-auto"> <div className="w-full max-w-xl mx-auto">
<div className="flex flex-col gap-6"> <div className="flex flex-col gap-6">
<div className="flex items-center w-full h-12 px-3 text-sm rounded-xl bg-black/5 dark:bg-white/5"> <div className="flex items-center w-full px-3 text-sm rounded-lg h-11 bg-black/5 dark:bg-white/5">
* Setting changes require restarting the app to take effect. * Setting changes require restarting the app to take effect.
</div> </div>
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">

View File

@@ -1,9 +1,9 @@
import { CancelIcon, PlusIcon } from "@lume/icons"; import { CancelIcon, PlusIcon } from "@lume/icons";
import { NostrQuery } from "@lume/system"; import { NostrQuery } from "@lume/system";
import { createFileRoute } from "@tanstack/react-router"; import { createFileRoute } from "@tanstack/react-router";
import { message } from "@tauri-apps/plugin-dialog";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { toast } from "sonner";
export const Route = createFileRoute("/settings/relay")({ export const Route = createFileRoute("/settings/relay")({
loader: async () => { loader: async () => {
@@ -33,7 +33,7 @@ function Screen() {
} }
} catch (e) { } catch (e) {
setIsLoading(false); setIsLoading(false);
toast.error(String(e)); await message(String(e), { title: "Relay", kind: "error" });
} }
}; };
@@ -42,22 +42,22 @@ function Screen() {
}, [relayList]); }, [relayList]);
return ( return (
<div className="mx-auto w-full max-w-xl"> <div className="w-full max-w-xl mx-auto">
<div className="flex flex-col gap-6"> <div className="flex flex-col gap-6">
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<h2 className="font-semibold text-sm text-neutral-700 dark:text-neutral-300"> <h2 className="text-sm font-semibold text-neutral-700 dark:text-neutral-300">
Connected Relays Connected Relays
</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 px-3 divide-y divide-black/10 dark:divide-white/10 bg-black/5 dark:bg-white/5 rounded-xl">
{relays.map((relay) => ( {relays.map((relay) => (
<div <div
key={relay} key={relay}
className="flex justify-between items-center h-11" className="flex items-center justify-between h-11"
> >
<div className="inline-flex items-center gap-2 text-sm font-medium"> <div className="inline-flex items-center gap-2 text-sm font-medium">
<span className="relative flex size-2"> <span className="relative flex size-2">
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-teal-400 opacity-75"></span> <span className="absolute inline-flex w-full h-full bg-teal-400 rounded-full opacity-75 animate-ping" />
<span className="relative inline-flex rounded-full size-2 bg-teal-500"></span> <span className="relative inline-flex bg-teal-500 rounded-full size-2" />
</span> </span>
{relay} {relay}
</div> </div>
@@ -65,7 +65,7 @@ function Screen() {
<button <button
type="button" type="button"
onClick={() => NostrQuery.removeRelay(relay)} 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 rounded-md size-7 hover:bg-black/10 dark:hover:bg-white/10"
> >
<CancelIcon className="size-4" /> <CancelIcon className="size-4" />
</button> </button>
@@ -75,7 +75,7 @@ function Screen() {
<div className="flex items-center h-14"> <div className="flex items-center h-14">
<form <form
onSubmit={handleSubmit(onSubmit)} onSubmit={handleSubmit(onSubmit)}
className="w-full flex items-center gap-2 mb-0" className="flex items-center w-full gap-2 mb-0"
> >
<input <input
{...register("url", { {...register("url", {
@@ -85,12 +85,12 @@ function Screen() {
name="url" name="url"
placeholder="wss://..." placeholder="wss://..."
spellCheck={false} spellCheck={false}
className="h-9 flex-1 rounded-lg border-neutral-300 bg-transparent px-3 placeholder:text-neutral-500 focus:border-blue-500 focus:ring-0 dark:border-neutral-700 dark:placeholder:text-neutral-400" className="flex-1 px-3 bg-transparent rounded-lg h-9 border-neutral-300 placeholder:text-neutral-500 focus:border-blue-500 focus:ring-0 dark:border-neutral-700 dark:placeholder:text-neutral-400"
/> />
<button <button
type="submit" type="submit"
disabled={isLoading} disabled={isLoading}
className="shrink-0 inline-flex h-9 w-16 px-2 items-center justify-center rounded-lg bg-black/20 dark:bg-white/20 font-medium text-sm text-white hover:bg-blue-500 disabled:opacity-50" className="inline-flex items-center justify-center w-16 px-2 text-sm font-medium text-white rounded-lg shrink-0 h-9 bg-black/20 dark:bg-white/20 hover:bg-blue-500 disabled:opacity-50"
> >
<PlusIcon className="size-7" /> <PlusIcon className="size-7" />
</button> </button>
@@ -99,21 +99,21 @@ function Screen() {
</div> </div>
</div> </div>
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<h2 className="font-semibold text-sm text-neutral-700 dark:text-neutral-300"> <h2 className="text-sm font-semibold text-neutral-700 dark:text-neutral-300">
User Relays (NIP-65) User Relays (NIP-65)
</h2> </h2>
<div className="flex flex-col py-2 bg-black/5 dark:bg-white/5 rounded-xl px-3"> <div className="flex flex-col px-3 py-2 bg-black/5 dark:bg-white/5 rounded-xl">
<p className="text-sm text-yellow-500"> <p className="text-sm text-yellow-500">
Lume will automatically connect to the user's relay list, but the Lume will automatically connect to the user's relay list, but the
manager function (like adding, removing, changing relay purpose) manager function (like adding, removing, changing relay purpose)
is not yet available. is not yet available.
</p> </p>
</div> </div>
<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 px-3 divide-y divide-black/10 dark:divide-white/10 bg-black/5 dark:bg-white/5 rounded-xl">
{relayList.read?.map((relay) => ( {relayList.read?.map((relay) => (
<div <div
key={relay} key={relay}
className="flex justify-between items-center h-11" className="flex items-center justify-between h-11"
> >
<div className="text-sm font-medium">{relay}</div> <div className="text-sm font-medium">{relay}</div>
<div className="text-xs font-semibold">READ</div> <div className="text-xs font-semibold">READ</div>
@@ -122,7 +122,7 @@ function Screen() {
{relayList.write?.map((relay) => ( {relayList.write?.map((relay) => (
<div <div
key={relay} key={relay}
className="flex justify-between items-center h-11" className="flex items-center justify-between h-11"
> >
<div className="text-sm font-medium">{relay}</div> <div className="text-sm font-medium">{relay}</div>
<div className="text-xs font-semibold">WRITE</div> <div className="text-xs font-semibold">WRITE</div>
@@ -131,7 +131,7 @@ function Screen() {
{relayList.both?.map((relay) => ( {relayList.both?.map((relay) => (
<div <div
key={relay} key={relay}
className="flex justify-between items-center h-11" className="flex items-center justify-between h-11"
> >
<div className="text-sm font-medium">{relay}</div> <div className="text-sm font-medium">{relay}</div>
<div className="text-xs font-semibold">READ + WRITE</div> <div className="text-xs font-semibold">READ + WRITE</div>

View File

@@ -5,9 +5,9 @@ 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";
import { createFileRoute } from "@tanstack/react-router"; import { createFileRoute } from "@tanstack/react-router";
import { message } from "@tauri-apps/plugin-dialog";
import { useState } from "react"; import { useState } from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { toast } from "sonner";
export const Route = createFileRoute("/settings/user")({ export const Route = createFileRoute("/settings/user")({
beforeLoad: async () => { beforeLoad: async () => {
@@ -34,31 +34,31 @@ function Screen() {
setLoading(false); setLoading(false);
} catch (e) { } catch (e) {
setLoading(false); setLoading(false);
toast.error(String(e)); await message(String(e), { title: "Profile", kind: "error" });
} }
}; };
return ( return (
<div className="flex w-full h-full"> <div className="flex w-full h-full">
<div className="flex-1 h-full flex items-center flex-col justify-center gap-3"> <div className="flex flex-col items-center justify-center flex-1 h-full gap-3">
<div className="relative size-24 rounded-full bg-gradient-to-tr from-orange-100 via-red-50 to-blue-200"> <div className="relative rounded-full size-24 bg-gradient-to-tr from-orange-100 via-red-50 to-blue-200">
{profile.picture ? ( {profile.picture ? (
<img <img
src={picture || profile.picture} src={picture || profile.picture}
alt="avatar" alt="avatar"
loading="lazy" loading="lazy"
decoding="async" decoding="async"
className="absolute inset-0 z-10 h-full w-full rounded-full object-cover" className="absolute inset-0 z-10 object-cover w-full h-full rounded-full"
/> />
) : null} ) : null}
<AvatarUploader <AvatarUploader
setPicture={setPicture} setPicture={setPicture}
className="absolute inset-0 z-20 flex h-full w-full items-center justify-center rounded-full bg-black/10 text-white hover:bg-black/20 dark:bg-white/10 dark:hover:bg-white/20" className="absolute inset-0 z-20 flex items-center justify-center w-full h-full text-white rounded-full bg-black/10 hover:bg-black/20 dark:bg-white/10 dark:hover:bg-white/20"
> >
<PlusIcon className="size-8" /> <PlusIcon className="size-8" />
</AvatarUploader> </AvatarUploader>
</div> </div>
<div className="text-center flex flex-col items-center"> <div className="flex flex-col items-center text-center">
<div className="text-lg font-semibold">{profile.display_name}</div> <div className="text-lg font-semibold">{profile.display_name}</div>
<div className="text-neutral-800 dark:text-neutral-200"> <div className="text-neutral-800 dark:text-neutral-200">
{profile.nip05} {profile.nip05}
@@ -66,7 +66,7 @@ function Screen() {
<div className="mt-4"> <div className="mt-4">
<Link <Link
to="/settings/backup" to="/settings/backup"
className="px-5 h-9 border border-blue-300 text-sm font-medium hover:bg-blue-200 dark:bg-blue-900 dark:hover:bg-blue-800 rounded-full bg-blue-100 text-blue-500 inline-flex items-center justify-center" className="inline-flex items-center justify-center px-5 text-sm font-medium text-blue-500 bg-blue-100 border border-blue-300 rounded-full h-9 hover:bg-blue-200 dark:bg-blue-900 dark:hover:bg-blue-800"
> >
Backup Account Backup Account
</Link> </Link>
@@ -78,7 +78,7 @@ function Screen() {
onSubmit={handleSubmit(onSubmit)} onSubmit={handleSubmit(onSubmit)}
className="flex flex-col gap-3 mb-0" className="flex flex-col gap-3 mb-0"
> >
<div className="flex w-full flex-col gap-1"> <div className="flex flex-col w-full gap-1">
<label <label
htmlFor="display_name" htmlFor="display_name"
className="text-sm font-medium text-neutral-700 dark:text-neutral-300" className="text-sm font-medium text-neutral-700 dark:text-neutral-300"
@@ -89,10 +89,10 @@ function Screen() {
name="display_name" name="display_name"
{...register("display_name")} {...register("display_name")}
spellCheck={false} spellCheck={false}
className="h-9 w-full rounded-lg border-neutral-300 bg-transparent px-3 placeholder:text-neutral-500 focus:border-blue-500 focus:ring focus:ring-blue-200 dark:border-neutral-700 dark:placeholder:text-neutral-400 dark:focus:ring-blue-800" className="w-full px-3 bg-transparent rounded-lg h-9 border-neutral-300 placeholder:text-neutral-500 focus:border-blue-500 focus:ring focus:ring-blue-200 dark:border-neutral-700 dark:placeholder:text-neutral-400 dark:focus:ring-blue-800"
/> />
</div> </div>
<div className="flex w-full flex-col gap-1"> <div className="flex flex-col w-full gap-1">
<label <label
htmlFor="name" htmlFor="name"
className="text-sm font-medium text-neutral-700 dark:text-neutral-300" className="text-sm font-medium text-neutral-700 dark:text-neutral-300"
@@ -103,10 +103,10 @@ function Screen() {
name="name" name="name"
{...register("name")} {...register("name")}
spellCheck={false} spellCheck={false}
className="h-9 w-full rounded-lg border-neutral-300 bg-transparent px-3 placeholder:text-neutral-500 focus:border-blue-500 focus:ring focus:ring-blue-200 dark:border-neutral-700 dark:placeholder:text-neutral-400 dark:focus:ring-blue-800" className="w-full px-3 bg-transparent rounded-lg h-9 border-neutral-300 placeholder:text-neutral-500 focus:border-blue-500 focus:ring focus:ring-blue-200 dark:border-neutral-700 dark:placeholder:text-neutral-400 dark:focus:ring-blue-800"
/> />
</div> </div>
<div className="flex w-full flex-col gap-1"> <div className="flex flex-col w-full gap-1">
<label <label
htmlFor="website" htmlFor="website"
className="text-sm font-medium text-neutral-700 dark:text-neutral-300" className="text-sm font-medium text-neutral-700 dark:text-neutral-300"
@@ -118,10 +118,10 @@ function Screen() {
type="url" type="url"
{...register("website")} {...register("website")}
spellCheck={false} spellCheck={false}
className="h-9 w-full rounded-lg border-neutral-300 bg-transparent px-3 placeholder:text-neutral-500 focus:border-blue-500 focus:ring focus:ring-blue-200 dark:border-neutral-700 dark:placeholder:text-neutral-400 dark:focus:ring-blue-800" className="w-full px-3 bg-transparent rounded-lg h-9 border-neutral-300 placeholder:text-neutral-500 focus:border-blue-500 focus:ring focus:ring-blue-200 dark:border-neutral-700 dark:placeholder:text-neutral-400 dark:focus:ring-blue-800"
/> />
</div> </div>
<div className="flex w-full flex-col gap-1"> <div className="flex flex-col w-full gap-1">
<label <label
htmlFor="banner" htmlFor="banner"
className="text-sm font-medium text-neutral-700 dark:text-neutral-300" className="text-sm font-medium text-neutral-700 dark:text-neutral-300"
@@ -133,10 +133,10 @@ function Screen() {
type="url" type="url"
{...register("banner")} {...register("banner")}
spellCheck={false} spellCheck={false}
className="h-9 w-full rounded-lg border-neutral-300 bg-transparent px-3 placeholder:text-neutral-500 focus:border-blue-500 focus:ring focus:ring-blue-200 dark:border-neutral-700 dark:placeholder:text-neutral-400 dark:focus:ring-blue-800" className="w-full px-3 bg-transparent rounded-lg h-9 border-neutral-300 placeholder:text-neutral-500 focus:border-blue-500 focus:ring focus:ring-blue-200 dark:border-neutral-700 dark:placeholder:text-neutral-400 dark:focus:ring-blue-800"
/> />
</div> </div>
<div className="flex w-full flex-col gap-1"> <div className="flex flex-col w-full gap-1">
<label <label
htmlFor="nip05" htmlFor="nip05"
className="text-sm font-medium text-neutral-700 dark:text-neutral-300" className="text-sm font-medium text-neutral-700 dark:text-neutral-300"
@@ -148,10 +148,10 @@ function Screen() {
type="email" type="email"
{...register("nip05")} {...register("nip05")}
spellCheck={false} spellCheck={false}
className="h-9 w-full rounded-lg border-neutral-300 bg-transparent px-3 placeholder:text-neutral-500 focus:border-blue-500 focus:ring focus:ring-blue-200 dark:border-neutral-700 dark:placeholder:text-neutral-400 dark:focus:ring-blue-800" className="w-full px-3 bg-transparent rounded-lg h-9 border-neutral-300 placeholder:text-neutral-500 focus:border-blue-500 focus:ring focus:ring-blue-200 dark:border-neutral-700 dark:placeholder:text-neutral-400 dark:focus:ring-blue-800"
/> />
</div> </div>
<div className="flex w-full flex-col gap-1"> <div className="flex flex-col w-full gap-1">
<label <label
htmlFor="lnaddress" htmlFor="lnaddress"
className="text-sm font-medium text-neutral-700 dark:text-neutral-300" className="text-sm font-medium text-neutral-700 dark:text-neutral-300"
@@ -162,13 +162,13 @@ function Screen() {
name="lnaddress" name="lnaddress"
type="email" type="email"
{...register("lud16")} {...register("lud16")}
className="h-9 w-full rounded-lg border-neutral-300 bg-transparent px-3 placeholder:text-neutral-500 focus:border-blue-500 focus:ring focus:ring-blue-200 dark:border-neutral-700 dark:placeholder:text-neutral-400 dark:focus:ring-blue-800" className="w-full px-3 bg-transparent rounded-lg h-9 border-neutral-300 placeholder:text-neutral-500 focus:border-blue-500 focus:ring focus:ring-blue-200 dark:border-neutral-700 dark:placeholder:text-neutral-400 dark:focus:ring-blue-800"
/> />
</div> </div>
<div className="flex items-center justify-end"> <div className="flex items-center justify-end">
<button <button
type="submit" type="submit"
className="inline-flex h-9 w-32 px-2 items-center justify-center rounded-lg bg-blue-500 font-medium text-sm text-white hover:bg-blue-600 disabled:opacity-50" className="inline-flex items-center justify-center w-32 px-2 text-sm font-medium text-white bg-blue-500 rounded-lg h-9 hover:bg-blue-600 disabled:opacity-50"
> >
{loading ? <Spinner className="size-4" /> : "Update Profile"} {loading ? <Spinner className="size-4" /> : "Update Profile"}
</button> </button>

View File

@@ -0,0 +1,59 @@
import { NostrAccount } from "@lume/system";
import { getBitcoinDisplayValues } from "@lume/utils";
import { createFileRoute, redirect } from "@tanstack/react-router";
export const Route = createFileRoute("/settings/wallet")({
beforeLoad: async () => {
const wallet = await NostrAccount.loadWallet();
if (!wallet) {
throw redirect({ to: "/settings/bitcoin-connect" });
}
const balance = getBitcoinDisplayValues(wallet);
return { balance };
},
component: Screen,
});
function Screen() {
const { balance } = Route.useRouteContext();
const disconnect = async () => {
window.localStorage.removeItem("bc:config");
await NostrAccount.removeWallet();
return redirect({ to: "/settings/bitcoin-connect" });
};
return (
<div className="w-full max-w-xl mx-auto">
<div className="flex flex-col w-full gap-3">
<div className="flex flex-col w-full px-3 bg-black/5 dark:bg-white/5 rounded-xl">
<div className="flex items-center justify-between w-full gap-4 py-3">
<div className="flex-1">
<h3 className="font-medium">Connection</h3>
</div>
<div className="flex justify-end w-36 shrink-0">
<button
type="button"
onClick={() => disconnect()}
className="h-8 w-max px-2.5 text-sm rounded-lg inline-flex items-center justify-center bg-black/10 dark:bg-white/10 hover:bg-black/20 dark:hover:bg-white/20"
>
Disconnect
</button>
</div>
</div>
</div>
<div className="flex flex-col w-full px-3 bg-black/5 dark:bg-white/5 rounded-xl">
<div className="flex items-center justify-between w-full gap-4 py-3">
<div className="flex-1">
<h3 className="font-medium">Current Balance</h3>
</div>
<div className="flex justify-end w-36 shrink-0">
{balance.bitcoinFormatted}
</div>
</div>
</div>
</div>
</div>
);
}

View File

@@ -1,102 +0,0 @@
import { createLazyFileRoute } from "@tanstack/react-router";
import { invoke } from "@tauri-apps/api/core";
import { useState } from "react";
import { toast } from "sonner";
export const Route = createLazyFileRoute("/settings/zap")({
component: Screen,
});
function Screen() {
return (
<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-6 py-3">
<Connection />
<DefaultAmount />
</div>
</div>
</div>
);
}
function Connection() {
const [uri, setUri] = useState("");
const connect = async () => {
try {
await invoke("set_nwc", { uri });
} catch (e) {
toast.error(String(e));
}
};
return (
<div className="flex items-start gap-6">
<div className="w-36 shrink-0 text-end font-medium text-sm">
Connection
</div>
<div className="flex-1">
<div className="flex w-full flex-col gap-1">
<label
htmlFor="nwc"
className="text-sm font-medium text-neutral-700 dark:text-neutral-300"
>
Nostr Wallet Connect
</label>
<div className="flex items-center gap-2">
<input
name="nwc"
type="text"
value={uri}
onChange={(e) => setUri(e.target.value)}
placeholder="nostrconnect://"
className="h-9 w-full rounded-lg border-neutral-300 bg-transparent px-3 placeholder:text-neutral-500 focus:border-blue-500 focus:ring focus:ring-blue-200 dark:border-neutral-700 dark:placeholder:text-neutral-400 dark:focus:ring-blue-800"
/>
<button
type="button"
onClick={() => connect()}
className="inline-flex h-9 w-24 items-center justify-center rounded-lg bg-neutral-200 text-sm font-medium hover:bg-neutral-300 dark:bg-neutral-900 dark:hover:bg-neutral-700"
>
Connect
</button>
</div>
</div>
</div>
</div>
);
}
function DefaultAmount() {
return (
<div className="flex items-start gap-6">
<div className="w-36 shrink-0 text-end font-medium text-sm">
Default amount
</div>
<div className="flex-1">
<div className="flex w-full flex-col gap-1">
<label
htmlFor="amount"
className="text-sm font-medium text-neutral-700 dark:text-neutral-300"
>
Set default amount for quick zapping
</label>
<div className="flex items-center gap-2">
<input
name="amount"
type="number"
value={21}
className="h-9 w-full rounded-lg border-neutral-300 bg-transparent px-3 placeholder:text-neutral-500 focus:border-blue-500 focus:ring focus:ring-blue-200 dark:border-neutral-700 dark:placeholder:text-neutral-400 dark:focus:ring-blue-800"
/>
<button
type="button"
className="inline-flex h-9 w-24 items-center justify-center rounded-lg bg-neutral-200 text-sm font-medium hover:bg-neutral-300 dark:bg-neutral-900 dark:hover:bg-neutral-700"
>
Update
</button>
</div>
</div>
</div>
</div>
);
}

View File

@@ -1,124 +0,0 @@
import { Balance } from "@/components/balance";
import { Box, Container } from "@lume/ui";
import { User } from "@/components/user";
import { createLazyFileRoute } from "@tanstack/react-router";
import { getCurrent } from "@tauri-apps/api/webviewWindow";
import { useState } from "react";
import CurrencyInput from "react-currency-input-field";
import { useTranslation } from "react-i18next";
import { toast } from "sonner";
import { LumeEvent } from "@lume/system";
const DEFAULT_VALUES = [69, 100, 200, 500];
export const Route = createLazyFileRoute("/zap/$id")({
component: Screen,
});
function Screen() {
const { t } = useTranslation();
const { id } = Route.useParams();
// @ts-ignore, magic !!!
const { pubkey, account } = Route.useSearch();
const [amount, setAmount] = useState(21);
const [message, setMessage] = useState("");
const [isCompleted, setIsCompleted] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const submit = async () => {
try {
// start loading
setIsLoading(true);
const val = await LumeEvent.zap(id, amount, message);
if (val) {
setIsCompleted(true);
const window = getCurrent();
// close current window
window.close();
}
} catch (e) {
setIsLoading(false);
toast.error(e);
}
};
return (
<Container>
<Balance account={account} />
<Box className="flex flex-col gap-3">
<div className="flex h-full flex-col justify-between py-5">
<div className="flex h-11 shrink-0 items-center justify-center gap-2">
{t("note.zap.modalTitle")}{" "}
<User.Provider pubkey={pubkey}>
<User.Root className="inline-flex items-center gap-2 rounded-full bg-neutral-100 p-1 dark:bg-neutral-900">
<User.Avatar className="size-6 rounded-full" />
<User.Name className="pr-2 text-sm font-medium" />
</User.Root>
</User.Provider>
</div>
<div className="flex flex-1 flex-col justify-between px-5">
<div className="relative flex flex-1 flex-col pb-8">
<div className="inline-flex h-full flex-1 items-center justify-center gap-1">
<CurrencyInput
placeholder="0"
defaultValue={21}
value={amount}
decimalsLimit={2}
min={0} // 0 sats
max={10000} // 1M sats
maxLength={10000} // 1M sats
onValueChange={(value) => setAmount(Number(value))}
className="w-full flex-1 border-none bg-transparent text-right text-4xl font-semibold placeholder:text-neutral-600 focus:outline-none focus:ring-0 dark:text-neutral-400"
/>
<span className="w-full flex-1 text-left text-4xl font-semibold text-neutral-500 dark:text-neutral-400">
sats
</span>
</div>
<div className="inline-flex items-center justify-center gap-2">
{DEFAULT_VALUES.map((value) => (
<button
key={value}
type="button"
onClick={() => setAmount(value)}
className="w-max rounded-full bg-neutral-100 px-2.5 py-1 text-sm font-medium hover:bg-neutral-200 dark:bg-neutral-900 dark:hover:bg-neutral-800"
>
{value} sats
</button>
))}
</div>
</div>
<div className="flex w-full flex-col gap-2">
<input
name="message"
value={message}
onChange={(e) => setMessage(e.target.value)}
spellCheck={false}
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
placeholder={t("note.zap.messagePlaceholder")}
className="h-11 w-full resize-none rounded-lg border-transparent bg-neutral-100 px-3 !outline-none placeholder:text-neutral-600 focus:border-blue-500 focus:ring focus:ring-blue-200 dark:bg-neutral-950 dark:text-neutral-400"
/>
<div className="flex flex-col gap-2">
<button
type="button"
onClick={() => submit()}
className="inline-flex h-9 w-full items-center justify-center rounded-lg border-t border-neutral-900 bg-neutral-950 pb-[2px] font-semibold text-neutral-50 hover:bg-neutral-900 dark:border-neutral-800 dark:bg-neutral-900 dark:hover:bg-neutral-800"
>
{isCompleted
? t("note.zap.buttonFinish")
: isLoading
? t("note.zap.buttonLoading")
: t("note.zap.zap")}
</button>
</div>
</div>
</div>
</div>
</Box>
</Container>
);
}

View File

@@ -0,0 +1,119 @@
import { User } from "@/components/user";
import { NostrQuery } from "@lume/system";
import { createFileRoute } from "@tanstack/react-router";
import { getCurrent } from "@tauri-apps/api/webviewWindow";
import { message } from "@tauri-apps/plugin-dialog";
import { useState } from "react";
import CurrencyInput from "react-currency-input-field";
const DEFAULT_VALUES = [21, 50, 100, 200];
export const Route = createFileRoute("/zap/$id")({
beforeLoad: async ({ params }) => {
const event = await NostrQuery.getEvent(params.id);
return { event };
},
component: Screen,
});
function Screen() {
const { event } = Route.useRouteContext();
const [amount, setAmount] = useState(21);
const [content, setContent] = useState("");
const [isCompleted, setIsCompleted] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const submit = async () => {
try {
// start loading
setIsLoading(true);
// Zap
const val = await event.zap(amount, content);
if (val) {
setIsCompleted(true);
// close current window
await getCurrent().close();
}
} catch (e) {
setIsLoading(false);
await message(String(e), {
title: "Zap",
kind: "error",
});
}
};
return (
<div data-tauri-drag-region className="flex flex-col pb-5 size-full">
<div
data-tauri-drag-region
className="flex items-center justify-center h-24 gap-2 shrink-0"
>
<p className="text-sm">Send zap to </p>
<User.Provider pubkey={event.pubkey}>
<User.Root className="inline-flex items-center gap-2 p-1 rounded-full bg-black/5 dark:bg-white/5">
<User.Avatar className="rounded-full size-6" />
<User.Name className="pr-2 text-sm font-medium" />
</User.Root>
</User.Provider>
</div>
<div className="flex flex-col justify-between h-full">
<div className="flex flex-col justify-between flex-1 px-5">
<div className="relative flex flex-col flex-1 pb-8">
<div className="inline-flex items-center justify-center flex-1 h-full gap-1">
<CurrencyInput
placeholder="0"
defaultValue={21}
value={amount}
decimalsLimit={2}
min={0} // 0 sats
max={10000} // 1M sats
maxLength={10000} // 1M sats
onValueChange={(value) => setAmount(Number(value))}
className="flex-1 w-full text-4xl font-semibold text-right bg-transparent border-none placeholder:text-neutral-600 focus:outline-none focus:ring-0 dark:text-neutral-400"
/>
<span className="flex-1 w-full text-4xl font-semibold text-left text-neutral-500 dark:text-neutral-400">
sats
</span>
</div>
<div className="inline-flex items-center justify-center gap-2">
{DEFAULT_VALUES.map((value) => (
<button
key={value}
type="button"
onClick={() => setAmount(value)}
className="w-max rounded-full bg-black/10 px-2.5 py-1 text-xs font-medium hover:bg-black/20 dark:bg-white/10 dark:hover:bg-white/20"
>
{value} sats
</button>
))}
</div>
</div>
<div className="flex flex-col w-full gap-2">
<input
name="message"
value={content}
onChange={(e) => setContent(e.target.value)}
spellCheck={false}
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
placeholder="Enter message (optional)"
className="h-11 w-full resize-none rounded-xl border-transparent bg-black/5 px-3 !outline-none placeholder:text-neutral-600 focus:border-blue-500 focus:ring-0 dark:bg-white/5"
/>
<button
type="button"
onClick={() => submit()}
className="inline-flex items-center justify-center w-full h-10 font-medium rounded-xl bg-neutral-950 text-neutral-50 hover:bg-neutral-900 dark:bg-white/20 dark:hover:bg-white/30"
>
{isCompleted ? "Zapped" : isLoading ? "Processing..." : "Zap"}
</button>
</div>
</div>
</div>
</div>
);
}

View File

@@ -1,6 +1,6 @@
import type { Metadata } from "@lume/types"; import type { Metadata } from "@lume/types";
import { type Result, commands } from "./commands";
import { Window } from "@tauri-apps/api/window"; import { Window } from "@tauri-apps/api/window";
import { type Result, commands } from "./commands";
export class NostrAccount { export class NostrAccount {
static async getAccounts() { static async getAccounts() {
@@ -99,8 +99,28 @@ export class NostrAccount {
} }
} }
static async loadWallet() {
const query = await commands.loadWallet();
if (query.status === "ok") {
return Number.parseInt(query.data);
} else {
throw new Error(query.error);
}
}
static async setWallet(uri: string) { static async setWallet(uri: string) {
const query = await commands.setNwc(uri); const query = await commands.setWallet(uri);
if (query.status === "ok") {
return query.data;
} else {
throw new Error(query.error);
}
}
static async removeWallet() {
const query = await commands.removeWallet();
if (query.status === "ok") { if (query.status === "ok") {
return query.data; return query.data;
@@ -110,7 +130,7 @@ export class NostrAccount {
} }
static async getProfile() { static async getProfile() {
const query = await commands.getCurrentUserProfile(); const query = await commands.getCurrentProfile();
if (query.status === "ok") { if (query.status === "ok") {
return JSON.parse(query.data) as Metadata; return JSON.parse(query.data) as Metadata;
@@ -119,16 +139,6 @@ export class NostrAccount {
} }
} }
static async getBalance() {
const query = await commands.getBalance();
if (query.status === "ok") {
return Number.parseInt(query.data);
} else {
return 0;
}
}
static async getContactList() { static async getContactList() {
const query = await commands.getContactList(); const query = await commands.getContactList();

View File

@@ -180,25 +180,25 @@ try {
else return { status: "error", error: e as any }; else return { status: "error", error: e as any };
} }
}, },
async setNwc(uri: string) : Promise<Result<boolean, string>> { async setWallet(uri: string) : Promise<Result<boolean, string>> {
try { try {
return { status: "ok", data: await TAURI_INVOKE("set_nwc", { uri }) }; return { status: "ok", data: await TAURI_INVOKE("set_wallet", { uri }) };
} catch (e) { } catch (e) {
if(e instanceof Error) throw e; if(e instanceof Error) throw e;
else return { status: "error", error: e as any }; else return { status: "error", error: e as any };
} }
}, },
async loadNwc() : Promise<Result<boolean, string>> { async loadWallet() : Promise<Result<string, string>> {
try { try {
return { status: "ok", data: await TAURI_INVOKE("load_nwc") }; return { status: "ok", data: await TAURI_INVOKE("load_wallet") };
} catch (e) { } catch (e) {
if(e instanceof Error) throw e; if(e instanceof Error) throw e;
else return { status: "error", error: e as any }; else return { status: "error", error: e as any };
} }
}, },
async getBalance() : Promise<Result<string, string>> { async removeWallet() : Promise<Result<null, null>> {
try { try {
return { status: "ok", data: await TAURI_INVOKE("get_balance") }; return { status: "ok", data: await TAURI_INVOKE("remove_wallet") };
} catch (e) { } catch (e) {
if(e instanceof Error) throw e; if(e instanceof Error) throw e;
else return { status: "error", error: e as any }; else return { status: "error", error: e as any };
@@ -407,9 +407,9 @@ try {
else return { status: "error", error: e as any }; else return { status: "error", error: e as any };
} }
}, },
async openWindow(label: string, title: string, url: string, width: number, height: number) : Promise<Result<null, string>> { async openWindow(window: Window) : Promise<Result<null, string>> {
try { try {
return { status: "ok", data: await TAURI_INVOKE("open_window", { label, title, url, width, height }) }; return { status: "ok", data: await TAURI_INVOKE("open_window", { window }) };
} catch (e) { } catch (e) {
if(e instanceof Error) throw e; if(e instanceof Error) throw e;
else return { status: "error", error: e as any }; else return { status: "error", error: e as any };
@@ -438,6 +438,7 @@ export type Meta = { content: string; images: string[]; videos: string[]; events
export type Relays = { connected: string[]; read: string[] | null; write: string[] | null; both: string[] | null } export type Relays = { connected: string[]; read: string[] | null; write: string[] | null; both: string[] | null }
export type RichEvent = { raw: string; parsed: Meta | null } export type RichEvent = { raw: string; parsed: Meta | null }
export type Settings = { proxy: string | null; image_resize_service: string | null; use_relay_hint: boolean; content_warning: boolean; display_avatar: boolean; display_zap_button: boolean; display_repost_button: boolean; display_media: boolean } export type Settings = { proxy: string | null; image_resize_service: string | null; use_relay_hint: boolean; content_warning: boolean; display_avatar: boolean; display_zap_button: boolean; display_repost_button: boolean; display_media: boolean }
export type Window = { label: string; title: string; url: string; width: number; height: number; maximizable: boolean; minimizable: boolean }
/** tauri-specta globals **/ /** tauri-specta globals **/

View File

@@ -25,11 +25,6 @@ export class LumeEvent {
Object.assign(this, event); Object.assign(this, event);
} }
get isWarning() {
const tag = this.tags.find((tag) => tag[0] === "content-warning");
return tag?.[1]; // return: reason;
}
get isQuote() { get isQuote() {
return this.tags.filter((tag) => tag[0] === "q").length > 0; return this.tags.filter((tag) => tag[0] === "q").length > 0;
} }
@@ -95,6 +90,26 @@ export class LumeEvent {
return { id, relayHint }; return { id, relayHint };
} }
get warning() {
const warningTag = this.tags.filter(
(tag) => tag[0] === "content-warning",
)?.[0];
if (warningTag) {
return warningTag[1];
} else {
const nsfwTag = this.tags.filter(
(tag) => tag[0] === "t" && tag[1] === "NSFW",
)?.[0];
if (nsfwTag) {
return "NSFW";
} else {
return null;
}
}
}
public async getAllReplies() { public async getAllReplies() {
const query = await commands.getReplies(this.id); const query = await commands.getReplies(this.id);

View File

@@ -18,7 +18,15 @@ export class LumeWindow {
const label = `event-${event.id}`; const label = `event-${event.id}`;
const url = `/events/${root ?? reply ?? event.id}`; const url = `/events/${root ?? reply ?? event.id}`;
const query = await commands.openWindow(label, "Thread", url, 500, 800); const query = await commands.openWindow({
label,
url,
title: "Thread",
width: 500,
height: 800,
maximizable: true,
minimizable: true,
});
if (query.status === "ok") { if (query.status === "ok") {
return query.data; return query.data;
@@ -29,13 +37,15 @@ export class LumeWindow {
static async openProfile(pubkey: string) { static async openProfile(pubkey: string) {
const label = `user-${pubkey}`; const label = `user-${pubkey}`;
const query = await commands.openWindow( const query = await commands.openWindow({
label, label,
"Profile", url: `/users/${pubkey}`,
`/users/${pubkey}`, title: "Profile",
500, width: 500,
800, height: 800,
); maximizable: true,
minimizable: true,
});
if (query.status === "ok") { if (query.status === "ok") {
return query.data; return query.data;
@@ -60,7 +70,15 @@ export class LumeWindow {
} }
const label = `editor-${reply_to ? reply_to : 0}`; const label = `editor-${reply_to ? reply_to : 0}`;
const query = await commands.openWindow(label, "Editor", url, 560, 340); const query = await commands.openWindow({
label,
url,
title: "Editor",
width: 560,
height: 340,
maximizable: true,
minimizable: false,
});
if (query.status === "ok") { if (query.status === "ok") {
return query.data; return query.data;
@@ -69,45 +87,35 @@ export class LumeWindow {
} }
} }
static async openZap(id: string, pubkey: string) { static async openZap(id: string) {
const nwc = await commands.loadNwc(); const wallet = await commands.loadWallet();
if (nwc.status === "ok") { if (wallet.status === "ok") {
const status = nwc.data; await commands.openWindow({
label: `zap-${id}`,
if (!status) { url: `/zap/${id}`,
const label = "nwc"; title: "Zap",
await commands.openWindow( width: 360,
label, height: 460,
"Nostr Wallet Connect", maximizable: false,
"/nwc", minimizable: false,
400, });
600,
);
} else {
const label = `zap-${id}`;
await commands.openWindow(
label,
"Zap",
`/zap/${id}?pubkey=${pubkey}`,
400,
500,
);
}
} else { } else {
throw new Error(nwc.error); await LumeWindow.openSettings("bitcoin-connect");
} }
} }
static async openSettings() { static async openSettings(path?: string) {
const label = "settings"; const label = "settings";
const query = await commands.openWindow( const query = await commands.openWindow({
label, label,
"Settings", url: path ? `/settings/${path}` : "/settings/general",
"/settings/general", title: "Settings",
800, width: 800,
500, height: 500,
); maximizable: false,
minimizable: false,
});
if (query.status === "ok") { if (query.status === "ok") {
return query.data; return query.data;
@@ -118,30 +126,15 @@ export class LumeWindow {
static async openSearch() { static async openSearch() {
const label = "search"; const label = "search";
const query = await commands.openWindow( const query = await commands.openWindow({
label, label,
"Search", url: "/search",
"/search", title: "Search",
400, width: 400,
600, height: 600,
); maximizable: false,
minimizable: false,
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") { if (query.status === "ok") {
return query.data; return query.data;

View File

@@ -81,7 +81,7 @@ export function displayLongHandle(str: string) {
const handle = split[0]; const handle = split[0];
const service = split[1]; const service = split[1];
return handle.substring(0, 16) + "..." + "@" + service; return `${handle.substring(0, 16)}...@${service}`;
} }
// convert number to K, M, B, T, etc. // convert number to K, M, B, T, etc.
@@ -127,7 +127,7 @@ export function getBitcoinDisplayValues(satoshis: number) {
.reverse() .reverse()
.forEach((c, index) => { .forEach((c, index) => {
if (index > 0 && index % 3 === 0) { if (index > 0 && index % 3 === 0) {
res = " " + res; res = ` ${res}`;
} }
res = c + res; res = c + res;
}); });

308
pnpm-lock.yaml generated
View File

@@ -54,6 +54,9 @@ importers:
apps/desktop2: apps/desktop2:
dependencies: dependencies:
'@getalby/bitcoin-connect-react':
specifier: ^3.5.3
version: 3.5.3(@types/react@18.3.3)(react@18.3.1)(typescript@5.4.5)
'@lume/icons': '@lume/icons':
specifier: workspace:^ specifier: workspace:^
version: link:../../packages/icons version: link:../../packages/icons
@@ -72,18 +75,6 @@ importers:
'@radix-ui/react-checkbox': '@radix-ui/react-checkbox':
specifier: ^1.0.4 specifier: ^1.0.4
version: 1.0.4(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1) version: 1.0.4(@types/react-dom@18.3.0)(@types/react@18.3.3)(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.3)(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.3)(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.3)(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.3)(react-dom@18.3.1)(react@18.3.1)
'@radix-ui/react-popover': '@radix-ui/react-popover':
specifier: ^1.0.7 specifier: ^1.0.7
version: 1.0.7(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1) version: 1.0.7(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1)
@@ -138,9 +129,6 @@ importers:
react-hook-form: react-hook-form:
specifier: ^7.52.0 specifier: ^7.52.0
version: 7.52.0(react@18.3.1) version: 7.52.0(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: react-i18next:
specifier: ^14.1.2 specifier: ^14.1.2
version: 14.1.2(i18next@23.11.5)(react-dom@18.3.1)(react@18.3.1) version: 14.1.2(i18next@23.11.5)(react-dom@18.3.1)(react@18.3.1)
@@ -153,9 +141,6 @@ importers:
slate-react: slate-react:
specifier: ^0.105.0 specifier: ^0.105.0
version: 0.105.0(react-dom@18.3.1)(react@18.3.1)(slate@0.103.0) version: 0.105.0(react-dom@18.3.1)(react@18.3.1)(slate@0.103.0)
sonner:
specifier: ^1.5.0
version: 1.5.0(react-dom@18.3.1)(react@18.3.1)
use-debounce: use-debounce:
specifier: ^10.0.1 specifier: ^10.0.1
version: 10.0.1(react@18.3.1) version: 10.0.1(react@18.3.1)
@@ -1214,6 +1199,49 @@ 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/bitcoin-connect-react@3.5.3(@types/react@18.3.3)(react@18.3.1)(typescript@5.4.5):
resolution: {integrity: sha512-/oAPFFva/T946JzuNv6X/AuCGb46co2rLxfiINy4am/jFN+mAZ1HNGjOycTodpsTXnNpr3Ih37wV+YuI/03ueQ==}
peerDependencies:
react: ^18.2.0
dependencies:
'@getalby/bitcoin-connect': 3.5.3(@types/react@18.3.3)(react@18.3.1)(typescript@5.4.5)
react: 18.3.1
transitivePeerDependencies:
- '@types/react'
- immer
- typescript
dev: false
/@getalby/bitcoin-connect@3.5.3(@types/react@18.3.3)(react@18.3.1)(typescript@5.4.5):
resolution: {integrity: sha512-csVNT4gXzuJtXP3ZyXnNnSGpt4JEQJvynLhc/aG3VNZVqPhNgot5Npj1J4XIewrsn4sWVIr9WkAtCxGfQ6XmSQ==}
dependencies:
'@getalby/lightning-tools': 5.0.3
'@getalby/sdk': 3.5.1(typescript@5.4.5)
'@lightninglabs/lnc-web': 0.3.1-alpha
qrcode-generator: 1.4.4
zustand: 4.5.2(@types/react@18.3.3)(react@18.3.1)
transitivePeerDependencies:
- '@types/react'
- immer
- react
- typescript
dev: false
/@getalby/lightning-tools@5.0.3:
resolution: {integrity: sha512-QG3/SBI5n2py5IgsjP3K+c8eq55eiI3PQB12yo9Pot0b5hcN7TNNoTKn0fgLJjO1iEVCUkF513kDOpjjXwK0hQ==}
engines: {node: '>=14'}
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.4: /@img/sharp-darwin-arm64@0.33.4:
resolution: {integrity: sha512-p0suNqXufJs9t3RqLBO6vvrgr5OhgbWp76s5gTRvdmxmuv9E1rcaqGUsl3l4mKVmXPkTkTErXediAui4x+8PSA==} resolution: {integrity: sha512-p0suNqXufJs9t3RqLBO6vvrgr5OhgbWp76s5gTRvdmxmuv9E1rcaqGUsl3l4mKVmXPkTkTErXediAui4x+8PSA==}
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'}
@@ -1451,6 +1479,21 @@ packages:
resolution: {integrity: sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA==} resolution: {integrity: sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA==}
dev: false dev: false
/@lightninglabs/lnc-core@0.3.1-alpha:
resolution: {integrity: sha512-I/hThdItLWJ6RU8Z27ZIXhpBS2JJuD3+TjtaQXX2CabaUYXlcN4sk+Kx8N/zG/fk8qZvjlRWum4vHu4ZX554Fg==}
dev: false
/@lightninglabs/lnc-web@0.3.1-alpha:
resolution: {integrity: sha512-yL5SgBkl6kd6ISzJHGlSN7TXbiDoo1pfGvTOIdVWYVyXtEeW8PT+x6YGOmyQXGFT2OOf7fC7PfP9VnskDPuFaA==}
dependencies:
'@lightninglabs/lnc-core': 0.3.1-alpha
crypto-js: 4.2.0
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
@@ -1588,34 +1631,6 @@ packages:
react-dom: 18.3.1(react@18.3.1) react-dom: 18.3.1(react@18.3.1)
dev: false dev: false
/@radix-ui/react-collapsible@1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1):
resolution: {integrity: sha512-UBmVDkmR6IvDsloHVN+3rtx4Mi5TFvylYXpluuv0f37dtaz3H99bp8No0LGXRigVpl3UAT4l9j6bIchh42S/Gg==}
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.7
'@radix-ui/primitive': 1.0.1
'@radix-ui/react-compose-refs': 1.0.1(@types/react@18.3.3)(react@18.3.1)
'@radix-ui/react-context': 1.0.1(@types/react@18.3.3)(react@18.3.1)
'@radix-ui/react-id': 1.0.1(@types/react@18.3.3)(react@18.3.1)
'@radix-ui/react-presence': 1.0.1(@types/react-dom@18.3.0)(@types/react@18.3.3)(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.3)(react-dom@18.3.1)(react@18.3.1)
'@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.3.3)(react@18.3.1)
'@radix-ui/react-use-layout-effect': 1.0.1(@types/react@18.3.3)(react@18.3.1)
'@types/react': 18.3.3
'@types/react-dom': 18.3.0
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
dev: false
/@radix-ui/react-collection@1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(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.3)(react-dom@18.3.1)(react@18.3.1):
resolution: {integrity: sha512-3SzW+0PW7yBBoQlT8wNcGtaxaD0XSu0uLUFgrtHY08Acx05TaHaOmVLR73c0j/cqpDy53KBMO7s0dx2wmOIDIA==} resolution: {integrity: sha512-3SzW+0PW7yBBoQlT8wNcGtaxaD0XSu0uLUFgrtHY08Acx05TaHaOmVLR73c0j/cqpDy53KBMO7s0dx2wmOIDIA==}
peerDependencies: peerDependencies:
@@ -1694,40 +1709,6 @@ packages:
react: 18.3.1 react: 18.3.1
dev: false dev: false
/@radix-ui/react-dialog@1.0.5(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1):
resolution: {integrity: sha512-GjWJX/AUpB703eEBanuBnIWdIXg6NvJFCXcNlSZk4xdszCdhrJgBoUd1cGk67vFO+WdA2pfI/plOpqz/5GUP6Q==}
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.7
'@radix-ui/primitive': 1.0.1
'@radix-ui/react-compose-refs': 1.0.1(@types/react@18.3.3)(react@18.3.1)
'@radix-ui/react-context': 1.0.1(@types/react@18.3.3)(react@18.3.1)
'@radix-ui/react-dismissable-layer': 1.0.5(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1)
'@radix-ui/react-focus-guards': 1.0.1(@types/react@18.3.3)(react@18.3.1)
'@radix-ui/react-focus-scope': 1.0.4(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1)
'@radix-ui/react-id': 1.0.1(@types/react@18.3.3)(react@18.3.1)
'@radix-ui/react-portal': 1.0.4(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1)
'@radix-ui/react-presence': 1.0.1(@types/react-dom@18.3.0)(@types/react@18.3.3)(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.3)(react-dom@18.3.1)(react@18.3.1)
'@radix-ui/react-slot': 1.0.2(@types/react@18.3.3)(react@18.3.1)
'@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.3.3)(react@18.3.1)
'@types/react': 18.3.3
'@types/react-dom': 18.3.0
aria-hidden: 1.2.4
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
react-remove-scroll: 2.5.5(@types/react@18.3.3)(react@18.3.1)
dev: false
/@radix-ui/react-direction@1.0.1(@types/react@18.3.3)(react@18.3.1): /@radix-ui/react-direction@1.0.1(@types/react@18.3.3)(react@18.3.1):
resolution: {integrity: sha512-RXcvnXgyvYvBEOhCBuddKecVkoMiI10Jcm5cTI7abJRAHYfFxeu+FBQs/DvdxSYucxR5mna0dNsL6QFlds5TMA==} resolution: {integrity: sha512-RXcvnXgyvYvBEOhCBuddKecVkoMiI10Jcm5cTI7abJRAHYfFxeu+FBQs/DvdxSYucxR5mna0dNsL6QFlds5TMA==}
peerDependencies: peerDependencies:
@@ -1780,33 +1761,6 @@ packages:
react-dom: 18.3.1(react@18.3.1) react-dom: 18.3.1(react@18.3.1)
dev: false dev: false
/@radix-ui/react-dropdown-menu@2.0.6(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1):
resolution: {integrity: sha512-i6TuFOoWmLWq+M/eCLGd/bQ2HfAX1RJgvrBQ6AQLmzfvsLdefxbWu8G9zczcPFfcSPehz9GcpF6K9QYreFV8hA==}
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.7
'@radix-ui/primitive': 1.0.1
'@radix-ui/react-compose-refs': 1.0.1(@types/react@18.3.3)(react@18.3.1)
'@radix-ui/react-context': 1.0.1(@types/react@18.3.3)(react@18.3.1)
'@radix-ui/react-id': 1.0.1(@types/react@18.3.3)(react@18.3.1)
'@radix-ui/react-menu': 2.0.6(@types/react-dom@18.3.0)(@types/react@18.3.3)(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.3)(react-dom@18.3.1)(react@18.3.1)
'@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.3.3)(react@18.3.1)
'@types/react': 18.3.3
'@types/react-dom': 18.3.0
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
dev: false
/@radix-ui/react-focus-guards@1.0.1(@types/react@18.3.3)(react@18.3.1): /@radix-ui/react-focus-guards@1.0.1(@types/react@18.3.3)(react@18.3.1):
resolution: {integrity: sha512-Rect2dWbQ8waGzhMavsIbmSVCgYxkXLxxR3ZvCX79JOglzdEy4JXMb98lq4hPxUbLr77nP0UOGf4rcMU+s1pUA==} resolution: {integrity: sha512-Rect2dWbQ8waGzhMavsIbmSVCgYxkXLxxR3ZvCX79JOglzdEy4JXMb98lq4hPxUbLr77nP0UOGf4rcMU+s1pUA==}
peerDependencies: peerDependencies:
@@ -1844,35 +1798,6 @@ packages:
react-dom: 18.3.1(react@18.3.1) react-dom: 18.3.1(react@18.3.1)
dev: false dev: false
/@radix-ui/react-hover-card@1.0.7(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1):
resolution: {integrity: sha512-OcUN2FU0YpmajD/qkph3XzMcK/NmSk9hGWnjV68p6QiZMgILugusgQwnLSDs3oFSJYGKf3Y49zgFedhGh04k9A==}
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.7
'@radix-ui/primitive': 1.0.1
'@radix-ui/react-compose-refs': 1.0.1(@types/react@18.3.3)(react@18.3.1)
'@radix-ui/react-context': 1.0.1(@types/react@18.3.3)(react@18.3.1)
'@radix-ui/react-dismissable-layer': 1.0.5(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1)
'@radix-ui/react-popper': 1.1.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1)
'@radix-ui/react-portal': 1.0.4(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1)
'@radix-ui/react-presence': 1.0.1(@types/react-dom@18.3.0)(@types/react@18.3.3)(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.3)(react-dom@18.3.1)(react@18.3.1)
'@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.3.3)(react@18.3.1)
'@types/react': 18.3.3
'@types/react-dom': 18.3.0
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
dev: false
/@radix-ui/react-id@1.0.1(@types/react@18.3.3)(react@18.3.1): /@radix-ui/react-id@1.0.1(@types/react@18.3.3)(react@18.3.1):
resolution: {integrity: sha512-tI7sT/kqYp8p96yGWY1OAnLHrqDgzHefRBKQ2YAkBS5ja7QLcZ9Z/uY7bEjPUatf8RomoXM8/1sMj1IJaE5UzQ==} resolution: {integrity: sha512-tI7sT/kqYp8p96yGWY1OAnLHrqDgzHefRBKQ2YAkBS5ja7QLcZ9Z/uY7bEjPUatf8RomoXM8/1sMj1IJaE5UzQ==}
peerDependencies: peerDependencies:
@@ -1888,44 +1813,6 @@ packages:
react: 18.3.1 react: 18.3.1
dev: false dev: false
/@radix-ui/react-menu@2.0.6(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1):
resolution: {integrity: sha512-BVkFLS+bUC8HcImkRKPSiVumA1VPOOEC5WBMiT+QAVsPzW1FJzI9KnqgGxVDPBcql5xXrHkD3JOVoXWEXD8SYA==}
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.7
'@radix-ui/primitive': 1.0.1
'@radix-ui/react-collection': 1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1)
'@radix-ui/react-compose-refs': 1.0.1(@types/react@18.3.3)(react@18.3.1)
'@radix-ui/react-context': 1.0.1(@types/react@18.3.3)(react@18.3.1)
'@radix-ui/react-direction': 1.0.1(@types/react@18.3.3)(react@18.3.1)
'@radix-ui/react-dismissable-layer': 1.0.5(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1)
'@radix-ui/react-focus-guards': 1.0.1(@types/react@18.3.3)(react@18.3.1)
'@radix-ui/react-focus-scope': 1.0.4(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1)
'@radix-ui/react-id': 1.0.1(@types/react@18.3.3)(react@18.3.1)
'@radix-ui/react-popper': 1.1.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1)
'@radix-ui/react-portal': 1.0.4(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1)
'@radix-ui/react-presence': 1.0.1(@types/react-dom@18.3.0)(@types/react@18.3.3)(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.3)(react-dom@18.3.1)(react@18.3.1)
'@radix-ui/react-roving-focus': 1.0.4(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1)
'@radix-ui/react-slot': 1.0.2(@types/react@18.3.3)(react@18.3.1)
'@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.3.3)(react@18.3.1)
'@types/react': 18.3.3
'@types/react-dom': 18.3.0
aria-hidden: 1.2.4
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
react-remove-scroll: 2.5.5(@types/react@18.3.3)(react@18.3.1)
dev: false
/@radix-ui/react-popover@1.0.7(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1): /@radix-ui/react-popover@1.0.7(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1):
resolution: {integrity: sha512-shtvVnlsxT6faMnK/a7n0wptwBD23xc1Z5mdrtKLwVEfsEMXodS0r5s0/g5P0hX//EKYZS2sxUjqfzlg52ZSnQ==} resolution: {integrity: sha512-shtvVnlsxT6faMnK/a7n0wptwBD23xc1Z5mdrtKLwVEfsEMXodS0r5s0/g5P0hX//EKYZS2sxUjqfzlg52ZSnQ==}
peerDependencies: peerDependencies:
@@ -3608,6 +3495,10 @@ packages:
shebang-command: 2.0.0 shebang-command: 2.0.0
which: 2.0.2 which: 2.0.2
/crypto-js@4.2.0:
resolution: {integrity: sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==}
dev: false
/cssesc@3.0.0: /cssesc@3.0.0:
resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==}
engines: {node: '>=4'} engines: {node: '>=4'}
@@ -4894,6 +4785,23 @@ 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'}
/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.7.0(typescript@5.4.5): /nostr-tools@2.7.0(typescript@5.4.5):
resolution: {integrity: sha512-jJoL2J1CBiKDxaXZww27nY/Wsuxzx7AULxmGKFce4sskDu1tohNyfnzYQ8BvDyvkstU8kNZUAXPL32tre33uig==} resolution: {integrity: sha512-jJoL2J1CBiKDxaXZww27nY/Wsuxzx7AULxmGKFce4sskDu1tohNyfnzYQ8BvDyvkstU8kNZUAXPL32tre33uig==}
peerDependencies: peerDependencies:
@@ -5193,6 +5101,10 @@ packages:
resolution: {integrity: sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==} resolution: {integrity: sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==}
dev: false dev: false
/qrcode-generator@1.4.4:
resolution: {integrity: sha512-HM7yY8O2ilqhmULxGMpcHSF1EhJJ9yBj8gvDEuZ6M+KGJ0YY2hKpnXvRD+hZPLrDVck3ExIGhmPtSdcjC+guuw==}
dev: false
/queue-microtask@1.2.3: /queue-microtask@1.2.3:
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
@@ -5222,16 +5134,6 @@ packages:
react: 18.3.1 react: 18.3.1
dev: false dev: false
/react-hotkeys-hook@4.5.0(react-dom@18.3.1)(react@18.3.1):
resolution: {integrity: sha512-Samb85GSgAWFQNvVt3PS90LPPGSf9mkH/r4au81ZP1yOIFayLC3QAvqTgGtJ8YEDMXtPmaVBs6NgipHO6h4Mug==}
peerDependencies:
react: '>=16.8.1'
react-dom: '>=16.8.1'
dependencies:
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
dev: false
/react-i18next@14.1.2(i18next@23.11.5)(react-dom@18.3.1)(react@18.3.1): /react-i18next@14.1.2(i18next@23.11.5)(react-dom@18.3.1)(react@18.3.1):
resolution: {integrity: sha512-FSIcJy6oauJbGEXfhUgVeLzvWBhIBIS+/9c6Lj4niwKZyGaGb4V4vUbATXSlsHJDXXB+ociNxqFNiFuV1gmoqg==} resolution: {integrity: sha512-FSIcJy6oauJbGEXfhUgVeLzvWBhIBIS+/9c6Lj4niwKZyGaGb4V4vUbATXSlsHJDXXB+ociNxqFNiFuV1gmoqg==}
peerDependencies: peerDependencies:
@@ -5653,16 +5555,6 @@ packages:
tiny-warning: 1.0.3 tiny-warning: 1.0.3
dev: false dev: false
/sonner@1.5.0(react-dom@18.3.1)(react@18.3.1):
resolution: {integrity: sha512-FBjhG/gnnbN6FY0jaNnqZOMmB73R+5IiyYAw8yBj7L54ER7HB3fOSE5OFiQiE2iXWxeXKvg6fIP4LtVppHEdJA==}
peerDependencies:
react: ^18.0.0
react-dom: ^18.0.0
dependencies:
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
dev: false
/source-map-js@1.2.0: /source-map-js@1.2.0:
resolution: {integrity: sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==} resolution: {integrity: sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
@@ -6115,6 +6007,14 @@ packages:
tslib: 2.6.3 tslib: 2.6.3
dev: false dev: false
/use-sync-external-store@1.2.0(react@18.3.1):
resolution: {integrity: sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==}
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0
dependencies:
react: 18.3.1
dev: false
/use-sync-external-store@1.2.2(react@18.3.1): /use-sync-external-store@1.2.2(react@18.3.1):
resolution: {integrity: sha512-PElTlVMwpblvbNqQ82d2n6RjStvdSoNe9FG28kNfz3WiXilJm4DdNkEzRhCZuIDwY8U08WVihhGR5iRqAwfDiw==} resolution: {integrity: sha512-PElTlVMwpblvbNqQ82d2n6RjStvdSoNe9FG28kNfz3WiXilJm4DdNkEzRhCZuIDwY8U08WVihhGR5iRqAwfDiw==}
peerDependencies: peerDependencies:
@@ -6521,6 +6421,26 @@ packages:
/zod@3.23.8: /zod@3.23.8:
resolution: {integrity: sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==} resolution: {integrity: sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==}
/zustand@4.5.2(@types/react@18.3.3)(react@18.3.1):
resolution: {integrity: sha512-2cN1tPkDVkwCy5ickKrI7vijSjPksFRfqS6237NzT0vqSsztTNnQdHw9mmN7uBdk3gceVXU0a+21jFzFzAc9+g==}
engines: {node: '>=12.7.0'}
peerDependencies:
'@types/react': '>=16.8'
immer: '>=9.0.6'
react: '>=16.8'
peerDependenciesMeta:
'@types/react':
optional: true
immer:
optional: true
react:
optional: true
dependencies:
'@types/react': 18.3.3
react: 18.3.1
use-sync-external-store: 1.2.0(react@18.3.1)
dev: false
/zwitch@2.0.4: /zwitch@2.0.4:
resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==}
dev: false dev: false

View File

@@ -3,6 +3,8 @@ use std::str::FromStr;
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
use cocoa::{appkit::NSApp, base::nil, foundation::NSString}; use cocoa::{appkit::NSApp, base::nil, foundation::NSString};
use serde::{Deserialize, Serialize};
use specta::Type;
use tauri::{LogicalPosition, LogicalSize, Manager, State, WebviewUrl}; use tauri::{LogicalPosition, LogicalSize, Manager, State, WebviewUrl};
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
use tauri::TitleBarStyle; use tauri::TitleBarStyle;
@@ -14,6 +16,17 @@ use url::Url;
use crate::Nostr; use crate::Nostr;
#[derive(Serialize, Deserialize, Type)]
pub struct Window {
label: String,
title: String,
url: String,
width: f64,
height: f64,
maximizable: bool,
minimizable: bool,
}
#[tauri::command] #[tauri::command]
#[specta::specta] #[specta::specta]
pub fn create_column( pub fn create_column(
@@ -121,15 +134,8 @@ pub fn resize_column(
#[tauri::command] #[tauri::command]
#[specta::specta] #[specta::specta]
pub fn open_window( pub fn open_window(window: Window, app_handle: tauri::AppHandle) -> Result<(), String> {
label: &str, if let Some(window) = app_handle.get_window(&window.label) {
title: &str,
url: &str,
width: f64,
height: f64,
app_handle: tauri::AppHandle,
) -> Result<(), String> {
if let Some(window) = app_handle.get_window(label) {
if window.is_visible().unwrap_or_default() { if window.is_visible().unwrap_or_default() {
let _ = window.set_focus(); let _ = window.set_focus();
} else { } else {
@@ -138,21 +144,27 @@ pub fn open_window(
}; };
} else { } else {
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
let window = WebviewWindowBuilder::new(&app_handle, label, WebviewUrl::App(PathBuf::from(url))) let window = WebviewWindowBuilder::new(
.title(title) &app_handle,
.min_inner_size(width, height) &window.label,
.inner_size(width, height) WebviewUrl::App(PathBuf::from(window.url)),
.hidden_title(true) )
.title_bar_style(TitleBarStyle::Overlay) .title(&window.title)
.transparent(true) .min_inner_size(window.width, window.height)
.effects(WindowEffectsConfig { .inner_size(window.width, window.height)
state: None, .hidden_title(true)
effects: vec![Effect::WindowBackground], .title_bar_style(TitleBarStyle::Overlay)
radius: None, .transparent(true)
color: None, .minimizable(window.minimizable)
}) .maximizable(window.maximizable)
.build() .effects(WindowEffectsConfig {
.unwrap(); state: None,
effects: vec![Effect::WindowBackground],
radius: None,
color: None,
})
.build()
.unwrap();
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
let window = WebviewWindowBuilder::new(&app_handle, label, WebviewUrl::App(PathBuf::from(url))) let window = WebviewWindowBuilder::new(&app_handle, label, WebviewUrl::App(PathBuf::from(url)))
@@ -180,10 +192,6 @@ pub fn open_window(
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
// Create a custom titlebar for Windows // Create a custom titlebar for Windows
window.create_overlay_titlebar().unwrap(); window.create_overlay_titlebar().unwrap();
// Set a custom inset to the traffic lights
#[cfg(target_os = "macos")]
window.set_traffic_lights_inset(8.0, 16.0).unwrap();
} }
Ok(()) Ok(())

View File

@@ -9,8 +9,8 @@ use tauri_nspanel::{
base::{id, nil}, base::{id, nil},
foundation::{NSPoint, NSRect}, foundation::{NSPoint, NSRect},
}, },
ManagerExt, objc::{class, msg_send, runtime::NO, sel, sel_impl},
objc::{class, msg_send, runtime::NO, sel, sel_impl}, panel_delegate, WebviewWindowExt, panel_delegate, ManagerExt, WebviewWindowExt,
}; };
#[allow(non_upper_case_globals)] #[allow(non_upper_case_globals)]
@@ -28,11 +28,8 @@ pub fn swizzle_to_menubar_panel(app_handle: &tauri::AppHandle) {
let handle = app_handle.clone(); let handle = app_handle.clone();
panel_delegate.set_listener(Box::new(move |delegate_name: String| { panel_delegate.set_listener(Box::new(move |delegate_name: String| {
match delegate_name.as_str() { if delegate_name.as_str() == "window_did_resign_key" {
"window_did_resign_key" => { let _ = handle.emit("menubar_panel_did_resign_key", ());
let _ = handle.emit("menubar_panel_did_resign_key", ());
}
_ => (),
} }
})); }));

View File

@@ -9,20 +9,20 @@ extern crate cocoa;
#[macro_use] #[macro_use]
extern crate objc; extern crate objc;
use std::sync::Mutex;
use std::time::Duration;
use std::{ use std::{
fs, fs,
io::{self, BufRead}, io::{self, BufRead},
str::FromStr, str::FromStr,
}; };
use std::sync::Mutex;
use std::time::Duration;
use nostr_sdk::prelude::*; use nostr_sdk::prelude::*;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use specta::Type; use specta::Type;
use tauri::{Manager, path::BaseDirectory};
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
use tauri::tray::{MouseButtonState, TrayIconEvent}; use tauri::tray::{MouseButtonState, TrayIconEvent};
use tauri::{path::BaseDirectory, Manager};
use tauri_nspanel::ManagerExt; use tauri_nspanel::ManagerExt;
use tauri_plugin_decorum::WebviewWindowExt; use tauri_plugin_decorum::WebviewWindowExt;
@@ -98,9 +98,9 @@ fn main() {
nostr::metadata::toggle_contact, nostr::metadata::toggle_contact,
nostr::metadata::get_nstore, nostr::metadata::get_nstore,
nostr::metadata::set_nstore, nostr::metadata::set_nstore,
nostr::metadata::set_nwc, nostr::metadata::set_wallet,
nostr::metadata::load_nwc, nostr::metadata::load_wallet,
nostr::metadata::get_balance, nostr::metadata::remove_wallet,
nostr::metadata::zap_profile, nostr::metadata::zap_profile,
nostr::metadata::zap_event, nostr::metadata::zap_event,
nostr::metadata::friend_to_friend, nostr::metadata::friend_to_friend,

View File

@@ -310,12 +310,12 @@ pub async fn get_nstore(key: &str, state: State<'_, Nostr>) -> Result<String, St
#[tauri::command] #[tauri::command]
#[specta::specta] #[specta::specta]
pub async fn set_nwc(uri: &str, state: State<'_, Nostr>) -> Result<bool, String> { pub async fn set_wallet(uri: &str, state: State<'_, Nostr>) -> Result<bool, String> {
let client = &state.client; let client = &state.client;
if let Ok(nwc_uri) = NostrWalletConnectURI::from_str(uri) { if let Ok(nwc_uri) = NostrWalletConnectURI::from_str(uri) {
let nwc = NWC::new(nwc_uri); let nwc = NWC::new(nwc_uri);
let keyring = Entry::new("Lume Secret Storage", "NWC").map_err(|e| e.to_string())?; let keyring = Entry::new("Lume Secret", "Bitcoin Connect").map_err(|e| e.to_string())?;
keyring.set_password(uri).map_err(|e| e.to_string())?; keyring.set_password(uri).map_err(|e| e.to_string())?;
client.set_zapper(nwc).await; client.set_zapper(nwc).await;
@@ -327,38 +327,42 @@ pub async fn set_nwc(uri: &str, state: State<'_, Nostr>) -> Result<bool, String>
#[tauri::command] #[tauri::command]
#[specta::specta] #[specta::specta]
pub async fn load_nwc(state: State<'_, Nostr>) -> Result<bool, String> { pub async fn load_wallet(state: State<'_, Nostr>) -> Result<String, String> {
let client = &state.client; let client = &state.client;
let keyring = Entry::new("Lume Secret Storage", "NWC").map_err(|e| e.to_string())?; let keyring = Entry::new("Lume Secret", "Bitcoin Connect").unwrap();
match keyring.get_password() { match keyring.get_password() {
Ok(val) => { Ok(val) => {
let uri = NostrWalletConnectURI::from_str(&val).map_err(|e| e.to_string())?; let uri = NostrWalletConnectURI::from_str(&val).unwrap();
let nwc = NWC::new(uri); let nwc = NWC::new(uri);
// Get current balance
let balance = nwc.get_balance().await;
// Update zapper
client.set_zapper(nwc).await; client.set_zapper(nwc).await;
Ok(true) match balance {
Ok(val) => Ok(val.to_string()),
Err(_) => Err("Get balance failed.".into()),
}
} }
Err(_) => Ok(false), Err(_) => Err("NWC not found.".into()),
} }
} }
#[tauri::command] #[tauri::command]
#[specta::specta] #[specta::specta]
pub async fn get_balance() -> Result<String, String> { pub async fn remove_wallet(state: State<'_, Nostr>) -> Result<(), ()> {
let keyring = Entry::new("Lume Secret Storage", "NWC").map_err(|e| e.to_string())?; let client = &state.client;
let keyring = Entry::new("Lume Secret", "Bitcoin Connect").unwrap();
match keyring.get_password() { match keyring.delete_password() {
Ok(val) => { Ok(_) => {
let uri = NostrWalletConnectURI::from_str(&val).map_err(|e| e.to_string())?; client.unset_zapper().await;
let nwc = NWC::new(uri); Ok(())
nwc
.get_balance()
.await
.map(|balance| balance.to_string())
.map_err(|_| "Get balance failed".into())
} }
Err(_) => Err("Something wrong".into()), Err(_) => Err(()),
} }
} }
@@ -371,32 +375,28 @@ pub async fn zap_profile(
state: State<'_, Nostr>, state: State<'_, Nostr>,
) -> Result<bool, String> { ) -> Result<bool, String> {
let client = &state.client; let client = &state.client;
let public_key: Option<PublicKey> = match Nip19::from_bech32(id) { let public_key = match Nip19::from_bech32(id) {
Ok(val) => match val { Ok(val) => match val {
Nip19::Pubkey(key) => Some(key), Nip19::Pubkey(key) => key,
Nip19::Profile(profile) => Some(profile.public_key), Nip19::Profile(profile) => profile.public_key,
_ => None, _ => return Err("Public Key is not valid.".into()),
}, },
Err(_) => match PublicKey::from_str(id) { Err(_) => match PublicKey::from_str(id) {
Ok(val) => Some(val), Ok(val) => val,
Err(_) => None, Err(_) => return Err("Public Key is not valid.".into()),
}, },
}; };
if let Some(recipient) = public_key { let details = ZapDetails::new(ZapType::Private).message(message);
let details = ZapDetails::new(ZapType::Public).message(message); let num = match amount.parse::<u64>() {
let num = match amount.parse::<u64>() { Ok(val) => val,
Ok(val) => val, Err(_) => return Err("Invalid amount.".into()),
Err(_) => return Err("Invalid amount.".into()), };
};
if client.zap(recipient, num, Some(details)).await.is_ok() { if client.zap(public_key, num, Some(details)).await.is_ok() {
Ok(true) Ok(true)
} else {
Err("Zap profile failed".into())
}
} else { } else {
Err("Parse public key failed".into()) Err("Zap profile failed".into())
} }
} }
@@ -409,32 +409,28 @@ pub async fn zap_event(
state: State<'_, Nostr>, state: State<'_, Nostr>,
) -> Result<bool, String> { ) -> Result<bool, String> {
let client = &state.client; let client = &state.client;
let event_id: Option<EventId> = match Nip19::from_bech32(id) { let event_id = match Nip19::from_bech32(id) {
Ok(val) => match val { Ok(val) => match val {
Nip19::EventId(id) => Some(id), Nip19::EventId(id) => id,
Nip19::Event(event) => Some(event.event_id), Nip19::Event(event) => event.event_id,
_ => None, _ => return Err("Event ID is invalid.".into()),
}, },
Err(_) => match EventId::from_hex(id) { Err(_) => match EventId::from_hex(id) {
Ok(val) => Some(val), Ok(val) => val,
Err(_) => None, Err(_) => return Err("Event ID is invalid.".into()),
}, },
}; };
if let Some(recipient) = event_id { let details = ZapDetails::new(ZapType::Private).message(message);
let details = ZapDetails::new(ZapType::Public).message(message); let num = match amount.parse::<u64>() {
let num = match amount.parse::<u64>() { Ok(val) => val,
Ok(val) => val, Err(_) => return Err("Invalid amount.".into()),
Err(_) => return Err("Invalid amount.".into()), };
};
if client.zap(recipient, num, Some(details)).await.is_ok() { if client.zap(event_id, num, Some(details)).await.is_ok() {
Ok(true) Ok(true)
} else {
Err("Zap event failed".into())
}
} else { } else {
Err("Parse event ID failed".into()) Err("Zap event failed".into())
} }
} }

View File

@@ -19,7 +19,7 @@ pub struct Relays {
#[tauri::command] #[tauri::command]
#[specta::specta] #[specta::specta]
pub async fn get_relays(state: State<'_, Nostr>) -> Result<Relays, ()> { pub async fn get_relays(state: State<'_, Nostr>) -> Result<Relays, String> {
let client = &state.client; let client = &state.client;
// Get connected relays // Get connected relays
@@ -27,8 +27,8 @@ pub async fn get_relays(state: State<'_, Nostr>) -> Result<Relays, ()> {
let connected_relays: Vec<String> = list.into_keys().map(|url| url.to_string()).collect(); let connected_relays: Vec<String> = list.into_keys().map(|url| url.to_string()).collect();
// Get NIP-65 relay list // Get NIP-65 relay list
let signer = client.signer().await.unwrap(); let signer = client.signer().await.map_err(|e| e.to_string())?;
let public_key = signer.public_key().await.unwrap(); let public_key = signer.public_key().await.map_err(|e| e.to_string())?;
let filter = Filter::new() let filter = Filter::new()
.author(public_key) .author(public_key)
.kind(Kind::RelayList) .kind(Kind::RelayList)
@@ -71,53 +71,50 @@ pub async fn get_relays(state: State<'_, Nostr>) -> Result<Relays, ()> {
}) })
} }
} }
Err(_) => Ok(Relays { Err(e) => Err(e.to_string()),
connected: connected_relays,
read: None,
write: None,
both: None,
}),
} }
} }
#[tauri::command] #[tauri::command]
#[specta::specta] #[specta::specta]
pub async fn connect_relay(relay: &str, state: State<'_, Nostr>) -> Result<bool, ()> { pub async fn connect_relay(relay: &str, state: State<'_, Nostr>) -> Result<bool, String> {
let client = &state.client; let client = &state.client;
if let Ok(status) = client.add_relay(relay).await { match client.add_relay(relay).await {
if status { Ok(status) => {
println!("connecting to relay: {}", relay); if status {
let _ = client.connect_relay(relay).await; println!("connecting to relay: {}", relay);
Ok(true) let _ = client.connect_relay(relay).await;
} else { Ok(true)
Ok(false) } else {
Ok(false)
}
} }
} else { Err(e) => Err(e.to_string()),
Ok(false)
} }
} }
#[tauri::command] #[tauri::command]
#[specta::specta] #[specta::specta]
pub async fn remove_relay(relay: &str, state: State<'_, Nostr>) -> Result<bool, ()> { pub async fn remove_relay(relay: &str, state: State<'_, Nostr>) -> Result<bool, String> {
let client = &state.client; let client = &state.client;
if (client.remove_relay(relay).await).is_ok() { match client.remove_relay(relay).await {
let _ = client.disconnect_relay(relay).await; Ok(_) => {
Ok(true) let _ = client.disconnect_relay(relay).await;
} else { Ok(true)
Ok(false) }
Err(e) => Err(e.to_string()),
} }
} }
#[tauri::command] #[tauri::command]
#[specta::specta] #[specta::specta]
pub fn get_bootstrap_relays(app: tauri::AppHandle) -> Result<Vec<String>, ()> { pub fn get_bootstrap_relays(app: tauri::AppHandle) -> Result<Vec<String>, String> {
let relays_path = app let relays_path = app
.path() .path()
.resolve("resources/relays.txt", BaseDirectory::Resource) .resolve("resources/relays.txt", BaseDirectory::Resource)
.expect("Bootstrap relays not found."); .map_err(|e| e.to_string())?;
let file = std::fs::File::open(relays_path).unwrap(); let file = std::fs::File::open(relays_path).map_err(|e| e.to_string())?;
let lines = io::BufReader::new(file).lines(); let lines = io::BufReader::new(file).lines();
let mut relays = Vec::new(); let mut relays = Vec::new();
@@ -135,15 +132,15 @@ pub fn save_bootstrap_relays(relays: &str, app: tauri::AppHandle) -> Result<(),
let relays_path = app let relays_path = app
.path() .path()
.resolve("resources/relays.txt", BaseDirectory::Resource) .resolve("resources/relays.txt", BaseDirectory::Resource)
.expect("Bootstrap relays not found."); .map_err(|e| e.to_string())?;
let mut file = fs::OpenOptions::new() let mut file = fs::OpenOptions::new()
.write(true) .write(true)
.open(relays_path) .open(relays_path)
.unwrap(); .map_err(|e| e.to_string())?;
match file.write_all(relays.as_bytes()) { match file.write_all(relays.as_bytes()) {
Ok(_) => Ok(()), Ok(_) => Ok(()),
Err(_) => Err("Cannot save bootstrap relays, please try again later.".into()), Err(e) => Err(e.to_string()),
} }
} }

View File

@@ -1,7 +1,7 @@
{ {
"$schema": "../node_modules/@tauri-apps/cli/schema.json", "$schema": "../node_modules/@tauri-apps/cli/schema.json",
"productName": "Lume", "productName": "Lume",
"version": "4.0.11", "version": "4.0.12",
"identifier": "nu.lume.Lume", "identifier": "nu.lume.Lume",
"build": { "build": {
"beforeBuildCommand": "pnpm desktop:build", "beforeBuildCommand": "pnpm desktop:build",