NIP-65: Relay list metada #238

Merged
verbiricha merged 17 commits from relays-list into main 2023-02-10 19:23:52 +00:00
18 changed files with 233 additions and 3 deletions
Showing only changes of commit 68771779c9 - Show all commits

26
src/Element/Relays.css Normal file
View File

@ -0,0 +1,26 @@
.favicon {
width: 21px;
height: 21px;
border-radius: 100%;
margin-right: 12px;
}
.relay-card {
display: flex;
flex-direction: row;
align-items: center;
}
.relay-settings {
margin-left: auto;
}
.relay-settings svg:not(:last-child) {
margin-right: 12px;
}
.relay-settings svg.enabled {
color: var(--highlight);
}
.relay-settings svg.disabled {
opacity: 0.3;
}

43
src/Element/Relays.tsx Normal file
View File

@ -0,0 +1,43 @@
import "./Relays.css";
import Nostrich from "nostrich.webp";
import { useState } from "react";
import Read from "Icons/Read";
import Write from "Icons/Write";
export interface RelaySpec {
url: string;
settings: { read: boolean; write: boolean };
}
interface RelaysProps {
relays: RelaySpec[];
}
const RelayFavicon = ({ url }: { url: string }) => {
const cleanUrl = url.replace("wss://relay.", "https://").replace("wss://nostr.", "https://");
const [faviconUrl, setFaviconUrl] = useState(`${cleanUrl}/favicon.ico`);
return <img className="favicon" src={faviconUrl} onError={() => setFaviconUrl(Nostrich)} />;
};
const Relays = ({ relays }: RelaysProps) => {
return (
<div className="main-content">
{relays?.map(({ url, settings }) => {
return (
<div className="card relay-card">
<RelayFavicon url={url} />
<code>{url}</code>
<div className="relay-settings">
<Read className={settings.read ? "enabled" : "disabled"} />
<Write className={settings.write ? "enabled" : "disabled"} />
</div>
</div>
);
})}
</div>
);
};
export default Relays;

View File

@ -12,7 +12,7 @@
}
.zap .header .amount {
font-size: 32px;
font-size: 24px;
}
@media (max-width: 520px) {

View File

@ -220,6 +220,24 @@ export default function useEventPublisher() {
return await signEvent(ev);
}
},
saveRelaysSettings: async () => {
if (pubKey) {
let ev = NEvent.ForPubKey(pubKey);
ev.Kind = EventKind.Relays;
ev.Content = "";
for (let [url, settings] of Object.entries(relays)) {
let rTag = ["r", url];
if (settings.read) {
rTag.push("read");
}
if (settings.write) {
rTag.push("write");
}
ev.Tags.push(new Tag(rTag, ev.Tags.length));
}
return await signEvent(ev);
}
},
addFollow: async (pkAdd: HexKey | HexKey[], newRelays?: Record<string, RelaySettings>) => {
if (pubKey) {
const ev = NEvent.ForPubKey(pubKey);

37
src/Feed/RelaysFeed.tsx Normal file
View File

@ -0,0 +1,37 @@
import { useMemo } from "react";
import { HexKey } from "Nostr";
import EventKind from "Nostr/EventKind";
import { RelaySpec } from "Element/Relays";
import { Subscriptions } from "Nostr/Subscriptions";
import useSubscription from "./Subscription";
export default function useRelaysFeed(pubkey: HexKey) {
const sub = useMemo(() => {
const x = new Subscriptions();
x.Id = `relays:${pubkey.slice(0, 12)}`;
x.Kinds = new Set([EventKind.Relays]);
x.Authors = new Set([pubkey]);
x.Limit = 1;
return x;
}, [pubkey]);
const relays = useSubscription(sub, { leaveOpen: true, cache: true });
const notes = relays.store.notes;
const tags = notes.slice(-1)[0]?.tags || [];
return tags.reduce((rs, tag) => {
const [t, url, ...settings] = tag;
if (t === "r") {
return [
...rs,
{
url,
settings: {
read: settings.includes("read"),
write: settings.includes("write"),
},
},
];
}
return rs;
}, [] as RelaySpec[]);
}

17
src/Icons/Read.tsx Normal file
View File

@ -0,0 +1,17 @@
import IconProps from "./IconProps";
const Read = (props: IconProps) => {
return (
<svg width="22" height="22" viewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
<path
d="M14.9996 2V7M14.9996 7L19.9996 7M14.9996 7L20.9996 1M10.2266 11.8631C9.02506 10.6615 8.07627 9.30285 7.38028 7.85323C7.32041 7.72854 7.29048 7.66619 7.26748 7.5873C7.18576 7.30695 7.24446 6.96269 7.41447 6.72526C7.46231 6.65845 7.51947 6.60129 7.63378 6.48698C7.98338 6.13737 8.15819 5.96257 8.27247 5.78679C8.70347 5.1239 8.70347 4.26932 8.27247 3.60643C8.15819 3.43065 7.98338 3.25585 7.63378 2.90624L7.43891 2.71137C6.90747 2.17993 6.64174 1.91421 6.35636 1.76987C5.7888 1.4828 5.11854 1.4828 4.55098 1.76987C4.2656 1.91421 3.99987 2.17993 3.46843 2.71137L3.3108 2.86901C2.78117 3.39863 2.51636 3.66344 2.31411 4.02348C2.08969 4.42298 1.92833 5.04347 1.9297 5.5017C1.93092 5.91464 2.01103 6.19687 2.17124 6.76131C3.03221 9.79471 4.65668 12.6571 7.04466 15.045C9.43264 17.433 12.295 19.0575 15.3284 19.9185C15.8928 20.0787 16.1751 20.1588 16.588 20.16C17.0462 20.1614 17.6667 20 18.0662 19.7756C18.4263 19.5733 18.6911 19.3085 19.2207 18.7789L19.3783 18.6213C19.9098 18.0898 20.1755 17.8241 20.3198 17.5387C20.6069 16.9712 20.6069 16.3009 20.3198 15.7333C20.1755 15.448 19.9098 15.1822 19.3783 14.6508L19.1835 14.4559C18.8339 14.1063 18.6591 13.9315 18.4833 13.8172C17.8204 13.3862 16.9658 13.3862 16.3029 13.8172C16.1271 13.9315 15.9523 14.1063 15.6027 14.4559C15.4884 14.5702 15.4313 14.6274 15.3644 14.6752C15.127 14.8453 14.7828 14.904 14.5024 14.8222C14.4235 14.7992 14.3612 14.7693 14.2365 14.7094C12.7869 14.0134 11.4282 13.0646 10.2266 11.8631Z"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
};
export default Read;

17
src/Icons/Write.tsx Normal file
View File

@ -0,0 +1,17 @@
import IconProps from "./IconProps";
const Write = (props: IconProps) => {
return (
<svg width="22" height="22" viewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
<path
d="M20.9996 6V1M20.9996 1H15.9996M20.9996 1L14.9996 7M10.2266 11.8631C9.02506 10.6615 8.07627 9.30285 7.38028 7.85323C7.32041 7.72854 7.29048 7.66619 7.26748 7.5873C7.18576 7.30695 7.24446 6.96269 7.41447 6.72526C7.46231 6.65845 7.51947 6.60129 7.63378 6.48698C7.98338 6.13737 8.15819 5.96257 8.27247 5.78679C8.70347 5.1239 8.70347 4.26932 8.27247 3.60643C8.15819 3.43065 7.98338 3.25585 7.63378 2.90624L7.43891 2.71137C6.90747 2.17993 6.64174 1.91421 6.35636 1.76987C5.7888 1.4828 5.11854 1.4828 4.55098 1.76987C4.2656 1.91421 3.99987 2.17993 3.46843 2.71137L3.3108 2.86901C2.78117 3.39863 2.51636 3.66344 2.31411 4.02348C2.08969 4.42298 1.92833 5.04347 1.9297 5.5017C1.93092 5.91464 2.01103 6.19687 2.17124 6.76131C3.03221 9.79471 4.65668 12.6571 7.04466 15.045C9.43264 17.433 12.295 19.0575 15.3284 19.9185C15.8928 20.0787 16.1751 20.1588 16.588 20.16C17.0462 20.1614 17.6667 20 18.0662 19.7756C18.4263 19.5733 18.6911 19.3085 19.2207 18.7789L19.3783 18.6213C19.9098 18.0898 20.1755 17.8241 20.3198 17.5387C20.6069 16.9712 20.6069 16.3009 20.3198 15.7333C20.1755 15.448 19.9098 15.1822 19.3783 14.6508L19.1835 14.4559C18.8339 14.1063 18.6591 13.9315 18.4833 13.8172C17.8204 13.3862 16.9658 13.3862 16.3029 13.8172C16.1271 13.9315 15.9523 14.1063 15.6027 14.4559C15.4884 14.5702 15.4313 14.6274 15.3644 14.6752C15.127 14.8453 14.7828 14.904 14.5024 14.8222C14.4235 14.7992 14.3612 14.7693 14.2365 14.7094C12.7869 14.0134 11.4282 13.0646 10.2266 11.8631Z"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
};
export default Write;

View File

@ -8,6 +8,7 @@ const enum EventKind {
Deletion = 5, // NIP-09
Repost = 6, // NIP-18
Reaction = 7, // NIP-25
Relays = 10002, // NIP-65
Auth = 22242, // NIP-42
Lists = 30000, // NIP-51
ZapRequest = 9734, // NIP tba

View File

@ -47,6 +47,11 @@ export class Subscriptions {
*/
DTags?: Set<string>;
/**
* A litst of "r" tags to search
*/
RTags?: Set<string>;
/**
* A list of search terms
*/
@ -100,6 +105,7 @@ export class Subscriptions {
this.ETags = sub?.["#e"] ? new Set(sub["#e"]) : undefined;
this.PTags = sub?.["#p"] ? new Set(sub["#p"]) : undefined;
this.DTags = sub?.["#d"] ? new Set(["#d"]) : undefined;
this.RTags = sub?.["#r"] ? new Set(["#r"]) : undefined;
this.Search = sub?.search ?? undefined;
this.Since = sub?.since ?? undefined;
this.Until = sub?.until ?? undefined;
@ -150,6 +156,9 @@ export class Subscriptions {
if (this.DTags) {
ret["#d"] = Array.from(this.DTags);
}
if (this.RTags) {
ret["#r"] = Array.from(this.RTags);
}
if (this.Search) {
ret.search = this.Search;
}

View File

@ -41,6 +41,7 @@ export type RawReqFilter = {
"#p"?: u256[];
"#t"?: string[];
"#d"?: string[];
"#r"?: string[];
search?: string;
since?: number;
until?: number;

View File

@ -4,12 +4,15 @@ import { useIntl, FormattedMessage } from "react-intl";
import { useSelector } from "react-redux";
import { useNavigate, useParams } from "react-router-dom";
import { unwrap } from "Util";
import { formatShort } from "Number";
import Relays from "Element/Relays";
import { Tab, TabElement } from "Element/Tabs";
import Link from "Icons/Link";
import Qr from "Icons/Qr";
import Zap from "Icons/Zap";
import Envelope from "Icons/Envelope";
import useRelaysFeed from "Feed/RelaysFeed";
import { useUserProfile } from "Feed/ProfileFeed";
import useZapsFeed from "Feed/ZapsFeed";
import { default as ZapElement, parseZap } from "Element/Zap";
@ -46,6 +49,7 @@ const FOLLOWS = 3;
const ZAPS = 4;
const MUTED = 5;
const BLOCKED = 6;
const RELAYS = 7;
export default function ProfilePage() {
const { formatMessage } = useIntl();
@ -69,6 +73,7 @@ export default function ProfilePage() {
const lnurl = extractLnAddress(user?.lud16 || user?.lud06 || "");
const website_url =
user?.website && !user.website.startsWith("http") ? "https://" + user.website : user?.website || "";
const relays = useRelaysFeed(id);
const zapFeed = useZapsFeed(id);
const zaps = useMemo(() => {
const profileZaps = zapFeed.store.notes.map(parseZap).filter(z => z.valid && z.p === id && !z.e && z.zapper !== id);
@ -85,8 +90,12 @@ export default function ProfilePage() {
Zaps: { text: formatMessage(messages.Zaps), value: ZAPS },
Muted: { text: formatMessage(messages.Muted), value: MUTED },
Blocked: { text: formatMessage(messages.Blocked), value: BLOCKED },
Relays: { text: formatMessage(messages.Relays), value: RELAYS },
};
const [tab, setTab] = useState<Tab>(ProfileTab.Notes);
const optionalTabs = [zapsTotal > 0 && ProfileTab.Zaps, relays.length > 0 && ProfileTab.Relays].filter(a =>
unwrap(a)
) as Tab[];
useEffect(() => {
setTab(ProfileTab.Notes);
@ -204,6 +213,9 @@ export default function ProfilePage() {
case BLOCKED: {
return isMe ? <BlockList variant="blocked" /> : null;
}
case RELAYS: {
return <Relays relays={relays} />;
}
}
}
@ -282,7 +294,8 @@ export default function ProfilePage() {
</div>
</div>
<div className="tabs main-content" ref={horizontalScroll}>
{[ProfileTab.Notes, ProfileTab.Followers, ProfileTab.Follows, ProfileTab.Zaps, ProfileTab.Muted].map(renderTab)}
{[ProfileTab.Notes, ProfileTab.Followers, ProfileTab.Follows, ProfileTab.Muted].map(renderTab)}
{optionalTabs.map(renderTab)}
{isMe && renderTab(ProfileTab.Blocked)}
</div>
{tabContent()}

35
src/Pages/messages.js Normal file
View File

@ -0,0 +1,35 @@
import { defineMessages } from "react-intl";
export default defineMessages({
Login: "Login",
Posts: "Posts",
Conversations: "Conversations",
Global: "Global",
NewUsers: "New users page",
NoFollows: "Hmm nothing here.. Checkout {newUsersPage} to follow some recommended nostrich's!",
Notes: "Notes",
Reactions: "Reactions",
Followers: "Followers",
Follows: "Follows",
Zaps: "Zaps",
ZapsCount: "{n} Zaps",
Muted: "Muted",
Blocked: "Blocked",
Sats: "{n} {n, plural, =1 {sat} other {sats}}",
Following: "Following {n}",
Settings: "Settings",
Search: "Search",
SearchPlaceholder: "Search...",
Messages: "Messages",
MarkAllRead: "Mark All Read",
GetVerified: "Get Verified",
Nip05: `NIP-05 is a DNS based verification spec which helps to validate you as a real user.`,
Nip05Pros: `Getting NIP-05 verified can help:`,
AvoidImpersonators: "Prevent fake accounts from imitating you",
EasierToFind: "Make your profile easier to find and share",
Funding: "Fund developers and platforms providing NIP-05 verification services",
SnortSocialNip: `Our very own NIP-05 verification service, help support the development of this site and get a shiny special badge on our site!`,
NostrPlebsNip: `Nostr Plebs is one of the first NIP-05 providers in the space and offers a good collection of domains at reasonable prices`,
Relays: "Relays",
RelaysCount: "{n} Relays",
});

View File

@ -20,6 +20,8 @@ const RelaySettingsPage = () => {
const ev = await publisher.saveRelays();
publisher.broadcast(ev);
publisher.broadcastForBootstrap(ev);
let settingsEv = await publisher.saveRelaysSettings();
publisher.broadcast(settingsEv);
}
function addRelay() {

View File

@ -209,4 +209,4 @@
"zjJZBd": "You're ready!",
"zonsdq": "Failed to load LNURL service",
"zvCDao": "Automatically show latest notes"
}
}

View File

@ -111,12 +111,15 @@
"Pages.Notes": "Notas",
"Pages.Posts": "Notas",
"Pages.Reactions": "Reacciones",
"Pages.Relays": "",
"Pages.RelaysCount": "",
"Pages.Sats": "{n} {n, plural, =1 {sat} other {sats}}",
"Pages.Search": "Búsqueda",
"Pages.SearchPlaceholder": "Buscar...",
"Pages.Settings": "Configuración",
"Pages.SnortSocialNip": "Nuestro servicio de verificación NIP-05, apoya el desarrollo de este proyecto y obtén una apariencia especial en nuestra web!",
"Pages.Zaps": "Zaps",
"Pages.ZapsCount": "",
"Pages.new.Bitcoin": "",
"Pages.new.Check": "",
"Pages.new.DigitalSignatures": "",

View File

@ -110,12 +110,15 @@
"Pages.Notes": "Notes",
"Pages.Posts": "Publications",
"Pages.Reactions": "Réactions",
"Pages.Relays": "",
"Pages.RelaysCount": "",
"Pages.Sats": "{n} {n, plural, =1 {sat} other {sats}}",
"Pages.Search": "Chercher",
"Pages.SearchPlaceholder": "Chercher...",
"Pages.Settings": "Paramètres",
"Pages.SnortSocialNip": "Notre propre service de vérification NIP-05, aidez à soutenir le développement de ce site et obtenez un badge spécial brillant sur notre site !",
"Pages.Zaps": "Zaps",
"Pages.ZapsCount": "",
"Pages.new.Bitcoin": "",
"Pages.new.Check": "",
"Pages.new.DigitalSignatures": "",

View File

@ -112,11 +112,13 @@
"Pages.Posts": "ポスト",
"Pages.Reactions": "リアクション",
"Pages.Sats": "{n} {n, plural, =1 {sat} other {sats}}",
"Pages.Relays": "",
"Pages.Search": "検索",
"Pages.SearchPlaceholder": "検索する",
"Pages.Settings": "設定",
"Pages.SnortSocialNip": "私たち独自のNIP-05認証サービスです。このサイトの開発を支援し、ピカピカの特別なバッジを私たちのサイトで使えるようになります",
"Pages.Zaps": "Zap",
"Pages.ZapsCount": "",
"Pages.new.Bitcoin": "",
"Pages.new.Check": "",
"Pages.new.DigitalSignatures": "",

View File

@ -111,12 +111,15 @@
"Pages.Notes": "",
"Pages.Posts": "",
"Pages.Reactions": "",
"Pages.Relays": "",
"Pages.RelaysCount": "",
"Pages.Sats": "",
"Pages.Search": "",
"Pages.SearchPlaceholder": "",
"Pages.Settings": "",
"Pages.SnortSocialNip": "",
"Pages.Zaps": "",
"Pages.ZapsCount": "",
"Pages.new.Bitcoin": "",
"Pages.new.Check": "",
"Pages.new.DigitalSignatures": "",