commit
36dc9f6543
56
src/Nostr/Links.ts
Normal file
56
src/Nostr/Links.ts
Normal file
@ -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;
|
||||||
|
}
|
@ -65,9 +65,9 @@ export default function Layout() {
|
|||||||
() =>
|
() =>
|
||||||
publicKey
|
publicKey
|
||||||
? totalUnread(
|
? totalUnread(
|
||||||
dms.filter(a => !isMuted(a.pubkey)),
|
dms.filter(a => !isMuted(a.pubkey)),
|
||||||
publicKey
|
publicKey
|
||||||
)
|
)
|
||||||
: 0,
|
: 0,
|
||||||
[dms, publicKey]
|
[dms, publicKey]
|
||||||
);
|
);
|
||||||
@ -138,6 +138,15 @@ export default function Layout() {
|
|||||||
|
|
||||||
console.debug(`Using db: ${dbType}`);
|
console.debug(`Using db: ${dbType}`);
|
||||||
dispatch(init(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);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
26
src/Pages/NostrLinkHandler.tsx
Normal file
26
src/Pages/NostrLinkHandler.tsx
Normal file
@ -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}</>
|
||||||
|
)
|
||||||
|
}
|
@ -42,7 +42,7 @@ import QrCode from "Element/QrCode";
|
|||||||
import Modal from "Element/Modal";
|
import Modal from "Element/Modal";
|
||||||
import { ProxyImg } from "Element/ProxyImg";
|
import { ProxyImg } from "Element/ProxyImg";
|
||||||
import useHorizontalScroll from "Hooks/useHorizontalScroll";
|
import useHorizontalScroll from "Hooks/useHorizontalScroll";
|
||||||
|
import { NostrPrefix } from "Nostr/Links";
|
||||||
import messages from "./messages";
|
import messages from "./messages";
|
||||||
|
|
||||||
const NOTES = 0;
|
const NOTES = 0;
|
||||||
@ -262,7 +262,7 @@ export default function ProfilePage() {
|
|||||||
{showProfileQr && (
|
{showProfileQr && (
|
||||||
<Modal className="qr-modal" onClose={() => setShowProfileQr(false)}>
|
<Modal className="qr-modal" onClose={() => setShowProfileQr(false)}>
|
||||||
<ProfileImage pubkey={id} />
|
<ProfileImage pubkey={id} />
|
||||||
<QrCode data={`nostr:${hexToBech32("npub", id)}`} link={undefined} className="m10" />
|
<QrCode data={`nostr:${hexToBech32(NostrPrefix.PublicKey, id)}`} link={undefined} className="m10" />
|
||||||
</Modal>
|
</Modal>
|
||||||
)}
|
)}
|
||||||
{isMe ? (
|
{isMe ? (
|
||||||
@ -280,7 +280,7 @@ export default function ProfilePage() {
|
|||||||
)}
|
)}
|
||||||
{!loggedOut && (
|
{!loggedOut && (
|
||||||
<>
|
<>
|
||||||
<IconButton onClick={() => navigate(`/messages/${hexToBech32("npub", id)}`)}>
|
<IconButton onClick={() => navigate(`/messages/${hexToBech32(NostrPrefix.PublicKey, id)}`)}>
|
||||||
<Envelope width={16} height={13} />
|
<Envelope width={16} height={13} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</>
|
</>
|
||||||
|
13
src/Util.ts
13
src/Util.ts
@ -3,6 +3,7 @@ import { sha256 as hash } from "@noble/hashes/sha256";
|
|||||||
import { bech32 } from "bech32";
|
import { bech32 } from "bech32";
|
||||||
import { HexKey, TaggedRawEvent, u256 } from "Nostr";
|
import { HexKey, TaggedRawEvent, u256 } from "Nostr";
|
||||||
import EventKind from "Nostr/EventKind";
|
import EventKind from "Nostr/EventKind";
|
||||||
|
import { encodeTLV, NostrPrefix } from "Nostr/Links";
|
||||||
|
|
||||||
export const sha256 = (str: string) => {
|
export const sha256 = (str: string) => {
|
||||||
return secp.utils.bytesToHex(hash(str));
|
return secp.utils.bytesToHex(hash(str));
|
||||||
@ -64,7 +65,7 @@ export function bech32ToText(str: string) {
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export function eventLink(hex: u256) {
|
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 {
|
try {
|
||||||
const buf = secp.utils.hexToBytes(hex);
|
if (hrp === NostrPrefix.Note || hrp === NostrPrefix.PrivateKey || hrp === NostrPrefix.PublicKey) {
|
||||||
return bech32.encode(hrp, bech32.toWords(buf));
|
let buf = secp.utils.hexToBytes(hex);
|
||||||
|
return bech32.encode(hrp, bech32.toWords(buf));
|
||||||
|
} else {
|
||||||
|
return encodeTLV(hex, hrp as NostrPrefix);
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn("Invalid hex", hex, e);
|
console.warn("Invalid hex", hex, e);
|
||||||
return "";
|
return "";
|
||||||
@ -91,7 +96,7 @@ export function hexToBech32(hrp: string, hex?: string) {
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export function profileLink(hex: HexKey) {
|
export function profileLink(hex: HexKey) {
|
||||||
return `/p/${hexToBech32("npub", hex)}`;
|
return `/p/${hexToBech32(NostrPrefix.PublicKey, hex)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -25,6 +25,7 @@ import HashTagsPage from "Pages/HashTagsPage";
|
|||||||
import SearchPage from "Pages/SearchPage";
|
import SearchPage from "Pages/SearchPage";
|
||||||
import HelpPage from "Pages/HelpPage";
|
import HelpPage from "Pages/HelpPage";
|
||||||
import { NewUserRoutes } from "Pages/new";
|
import { NewUserRoutes } from "Pages/new";
|
||||||
|
import NostrLinkHandler from 'Pages/NostrLinkHandler';
|
||||||
import { IntlProvider } from "./IntlProvider";
|
import { IntlProvider } from "./IntlProvider";
|
||||||
import { unwrap } from "Util";
|
import { unwrap } from "Util";
|
||||||
|
|
||||||
@ -93,6 +94,10 @@ export const router = createBrowserRouter([
|
|||||||
path: "/search/:keyword?",
|
path: "/search/:keyword?",
|
||||||
element: <SearchPage />,
|
element: <SearchPage />,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "/handler/*",
|
||||||
|
element: <NostrLinkHandler />
|
||||||
|
},
|
||||||
...NewUserRoutes,
|
...NewUserRoutes,
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
Loading…
Reference in New Issue
Block a user