From 5153f5c90a463577be7d3ed1178743b20a36cbcf Mon Sep 17 00:00:00 2001 From: Alejandro Date: Fri, 10 Feb 2023 20:23:52 +0100 Subject: [PATCH] NIP-65: Relay list metada (#238) --- .babelrc | 8 ++---- README.md | 1 + src/Element/NoteFooter.tsx | 4 ++- src/Element/RelaysMetadata.css | 36 ++++++++++++++++++++++++ src/Element/RelaysMetadata.tsx | 44 ++++++++++++++++++++++++++++++ src/Element/Zap.css | 4 +-- src/Feed/EventPublisher.ts | 28 +++++++++++++++++++ src/Feed/RelaysFeed.tsx | 36 ++++++++++++++++++++++++ src/Icons/Gear.tsx | 6 ++-- src/Icons/Read.tsx | 17 ++++++++++++ src/Icons/Write.tsx | 17 ++++++++++++ src/Nostr/Connection.ts | 2 +- src/Nostr/EventKind.ts | 1 + src/Nostr/Subscriptions.ts | 9 ++++++ src/Nostr/index.ts | 6 ++++ src/Pages/Layout.tsx | 6 ++-- src/Pages/ProfilePage.tsx | 17 ++++++++++-- src/Pages/messages.ts | 3 ++ src/Pages/settings/Preferences.tsx | 35 ++++++++++++++++++++++++ src/Pages/settings/Relays.tsx | 9 ++++++ src/Pages/settings/messages.ts | 2 ++ src/State/Login.ts | 6 ++++ src/Util.ts | 5 ++++ src/lang.json | 6 ++++ src/translations/en.json | 4 ++- src/translations/es.json | 3 ++ src/translations/fr.json | 3 ++ src/translations/ja.json | 3 ++ src/translations/zh.json | 3 ++ 29 files changed, 304 insertions(+), 20 deletions(-) create mode 100644 src/Element/RelaysMetadata.css create mode 100644 src/Element/RelaysMetadata.tsx create mode 100644 src/Feed/RelaysFeed.tsx create mode 100644 src/Icons/Read.tsx create mode 100644 src/Icons/Write.tsx diff --git a/.babelrc b/.babelrc index 50f0f69e..a05f7d9c 100644 --- a/.babelrc +++ b/.babelrc @@ -1,7 +1,3 @@ { - "plugins": [ - [ - "formatjs" - ] - ] -} \ No newline at end of file + "plugins": [["formatjs"]] +} diff --git a/README.md b/README.md index 54f68e5b..7d28d53d 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/src/Element/NoteFooter.tsx b/src/Element/NoteFooter.tsx index 44b90586..4b0a7de5 100644 --- a/src/Element/NoteFooter.tsx +++ b/src/Element/NoteFooter.tsx @@ -161,7 +161,9 @@ export default function NoteFooter(props: NoteFooterProps) { } return ( <> -
react("+")}> +
react(prefs.reactionEmoji)}>
diff --git a/src/Element/RelaysMetadata.css b/src/Element/RelaysMetadata.css new file mode 100644 index 00000000..8bfb2fa2 --- /dev/null +++ b/src/Element/RelaysMetadata.css @@ -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; + } +} diff --git a/src/Element/RelaysMetadata.tsx b/src/Element/RelaysMetadata.tsx new file mode 100644 index 00000000..54ea5281 --- /dev/null +++ b/src/Element/RelaysMetadata.tsx @@ -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 setFaviconUrl(Nostrich)} />; +}; + +interface RelaysMetadataProps { + relays: RelaySettings[]; +} + +const RelaysMetadata = ({ relays }: RelaysMetadataProps) => { + return ( +
+ {relays?.map(({ url, settings }) => { + return ( +
+ + {url} +
+ + +
+
+ ); + })} +
+ ); +}; + +export default RelaysMetadata; diff --git a/src/Element/Zap.css b/src/Element/Zap.css index 2dd2c9e0..d934e65c 100644 --- a/src/Element/Zap.css +++ b/src/Element/Zap.css @@ -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; } } diff --git a/src/Feed/EventPublisher.ts b/src/Feed/EventPublisher.ts index eae20568..cbb83b2a 100644 --- a/src/Feed/EventPublisher.ts +++ b/src/Feed/EventPublisher.ts @@ -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) => { if (pubKey) { const ev = NEvent.ForPubKey(pubKey); diff --git a/src/Feed/RelaysFeed.tsx b/src/Feed/RelaysFeed.tsx new file mode 100644 index 00000000..eb6d1221 --- /dev/null +++ b/src/Feed/RelaysFeed.tsx @@ -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[]); +} diff --git a/src/Icons/Gear.tsx b/src/Icons/Gear.tsx index 6c29be98..f12950f0 100644 --- a/src/Icons/Gear.tsx +++ b/src/Icons/Gear.tsx @@ -13,9 +13,9 @@ const Gear = (props: IconProps) => { ); diff --git a/src/Icons/Read.tsx b/src/Icons/Read.tsx new file mode 100644 index 00000000..e19a66b5 --- /dev/null +++ b/src/Icons/Read.tsx @@ -0,0 +1,17 @@ +import IconProps from "./IconProps"; + +const Read = (props: IconProps) => { + return ( + + + + ); +}; + +export default Read; diff --git a/src/Icons/Write.tsx b/src/Icons/Write.tsx new file mode 100644 index 00000000..9c016e57 --- /dev/null +++ b/src/Icons/Write.tsx @@ -0,0 +1,17 @@ +import IconProps from "./IconProps"; + +const Write = (props: IconProps) => { + return ( + + + + ); +}; + +export default Write; diff --git a/src/Nostr/Connection.ts b/src/Nostr/Connection.ts index 12913e85..d07cead0 100644 --- a/src/Nostr/Connection.ts +++ b/src/Nostr/Connection.ts @@ -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)); diff --git a/src/Nostr/EventKind.ts b/src/Nostr/EventKind.ts index 8d793efb..ec16373b 100644 --- a/src/Nostr/EventKind.ts +++ b/src/Nostr/EventKind.ts @@ -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 diff --git a/src/Nostr/Subscriptions.ts b/src/Nostr/Subscriptions.ts index dc57283a..d22050d8 100644 --- a/src/Nostr/Subscriptions.ts +++ b/src/Nostr/Subscriptions.ts @@ -47,6 +47,11 @@ export class Subscriptions { */ DTags?: Set; + /** + * A litst of "r" tags to search + */ + RTags?: Set; + /** * 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; } diff --git a/src/Nostr/index.ts b/src/Nostr/index.ts index b59491ff..f3949914 100644 --- a/src/Nostr/index.ts +++ b/src/Nostr/index.ts @@ -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 }; +} diff --git a/src/Pages/Layout.tsx b/src/Pages/Layout.tsx index 4697e5d8..db90a88a 100644 --- a/src/Pages/Layout.tsx +++ b/src/Pages/Layout.tsx @@ -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( diff --git a/src/Pages/ProfilePage.tsx b/src/Pages/ProfilePage.tsx index 7db6adfe..523af5e9 100644 --- a/src/Pages/ProfilePage.tsx +++ b/src/Pages/ProfilePage.tsx @@ -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(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 ? : null; } + case RELAYS: { + return ; + } } } @@ -229,7 +240,6 @@ export default function ProfilePage() { )} {isMe ? ( <> - @@ -282,7 +292,8 @@ export default function ProfilePage() {
- {[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)}
{tabContent()} diff --git a/src/Pages/messages.ts b/src/Pages/messages.ts index 8510cf83..e23f47f7 100644 --- a/src/Pages/messages.ts +++ b/src/Pages/messages.ts @@ -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", + }, }); diff --git a/src/Pages/settings/Preferences.tsx b/src/Pages/settings/Preferences.tsx index 2f63304e..71db4d9c 100644 --- a/src/Pages/settings/Preferences.tsx +++ b/src/Pages/settings/Preferences.tsx @@ -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 = () => { /> +
+
+
+ +
+ + + +
+
+ +
+
diff --git a/src/Pages/settings/Relays.tsx b/src/Pages/settings/Relays.tsx index 455e2e3b..e586f16a 100644 --- a/src/Pages/settings/Relays.tsx +++ b/src/Pages/settings/Relays.tsx @@ -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() { diff --git a/src/Pages/settings/messages.ts b/src/Pages/settings/messages.ts index c28bc88b..11cdf501 100644 --- a/src/Pages/settings/messages.ts +++ b/src/Pages/settings/messages.ts @@ -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" }, }); diff --git a/src/State/Login.ts b/src/State/Login.ts index 202aac6a..8e892dd3 100644 --- a/src/State/Login.ts +++ b/src/State/Login.ts @@ -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, diff --git a/src/Util.ts b/src/Util.ts index 2e14ec2b..b63c2168 100644 --- a/src/Util.ts +++ b/src/Util.ts @@ -185,3 +185,8 @@ export function unwrap(v: T | undefined | null): T { } return v; } + +export function randomSample(coll: T[], size: number) { + const random = [...coll]; + return random.sort(() => (Math.random() >= 0.5 ? 1 : -1)).slice(0, size); +} diff --git a/src/lang.json b/src/lang.json index dbcdb05b..26eb6253 100644 --- a/src/lang.json +++ b/src/lang.json @@ -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..." }, diff --git a/src/translations/en.json b/src/translations/en.json index d0e89c97..b839bbd3 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -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" -} \ No newline at end of file +} diff --git a/src/translations/es.json b/src/translations/es.json index c9590df8..88228f03 100644 --- a/src/translations/es.json +++ b/src/translations/es.json @@ -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", diff --git a/src/translations/fr.json b/src/translations/fr.json index 074fdf79..3ff3305f 100644 --- a/src/translations/fr.json +++ b/src/translations/fr.json @@ -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", diff --git a/src/translations/ja.json b/src/translations/ja.json index 2171e6bd..ae1c6a44 100644 --- a/src/translations/ja.json +++ b/src/translations/ja.json @@ -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": "保存", diff --git a/src/translations/zh.json b/src/translations/zh.json index b4f6c6cb..e8a00cef 100644 --- a/src/translations/zh.json +++ b/src/translations/zh.json @@ -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": "",