diff --git a/packages/app/src/Element/Nip05.css b/packages/app/src/Element/Nip05.css index bde71157..0c2eabae 100644 --- a/packages/app/src/Element/Nip05.css +++ b/packages/app/src/Element/Nip05.css @@ -1,7 +1,5 @@ .nip05 { color: var(--font-secondary-color); - justify-content: flex-start; - align-items: center; font-weight: normal; } @@ -52,10 +50,4 @@ .zap .pfp .display-name { align-items: center; } - .nip05 .nick { - display: none; - } - .nip05 .domain { - display: none; - } } diff --git a/packages/app/src/Element/ProfileImage.css b/packages/app/src/Element/ProfileImage.css index 193d88f1..e8493226 100644 --- a/packages/app/src/Element/ProfileImage.css +++ b/packages/app/src/Element/ProfileImage.css @@ -25,24 +25,6 @@ font-weight: 600; } -.pfp .profile-name { - display: flex; - flex-direction: column; -} - -.pfp .display-name { - display: flex; - flex-direction: column; - align-items: flex-start; -} - -@media (max-width: 420px) { - .pfp .display-name { - flex-direction: row; - align-items: center; - } -} - .pfp .subheader .about { max-width: calc(100vw - 140px); } diff --git a/packages/app/src/Nip05/ServiceProvider.ts b/packages/app/src/Nip05/ServiceProvider.ts index 09ea6809..610d4ed5 100644 --- a/packages/app/src/Nip05/ServiceProvider.ts +++ b/packages/app/src/Nip05/ServiceProvider.ts @@ -96,7 +96,7 @@ export class ServiceProvider { protected async getJson( path: string, method?: "GET" | string, - body?: { [key: string]: string }, + body?: unknown, headers?: { [key: string]: string } ): Promise { try { diff --git a/packages/app/src/Nip05/SnortServiceProvider.ts b/packages/app/src/Nip05/SnortServiceProvider.ts index 8cce85fa..fb603f3f 100644 --- a/packages/app/src/Nip05/SnortServiceProvider.ts +++ b/packages/app/src/Nip05/SnortServiceProvider.ts @@ -8,6 +8,18 @@ export interface ManageHandle { domain: string; pubkey: string; created: Date; + lnAddress?: string; +} + +export enum ForwardType { + Redirect = 0, + ProxyDirect = 1, + ProxyTrusted = 2, +} + +export interface PatchHandle { + lnAddress?: string; + forwardType?: ForwardType; } export default class SnortServiceProvider extends ServiceProvider { @@ -23,13 +35,17 @@ export default class SnortServiceProvider extends ServiceProvider { } async transfer(id: string, to: string) { - return this.getJsonAuthd(`/${id}?to=${to}`, "PATCH"); + return this.getJsonAuthd(`/${id}/transfer?to=${to}`, "PATCH"); + } + + async patch(id: string, obj: PatchHandle) { + return this.getJsonAuthd(`/${id}`, "PATCH", obj); } async getJsonAuthd( path: string, method?: "GET" | string, - body?: { [key: string]: string }, + body?: unknown, headers?: { [key: string]: string } ): Promise { const auth = await this.#publisher.generic("", EventKind.HttpAuthentication, [ diff --git a/packages/app/src/Pages/ProfilePage.css b/packages/app/src/Pages/ProfilePage.css index 8dbb2509..83735919 100644 --- a/packages/app/src/Pages/ProfilePage.css +++ b/packages/app/src/Pages/ProfilePage.css @@ -72,11 +72,6 @@ line-height: 23px; } -.profile .nip05 { - display: flex; - font-size: 16px; -} - .profile-wrapper > .avatar-wrapper { z-index: 1; } diff --git a/packages/app/src/Pages/SettingsPage.tsx b/packages/app/src/Pages/SettingsPage.tsx index 3228314d..9cc786e8 100644 --- a/packages/app/src/Pages/SettingsPage.tsx +++ b/packages/app/src/Pages/SettingsPage.tsx @@ -6,7 +6,7 @@ import Relay from "Pages/settings/Relays"; import Preferences from "Pages/settings/Preferences"; import RelayInfo from "Pages/settings/RelayInfo"; import { WalletSettingsRoutes } from "Pages/settings/WalletSettings"; -import Nip5ManagePage from "Pages/settings/ManageNip5"; +import { ManageHandleRoutes } from "Pages/settings/handle"; import messages from "./messages"; @@ -44,9 +44,6 @@ export const SettingsRoutes: RouteObject[] = [ path: "preferences", element: , }, - { - path: "nip5", - element: , - }, + ...ManageHandleRoutes, ...WalletSettingsRoutes, ]; diff --git a/packages/app/src/Pages/settings/Index.tsx b/packages/app/src/Pages/settings/Index.tsx index 678758e6..d5f7190a 100644 --- a/packages/app/src/Pages/settings/Index.tsx +++ b/packages/app/src/Pages/settings/Index.tsx @@ -47,7 +47,7 @@ const SettingsIndex = () => { -
navigate("nip5")}> +
navigate("handle")}> diff --git a/packages/app/src/Pages/settings/ManageNip5.tsx b/packages/app/src/Pages/settings/ManageNip5.tsx deleted file mode 100644 index 048f98f7..00000000 --- a/packages/app/src/Pages/settings/ManageNip5.tsx +++ /dev/null @@ -1,113 +0,0 @@ -import { useEffect, useState } from "react"; -import { FormattedMessage, useIntl } from "react-intl"; -import { Link } from "react-router-dom"; - -import { ApiHost } from "Const"; -import Modal from "Element/Modal"; -import useEventPublisher from "Feed/EventPublisher"; -import { ServiceError } from "Nip05/ServiceProvider"; -import SnortServiceProvider, { ManageHandle } from "Nip05/SnortServiceProvider"; - -export default function Nip5ManagePage() { - const publisher = useEventPublisher(); - const { formatMessage } = useIntl(); - const [handles, setHandles] = useState>(); - const [transfer, setTransfer] = useState(""); - const [newKey, setNewKey] = useState(""); - const [error, setError] = useState>([]); - const sp = new SnortServiceProvider(publisher, `${ApiHost}/api/v1/n5sp`); - - useEffect(() => { - loadHandles().catch(console.error); - }, []); - - async function loadHandles() { - const list = await sp.list(); - setHandles(list as Array); - } - - async function startTransfer() { - if (!transfer || !newKey) return; - setError([]); - const rsp = await sp.transfer(transfer, newKey); - if ("error" in rsp) { - setError((rsp as ServiceError).errors); - return; - } - await loadHandles(); - setTransfer(""); - setNewKey(""); - } - - function close() { - setTransfer(""); - setNewKey(""); - setError([]); - } - - if (!handles) { - return null; - } - return ( - <> -

- -

- {handles.length === 0 && ( - - - - ), - }} - /> - )} - {handles.map(a => ( - <> -
-
-

- {a.handle}@ - - {a.domain} - -

-
-
- -
-
- - ))} - {transfer && ( - -

- -

-
-
- setNewKey(e.target.value)} - /> -
- -
- {error && {error}} -
- )} - - ); -} diff --git a/packages/app/src/Pages/settings/handle/LNAddress.tsx b/packages/app/src/Pages/settings/handle/LNAddress.tsx new file mode 100644 index 00000000..2f17f634 --- /dev/null +++ b/packages/app/src/Pages/settings/handle/LNAddress.tsx @@ -0,0 +1,69 @@ +import { useState } from "react"; +import { FormattedMessage, useIntl } from "react-intl"; + +import { ApiHost } from "Const"; +import AsyncButton from "Element/AsyncButton"; +import useEventPublisher from "Feed/EventPublisher"; +import { LNURL } from "LNURL"; +import SnortServiceProvider, { ManageHandle } from "Nip05/SnortServiceProvider"; + +export default function LNForwardAddress({ handle }: { handle: ManageHandle }) { + const { formatMessage } = useIntl(); + const publisher = useEventPublisher(); + const sp = new SnortServiceProvider(publisher, `${ApiHost}/api/v1/n5sp`); + + const [newAddress, setNewAddress] = useState(handle.lnAddress ?? ""); + const [error, setError] = useState(""); + + async function startUpdate() { + const req = { + lnAddress: newAddress, + }; + + setError(""); + try { + const svc = new LNURL(newAddress); + await svc.load(); + } catch { + setError( + formatMessage({ + defaultMessage: "Invalid LNURL", + }) + ); + return; + } + + const rsp = await sp.patch(handle.id, req); + if ("error" in rsp) { + setError(rsp.error); + } + } + + return ( +
+

+ +

+

+ +

+
+
+ setNewAddress(e.target.value)} + /> +
+ startUpdate()}> + + +
+ {error && {error}} +
+ ); +} diff --git a/packages/app/src/Pages/settings/handle/ListHandles.tsx b/packages/app/src/Pages/settings/handle/ListHandles.tsx new file mode 100644 index 00000000..612f4895 --- /dev/null +++ b/packages/app/src/Pages/settings/handle/ListHandles.tsx @@ -0,0 +1,62 @@ +import { useEffect, useState } from "react"; +import { FormattedMessage } from "react-intl"; +import { Link, useNavigate } from "react-router-dom"; + +import { ApiHost } from "Const"; +import useEventPublisher from "Feed/EventPublisher"; +import SnortServiceProvider, { ManageHandle } from "Nip05/SnortServiceProvider"; + +export default function ListHandles() { + const navigate = useNavigate(); + const publisher = useEventPublisher(); + const [handles, setHandles] = useState>([]); + const sp = new SnortServiceProvider(publisher, `${ApiHost}/api/v1/n5sp`); + + useEffect(() => { + loadHandles().catch(console.error); + }, []); + + async function loadHandles() { + const list = await sp.list(); + setHandles(list as Array); + } + + return ( + <> + {handles.length === 0 && ( + + + + ), + }} + /> + )} + {handles.map(a => ( +
+
+

+ {a.handle}@ + + {a.domain} + +

+
+
+ +
+
+ ))} + + ); +} diff --git a/packages/app/src/Pages/settings/handle/Manage.tsx b/packages/app/src/Pages/settings/handle/Manage.tsx new file mode 100644 index 00000000..33f87940 --- /dev/null +++ b/packages/app/src/Pages/settings/handle/Manage.tsx @@ -0,0 +1,21 @@ +import { ManageHandle } from "Nip05/SnortServiceProvider"; +import { useLocation } from "react-router-dom"; +import LNForwardAddress from "./LNAddress"; +import TransferHandle from "./TransferHandle"; + +export default function ManageHandleIndex() { + const location = useLocation(); + const handle = location.state as ManageHandle; + return ( + <> +

+ {handle.handle}@ + + {handle.domain} + +

+ + + + ); +} diff --git a/packages/app/src/Pages/settings/handle/TransferHandle.tsx b/packages/app/src/Pages/settings/handle/TransferHandle.tsx new file mode 100644 index 00000000..d18712fc --- /dev/null +++ b/packages/app/src/Pages/settings/handle/TransferHandle.tsx @@ -0,0 +1,54 @@ +import { ApiHost } from "Const"; +import AsyncButton from "Element/AsyncButton"; +import useEventPublisher from "Feed/EventPublisher"; +import { ServiceError } from "Nip05/ServiceProvider"; +import SnortServiceProvider, { ManageHandle } from "Nip05/SnortServiceProvider"; +import { useState } from "react"; +import { FormattedMessage, useIntl } from "react-intl"; +import { useNavigate } from "react-router-dom"; + +export default function TransferHandle({ handle }: { handle: ManageHandle }) { + const publisher = useEventPublisher(); + const navigate = useNavigate(); + const { formatMessage } = useIntl(); + const sp = new SnortServiceProvider(publisher, `${ApiHost}/api/v1/n5sp`); + + const [newKey, setNewKey] = useState(""); + const [error, setError] = useState>([]); + + async function startTransfer() { + if (!newKey) return; + setError([]); + const rsp = await sp.transfer(handle.id, newKey); + if ("error" in rsp) { + setError((rsp as ServiceError).errors); + return; + } + navigate(-1); + } + + return ( +
+

+ +

+
+
+ setNewKey(e.target.value)} + /> +
+ startTransfer()}> + + +
+ {error && {error}} +
+ ); +} diff --git a/packages/app/src/Pages/settings/handle/index.tsx b/packages/app/src/Pages/settings/handle/index.tsx new file mode 100644 index 00000000..5a60b380 --- /dev/null +++ b/packages/app/src/Pages/settings/handle/index.tsx @@ -0,0 +1,35 @@ +import { FormattedMessage } from "react-intl"; +import { Outlet, RouteObject, useNavigate } from "react-router-dom"; + +import ListHandles from "./ListHandles"; +import ManageHandleIndex from "./Manage"; + +export default function ManageHandlePage() { + const navigate = useNavigate(); + + return ( + <> +

navigate("/settings/handle")} className="pointer"> + +

+ + + ); +} + +export const ManageHandleRoutes = [ + { + path: "/settings/handle", + element: , + children: [ + { + path: "", + element: , + }, + { + path: "manage", + element: , + }, + ], + }, +] as Array; diff --git a/packages/app/src/lang.json b/packages/app/src/lang.json index 7a83f644..9bb4b50f 100644 --- a/packages/app/src/lang.json +++ b/packages/app/src/lang.json @@ -30,6 +30,9 @@ "/n5KSF": { "defaultMessage": "{n} ms" }, + "0Azlrb": { + "defaultMessage": "Manage" + }, "0BUTMv": { "defaultMessage": "Search..." }, @@ -224,6 +227,9 @@ "BOr9z/": { "defaultMessage": "Snort is an open source project built by passionate people in their free time" }, + "BWpuKl": { + "defaultMessage": "Update" + }, "BcGMo+": { "defaultMessage": "Notes hold text content, the most popular usage of these notes is to store \"tweet like\" messages." }, @@ -517,6 +523,9 @@ "Rs4kCE": { "defaultMessage": "Bookmark" }, + "SOqbe9": { + "defaultMessage": "Update Lightning Address" + }, "Sjo1P4": { "defaultMessage": "Custom" }, @@ -612,6 +621,9 @@ "aWpBzj": { "defaultMessage": "Show more" }, + "b5vAk0": { + "defaultMessage": "Your handle will act like a lightning address and will redirect to your chosen LNURL or Lightning address" + }, "bQdA2k": { "defaultMessage": "Sensitive Content" }, @@ -1015,6 +1027,9 @@ "y1Z3or": { "defaultMessage": "Language" }, + "yCLnBC": { + "defaultMessage": "LNURL or Lightning Address" + }, "yCmnnm": { "defaultMessage": "Read global from", "description": "Label for reading global feed from specific relays" diff --git a/packages/app/src/translations/en.json b/packages/app/src/translations/en.json index 598da524..d81fbd86 100644 --- a/packages/app/src/translations/en.json +++ b/packages/app/src/translations/en.json @@ -9,6 +9,7 @@ "/RD0e2": "Nostr uses digital signature technology to provide tamper proof notes which can safely be replicated to many relays to provide redundant storage of your content.", "/d6vEc": "Make your profile easier to find and share", "/n5KSF": "{n} ms", + "0Azlrb": "Manage", "0BUTMv": "Search...", "0jOEtS": "Invalid LNURL", "0mch2Y": "name has disallowed characters", @@ -72,6 +73,7 @@ "B6+XJy": "zapped", "BOUMjw": "No nostr users found for {twitterUsername}", "BOr9z/": "Snort is an open source project built by passionate people in their free time", + "BWpuKl": "Update", "BcGMo+": "Notes hold text content, the most popular usage of these notes is to store \"tweet like\" messages.", "C81/uG": "Logout", "CHTbO3": "Failed to load invoice", @@ -168,6 +170,7 @@ "RhDAoS": "Are you sure you want to delete {id}", "RoOyAh": "Relays", "Rs4kCE": "Bookmark", + "SOqbe9": "Update Lightning Address", "Sjo1P4": "Custom", "TpgeGw": "Hex Salt..", "UDYlxu": "Pending Subscriptions", @@ -199,6 +202,7 @@ "a5UPxh": "Fund developers and platforms providing NIP-05 verification services", "aJEO/4": "Can't create vote, maybe you're not logged in?", "aWpBzj": "Show more", + "b5vAk0": "Your handle will act like a lightning address and will redirect to your chosen LNURL or Lightning address", "bQdA2k": "Sensitive Content", "brAXSu": "Pick a username", "bxv59V": "Just now", @@ -330,6 +334,7 @@ "xbVgIm": "Automatically load media", "xmcVZ0": "Search", "y1Z3or": "Language", + "yCLnBC": "LNURL or Lightning Address", "yCmnnm": "Read global from", "zFegDD": "Contact", "zINlao": "Owner",