Profile urls, scrollbar, ProfilePage refactoring #646

Merged
mmalmi merged 10 commits from mmalmi/snort:main into main 2023-10-09 18:41:32 +00:00
9 changed files with 217 additions and 177 deletions

View File

@ -205,9 +205,10 @@ const TierThree = ({ active, isLastSubthread, notes, related, chains, onNavigate
); );
}; };
export function ThreadRoute() { export function ThreadRoute({ id }: { id?: string }) {
const params = useParams(); const params = useParams();
const link = parseNostrLink(params.id ?? "", NostrPrefix.Note); const resolvedId = id ?? params.id;
const link = parseNostrLink(resolvedId ?? "", NostrPrefix.Note);
return ( return (
<ThreadContextWrapper link={link}> <ThreadContextWrapper link={link}>

View File

@ -113,3 +113,10 @@
height: 100%; height: 100%;
display: block; display: block;
} }
.gallery:not(:first-child),
img:not(:first-child),
video:not(:first-child),
.link-preview-container:not(:first-child) {
margin-top: 10px;
}

View File

@ -2,7 +2,7 @@ import "./Nip05.css";
import { HexKey } from "@snort/system"; import { HexKey } from "@snort/system";
import { useUserProfile } from "@snort/system-react"; import { useUserProfile } from "@snort/system-react";
export function useIsVerified(pubkey: HexKey, bypassCheck?: boolean) { export function useIsVerified(pubkey?: HexKey, bypassCheck?: boolean) {
const profile = useUserProfile(pubkey); const profile = useUserProfile(pubkey);
return { isVerified: bypassCheck || profile?.isNostrAddressValid }; return { isVerified: bypassCheck || profile?.isNostrAddressValid };
} }

View File

@ -1,32 +1,33 @@
import { NostrPrefix, tryParseNostrLink } from "@snort/system"; import { NostrPrefix, tryParseNostrLink } from "@snort/system";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import FormattedMessage from "Element/FormattedMessage"; import FormattedMessage from "Element/FormattedMessage";
import { useNavigate, useParams } from "react-router-dom"; import { useParams } from "react-router-dom";
import Spinner from "Icons/Spinner"; import Spinner from "Icons/Spinner";
import { profileLink } from "SnortUtils";
import { getNip05PubKey } from "Pages/LoginPage"; import { getNip05PubKey } from "Pages/LoginPage";
import ProfilePage from "Pages/Profile/ProfilePage";
import { ThreadRoute } from "Element/Event/Thread";
export default function NostrLinkHandler() { export default function NostrLinkHandler() {
const params = useParams(); const params = useParams();
const navigate = useNavigate();
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [renderComponent, setRenderComponent] = useState<React.ReactNode | null>(null);
const link = decodeURIComponent(params["*"] ?? "").toLowerCase(); const link = decodeURIComponent(params["*"] ?? "").toLowerCase();
async function handleLink(link: string) { async function handleLink(link: string) {
const nav = tryParseNostrLink(link); const nav = tryParseNostrLink(link);
if (nav) { if (nav) {
if (nav.type === NostrPrefix.Event || nav.type === NostrPrefix.Note || nav.type === NostrPrefix.Address) { if (nav.type === NostrPrefix.Event || nav.type === NostrPrefix.Note || nav.type === NostrPrefix.Address) {
navigate(`/e/${nav.encode()}`); setRenderComponent(<ThreadRoute id={nav.encode()} />); // Directly render ThreadRoute
} else if (nav.type === NostrPrefix.PublicKey || nav.type === NostrPrefix.Profile) { } else if (nav.type === NostrPrefix.PublicKey || nav.type === NostrPrefix.Profile) {
navigate(`/p/${nav.encode()}`); setRenderComponent(<ProfilePage id={nav.encode()} />); // Directly render ProfilePage
} }
} else { } else {
try { try {
const pubkey = await getNip05PubKey(`${link}@${process.env.NIP05_DOMAIN}`); const pubkey = await getNip05PubKey(`${link}@${process.env.NIP05_DOMAIN}`);
if (pubkey) { if (pubkey) {
navigate(profileLink(pubkey)); setRenderComponent(<ProfilePage id={pubkey} />); // Directly render ProfilePage
} }
} catch { } catch {
//ignored //ignored
@ -41,6 +42,10 @@ export default function NostrLinkHandler() {
} }
}, [link]); }, [link]);
if (renderComponent) {
return renderComponent;
}
return ( return (
<div className="flex f-center"> <div className="flex f-center">
{loading ? ( {loading ? (

View File

@ -2,36 +2,19 @@ import "./ProfilePage.css";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import FormattedMessage from "Element/FormattedMessage"; import FormattedMessage from "Element/FormattedMessage";
import { useNavigate, useParams } from "react-router-dom"; import { useNavigate, useParams } from "react-router-dom";
import { import { encodeTLV, encodeTLVEntries, EventKind, NostrPrefix, TLVEntryType, tryParseNostrLink } from "@snort/system";
encodeTLV,
encodeTLVEntries,
EventKind,
HexKey,
NostrLink,
NostrPrefix,
TLVEntryType,
tryParseNostrLink,
} from "@snort/system";
import { LNURL } from "@snort/shared"; import { LNURL } from "@snort/shared";
import { useUserProfile } from "@snort/system-react"; import { useUserProfile } from "@snort/system-react";
import { findTag, getReactions, unwrap } from "SnortUtils"; import { findTag, getReactions, unwrap } from "SnortUtils";
import { formatShort } from "Number";
import Note from "Element/Event/Note"; import Note from "Element/Event/Note";
import Bookmarks from "Element/Bookmarks";
import RelaysMetadata from "Element/Relay/RelaysMetadata";
import { Tab, TabElement } from "Element/Tabs"; import { Tab, TabElement } from "Element/Tabs";
import Icon from "Icons/Icon"; import Icon from "Icons/Icon";
import useMutedFeed from "Feed/MuteList"; import useMutedFeed from "Feed/MuteList";
import useRelaysFeed from "Feed/RelaysFeed";
import usePinnedFeed from "Feed/PinnedFeed"; import usePinnedFeed from "Feed/PinnedFeed";
import useBookmarkFeed from "Feed/BookmarkFeed";
import useFollowersFeed from "Feed/FollowersFeed";
import useFollowsFeed from "Feed/FollowsFeed"; import useFollowsFeed from "Feed/FollowsFeed";
import useProfileBadges from "Feed/BadgesFeed"; import useProfileBadges from "Feed/BadgesFeed";
import useModeration from "Hooks/useModeration"; import useModeration from "Hooks/useModeration";
import useZapsFeed from "Feed/ZapsFeed";
import { default as ZapElement } from "Element/Event/Zap";
import FollowButton from "Element/User/FollowButton"; import FollowButton from "Element/User/FollowButton";
import { parseId, hexToBech32 } from "SnortUtils"; import { parseId, hexToBech32 } from "SnortUtils";
import Avatar from "Element/User/Avatar"; import Avatar from "Element/User/Avatar";
@ -57,62 +40,24 @@ import useLogin from "Hooks/useLogin";
import { ZapTarget } from "Zapper"; import { ZapTarget } from "Zapper";
import { useStatusFeed } from "Feed/StatusFeed"; import { useStatusFeed } from "Feed/StatusFeed";
import messages from "./messages"; import messages from "../messages";
import { SpotlightMediaModal } from "Element/Deck/SpotlightMedia"; import { SpotlightMediaModal } from "Element/Deck/SpotlightMedia";
import ProfileTab, {
BookMarksTab,
FollowersTab,
FollowsTab,
ProfileTabType,
RelaysTab,
ZapsProfileTab,
} from "Pages/Profile/ProfileTab";
import DisplayName from "../../Element/User/DisplayName";
import { UserWebsiteLink } from "Element/User/UserWebsiteLink"; import { UserWebsiteLink } from "Element/User/UserWebsiteLink";
const NOTES = 0; interface ProfilePageProps {
const REACTIONS = 1; id?: string;
const FOLLOWERS = 2;
const FOLLOWS = 3;
const ZAPS = 4;
const MUTED = 5;
const BLOCKED = 6;
const RELAYS = 7;
const BOOKMARKS = 8;
function ZapsProfileTab({ id }: { id: HexKey }) {
const zaps = useZapsFeed(new NostrLink(NostrPrefix.PublicKey, id));
const zapsTotal = zaps.reduce((acc, z) => acc + z.amount, 0);
return (
<div className="main-content">
<h2 className="p">
<FormattedMessage {...messages.Sats} values={{ n: formatShort(zapsTotal) }} />
</h2>
{zaps.map(z => (
<ZapElement showZapped={false} zap={z} />
))}
</div>
);
} }
function FollowersTab({ id }: { id: HexKey }) { export default function ProfilePage({ id: propId }: ProfilePageProps) {
const followers = useFollowersFeed(id);
return <FollowsList pubkeys={followers} showAbout={true} className="p" />;
}
function FollowsTab({ id }: { id: HexKey }) {
const follows = useFollowsFeed(id);
return <FollowsList pubkeys={follows} showAbout={true} className="p" />;
}
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.filter(e => e.kind === EventKind.TextNote)}
related={bookmarks.filter(e => e.kind !== EventKind.TextNote)}
/>
);
}
export default function ProfilePage() {
const params = useParams(); const params = useParams();
const navigate = useNavigate(); const navigate = useNavigate();
const [id, setId] = useState<string>(); const [id, setId] = useState<string>();
@ -145,89 +90,6 @@ export default function ProfilePage() {
const status = useStatusFeed(showStatus ? id : undefined, true); const status = useStatusFeed(showStatus ? id : undefined, true);
// tabs // tabs
const ProfileTab = {
Notes: {
text: (
<>
<Icon name="pencil" size={16} />
<FormattedMessage defaultMessage="Notes" />
</>
),
value: NOTES,
},
Reactions: {
text: (
<>
<Icon name="reaction" size={16} />
<FormattedMessage defaultMessage="Reactions" />
</>
),
value: REACTIONS,
},
Followers: {
text: (
<>
<Icon name="user-v2" size={16} />
<FormattedMessage defaultMessage="Followers" />
</>
),
value: FOLLOWERS,
},
Follows: {
text: (
<>
<Icon name="stars" size={16} />
<FormattedMessage defaultMessage="Follows" />
</>
),
value: FOLLOWS,
},
Zaps: {
text: (
<>
<Icon name="zap-solid" size={16} />
<FormattedMessage defaultMessage="Zaps" />
</>
),
value: ZAPS,
},
Muted: {
text: (
<>
<Icon name="mute" size={16} />
<FormattedMessage defaultMessage="Muted" />
</>
),
value: MUTED,
},
Blocked: {
text: (
<>
<Icon name="block" size={16} />
<FormattedMessage defaultMessage="Blocked" />
</>
),
value: BLOCKED,
},
Relays: {
text: (
<>
<Icon name="wifi" size={16} />
<FormattedMessage defaultMessage="Relays" />
</>
),
value: RELAYS,
},
Bookmarks: {
text: (
<>
<Icon name="bookmark-solid" size={16} />
<FormattedMessage defaultMessage="Bookmarks" />
</>
),
value: BOOKMARKS,
},
} as { [key: string]: Tab };
const [tab, setTab] = useState<Tab>(ProfileTab.Notes); const [tab, setTab] = useState<Tab>(ProfileTab.Notes);
const optionalTabs = [ProfileTab.Zaps, ProfileTab.Relays, ProfileTab.Bookmarks, ProfileTab.Muted].filter(a => const optionalTabs = [ProfileTab.Zaps, ProfileTab.Relays, ProfileTab.Bookmarks, ProfileTab.Muted].filter(a =>
unwrap(a), unwrap(a),
@ -235,21 +97,22 @@ export default function ProfilePage() {
const horizontalScroll = useHorizontalScroll(); const horizontalScroll = useHorizontalScroll();
useEffect(() => { useEffect(() => {
if (params.id?.match(EmailRegex)) { const resolvedId = propId || params.id;
getNip05PubKey(params.id).then(a => { if (resolvedId?.match(EmailRegex)) {
getNip05PubKey(resolvedId).then(a => {
setId(a); setId(a);
}); });
} else { } else {
const nav = tryParseNostrLink(params.id ?? ""); const nav = tryParseNostrLink(resolvedId ?? "");
if (nav?.type === NostrPrefix.PublicKey || nav?.type === NostrPrefix.Profile) { if (nav?.type === NostrPrefix.PublicKey || nav?.type === NostrPrefix.Profile) {
// todo: use relays if any for nprofile // todo: use relays if any for nprofile
setId(nav.id); setId(nav.id);
} else { } else {
setId(parseId(params.id ?? "")); setId(parseId(resolvedId ?? ""));
} }
} }
setTab(ProfileTab.Notes); setTab(ProfileTab.Notes);
}, [params]); }, [propId, params]);
function musicStatus() { function musicStatus() {
if (!status.music) return; if (!status.music) return;
@ -274,12 +137,21 @@ export default function ProfilePage() {
return inner(); return inner();
} }
useEffect(() => {
if (user?.nip05 && user?.isNostrAddressValid) {
if (user.nip05.endsWith(`@${process.env.NIP05_DOMAIN}`)) {
const username = user.nip05?.replace(`@${process.env.NIP05_DOMAIN}`, "");
navigate(`/${username}`, { replace: true });
}
}
mmalmi marked this conversation as resolved
Review

you should be able to use navigate(`/${username`, { replace: true });

you should be able to use ```navigate(`/${username`, { replace: true });```
Review

perfect!

perfect!
}, [user?.isNostrAddressValid, user?.nip05]);
function username() { function username() {
return ( return (
<> <>
<div className="flex-column g4"> <div className="flex-column g4">
<h2 className="flex g4"> <h2 className="flex g4">
{user?.display_name || user?.name || "Nostrich"} <DisplayName user={user} pubkey={user?.pubkey ?? ""} />
<FollowsYou followsMe={follows.includes(loginPubKey ?? "")} /> <FollowsYou followsMe={follows.includes(loginPubKey ?? "")} />
</h2> </h2>
mmalmi marked this conversation as resolved Outdated

Probably dont want this, people can still use their nip5 directly though if they wanted

Probably dont want this, people can still use their nip5 directly though if they wanted
{user?.nip05 && <Nip05 nip05={user.nip05} pubkey={user.pubkey} />} {user?.nip05 && <Nip05 nip05={user.nip05} pubkey={user.pubkey} />}
@ -350,7 +222,7 @@ export default function ProfilePage() {
if (!id) return null; if (!id) return null;
switch (tab.value) { switch (tab.value) {
case NOTES: case ProfileTabType.NOTES:
return ( return (
<> <>
{pinned {pinned
@ -380,29 +252,29 @@ export default function ProfilePage() {
/> />
</> </>
); );
case ZAPS: { case ProfileTabType.ZAPS: {
return <ZapsProfileTab id={id} />; return <ZapsProfileTab id={id} />;
} }
case FOLLOWS: { case ProfileTabType.FOLLOWS: {
if (isMe) { if (isMe) {
return <FollowsList pubkeys={follows} showFollowAll={!isMe} showAbout={false} className="p" />; return <FollowsList pubkeys={follows} showFollowAll={!isMe} showAbout={false} className="p" />;
} else { } else {
return <FollowsTab id={id} />; return <FollowsTab id={id} />;
} }
} }
case FOLLOWERS: { case ProfileTabType.FOLLOWERS: {
return <FollowersTab id={id} />; return <FollowersTab id={id} />;
} }
case MUTED: { case ProfileTabType.MUTED: {
return <MutedList pubkeys={muted} />; return <MutedList pubkeys={muted} />;
} }
case BLOCKED: { case ProfileTabType.BLOCKED: {
return <BlockList />; return <BlockList />;
} }
case RELAYS: { case ProfileTabType.RELAYS: {
return <RelaysTab id={id} />; return <RelaysTab id={id} />;
} }
case BOOKMARKS: { case ProfileTabType.BOOKMARKS: {
return <BookMarksTab id={id} />; return <BookMarksTab id={id} />;
} }
} }

View File

@ -0,0 +1,154 @@
import useZapsFeed from "../../Feed/ZapsFeed";
import FormattedMessage from "../../Element/FormattedMessage";
import messages from "../messages";
import { formatShort } from "../../Number";
import useFollowersFeed from "../../Feed/FollowersFeed";
import FollowsList from "../../Element/User/FollowListBase";
import useFollowsFeed from "../../Feed/FollowsFeed";
import useRelaysFeed from "../../Feed/RelaysFeed";
import RelaysMetadata from "../../Element/Relay/RelaysMetadata";
import useBookmarkFeed from "../../Feed/BookmarkFeed";
import Bookmarks from "../../Element/Bookmarks";
import Icon from "../../Icons/Icon";
import { Tab } from "../../Element/Tabs";
import { EventKind, HexKey, NostrLink, NostrPrefix } from "@snort/system";
import { default as ZapElement } from "Element/Event/Zap";
export enum ProfileTabType {
NOTES = 0,
REACTIONS = 1,
FOLLOWERS = 2,
FOLLOWS = 3,
ZAPS = 4,
MUTED = 5,
BLOCKED = 6,
RELAYS = 7,
BOOKMARKS = 8,
}
export function ZapsProfileTab({ id }: { id: HexKey }) {
const zaps = useZapsFeed(new NostrLink(NostrPrefix.PublicKey, id));
const zapsTotal = zaps.reduce((acc, z) => acc + z.amount, 0);
return (
<div className="main-content">
<h2 className="p">
<FormattedMessage {...messages.Sats} values={{ n: formatShort(zapsTotal) }} />
</h2>
{zaps.map(z => (
<ZapElement showZapped={false} zap={z} />
))}
</div>
);
}
export function FollowersTab({ id }: { id: HexKey }) {
const followers = useFollowersFeed(id);
return <FollowsList pubkeys={followers} showAbout={true} className="p" />;
}
export function FollowsTab({ id }: { id: HexKey }) {
const follows = useFollowsFeed(id);
return <FollowsList pubkeys={follows} showAbout={true} className="p" />;
}
export function RelaysTab({ id }: { id: HexKey }) {
const relays = useRelaysFeed(id);
return <RelaysMetadata relays={relays} />;
}
export function BookMarksTab({ id }: { id: HexKey }) {
const bookmarks = useBookmarkFeed(id);
return (
<Bookmarks
pubkey={id}
bookmarks={bookmarks.filter(e => e.kind === EventKind.TextNote)}
related={bookmarks.filter(e => e.kind !== EventKind.TextNote)}
/>
);
}
const ProfileTab = {
Notes: {
text: (
<>
<Icon name="pencil" size={16} />
<FormattedMessage defaultMessage="Notes" />
</>
),
value: ProfileTabType.NOTES,
},
Reactions: {
text: (
<>
<Icon name="reaction" size={16} />
<FormattedMessage defaultMessage="Reactions" />
</>
),
value: ProfileTabType.REACTIONS,
},
Followers: {
text: (
<>
<Icon name="user-v2" size={16} />
<FormattedMessage defaultMessage="Followers" />
</>
),
value: ProfileTabType.FOLLOWERS,
},
Follows: {
text: (
<>
<Icon name="stars" size={16} />
<FormattedMessage defaultMessage="Follows" />
</>
),
value: ProfileTabType.FOLLOWS,
},
Zaps: {
text: (
<>
<Icon name="zap-solid" size={16} />
<FormattedMessage defaultMessage="Zaps" />
</>
),
value: ProfileTabType.ZAPS,
},
Muted: {
text: (
<>
<Icon name="mute" size={16} />
<FormattedMessage defaultMessage="Muted" />
</>
),
value: ProfileTabType.MUTED,
},
Blocked: {
text: (
<>
<Icon name="block" size={16} />
<FormattedMessage defaultMessage="Blocked" />
</>
),
value: ProfileTabType.BLOCKED,
},
Relays: {
text: (
<>
<Icon name="wifi" size={16} />
<FormattedMessage defaultMessage="Relays" />
</>
),
value: ProfileTabType.RELAYS,
},
Bookmarks: {
text: (
<>
<Icon name="bookmark-solid" size={16} />
<FormattedMessage defaultMessage="Bookmarks" />
</>
),
value: ProfileTabType.BOOKMARKS,
},
} as { [key: string]: Tab };
export default ProfileTab;

View File

@ -107,6 +107,7 @@ body {
color: var(--font-color); color: var(--font-color);
font-size: var(--font-size); font-size: var(--font-size);
overflow-x: hidden; overflow-x: hidden;
overflow-y: scroll;
} }
code { code {

View File

@ -24,7 +24,7 @@ import { IntlProvider } from "IntlProvider";
import { unwrap } from "SnortUtils"; import { unwrap } from "SnortUtils";
import Layout from "Pages/Layout"; import Layout from "Pages/Layout";
import LoginPage from "Pages/LoginPage"; import LoginPage from "Pages/LoginPage";
import ProfilePage from "Pages/ProfilePage"; import ProfilePage from "Pages/Profile/ProfilePage";
import { RootRoutes, RootTabRoutes } from "Pages/Root"; import { RootRoutes, RootTabRoutes } from "Pages/Root";
import NotificationsPage from "Pages/Notifications"; import NotificationsPage from "Pages/Notifications";
import SettingsPage, { SettingsRoutes } from "Pages/SettingsPage"; import SettingsPage, { SettingsRoutes } from "Pages/SettingsPage";