NIP-65: Relay list metada (#238)
This commit is contained in:
parent
d13904b8e2
commit
5153f5c90a
@ -29,3 +29,4 @@ Snort supports the following NIP's:
|
||||
- [ ] NIP-42: Authentication of clients to relays
|
||||
- [x] NIP-50: Search
|
||||
- [x] NIP-51: Lists
|
||||
- [x] NIP-65: Relay List Metadata
|
||||
|
@ -161,7 +161,9 @@ export default function NoteFooter(props: NoteFooterProps) {
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<div className={`reaction-pill ${hasReacted("+") ? "reacted" : ""} `} onClick={() => react("+")}>
|
||||
<div
|
||||
className={`reaction-pill ${hasReacted("+") ? "reacted" : ""} `}
|
||||
onClick={() => react(prefs.reactionEmoji)}>
|
||||
<div className="reaction-pill-icon">
|
||||
<Heart />
|
||||
</div>
|
||||
|
36
src/Element/RelaysMetadata.css
Normal file
36
src/Element/RelaysMetadata.css
Normal file
@ -0,0 +1,36 @@
|
||||
.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;
|
||||
}
|
||||
|
||||
.relay-url {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
@media (min-width: 520px) {
|
||||
.relay-url {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
44
src/Element/RelaysMetadata.tsx
Normal file
44
src/Element/RelaysMetadata.tsx
Normal file
@ -0,0 +1,44 @@
|
||||
import "./RelaysMetadata.css";
|
||||
import Nostrich from "nostrich.webp";
|
||||
import { useState } from "react";
|
||||
|
||||
import { RelaySettings } from "Nostr";
|
||||
import Read from "Icons/Read";
|
||||
import Write from "Icons/Write";
|
||||
|
||||
const RelayFavicon = ({ url }: { url: string }) => {
|
||||
const cleanUrl = url
|
||||
.replace("wss://relay.", "https://")
|
||||
.replace("wss://nostr.", "https://")
|
||||
.replace("wss://", "https://")
|
||||
.replace("ws://", "http://")
|
||||
.replace(/\/$/, "");
|
||||
const [faviconUrl, setFaviconUrl] = useState(`${cleanUrl}/favicon.ico`);
|
||||
|
||||
return <img className="favicon" src={faviconUrl} onError={() => setFaviconUrl(Nostrich)} />;
|
||||
};
|
||||
|
||||
interface RelaysMetadataProps {
|
||||
relays: RelaySettings[];
|
||||
}
|
||||
|
||||
const RelaysMetadata = ({ relays }: RelaysMetadataProps) => {
|
||||
return (
|
||||
<div className="main-content">
|
||||
{relays?.map(({ url, settings }) => {
|
||||
return (
|
||||
<div className="card relay-card">
|
||||
<RelayFavicon url={url} />
|
||||
<code className="relay-url">{url}</code>
|
||||
<div className="relay-settings">
|
||||
<Read className={settings.read ? "enabled" : "disabled"} />
|
||||
<Write className={settings.write ? "enabled" : "disabled"} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default RelaysMetadata;
|
@ -12,12 +12,12 @@
|
||||
}
|
||||
|
||||
.zap .header .amount {
|
||||
font-size: 32px;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
@media (max-width: 520px) {
|
||||
.zap .header .amount {
|
||||
font-size: 21px;
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -109,6 +109,16 @@ export default function useEventPublisher() {
|
||||
}
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Write event to all given relays.
|
||||
*/
|
||||
broadcastAll: (ev: NEvent | undefined, relays: string[]) => {
|
||||
if (ev) {
|
||||
for (const k of relays) {
|
||||
System.WriteOnceToRelay(k, ev);
|
||||
}
|
||||
}
|
||||
},
|
||||
muted: async (keys: HexKey[], priv: HexKey[]) => {
|
||||
if (pubKey) {
|
||||
const ev = NEvent.ForPubKey(pubKey);
|
||||
@ -220,6 +230,24 @@ export default function useEventPublisher() {
|
||||
return await signEvent(ev);
|
||||
}
|
||||
},
|
||||
saveRelaysSettings: async () => {
|
||||
if (pubKey) {
|
||||
const ev = NEvent.ForPubKey(pubKey);
|
||||
ev.Kind = EventKind.Relays;
|
||||
ev.Content = "";
|
||||
for (const [url, settings] of Object.entries(relays)) {
|
||||
const rTag = ["r", url];
|
||||
if (settings.read && !settings.write) {
|
||||
rTag.push("read");
|
||||
}
|
||||
if (settings.write && !settings.read) {
|
||||
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);
|
||||
|
36
src/Feed/RelaysFeed.tsx
Normal file
36
src/Feed/RelaysFeed.tsx
Normal file
@ -0,0 +1,36 @@
|
||||
import { useMemo } from "react";
|
||||
import { HexKey, RelaySettings } from "Nostr";
|
||||
import EventKind from "Nostr/EventKind";
|
||||
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: false, 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.length === 0 || settings.includes("read"),
|
||||
write: settings.length === 0 || settings.includes("write"),
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
return rs;
|
||||
}, [] as RelaySettings[]);
|
||||
}
|
@ -13,9 +13,9 @@ const Gear = (props: IconProps) => {
|
||||
<path
|
||||
d="M9.99992 14C11.6568 14 12.9999 12.6569 12.9999 11C12.9999 9.34315 11.6568 8 9.99992 8C8.34307 8 6.99992 9.34315 6.99992 11C6.99992 12.6569 8.34307 14 9.99992 14Z"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
17
src/Icons/Read.tsx
Normal file
17
src/Icons/Read.tsx
Normal 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
17
src/Icons/Write.tsx
Normal 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;
|
@ -179,7 +179,7 @@ export default class Connection {
|
||||
}
|
||||
case "OK": {
|
||||
// feedback to broadcast call
|
||||
console.debug("OK: ", msg);
|
||||
console.debug(`${this.Address} OK: `, msg);
|
||||
const id = msg[1];
|
||||
if (this.EventsCallback.has(id)) {
|
||||
const cb = unwrap(this.EventsCallback.get(id));
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -41,6 +41,7 @@ export type RawReqFilter = {
|
||||
"#p"?: u256[];
|
||||
"#t"?: string[];
|
||||
"#d"?: string[];
|
||||
"#r"?: string[];
|
||||
search?: string;
|
||||
since?: number;
|
||||
until?: number;
|
||||
@ -68,3 +69,8 @@ export type UserMetadata = {
|
||||
export enum Lists {
|
||||
Muted = "mute",
|
||||
}
|
||||
|
||||
export interface RelaySettings {
|
||||
url: string;
|
||||
settings: { read: boolean; write: boolean };
|
||||
}
|
||||
|
@ -2,10 +2,11 @@ import "./Layout.css";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { Outlet, useLocation, useNavigate } from "react-router-dom";
|
||||
|
||||
import { randomSample } from "Util";
|
||||
import Envelope from "Icons/Envelope";
|
||||
import Bell from "Icons/Bell";
|
||||
import Search from "Icons/Search";
|
||||
|
||||
import { RootState } from "State/Store";
|
||||
import { init, setRelays } from "State/Login";
|
||||
import { System } from "Nostr/System";
|
||||
@ -147,8 +148,7 @@ export default function Layout() {
|
||||
const rsp = await fetch("https://api.nostr.watch/v1/online");
|
||||
if (rsp.ok) {
|
||||
const online: string[] = await rsp.json();
|
||||
const pickRandom = online.sort(() => (Math.random() >= 0.5 ? 1 : -1)).slice(0, 4); // pick 4 random relays
|
||||
|
||||
const pickRandom = randomSample(online, 4);
|
||||
const relayObjects = pickRandom.map(a => [a, { read: true, write: true }]);
|
||||
newRelays = Object.fromEntries(relayObjects);
|
||||
dispatch(
|
||||
|
@ -4,19 +4,21 @@ 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 RelaysMetadata from "Element/RelaysMetadata";
|
||||
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";
|
||||
import FollowButton from "Element/FollowButton";
|
||||
import { extractLnAddress, parseId, hexToBech32 } from "Util";
|
||||
import Avatar from "Element/Avatar";
|
||||
import LogoutButton from "Element/LogoutButton";
|
||||
import Timeline from "Element/Timeline";
|
||||
import Text from "Element/Text";
|
||||
import SendSats from "Element/SendSats";
|
||||
@ -46,6 +48,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 +72,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 +89,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 +212,9 @@ export default function ProfilePage() {
|
||||
case BLOCKED: {
|
||||
return isMe ? <BlockList variant="blocked" /> : null;
|
||||
}
|
||||
case RELAYS: {
|
||||
return <RelaysMetadata relays={relays} />;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -229,7 +240,6 @@ export default function ProfilePage() {
|
||||
)}
|
||||
{isMe ? (
|
||||
<>
|
||||
<LogoutButton />
|
||||
<button type="button" onClick={() => navigate("/settings")}>
|
||||
<FormattedMessage {...messages.Settings} />
|
||||
</button>
|
||||
@ -282,7 +292,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()}
|
||||
|
@ -33,4 +33,7 @@ export default defineMessages({
|
||||
NostrPlebsNip: {
|
||||
defaultMessage: `Nostr Plebs is one of the first NIP-05 providers in the space and offers a good collection of domains at reasonable prices`,
|
||||
},
|
||||
Relays: {
|
||||
defaultMessage: "Relays",
|
||||
},
|
||||
});
|
||||
|
@ -5,6 +5,7 @@ import { FormattedMessage } from "react-intl";
|
||||
|
||||
import { DefaultImgProxy, setPreferences, UserPreferences } from "State/Login";
|
||||
import { RootState } from "State/Store";
|
||||
import emoji from "@jukben/emoji-search";
|
||||
|
||||
import messages from "./messages";
|
||||
import { unwrap } from "Util";
|
||||
@ -197,6 +198,40 @@ const PreferencesPage = () => {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="card flex">
|
||||
<div className="flex f-col f-grow">
|
||||
<div>
|
||||
<FormattedMessage {...messages.ReactionEmoji} />
|
||||
</div>
|
||||
<small>
|
||||
<FormattedMessage {...messages.ReactionEmojiHelp} />
|
||||
</small>
|
||||
</div>
|
||||
<div>
|
||||
<select
|
||||
className="emoji-selector"
|
||||
value={perf.reactionEmoji}
|
||||
onChange={e =>
|
||||
dispatch(
|
||||
setPreferences({
|
||||
...perf,
|
||||
reactionEmoji: e.target.value,
|
||||
} as UserPreferences)
|
||||
)
|
||||
}>
|
||||
<option value="+">
|
||||
+ <FormattedMessage {...messages.Default} />
|
||||
</option>
|
||||
{emoji("").map(({ name, char }) => {
|
||||
return (
|
||||
<option value={char}>
|
||||
{name} {char}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div className="card flex">
|
||||
<div className="flex f-col f-grow">
|
||||
<div>
|
||||
|
@ -2,6 +2,7 @@ import { useState } from "react";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
|
||||
import { randomSample } from "Util";
|
||||
import Relay from "Element/Relay";
|
||||
import useEventPublisher from "Feed/EventPublisher";
|
||||
import { RootState } from "State/Store";
|
||||
@ -20,6 +21,14 @@ const RelaySettingsPage = () => {
|
||||
const ev = await publisher.saveRelays();
|
||||
publisher.broadcast(ev);
|
||||
publisher.broadcastForBootstrap(ev);
|
||||
try {
|
||||
const onlineRelays = await fetch("https://api.nostr.watch/v1/online").then(r => r.json());
|
||||
const settingsEv = await publisher.saveRelaysSettings();
|
||||
const rs = Object.keys(relays).concat(randomSample(onlineRelays, 20));
|
||||
publisher.broadcastAll(settingsEv, rs);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
function addRelay() {
|
||||
|
@ -55,4 +55,6 @@ export default defineMessages({
|
||||
DisplayName: { defaultMessage: "Display name" },
|
||||
Buy: { defaultMessage: "Buy" },
|
||||
Nip05: { defaultMessage: "NIP-05" },
|
||||
ReactionEmoji: { defaultMessage: "Reaction emoji" },
|
||||
ReactionEmojiHelp: { defaultMessage: "Emoji to send when reactiong to a note" },
|
||||
});
|
||||
|
@ -26,6 +26,11 @@ export interface UserPreferences {
|
||||
*/
|
||||
enableReactions: boolean;
|
||||
|
||||
/**
|
||||
* Reaction emoji
|
||||
*/
|
||||
reactionEmoji: string;
|
||||
|
||||
/**
|
||||
* Automatically load media (show link only) (bandwidth/privacy)
|
||||
*/
|
||||
@ -176,6 +181,7 @@ export const InitState = {
|
||||
dmInteraction: 0,
|
||||
preferences: {
|
||||
enableReactions: true,
|
||||
reactionEmoji: "+",
|
||||
autoLoadMedia: "follows-only",
|
||||
theme: "system",
|
||||
confirmReposts: false,
|
||||
|
@ -185,3 +185,8 @@ export function unwrap<T>(v: T | undefined | null): T {
|
||||
}
|
||||
return v;
|
||||
}
|
||||
|
||||
export function randomSample<T>(coll: T[], size: number) {
|
||||
const random = [...coll];
|
||||
return random.sort(() => (Math.random() >= 0.5 ? 1 : -1)).slice(0, size);
|
||||
}
|
||||
|
@ -383,6 +383,9 @@
|
||||
"e7qqly": {
|
||||
"string": "Mark All Read"
|
||||
},
|
||||
"eHAneD": {
|
||||
"string": "Reaction emoji"
|
||||
},
|
||||
"eJj8HD": {
|
||||
"string": "Get Verified"
|
||||
},
|
||||
@ -404,6 +407,9 @@
|
||||
"gDZkld": {
|
||||
"string": "Snort is a Nostr UI, nostr is a decentralised protocol for saving and distributing \"notes\"."
|
||||
},
|
||||
"gDzDRs": {
|
||||
"string": "Emoji to send when reactiong to a note"
|
||||
},
|
||||
"gjBiyj": {
|
||||
"string": "Loading..."
|
||||
},
|
||||
|
@ -127,6 +127,7 @@
|
||||
"d7d0/x": "LN Address",
|
||||
"dOQCL8": "Display name",
|
||||
"e7qqly": "Mark All Read",
|
||||
"eHAneD": "Reaction emoji",
|
||||
"eJj8HD": "Get Verified",
|
||||
"eR3YIn": "Posts",
|
||||
"filwqD": "Read",
|
||||
@ -134,6 +135,7 @@
|
||||
"g5pX+a": "About",
|
||||
"gBdUXk": "Save your keys!",
|
||||
"gDZkld": "Snort is a Nostr UI, nostr is a decentralised protocol for saving and distributing \"notes\".",
|
||||
"gDzDRs": "Emoji to send when reactiong to a note",
|
||||
"gjBiyj": "Loading...",
|
||||
"hCUivF": "Notes will stream in real time into global and posts tab",
|
||||
"hK5ZDk": "the world",
|
||||
@ -209,4 +211,4 @@
|
||||
"zjJZBd": "You're ready!",
|
||||
"zonsdq": "Failed to load LNURL service",
|
||||
"zvCDao": "Automatically show latest notes"
|
||||
}
|
||||
}
|
||||
|
@ -111,6 +111,7 @@
|
||||
"Pages.Notes": "Notas",
|
||||
"Pages.Posts": "Notas",
|
||||
"Pages.Reactions": "Reacciones",
|
||||
"Pages.Relays": "",
|
||||
"Pages.Sats": "{n} {n, plural, =1 {sat} other {sats}}",
|
||||
"Pages.Search": "Búsqueda",
|
||||
"Pages.SearchPlaceholder": "Buscar...",
|
||||
@ -201,6 +202,8 @@
|
||||
"Pages.settings.Preferences": "Preferencias",
|
||||
"Pages.settings.PrivateKey": "Tu Clave Privada (no la compartas con nadie) es",
|
||||
"Pages.settings.Profile": "Perfil",
|
||||
"Pages.settings.ReactionEmoji": "",
|
||||
"Pages.settings.ReactionEmojiHelp": "",
|
||||
"Pages.settings.Relays": "Relays",
|
||||
"Pages.settings.Remove": "Eliminar",
|
||||
"Pages.settings.Save": "Guardar",
|
||||
|
@ -110,6 +110,7 @@
|
||||
"Pages.Notes": "Notes",
|
||||
"Pages.Posts": "Publications",
|
||||
"Pages.Reactions": "Réactions",
|
||||
"Pages.Relays": "",
|
||||
"Pages.Sats": "{n} {n, plural, =1 {sat} other {sats}}",
|
||||
"Pages.Search": "Chercher",
|
||||
"Pages.SearchPlaceholder": "Chercher...",
|
||||
@ -200,6 +201,8 @@
|
||||
"Pages.settings.Preferences": "Préférences",
|
||||
"Pages.settings.PrivateKey": "Votre Clé Privée Est (ne la partagez avec personne)",
|
||||
"Pages.settings.Profile": "Profil",
|
||||
"Pages.settings.ReactionEmoji": "",
|
||||
"Pages.settings.ReactionEmojiHelp": "",
|
||||
"Pages.settings.Relays": "Relais",
|
||||
"Pages.settings.Remove": "Retirer",
|
||||
"Pages.settings.Save": "Sauvegarder",
|
||||
|
@ -112,6 +112,7 @@
|
||||
"Pages.Posts": "ポスト",
|
||||
"Pages.Reactions": "リアクション",
|
||||
"Pages.Sats": "{n} {n, plural, =1 {sat} other {sats}}",
|
||||
"Pages.Relays": "",
|
||||
"Pages.Search": "検索",
|
||||
"Pages.SearchPlaceholder": "検索する",
|
||||
"Pages.Settings": "設定",
|
||||
@ -201,6 +202,8 @@
|
||||
"Pages.settings.Preferences": "ユーザー設定",
|
||||
"Pages.settings.PrivateKey": "あなたの秘密鍵(誰とも共有しないこと)",
|
||||
"Pages.settings.Profile": "プロフィール",
|
||||
"Pages.settings.ReactionEmoji": "",
|
||||
"Pages.settings.ReactionEmojiHelp": "",
|
||||
"Pages.settings.Relays": "リレー",
|
||||
"Pages.settings.Remove": "削除",
|
||||
"Pages.settings.Save": "保存",
|
||||
|
@ -111,6 +111,7 @@
|
||||
"Pages.Notes": "",
|
||||
"Pages.Posts": "",
|
||||
"Pages.Reactions": "",
|
||||
"Pages.Relays": "",
|
||||
"Pages.Sats": "",
|
||||
"Pages.Search": "",
|
||||
"Pages.SearchPlaceholder": "",
|
||||
@ -201,6 +202,8 @@
|
||||
"Pages.settings.Preferences": "",
|
||||
"Pages.settings.PrivateKey": "",
|
||||
"Pages.settings.Profile": "",
|
||||
"Pages.settings.ReactionEmoji": "",
|
||||
"Pages.settings.ReactionEmojiHelp": "",
|
||||
"Pages.settings.Relays": "",
|
||||
"Pages.settings.Remove": "",
|
||||
"Pages.settings.Save": "",
|
||||
|
Loading…
x
Reference in New Issue
Block a user