Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6e5d0f0e76 | ||
|
|
f0712e5961 | ||
|
|
3fbd66dece | ||
|
|
1283432632 | ||
|
|
59eaaec903 | ||
|
|
4f0f210076 |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -35,4 +35,4 @@ dist/
|
|||||||
.DS_Store
|
.DS_Store
|
||||||
*.pem
|
*.pem
|
||||||
.vscode/
|
.vscode/
|
||||||
ndb/
|
.idea/
|
||||||
|
|||||||
5
.idea/.gitignore
generated
vendored
5
.idea/.gitignore
generated
vendored
@@ -1,5 +0,0 @@
|
|||||||
# Default ignored files
|
|
||||||
/shelf/
|
|
||||||
/workspace.xml
|
|
||||||
# Editor-based HTTP Client requests
|
|
||||||
/httpRequests/
|
|
||||||
5
.idea/codeStyles/codeStyleConfig.xml
generated
5
.idea/codeStyles/codeStyleConfig.xml
generated
@@ -1,5 +0,0 @@
|
|||||||
<component name="ProjectCodeStyleConfiguration">
|
|
||||||
<state>
|
|
||||||
<option name="PREFERRED_PROJECT_CODE_STYLE" value="Default" />
|
|
||||||
</state>
|
|
||||||
</component>
|
|
||||||
46
.idea/inspectionProfiles/Project_Default.xml
generated
46
.idea/inspectionProfiles/Project_Default.xml
generated
@@ -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
17
.idea/lume.iml
generated
@@ -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
8
.idea/modules.xml
generated
@@ -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
6
.idea/vcs.xml
generated
@@ -1,6 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="VcsDirectoryMappings">
|
|
||||||
<mapping directory="" vcs="Git" />
|
|
||||||
</component>
|
|
||||||
</project>
|
|
||||||
@@ -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 |
@@ -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" });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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",
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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}>
|
||||||
|
|||||||
@@ -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,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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",
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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" });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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" });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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" });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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));
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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",
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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" });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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",
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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",
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -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" });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
39
apps/desktop2/src/routes/settings/bitcoin-connect.tsx
Normal file
39
apps/desktop2/src/routes/settings/bitcoin-connect.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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">
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
59
apps/desktop2/src/routes/settings/wallet.tsx
Normal file
59
apps/desktop2/src/routes/settings/wallet.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
119
apps/desktop2/src/routes/zap.$id.tsx
Normal file
119
apps/desktop2/src/routes/zap.$id.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
|
||||||
|
|||||||
@@ -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 **/
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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
308
pnpm-lock.yaml
generated
@@ -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
|
||||||
|
|||||||
@@ -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(())
|
||||||
|
|||||||
@@ -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", ());
|
|
||||||
}
|
|
||||||
_ => (),
|
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
Reference in New Issue
Block a user