refactor: RequestBuilder
This commit is contained in:
@ -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>;
|
||||
}
|
||||
}
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
@ -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)}>
|
||||
|
@ -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[];
|
||||
|
@ -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>
|
||||
|
@ -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"
|
||||
|
@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -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" },
|
||||
});
|
||||
|
Reference in New Issue
Block a user