diff --git a/src/Nostr/Links.ts b/src/Nostr/Links.ts
new file mode 100644
index 00000000..086ddbef
--- /dev/null
+++ b/src/Nostr/Links.ts
@@ -0,0 +1,56 @@
+import * as secp from "@noble/secp256k1";
+import { bech32 } from "bech32";
+
+export enum NostrPrefix {
+ PublicKey = "npub",
+ PrivateKey = "nsec",
+ Note = "note",
+
+ // TLV prefixes
+ Profile = "nprofile",
+ Event = "nevent",
+ Relay = "nrelay"
+}
+
+export interface TLVEntry {
+ type: number,
+ length: number,
+ value: string // hex encoded data
+}
+
+export function encodeTLV(hex: string, prefix: NostrPrefix, relays?: string[]) {
+ if (typeof hex !== "string" || hex.length === 0 || hex.length % 2 !== 0) {
+ return "";
+ }
+
+ let enc = new TextEncoder();
+ let buf = secp.utils.hexToBytes(hex);
+
+ let tl0 = [0, buf.length, ...buf];
+ let tl1 = relays?.map(a => {
+ let data = enc.encode(a);
+ return [1, data.length, ...data];
+ }).flat() ?? [];
+
+ return bech32.encode(prefix, bech32.toWords([...tl0, ...tl1]));
+}
+
+export function decodeTLV(str: string) {
+ let decoded = bech32.decode(str);
+ let data = bech32.fromWords(decoded.words);
+
+ let entries: TLVEntry[] = [];
+ let x = 0;
+ while (x < data.length) {
+ let t = data[x];
+ let l = data[x + 1];
+ let v = data.slice(x + 2, x + 2 + l);
+ entries.push({
+ type: t,
+ length: l,
+ value: secp.utils.bytesToHex(new Uint8Array(v))
+ });
+ x += 2 + l;
+ }
+ return entries;
+}
\ No newline at end of file
diff --git a/src/Pages/Layout.tsx b/src/Pages/Layout.tsx
index 606f6e0c..9fa85ae8 100644
--- a/src/Pages/Layout.tsx
+++ b/src/Pages/Layout.tsx
@@ -65,9 +65,9 @@ export default function Layout() {
() =>
publicKey
? totalUnread(
- dms.filter(a => !isMuted(a.pubkey)),
- publicKey
- )
+ dms.filter(a => !isMuted(a.pubkey)),
+ publicKey
+ )
: 0,
[dms, publicKey]
);
@@ -138,6 +138,15 @@ export default function Layout() {
console.debug(`Using db: ${dbType}`);
dispatch(init(dbType));
+
+ try {
+ if ("registerProtocolHandler" in window.navigator) {
+ window.navigator.registerProtocolHandler("web+nostr", `${window.location.protocol}//${window.location.host}/handler/%s`);
+ console.info("Registered protocol handler for \"nostr\"");
+ }
+ } catch (e) {
+ console.error("Failed to register protocol handler", e);
+ }
});
}, []);
diff --git a/src/Pages/NostrLinkHandler.tsx b/src/Pages/NostrLinkHandler.tsx
new file mode 100644
index 00000000..e53f0fda
--- /dev/null
+++ b/src/Pages/NostrLinkHandler.tsx
@@ -0,0 +1,26 @@
+import { NostrPrefix } from "Nostr/Links";
+import { useEffect } from "react";
+import { redirect, useNavigate, useParams } from "react-router-dom";
+
+export default function NostrLinkHandler() {
+ const params = useParams();
+ const navigate = useNavigate();
+ const link = decodeURIComponent(params["*"] ?? "");
+
+ useEffect(() => {
+ if (link.length > 0) {
+ let ls = link.split(":");
+ let entity = ls[1];
+ if (entity.startsWith(NostrPrefix.PublicKey) || entity.startsWith(NostrPrefix.Profile)) {
+ navigate(`/p/${entity}`);
+ }
+ else if (entity.startsWith(NostrPrefix.Event) || entity.startsWith(NostrPrefix.Note)) {
+ navigate(`/e/${entity}`)
+ }
+ }
+ }, [link]);
+
+ return (
+ <>Could not handle {link}>
+ )
+}
\ No newline at end of file
diff --git a/src/Pages/ProfilePage.tsx b/src/Pages/ProfilePage.tsx
index d1daea1a..81dc8394 100644
--- a/src/Pages/ProfilePage.tsx
+++ b/src/Pages/ProfilePage.tsx
@@ -42,7 +42,7 @@ import QrCode from "Element/QrCode";
import Modal from "Element/Modal";
import { ProxyImg } from "Element/ProxyImg";
import useHorizontalScroll from "Hooks/useHorizontalScroll";
-
+import { NostrPrefix } from "Nostr/Links";
import messages from "./messages";
const NOTES = 0;
@@ -262,7 +262,7 @@ export default function ProfilePage() {
{showProfileQr && (
setShowProfileQr(false)}>
-
+
)}
{isMe ? (
@@ -280,7 +280,7 @@ export default function ProfilePage() {
)}
{!loggedOut && (
<>
- navigate(`/messages/${hexToBech32("npub", id)}`)}>
+ navigate(`/messages/${hexToBech32(NostrPrefix.PublicKey, id)}`)}>
>
diff --git a/src/Util.ts b/src/Util.ts
index 2c7685d1..bec605f2 100644
--- a/src/Util.ts
+++ b/src/Util.ts
@@ -3,6 +3,7 @@ import { sha256 as hash } from "@noble/hashes/sha256";
import { bech32 } from "bech32";
import { HexKey, TaggedRawEvent, u256 } from "Nostr";
import EventKind from "Nostr/EventKind";
+import { encodeTLV, NostrPrefix } from "Nostr/Links";
export const sha256 = (str: string) => {
return secp.utils.bytesToHex(hash(str));
@@ -64,7 +65,7 @@ export function bech32ToText(str: string) {
* @returns
*/
export function eventLink(hex: u256) {
- return `/e/${hexToBech32("note", hex)}`;
+ return `/e/${hexToBech32(NostrPrefix.Note, hex)}`;
}
/**
@@ -77,8 +78,12 @@ export function hexToBech32(hrp: string, hex?: string) {
}
try {
- const buf = secp.utils.hexToBytes(hex);
- return bech32.encode(hrp, bech32.toWords(buf));
+ if (hrp === NostrPrefix.Note || hrp === NostrPrefix.PrivateKey || hrp === NostrPrefix.PublicKey) {
+ let buf = secp.utils.hexToBytes(hex);
+ return bech32.encode(hrp, bech32.toWords(buf));
+ } else {
+ return encodeTLV(hex, hrp as NostrPrefix);
+ }
} catch (e) {
console.warn("Invalid hex", hex, e);
return "";
@@ -91,7 +96,7 @@ export function hexToBech32(hrp: string, hex?: string) {
* @returns
*/
export function profileLink(hex: HexKey) {
- return `/p/${hexToBech32("npub", hex)}`;
+ return `/p/${hexToBech32(NostrPrefix.PublicKey, hex)}`;
}
/**
diff --git a/src/index.tsx b/src/index.tsx
index d6b4a7c0..bef709ab 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -25,6 +25,7 @@ import HashTagsPage from "Pages/HashTagsPage";
import SearchPage from "Pages/SearchPage";
import HelpPage from "Pages/HelpPage";
import { NewUserRoutes } from "Pages/new";
+import NostrLinkHandler from 'Pages/NostrLinkHandler';
import { IntlProvider } from "./IntlProvider";
import { unwrap } from "Util";
@@ -93,6 +94,10 @@ export const router = createBrowserRouter([
path: "/search/:keyword?",
element: ,
},
+ {
+ path: "/handler/*",
+ element:
+ },
...NewUserRoutes,
],
},