refactor: RequestBuilder

This commit is contained in:
2023-03-28 15:34:01 +01:00
parent 1bf6c7031e
commit 465c59ea20
77 changed files with 3141 additions and 2343 deletions

View File

@ -1,17 +0,0 @@
import { useParams } from "react-router-dom";
import Thread from "Element/Thread";
import useThreadFeed from "Feed/ThreadFeed";
import { parseNostrLink, unwrap } from "Util";
export default function EventPage() {
const params = useParams();
const link = parseNostrLink(params.id ?? "");
const thread = useThreadFeed(unwrap(link));
if (link) {
return <Thread key={link.id} notes={thread.notes} selected={link.id} />;
} else {
return <b>{params.id}</b>;
}
}

View File

@ -2,8 +2,12 @@ import "./Layout.css";
import { useEffect, useMemo, useState } from "react";
import { useDispatch, useSelector } from "react-redux";
import { Outlet, useLocation, useNavigate } from "react-router-dom";
import { FormattedMessage } from "react-intl";
import { randomSample } from "Util";
import { RelaySettings } from "@snort/nostr";
import messages from "./messages";
import { bech32ToHex, randomSample } from "Util";
import Icon from "Icons/Icon";
import { RootState } from "State/Store";
import { init, setRelays } from "State/Login";
@ -11,34 +15,21 @@ import { System } from "System";
import ProfileImage from "Element/ProfileImage";
import useLoginFeed from "Feed/LoginFeed";
import { totalUnread } from "Pages/MessagesPage";
import { SearchRelays, SnortPubKey } from "Const";
import useEventPublisher from "Feed/EventPublisher";
import useModeration from "Hooks/useModeration";
import { bech32ToHex } from "Util";
import { NoteCreator } from "Element/NoteCreator";
import { RelaySettings } from "@snort/nostr";
import { FormattedMessage } from "react-intl";
import messages from "./messages";
import { db } from "Db";
import { UserCache } from "State/Users/UserCache";
import { FollowsRelays } from "State/Relays";
import useEventPublisher from "Feed/EventPublisher";
import { SnortPubKey } from "Const";
import SubDebug from "Element/SubDebug";
export default function Layout() {
const location = useLocation();
const [show, setShow] = useState(false);
const dispatch = useDispatch();
const navigate = useNavigate();
const {
loggedOut,
publicKey,
relays,
latestNotification,
readNotifications,
dms,
preferences,
newUserKey,
dmInteraction,
} = useSelector((s: RootState) => s.login);
const { isMuted } = useModeration();
const { loggedOut, publicKey, relays, preferences, newUserKey } = useSelector((s: RootState) => s.login);
const [pageClass, setPageClass] = useState("page");
const pub = useEventPublisher();
useLoginFeed();
@ -61,35 +52,22 @@ export default function Layout() {
}
}, [location]);
const hasNotifications = useMemo(
() => latestNotification > readNotifications,
[latestNotification, readNotifications]
);
const unreadDms = useMemo(
() =>
publicKey
? totalUnread(
dms.filter(a => !isMuted(a.pubkey)),
publicKey
)
: 0,
[dms, publicKey, dmInteraction]
);
useEffect(() => {
System.HandleAuth = pub.nip42Auth;
}, [pub]);
useEffect(() => {
if (relays) {
for (const [k, v] of Object.entries(relays)) {
System.ConnectToRelay(k, v);
}
for (const [k] of System.Sockets) {
if (!relays[k] && !SearchRelays.has(k)) {
System.DisconnectRelay(k);
(async () => {
for (const [k, v] of Object.entries(relays)) {
await System.ConnectToRelay(k, v);
}
}
for (const [k, c] of System.Sockets) {
if (!relays[k] && !c.Ephemeral) {
System.DisconnectRelay(k);
}
}
})();
}
}, [relays]);
@ -124,6 +102,7 @@ export default function Layout() {
db.ready = a;
if (a) {
await UserCache.preload();
await FollowsRelays.preload();
}
console.debug(`Using db: ${a ? "IndexedDB" : "In-Memory"}`);
dispatch(init());
@ -173,44 +152,6 @@ export default function Layout() {
}
}, [newUserKey]);
async function goToNotifications(e: React.MouseEvent) {
e.stopPropagation();
// request permissions to send notifications
if ("Notification" in window) {
try {
if (Notification.permission !== "granted") {
const res = await Notification.requestPermission();
console.debug(res);
}
} catch (e) {
console.error(e);
}
}
navigate("/notifications");
}
function accountHeader() {
return (
<div className="header-actions">
<div className="btn btn-rnd" onClick={() => navigate("/wallet")}>
<Icon name="bitcoin" />
</div>
<div className="btn btn-rnd" onClick={() => navigate("/search")}>
<Icon name="search" size={20} />
</div>
<div className="btn btn-rnd" onClick={() => navigate("/messages")}>
<Icon name="envelope" size={20} />
{unreadDms > 0 && <span className="has-unread"></span>}
</div>
<div className="btn btn-rnd" onClick={goToNotifications}>
<Icon name="bell" size={20} />
{hasNotifications && <span className="has-unread"></span>}
</div>
<ProfileImage pubkey={publicKey || ""} showUsername={false} />
</div>
);
}
if (typeof loggedOut !== "boolean") {
return null;
}
@ -223,7 +164,7 @@ export default function Layout() {
</div>
<div>
{publicKey ? (
accountHeader()
<AccountHeader />
) : (
<button type="button" onClick={() => navigate("/login")}>
<FormattedMessage {...messages.Login} />
@ -242,6 +183,65 @@ export default function Layout() {
<NoteCreator replyTo={undefined} autoFocus={true} show={show} setShow={setShow} />
</>
)}
{window.localStorage.getItem("debug") && <SubDebug />}
</div>
);
}
const AccountHeader = () => {
const navigate = useNavigate();
const { isMuted } = useModeration();
const { publicKey, latestNotification, readNotifications, dms } = useSelector((s: RootState) => s.login);
const hasNotifications = useMemo(
() => latestNotification > readNotifications,
[latestNotification, readNotifications]
);
const unreadDms = useMemo(
() =>
publicKey
? totalUnread(
dms.filter(a => !isMuted(a.pubkey)),
publicKey
)
: 0,
[dms, publicKey]
);
async function goToNotifications(e: React.MouseEvent) {
e.stopPropagation();
// request permissions to send notifications
if ("Notification" in window) {
try {
if (Notification.permission !== "granted") {
const res = await Notification.requestPermission();
console.debug(res);
}
} catch (e) {
console.error(e);
}
}
navigate("/notifications");
}
return (
<div className="header-actions">
<div className="btn btn-rnd" onClick={() => navigate("/wallet")}>
<Icon name="bitcoin" />
</div>
<div className="btn btn-rnd" onClick={() => navigate("/search")}>
<Icon name="search" />
</div>
<div className="btn btn-rnd" onClick={() => navigate("/messages")}>
<Icon name="envelope" />
{unreadDms > 0 && <span className="has-unread"></span>}
</div>
<div className="btn btn-rnd" onClick={goToNotifications}>
<Icon name="bell" />
{hasNotifications && <span className="has-unread"></span>}
</div>
<ProfileImage pubkey={publicKey || ""} showUsername={false} />
</div>
);
};

View File

@ -3,9 +3,9 @@ import { useEffect, useState } from "react";
import { useIntl, FormattedMessage } from "react-intl";
import { useSelector } from "react-redux";
import { useNavigate, useParams } from "react-router-dom";
import { encodeTLV, NostrPrefix } from "@snort/nostr";
import { encodeTLV, EventKind, HexKey, NostrPrefix } from "@snort/nostr";
import { parseNostrLink, unwrap } from "Util";
import { parseNostrLink, getReactions, unwrap } from "Util";
import { formatShort } from "Number";
import Note from "Element/Note";
import Bookmarks from "Element/Bookmarks";
@ -57,13 +57,53 @@ const BLOCKED = 6;
const RELAYS = 7;
const BOOKMARKS = 8;
function ZapsProfileTab({ id }: { id: HexKey }) {
const zaps = useZapsFeed(id);
const zapsTotal = zaps.reduce((acc, z) => acc + z.amount, 0);
return (
<div className="main-content">
<div className="zaps-total">
<FormattedMessage {...messages.Sats} values={{ n: formatShort(zapsTotal) }} />
</div>
{zaps.map(z => (
<ZapElement showZapped={false} zap={z} />
))}
</div>
);
}
function FollowersTab({ id }: { id: HexKey }) {
const followers = useFollowersFeed(id);
return <FollowsList pubkeys={followers} showAbout={true} />;
}
function FollowsTab({ id }: { id: HexKey }) {
const follows = useFollowsFeed(id);
return <FollowsList pubkeys={follows} showAbout={true} />;
}
function RelaysTab({ id }: { id: HexKey }) {
const relays = useRelaysFeed(id);
return <RelaysMetadata relays={relays} />;
}
function BookMarksTab({ id }: { id: HexKey }) {
const bookmarks = useBookmarkFeed(id);
return <Bookmarks pubkey={id} bookmarks={bookmarks} related={bookmarks} />;
}
export default function ProfilePage() {
const { formatMessage } = useIntl();
const params = useParams();
const navigate = useNavigate();
const [id, setId] = useState<string>();
const user = useUserProfile(id);
const loginPubKey = useSelector((s: RootState) => s.login.publicKey);
const { publicKey: loginPubKey, follows } = useSelector((s: RootState) => {
return {
publicKey: s.login.publicKey,
follows: s.login.follows,
};
});
const isMe = loginPubKey === id;
const [showLnQr, setShowLnQr] = useState<boolean>(false);
const [showProfileQr, setShowProfileQr] = useState<boolean>(false);
@ -80,34 +120,25 @@ export default function ProfilePage() {
user?.website && !user.website.startsWith("http") ? "https://" + user.website : user?.website || "";
// feeds
const { blocked } = useModeration();
const { notes: pinned, related: pinRelated } = usePinnedFeed(id);
const { notes: bookmarks, related: bookmarkRelated } = useBookmarkFeed(id);
const relays = useRelaysFeed(id);
const zaps = useZapsFeed(id);
const zapsTotal = zaps.reduce((acc, z) => acc + z.amount, 0);
const followers = useFollowersFeed(id);
const follows = useFollowsFeed(id);
const pinned = usePinnedFeed(id);
const muted = useMutedFeed(id);
const badges = useProfileBadges(id);
// tabs
const ProfileTab = {
Notes: { text: formatMessage(messages.Notes), value: NOTES },
Reactions: { text: formatMessage(messages.Reactions), value: REACTIONS },
Followers: { text: formatMessage(messages.FollowersCount, { n: followers.length }), value: FOLLOWERS },
Follows: { text: formatMessage(messages.FollowsCount, { n: follows.length }), value: FOLLOWS },
Zaps: { text: formatMessage(messages.ZapsCount, { n: zaps.length }), value: ZAPS },
Muted: { text: formatMessage(messages.MutedCount, { n: muted.length }), value: MUTED },
Followers: { text: formatMessage(messages.Followers), value: FOLLOWERS },
Follows: { text: formatMessage(messages.Follows), value: FOLLOWS },
Zaps: { text: formatMessage(messages.Zaps), value: ZAPS },
Muted: { text: formatMessage(messages.Muted), value: MUTED },
Blocked: { text: formatMessage(messages.BlockedCount, { n: blocked.length }), value: BLOCKED },
Relays: { text: formatMessage(messages.RelaysCount, { n: relays.length }), value: RELAYS },
Bookmarks: { text: formatMessage(messages.BookmarksCount, { n: bookmarks.length }), value: BOOKMARKS },
Relays: { text: formatMessage(messages.Relays), value: RELAYS },
Bookmarks: { text: formatMessage(messages.Bookmarks), value: BOOKMARKS },
};
const [tab, setTab] = useState<Tab>(ProfileTab.Notes);
const optionalTabs = [
zapsTotal > 0 && ProfileTab.Zaps,
relays.length > 0 && ProfileTab.Relays,
bookmarks.length > 0 && ProfileTab.Bookmarks,
muted.length > 0 && ProfileTab.Muted,
].filter(a => unwrap(a)) as Tab[];
const optionalTabs = [ProfileTab.Zaps, ProfileTab.Relays, ProfileTab.Bookmarks, ProfileTab.Muted].filter(a =>
unwrap(a)
) as Tab[];
const horizontalScroll = useHorizontalScroll();
useEffect(() => {
@ -190,16 +221,18 @@ export default function ProfilePage() {
return (
<>
<div className="main-content">
{pinned.map(n => {
return (
<Note
key={`pinned-${n.id}`}
data={n}
related={pinRelated}
options={{ showTime: false, showPinned: true, canUnpin: id === loginPubKey }}
/>
);
})}
{pinned
.filter(a => a.kind === EventKind.TextNote)
.map(n => {
return (
<Note
key={`pinned-${n.id}`}
data={n}
related={getReactions(pinned, n.id)}
options={{ showTime: false, showPinned: true, canUnpin: id === loginPubKey }}
/>
);
})}
</div>
<Timeline
key={id}
@ -216,23 +249,17 @@ export default function ProfilePage() {
</>
);
case ZAPS: {
return (
<div className="main-content">
<div className="zaps-total">
<FormattedMessage {...messages.Sats} values={{ n: formatShort(zapsTotal) }} />
</div>
{zaps.map(z => (
<ZapElement showZapped={false} zap={z} />
))}
</div>
);
return <ZapsProfileTab id={id} />;
}
case FOLLOWS: {
return <FollowsList pubkeys={follows} showFollowAll={!isMe} showAbout={!isMe} />;
if (isMe) {
return <FollowsList pubkeys={follows} showFollowAll={!isMe} showAbout={false} />;
} else {
return <FollowsTab id={id} />;
}
}
case FOLLOWERS: {
return <FollowsList pubkeys={followers} showAbout={true} />;
return <FollowersTab id={id} />;
}
case MUTED: {
return <MutedList pubkeys={muted} />;
@ -241,10 +268,10 @@ export default function ProfilePage() {
return <BlockList />;
}
case RELAYS: {
return <RelaysMetadata relays={relays} />;
return <RelaysTab id={id} />;
}
case BOOKMARKS: {
return <Bookmarks pubkey={id} bookmarks={bookmarks} related={bookmarkRelated} />;
return <BookMarksTab id={id} />;
}
}
}
@ -260,8 +287,7 @@ export default function ProfilePage() {
function renderIcons() {
if (!id) return;
const firstRelay = relays.find(a => a.settings.write)?.url;
const link = encodeTLV(id, NostrPrefix.Profile, firstRelay ? [firstRelay] : undefined);
const link = encodeTLV(id, NostrPrefix.Profile);
return (
<div className="icon-actions">
<IconButton onClick={() => setShowProfileQr(true)}>

View File

@ -1,7 +1,7 @@
import "./Root.css";
import { useEffect, useState } from "react";
import { useEffect, useMemo, useState } from "react";
import { useSelector } from "react-redux";
import { Link } from "react-router-dom";
import { Link, Outlet, RouteObject, useLocation, useNavigate, useParams } from "react-router-dom";
import { useIntl, FormattedMessage } from "react-intl";
import Tabs, { Tab } from "Element/Tabs";
@ -9,7 +9,7 @@ import { RootState } from "State/Store";
import Timeline from "Element/Timeline";
import { System } from "System";
import { TimelineSubject } from "Feed/TimelineFeed";
import { debounce, getRelayName, sha256, unwrap } from "Util";
import { debounce, getRelayName, sha256, unixNow, unwrap } from "Util";
import messages from "./messages";
@ -20,58 +20,98 @@ interface RelayOption {
export default function RootPage() {
const { formatMessage } = useIntl();
const { loggedOut, publicKey: pubKey, follows, tags, relays, preferences } = useSelector((s: RootState) => s.login);
const navigate = useNavigate();
const location = useLocation();
const { publicKey: pubKey, tags, preferences } = useSelector((s: RootState) => s.login);
const RootTab: Record<string, Tab> = {
Posts: {
text: formatMessage(messages.Posts),
value: 0,
data: "/posts",
},
PostsAndReplies: {
text: formatMessage(messages.Conversations),
value: 1,
data: "/conversations",
},
Global: {
text: formatMessage(messages.Global),
value: 2,
data: "/global",
},
};
const [tab, setTab] = useState<Tab>(() => {
switch (preferences.defaultRootTab) {
case "conversations":
const tab = useMemo(() => {
const pTab = location.pathname.split("/").slice(-1)[0];
switch (pTab) {
case "conversations": {
return RootTab.PostsAndReplies;
case "global":
}
case "global": {
return RootTab.Global;
default:
}
default: {
return RootTab.Posts;
}
}
});
const [relay, setRelay] = useState<RelayOption>();
const [allRelays, setAllRelays] = useState<RelayOption[]>();
}, [location]);
useEffect(() => {
if (location.pathname === "/") {
navigate(unwrap(preferences.defaultRootTab ?? tab.data), {
replace: true,
});
}
}, [location]);
const tagTabs = tags.map((t, idx) => {
return { text: `#${t}`, value: idx + 3 };
return { text: `#${t}`, value: idx + 3, data: `/tag/${t}` };
});
const tabs = [RootTab.Posts, RootTab.PostsAndReplies, RootTab.Global, ...tagTabs];
const isGlobal = loggedOut || tab.value === RootTab.Global.value;
function followHints() {
if (follows?.length === 0 && pubKey && tab !== RootTab.Global) {
return (
<FormattedMessage
{...messages.NoFollows}
values={{
newUsersPage: (
<Link to={"/new/discover"}>
<FormattedMessage {...messages.NewUsers} />
</Link>
),
}}
/>
);
}
return (
<>
<div className="main-content">
{pubKey && <Tabs tabs={tabs} tab={tab} setTab={t => navigate(unwrap(t.data))} />}
</div>
<Outlet />
</>
);
}
const FollowsHint = () => {
const { publicKey: pubKey, follows } = useSelector((s: RootState) => s.login);
if (follows?.length === 0 && pubKey) {
return (
<FormattedMessage
{...messages.NoFollows}
values={{
newUsersPage: (
<Link to={"/new/discover"}>
<FormattedMessage {...messages.NewUsers} />
</Link>
),
}}
/>
);
}
return null;
};
const GlobalTab = () => {
const { relays } = useSelector((s: RootState) => s.login);
const [relay, setRelay] = useState<RelayOption>();
const [allRelays, setAllRelays] = useState<RelayOption[]>();
const [now] = useState(unixNow());
const subject: TimelineSubject = {
type: "global",
items: [],
discriminator: `all-${sha256(relay?.url ?? "").slice(0, 12)}`,
};
function globalRelaySelector() {
if (!isGlobal || !allRelays || allRelays.length === 0) return null;
if (!allRelays || allRelays.length === 0) return null;
const paidRelays = allRelays.filter(a => a.paid);
const publicRelays = allRelays.filter(a => !a.paid);
@ -108,60 +148,80 @@ export default function RootPage() {
}
useEffect(() => {
if (isGlobal) {
return debounce(500, () => {
const ret: RelayOption[] = [];
System.Sockets.forEach((v, k) => {
ret.push({
url: k,
paid: v.Info?.limitation?.payment_required ?? false,
});
return debounce(500, () => {
const ret: RelayOption[] = [];
System.Sockets.forEach((v, k) => {
ret.push({
url: k,
paid: v.Info?.limitation?.payment_required ?? false,
});
ret.sort(a => (a.paid ? -1 : 1));
if (ret.length > 0 && !relay) {
setRelay(ret[0]);
}
setAllRelays(ret);
});
}
}, [relays, relay, tab]);
ret.sort(a => (a.paid ? -1 : 1));
const timelineSubect: TimelineSubject = (() => {
if (isGlobal) {
return { type: "global", items: [], discriminator: `all-${sha256(relay?.url ?? "").slice(0, 12)}` };
}
if (tab.value >= 3) {
const hashtag = tab.text.slice(1);
return { type: "hashtag", items: [hashtag], discriminator: hashtag };
}
return { type: "pubkey", items: follows, discriminator: "follows" };
})();
function renderTimeline() {
if (isGlobal && !relay) return null;
return (
<Timeline
key={tab.value}
subject={timelineSubect}
postsOnly={tab.value === RootTab.Posts.value}
method={"TIME_RANGE"}
window={undefined}
relay={isGlobal ? unwrap(relay).url : undefined}
/>
);
}
if (ret.length > 0 && !relay) {
setRelay(ret[0]);
}
setAllRelays(ret);
});
}, [relays, relay]);
return (
<>
<div className="main-content">
{pubKey && <Tabs tabs={tabs} tab={tab} setTab={setTab} />}
{globalRelaySelector()}
</div>
{followHints()}
{renderTimeline()}
{globalRelaySelector()}
{relay && (
<Timeline subject={subject} postsOnly={false} method={"TIME_RANGE"} window={600} relay={relay.url} now={now} />
)}
</>
);
}
};
const PostsTab = () => {
const follows = useSelector((s: RootState) => s.login.follows);
const subject: TimelineSubject = { type: "pubkey", items: follows, discriminator: "follows" };
return (
<>
<FollowsHint />
<Timeline subject={subject} postsOnly={true} method={"TIME_RANGE"} window={undefined} relay={undefined} />
</>
);
};
const ConversationsTab = () => {
const { follows } = useSelector((s: RootState) => s.login);
const subject: TimelineSubject = { type: "pubkey", items: follows, discriminator: "follows" };
return <Timeline subject={subject} postsOnly={false} method={"TIME_RANGE"} window={undefined} relay={undefined} />;
};
const TagsTab = () => {
const { tag } = useParams();
const subject: TimelineSubject = { type: "hashtag", items: [tag ?? ""], discriminator: `tags-${tag}` };
return <Timeline subject={subject} postsOnly={false} method={"TIME_RANGE"} window={undefined} relay={undefined} />;
};
export const RootRoutes = [
{
path: "/",
element: <RootPage />,
children: [
{
path: "global",
element: <GlobalTab />,
},
{
path: "posts",
element: <PostsTab />,
},
{
path: "conversations",
element: <ConversationsTab />,
},
{
path: "tag/:tag",
element: <TagsTab />,
},
],
},
] as RouteObject[];

View File

@ -401,23 +401,6 @@ const PreferencesPage = () => {
/>
</div>
</div>
<div className="card flex">
<div className="flex f-col f-grow">
<div>
<FormattedMessage {...messages.RewriteTwitterPosts} />
</div>
<small>
<FormattedMessage {...messages.RewriteTwitterPostsHelp} />
</small>
</div>
<div>
<input
type="checkbox"
checked={perf.rewriteTwitterPosts}
onChange={e => dispatch(setPreferences({ ...perf, rewriteTwitterPosts: e.target.checked }))}
/>
</div>
</div>
<div className="card flex">
<div className="flex f-col f-grow">
<div>

View File

@ -75,18 +75,23 @@ const RelayInfo = () => {
</h4>
<div className="f-grow">
{stats.info.supported_nips.map(a => (
<span
key={a}
className="pill"
onClick={() =>
navigate(`https://github.com/nostr-protocol/nips/blob/master/${a.toString().padStart(2, "0")}.md`)
}>
<a
target="_blank"
rel="noreferrer"
href={`https://github.com/nostr-protocol/nips/blob/master/${a.toString().padStart(2, "0")}.md`}
className="pill">
NIP-{a.toString().padStart(2, "0")}
</span>
</a>
))}
</div>
</>
)}
<h4>
<FormattedMessage defaultMessage="Active Subscriptions" />
</h4>
<div className="f-grow">
<span className="pill">TBD</span>
</div>
<div className="flex mt10 f-end">
<div
className="btn error"

View File

@ -1,4 +1,4 @@
import { useState } from "react";
import { useMemo, useState } from "react";
import { FormattedMessage } from "react-intl";
import { useDispatch, useSelector } from "react-redux";
@ -6,17 +6,21 @@ import { randomSample } from "Util";
import Relay from "Element/Relay";
import useEventPublisher from "Feed/EventPublisher";
import { RootState } from "State/Store";
import { RelaySettings } from "@snort/nostr";
import { setRelays } from "State/Login";
import { System } from "System";
import messages from "./messages";
const RelaySettingsPage = () => {
const dispatch = useDispatch();
const publisher = useEventPublisher();
const relays = useSelector<RootState, Record<string, RelaySettings>>(s => s.login.relays);
const relays = useSelector((s: RootState) => s.login.relays);
const [newRelay, setNewRelay] = useState<string>();
const otherConnections = useMemo(() => {
return [...System.Sockets.keys()].filter(a => relays[a] === undefined);
}, [relays]);
async function saveRelays() {
const ev = await publisher.saveRelays();
publisher.broadcast(ev);
@ -84,6 +88,14 @@ const RelaySettingsPage = () => {
</button>
</div>
{addRelay()}
<h3>
<FormattedMessage defaultMessage="Other Connections" />
</h3>
<div className="flex f-col mb10">
{otherConnections.map(a => (
<Relay addr={a} key={a} />
))}
</div>
</>
);
};

View File

@ -61,6 +61,4 @@ export default defineMessages({
Nip05: { defaultMessage: "NIP-05" },
ReactionEmoji: { defaultMessage: "Reaction emoji" },
ReactionEmojiHelp: { defaultMessage: "Emoji to send when reactiong to a note" },
RewriteTwitterPosts: { defaultMessage: "Nitter Rewrite" },
RewriteTwitterPostsHelp: { defaultMessage: "Rewrite Twitter links to Nitter links" },
});