count in profile page

This commit is contained in:
Alejandro Gomez 2023-02-10 12:12:11 +01:00 committed by Kieran
parent fbd31526b7
commit 73957e6510
Signed by: Kieran
GPG Key ID: DE71CEB3925BE941
22 changed files with 205 additions and 219 deletions

View File

@ -1,42 +1,15 @@
import { FormattedMessage } from "react-intl";
import MuteButton from "Element/MuteButton";
import BlockButton from "Element/BlockButton"; import BlockButton from "Element/BlockButton";
import ProfilePreview from "Element/ProfilePreview"; import ProfilePreview from "Element/ProfilePreview";
import useModeration from "Hooks/useModeration"; import useModeration from "Hooks/useModeration";
import messages from "./messages"; export default function BlockList() {
const { blocked } = useModeration();
interface BlockListProps {
variant: "muted" | "blocked";
}
export default function BlockList({ variant }: BlockListProps) {
const { blocked, muted } = useModeration();
return ( return (
<div className="main-content"> <div className="main-content">
{variant === "muted" && ( {blocked.map(a => {
<> return <ProfilePreview actions={<BlockButton pubkey={a} />} pubkey={a} options={{ about: false }} key={a} />;
<h4> })}
<FormattedMessage {...messages.MuteCount} values={{ n: muted.length }} />
</h4>
{muted.map(a => {
return <ProfilePreview actions={<MuteButton pubkey={a} />} pubkey={a} options={{ about: false }} key={a} />;
})}
</>
)}
{variant === "blocked" && (
<>
<h4>
<FormattedMessage {...messages.BlockCount} values={{ n: blocked.length }} />
</h4>
{blocked.map(a => {
return (
<ProfilePreview actions={<BlockButton pubkey={a} />} pubkey={a} options={{ about: false }} key={a} />
);
})}
</>
)}
</div> </div>
); );
} }

View File

@ -16,7 +16,7 @@ export default function Copy({ text, maxSize = 32 }: CopyProps) {
<div className="flex flex-row copy" onClick={() => copy(text)}> <div className="flex flex-row copy" onClick={() => copy(text)}>
<span className="body">{trimmed}</span> <span className="body">{trimmed}</span>
<span className="icon" style={{ color: copied ? "var(--success)" : "var(--highlight)" }}> <span className="icon" style={{ color: copied ? "var(--success)" : "var(--highlight)" }}>
{copied ? <Check width={13} height={13} /> : <CopyIcon width={13} height={13} />} {copied ? <Check width={14} height={14} /> : <CopyIcon width={14} height={14} />}
</span> </span>
</div> </div>
); );

View File

@ -1,27 +0,0 @@
import { useMemo } from "react";
import { useIntl } from "react-intl";
import useFollowersFeed from "Feed/FollowersFeed";
import { HexKey } from "@snort/nostr";
import { EventKind } from "@snort/nostr";
import FollowListBase from "Element/FollowListBase";
import messages from "./messages";
export interface FollowersListProps {
pubkey: HexKey;
}
export default function FollowersList({ pubkey }: FollowersListProps) {
const { formatMessage } = useIntl();
const feed = useFollowersFeed(pubkey);
const pubkeys = useMemo(() => {
const contactLists = feed?.store.notes.filter(
a => a.kind === EventKind.ContactList && a.tags.some(b => b[0] === "p" && b[1] === pubkey)
);
return [...new Set(contactLists?.map(a => a.pubkey))];
}, [feed, pubkey]);
return <FollowListBase pubkeys={pubkeys} title={formatMessage(messages.FollowerCount, { n: pubkeys?.length })} />;
}

View File

@ -1,24 +0,0 @@
import { useMemo } from "react";
import { useIntl } from "react-intl";
import useFollowsFeed from "Feed/FollowsFeed";
import { HexKey } from "@snort/nostr";
import FollowListBase from "Element/FollowListBase";
import { getFollowers } from "Feed/FollowsFeed";
import messages from "./messages";
export interface FollowsListProps {
pubkey: HexKey;
}
export default function FollowsList({ pubkey }: FollowsListProps) {
const feed = useFollowsFeed(pubkey);
const { formatMessage } = useIntl();
const pubkeys = useMemo(() => {
return getFollowers(feed.store, pubkey);
}, [feed, pubkey]);
return <FollowListBase pubkeys={pubkeys} title={formatMessage(messages.FollowingCount, { n: pubkeys?.length })} />;
}

View File

@ -1,29 +1,13 @@
import "./FollowsYou.css"; import "./FollowsYou.css";
import { useMemo } from "react";
import { useSelector } from "react-redux";
import { useIntl } from "react-intl"; import { useIntl } from "react-intl";
import { HexKey } from "@snort/nostr";
import { RootState } from "State/Store";
import useFollowsFeed from "Feed/FollowsFeed";
import { getFollowers } from "Feed/FollowsFeed";
import messages from "./messages"; import messages from "./messages";
export interface FollowsYouProps { export interface FollowsYouProps {
pubkey: HexKey; followsMe: boolean;
} }
export default function FollowsYou({ pubkey }: FollowsYouProps) { export default function FollowsYou({ followsMe }: FollowsYouProps) {
const { formatMessage } = useIntl(); const { formatMessage } = useIntl();
const feed = useFollowsFeed(pubkey);
const loginPubKey = useSelector<RootState, HexKey | undefined>(s => s.login.publicKey);
const pubkeys = useMemo(() => {
return getFollowers(feed.store, pubkey);
}, [feed, pubkey]);
const followsMe = loginPubKey ? pubkeys.includes(loginPubKey) : false;
return followsMe ? <span className="follows-you">{formatMessage(messages.FollowsYou)}</span> : null; return followsMe ? <span className="follows-you">{formatMessage(messages.FollowsYou)}</span> : null;
} }

View File

@ -1,23 +1,17 @@
import { useMemo } from "react";
import { FormattedMessage } from "react-intl"; import { FormattedMessage } from "react-intl";
import { HexKey } from "@snort/nostr"; import { HexKey } from "@snort/nostr";
import MuteButton from "Element/MuteButton"; import MuteButton from "Element/MuteButton";
import ProfilePreview from "Element/ProfilePreview"; import ProfilePreview from "Element/ProfilePreview";
import useMutedFeed, { getMuted } from "Feed/MuteList";
import useModeration from "Hooks/useModeration"; import useModeration from "Hooks/useModeration";
import messages from "./messages"; import messages from "./messages";
export interface MutedListProps { export interface MutedListProps {
pubkey: HexKey; pubkeys: HexKey[];
} }
export default function MutedList({ pubkey }: MutedListProps) { export default function MutedList({ pubkeys }: MutedListProps) {
const { isMuted, muteAll } = useModeration(); const { isMuted, muteAll } = useModeration();
const feed = useMutedFeed(pubkey);
const pubkeys = useMemo(() => {
return getMuted(feed.store, pubkey);
}, [feed, pubkey]);
const hasAllMuted = pubkeys.every(isMuted); const hasAllMuted = pubkeys.every(isMuted);
return ( return (

View File

@ -43,5 +43,6 @@
} }
.nip05 .badge { .nip05 .badge {
color: var(--highlight);
margin: 0.1em 0.2em; margin: 0.1em 0.2em;
} }

View File

@ -1,9 +1,6 @@
import { useQuery } from "react-query";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faCircleCheck, faSpinner, faTriangleExclamation } from "@fortawesome/free-solid-svg-icons";
import "./Nip05.css"; import "./Nip05.css";
import { useQuery } from "react-query";
import Badge from "Icons/Badge";
import { HexKey } from "@snort/nostr"; import { HexKey } from "@snort/nostr";
interface NostrJson { interface NostrJson {
@ -59,15 +56,17 @@ const Nip05 = ({ nip05, pubkey, verifyNip = true }: Nip05Params) => {
return ( return (
<div className={`flex nip05${couldNotVerify ? " failed" : ""}`} onClick={ev => ev.stopPropagation()}> <div className={`flex nip05${couldNotVerify ? " failed" : ""}`} onClick={ev => ev.stopPropagation()}>
{!isDefaultUser && <div className="nick">{`${name}@`}</div>} {!isDefaultUser && isVerified && <div className="nick">{`${name}@`}</div>}
<span className="domain" data-domain={domain?.toLowerCase()}> {isVerified && (
{domain} <>
</span> <span className="domain" data-domain={domain?.toLowerCase()}>
<span className="badge"> {domain}
{isVerified && <FontAwesomeIcon color={"var(--highlight)"} icon={faCircleCheck} size="xs" />} </span>
{!isVerified && !couldNotVerify && <FontAwesomeIcon color={"var(--fg-color)"} icon={faSpinner} size="xs" />} <span className="badge">
{couldNotVerify && <FontAwesomeIcon color={"var(--error)"} icon={faTriangleExclamation} size="xs" />} <Badge />
</span> </span>
</>
)}
</div> </div>
); );
}; };

View File

@ -115,12 +115,12 @@
border-bottom-right-radius: 16px; border-bottom-right-radius: 16px;
} }
.light .note > .footer .ctx-menu li:hover { .note > .footer .ctx-menu li:hover {
color: white; color: white;
background: #2a2a2a; background: #2a2a2a;
} }
.note > .footer .ctx-menu li:hover { .light .note > .footer .ctx-menu li:hover {
color: white; color: white;
background: var(--font-secondary-color); background: var(--font-secondary-color);
} }
@ -151,6 +151,7 @@
.reaction-pill .reaction-pill-number { .reaction-pill .reaction-pill-number {
margin-left: 8px; margin-left: 8px;
font-feature-settings: "tnum";
} }
.reaction-pill.reacted { .reaction-pill.reacted {

View File

@ -13,6 +13,8 @@
.relay-settings { .relay-settings {
margin-left: auto; margin-left: auto;
display: flex;
align-items: center;
} }
.relay-settings svg:not(:last-child) { .relay-settings svg:not(:last-child) {
@ -25,6 +27,13 @@
opacity: 0.3; opacity: 0.3;
} }
@media (max-width: 520px) {
.relay-settings svg {
width: 16px;
height: 16px;
}
}
.relay-url { .relay-url {
font-size: 14px; font-size: 14px;
} }

View File

@ -14,16 +14,17 @@
.tab { .tab {
color: var(--font-tertiary-color); color: var(--font-tertiary-color);
border: 1px solid var(--font-tertiary-color); border: 1px solid var(--border-color);
border-radius: 16px; border-radius: 16px;
text-align: center;
font-weight: 600;
line-height: 19px;
padding: 8px 12px;
font-weight: 600; font-weight: 600;
font-size: 14px; font-size: 14px;
line-height: 17px; padding: 6px 8px;
margin-right: 12px; text-align: center;
font-feature-settings: "tnum";
}
.tab:not(:last-of-type) {
margin-right: 8px;
} }
.tab.active { .tab.active {
@ -40,3 +41,7 @@
cursor: not-allowed; cursor: not-allowed;
pointer-events: none; pointer-events: none;
} }
.tab:hover {
border-color: var(--font-color);
}

View File

@ -1,5 +1,6 @@
import "./Tabs.css"; import "./Tabs.css";
import useHorizontalScroll from "Hooks/useHorizontalScroll"; import useHorizontalScroll from "Hooks/useHorizontalScroll";
import { CSSProperties } from "react";
export interface Tab { export interface Tab {
text: string; text: string;
@ -18,9 +19,11 @@ interface TabElementProps extends Omit<TabsProps, "tabs"> {
} }
export const TabElement = ({ t, tab, setTab }: TabElementProps) => { export const TabElement = ({ t, tab, setTab }: TabElementProps) => {
const style = { minWidth: `${t.text.length * 0.6}em` } as CSSProperties;
return ( return (
<div <div
className={`tab ${tab.value === t.value ? "active" : ""} ${t.disabled ? "disabled" : ""}`} className={`tab ${tab.value === t.value ? "active" : ""} ${t.disabled ? "disabled" : ""}`}
style={style}
onClick={() => !t.disabled && setTab(t)}> onClick={() => !t.disabled && setTab(t)}>
{t.text} {t.text}
</div> </div>

View File

@ -13,5 +13,14 @@ export default function useFollowersFeed(pubkey: HexKey) {
return x; return x;
}, [pubkey]); }, [pubkey]);
return useSubscription(sub); const followersFeed = useSubscription(sub, { leaveOpen: false, cache: true });
const followers = useMemo(() => {
const contactLists = followersFeed?.store.notes.filter(
a => a.kind === EventKind.ContactList && a.tags.some(b => b[0] === "p" && b[1] === pubkey)
);
return [...new Set(contactLists?.map(a => a.pubkey))];
}, [followersFeed, pubkey]);
return followers;
} }

View File

@ -1,23 +1,32 @@
import { useMemo } from "react"; import { useMemo } from "react";
import { HexKey } from "@snort/nostr"; import { useSelector } from "react-redux";
import { EventKind, Subscriptions } from "@snort/nostr"; import { HexKey, TaggedRawEvent, EventKind, Subscriptions } from "@snort/nostr";
import useSubscription, { NoteStore } from "Feed/Subscription";
import useSubscription from "Feed/Subscription";
import { RootState } from "State/Store";
export default function useFollowsFeed(pubkey: HexKey) { export default function useFollowsFeed(pubkey: HexKey) {
const { publicKey, follows } = useSelector((s: RootState) => s.login);
const isMe = publicKey === pubkey;
const sub = useMemo(() => { const sub = useMemo(() => {
if (isMe) return null;
const x = new Subscriptions(); const x = new Subscriptions();
x.Id = `follows:${pubkey.slice(0, 12)}`; x.Id = `follows:${pubkey.slice(0, 12)}`;
x.Kinds = new Set([EventKind.ContactList]); x.Kinds = new Set([EventKind.ContactList]);
x.Authors = new Set([pubkey]); x.Authors = new Set([pubkey]);
return x; return x;
}, [pubkey]); }, [isMe, pubkey]);
return useSubscription(sub); const contactFeed = useSubscription(sub, { leaveOpen: false, cache: true });
const following = useMemo(() => {
return getFollowing(contactFeed.store.notes ?? [], pubkey);
}, [contactFeed.store.notes]);
return isMe ? follows : following;
} }
export function getFollowers(feed: NoteStore, pubkey: HexKey) { export function getFollowing(notes: TaggedRawEvent[], pubkey: HexKey) {
const contactLists = feed?.notes.filter(a => a.kind === EventKind.ContactList && a.pubkey === pubkey); const contactLists = notes.filter(a => a.kind === EventKind.ContactList && a.pubkey === pubkey);
const pTags = contactLists?.map(a => a.tags.filter(b => b[0] === "p").map(c => c[1])); const pTags = contactLists?.map(a => a.tags.filter(b => b[0] === "p").map(c => c[1]));
return [...new Set(pTags?.flat())]; return [...new Set(pTags?.flat())];
} }

View File

@ -1,12 +1,18 @@
import { useMemo } from "react"; import { useMemo } from "react";
import { useSelector } from "react-redux";
import { getNewest } from "Util"; import { getNewest } from "Util";
import { HexKey, TaggedRawEvent, Lists } from "@snort/nostr"; import { HexKey, TaggedRawEvent, Lists } from "@snort/nostr";
import { EventKind, Subscriptions } from "@snort/nostr"; import { EventKind, Subscriptions } from "@snort/nostr";
import useSubscription, { NoteStore } from "Feed/Subscription"; import useSubscription, { NoteStore } from "Feed/Subscription";
import { RootState } from "State/Store";
export default function useMutedFeed(pubkey: HexKey) { export default function useMutedFeed(pubkey: HexKey) {
const { publicKey, muted } = useSelector((s: RootState) => s.login);
const isMe = publicKey === pubkey;
const sub = useMemo(() => { const sub = useMemo(() => {
if (isMe) return null;
const sub = new Subscriptions(); const sub = new Subscriptions();
sub.Id = `muted:${pubkey.slice(0, 12)}`; sub.Id = `muted:${pubkey.slice(0, 12)}`;
sub.Kinds = new Set([EventKind.PubkeyLists]); sub.Kinds = new Set([EventKind.PubkeyLists]);
@ -16,7 +22,13 @@ export default function useMutedFeed(pubkey: HexKey) {
return sub; return sub;
}, [pubkey]); }, [pubkey]);
return useSubscription(sub); const mutedFeed = useSubscription(sub, { leaveOpen: false, cache: true });
const mutedList = useMemo(() => {
return getMuted(mutedFeed.store, pubkey);
}, [mutedFeed.store]);
return isMe ? muted : mutedList;
} }
export function getMutedKeys(rawNotes: TaggedRawEvent[]): { export function getMutedKeys(rawNotes: TaggedRawEvent[]): {

View File

@ -1,6 +1,6 @@
import { useMemo } from "react"; import { useMemo } from "react";
import { HexKey } from "@snort/nostr"; import { HexKey, EventKind, Subscriptions } from "@snort/nostr";
import { EventKind, Subscriptions } from "@snort/nostr"; import { parseZap } from "Element/Zap";
import useSubscription from "./Subscription"; import useSubscription from "./Subscription";
export default function useZapsFeed(pubkey: HexKey) { export default function useZapsFeed(pubkey: HexKey) {
@ -12,5 +12,15 @@ export default function useZapsFeed(pubkey: HexKey) {
return x; return x;
}, [pubkey]); }, [pubkey]);
return useSubscription(sub, { leaveOpen: true, cache: true }); const zapsFeed = useSubscription(sub, { leaveOpen: false, cache: true });
const zaps = useMemo(() => {
const profileZaps = zapsFeed.store.notes
.map(parseZap)
.filter(z => z.valid && z.p === pubkey && z.zapper !== pubkey && !z.e);
profileZaps.sort((a, b) => b.amount - a.amount);
return profileZaps;
}, [zapsFeed]);
return zaps;
} }

View File

@ -0,0 +1,15 @@
const Badge = () => {
return (
<svg width="16" height="15" viewBox="0 0 16 15" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M6.00004 7.50065L7.33337 8.83398L10.3334 5.83398M11.9342 2.83299C12.0714 3.16501 12.3349 3.42892 12.6667 3.5667L13.8302 4.04864C14.1622 4.18617 14.426 4.44998 14.5636 4.78202C14.7011 5.11407 14.7011 5.48715 14.5636 5.81919L14.082 6.98185C13.9444 7.31404 13.9442 7.6875 14.0824 8.01953L14.5632 9.18185C14.6313 9.34631 14.6664 9.52259 14.6665 9.70062C14.6665 9.87865 14.6315 10.0549 14.5633 10.2194C14.4952 10.3839 14.3953 10.5333 14.2694 10.6592C14.1435 10.7851 13.9941 10.8849 13.8296 10.953L12.6669 11.4346C12.3349 11.5718 12.071 11.8354 11.9333 12.1672L11.4513 13.3307C11.3138 13.6627 11.05 13.9265 10.718 14.0641C10.3859 14.2016 10.0129 14.2016 9.68085 14.0641L8.51823 13.5825C8.18619 13.4453 7.81326 13.4455 7.48143 13.5832L6.31797 14.0645C5.98612 14.2017 5.61338 14.2016 5.28162 14.0642C4.94986 13.9267 4.68621 13.6632 4.54858 13.3316L4.06652 12.1677C3.92924 11.8357 3.66574 11.5718 3.33394 11.434L2.17048 10.9521C1.8386 10.8146 1.57488 10.5509 1.4373 10.2191C1.29971 9.88724 1.29953 9.51434 1.43678 9.18235L1.91835 8.01968C2.05554 7.68763 2.05526 7.31469 1.91757 6.98284L1.43669 5.81851C1.36851 5.65405 1.3334 5.47777 1.33337 5.29974C1.33335 5.12171 1.3684 4.94542 1.43652 4.78094C1.50465 4.61646 1.60452 4.46702 1.73042 4.34115C1.85632 4.21529 2.00579 4.11546 2.17028 4.04739L3.33291 3.5658C3.66462 3.42863 3.92836 3.16545 4.06624 2.83402L4.54816 1.67052C4.68569 1.33848 4.94949 1.07467 5.28152 0.937137C5.61355 0.7996 5.98662 0.7996 6.31865 0.937137L7.48127 1.41873C7.81331 1.55593 8.18624 1.55565 8.51808 1.41795L9.68202 0.937884C10.014 0.800424 10.387 0.800452 10.719 0.937962C11.0509 1.07547 11.3147 1.3392 11.4522 1.67116L11.9343 2.835L11.9342 2.83299Z"
stroke="currentColor"
strokeWidth="1.33333"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
};
export default Badge;

View File

@ -30,13 +30,18 @@ header .pfp .avatar-wrapper {
align-items: center; align-items: center;
} }
.header-actions .avatar {
width: 40px;
height: 40px;
}
.header-actions .btn-rnd { .header-actions .btn-rnd {
position: relative; position: relative;
margin-right: 8px; margin-right: 8px;
} }
@media (min-width: 520px) { @media (min-width: 520px) {
.header-actions .btn-rnd { .header-actions .btn-rnd:last-of-type {
margin-right: 16px; margin-right: 16px;
} }
} }

View File

@ -231,23 +231,24 @@
margin-left: 4px; margin-left: 4px;
} }
.icon-title { .banner-bg {
font-weight: 600; width: 1438px;
font-size: 19px; height: 758px;
line-height: 23px; position: absolute;
display: flex; top: -120px;
align-items: center; left: calc(50% - 750px);
margin-bottom: 22px; background: radial-gradient(circle at top center, rgba(0, 0, 0, 0.7) 0%, #000000 81.25%), var(--img-url);
filter: blur(10px);
z-index: -1;
display: none;
} }
.icon-title svg { .light .banner-bg {
margin-right: 8px; background: radial-gradient(circle at top center, rgba(241, 241, 241, 0.7) 0%, var(--bg-color) 81.25%), var(--img-url);
} }
.icon-title h3 { @media (min-width: 1420px) {
margin: 0; .banner-bg {
} display: unset;
}
.icon-title select {
margin-left: auto;
} }

View File

@ -1,5 +1,5 @@
import "./ProfilePage.css"; import "./ProfilePage.css";
import { useEffect, useMemo, useState } from "react"; import { useEffect, useMemo, useState, CSSProperties } from "react";
import { useIntl, FormattedMessage } from "react-intl"; import { useIntl, FormattedMessage } from "react-intl";
import { useSelector } from "react-redux"; import { useSelector } from "react-redux";
import { useNavigate, useParams } from "react-router-dom"; import { useNavigate, useParams } from "react-router-dom";
@ -14,12 +14,16 @@ import Link from "Icons/Link";
import Qr from "Icons/Qr"; import Qr from "Icons/Qr";
import Zap from "Icons/Zap"; import Zap from "Icons/Zap";
import Envelope from "Icons/Envelope"; import Envelope from "Icons/Envelope";
import useMutedFeed from "Feed/MuteList";
import useRelaysFeed from "Feed/RelaysFeed"; import useRelaysFeed from "Feed/RelaysFeed";
import usePinnedFeed from "Feed/PinnedFeed"; import usePinnedFeed from "Feed/PinnedFeed";
import useBookmarkFeed from "Feed/BookmarkFeed"; import useBookmarkFeed from "Feed/BookmarkFeed";
import useFollowersFeed from "Feed/FollowersFeed";
import useFollowsFeed from "Feed/FollowsFeed";
import { useUserProfile } from "Feed/ProfileFeed"; import { useUserProfile } from "Feed/ProfileFeed";
import useModeration from "Hooks/useModeration";
import useZapsFeed from "Feed/ZapsFeed"; import useZapsFeed from "Feed/ZapsFeed";
import { default as ZapElement, parseZap } from "Element/Zap"; import { default as ZapElement } from "Element/Zap";
import FollowButton from "Element/FollowButton"; import FollowButton from "Element/FollowButton";
import { extractLnAddress, parseId, hexToBech32 } from "Util"; import { extractLnAddress, parseId, hexToBech32 } from "Util";
import Avatar from "Element/Avatar"; import Avatar from "Element/Avatar";
@ -30,10 +34,9 @@ import Nip05 from "Element/Nip05";
import Copy from "Element/Copy"; import Copy from "Element/Copy";
import ProfilePreview from "Element/ProfilePreview"; import ProfilePreview from "Element/ProfilePreview";
import ProfileImage from "Element/ProfileImage"; import ProfileImage from "Element/ProfileImage";
import FollowersList from "Element/FollowersList";
import BlockList from "Element/BlockList"; import BlockList from "Element/BlockList";
import MutedList from "Element/MutedList"; import MutedList from "Element/MutedList";
import FollowsList from "Element/FollowsList"; import FollowsList from "Element/FollowListBase";
import IconButton from "Element/IconButton"; import IconButton from "Element/IconButton";
import { RootState } from "State/Store"; import { RootState } from "State/Store";
import { HexKey, NostrPrefix } from "@snort/nostr"; import { HexKey, NostrPrefix } from "@snort/nostr";
@ -62,12 +65,9 @@ export default function ProfilePage() {
const user = useUserProfile(id); const user = useUserProfile(id);
const loggedOut = useSelector<RootState, boolean | undefined>(s => s.login.loggedOut); const loggedOut = useSelector<RootState, boolean | undefined>(s => s.login.loggedOut);
const loginPubKey = useSelector<RootState, HexKey | undefined>(s => s.login.publicKey); const loginPubKey = useSelector<RootState, HexKey | undefined>(s => s.login.publicKey);
const follows = useSelector<RootState, HexKey[]>(s => s.login.follows);
const isMe = loginPubKey === id; const isMe = loginPubKey === id;
const [showLnQr, setShowLnQr] = useState<boolean>(false); const [showLnQr, setShowLnQr] = useState<boolean>(false);
const [showProfileQr, setShowProfileQr] = useState<boolean>(false); const [showProfileQr, setShowProfileQr] = useState<boolean>(false);
const { notes: pinned, related: pinRelated } = usePinnedFeed(id);
const { notes: bookmarks, related: bookmarkRelated } = useBookmarkFeed(id);
const aboutText = user?.about || ""; const aboutText = user?.about || "";
const about = Text({ const about = Text({
content: aboutText, content: aboutText,
@ -78,32 +78,36 @@ export default function ProfilePage() {
const lnurl = extractLnAddress(user?.lud16 || user?.lud06 || ""); const lnurl = extractLnAddress(user?.lud16 || user?.lud06 || "");
const website_url = const website_url =
user?.website && !user.website.startsWith("http") ? "https://" + user.website : user?.website || ""; 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 relays = useRelaysFeed(id);
const zapFeed = useZapsFeed(id); const zaps = useZapsFeed(id);
const zaps = useMemo(() => {
const profileZaps = zapFeed.store.notes.map(parseZap).filter(z => z.valid && z.p === id && !z.e && z.zapper !== id);
profileZaps.sort((a, b) => b.amount - a.amount);
return profileZaps;
}, [zapFeed.store, id]);
const zapsTotal = zaps.reduce((acc, z) => acc + z.amount, 0); const zapsTotal = zaps.reduce((acc, z) => acc + z.amount, 0);
const horizontalScroll = useHorizontalScroll(); const followers = useFollowersFeed(id);
const follows = useFollowsFeed(id);
const muted = useMutedFeed(id);
// tabs
const ProfileTab = { const ProfileTab = {
Notes: { text: formatMessage(messages.Notes), value: NOTES }, Notes: { text: formatMessage(messages.Notes), value: NOTES },
Reactions: { text: formatMessage(messages.Reactions), value: REACTIONS }, Reactions: { text: formatMessage(messages.Reactions), value: REACTIONS },
Followers: { text: formatMessage(messages.Followers), value: FOLLOWERS }, Followers: { text: formatMessage(messages.FollowersCount, { n: followers.length }), value: FOLLOWERS },
Follows: { text: formatMessage(messages.Follows), value: FOLLOWS }, Follows: { text: formatMessage(messages.FollowsCount, { n: follows.length }), value: FOLLOWS },
Zaps: { text: formatMessage(messages.Zaps), value: ZAPS }, Zaps: { text: formatMessage(messages.ZapsCount, { n: zaps.length }), value: ZAPS },
Muted: { text: formatMessage(messages.Muted), value: MUTED }, Muted: { text: formatMessage(messages.MutedCount, { n: muted.length }), value: MUTED },
Blocked: { text: formatMessage(messages.Blocked), value: BLOCKED }, Blocked: { text: formatMessage(messages.BlockedCount, { n: blocked.length }), value: BLOCKED },
Relays: { text: formatMessage(messages.Relays), value: RELAYS }, Relays: { text: formatMessage(messages.RelaysCount, { n: relays.length }), value: RELAYS },
Bookmarks: { text: formatMessage(messages.Bookmarks), value: BOOKMARKS }, Bookmarks: { text: formatMessage(messages.BookmarksCount, { n: bookmarks.length }), value: BOOKMARKS },
}; };
const [tab, setTab] = useState<Tab>(ProfileTab.Notes); const [tab, setTab] = useState<Tab>(ProfileTab.Notes);
const optionalTabs = [ const optionalTabs = [
zapsTotal > 0 && ProfileTab.Zaps, zapsTotal > 0 && ProfileTab.Zaps,
relays.length > 0 && ProfileTab.Relays, relays.length > 0 && ProfileTab.Relays,
bookmarks.length > 0 && ProfileTab.Bookmarks, bookmarks.length > 0 && ProfileTab.Bookmarks,
muted.length > 0 && ProfileTab.Muted,
].filter(a => unwrap(a)) as Tab[]; ].filter(a => unwrap(a)) as Tab[];
const horizontalScroll = useHorizontalScroll();
useEffect(() => { useEffect(() => {
setTab(ProfileTab.Notes); setTab(ProfileTab.Notes);
@ -114,7 +118,7 @@ export default function ProfilePage() {
<div className="name"> <div className="name">
<h2> <h2>
{user?.display_name || user?.name || "Nostrich"} {user?.display_name || user?.name || "Nostrich"}
<FollowsYou pubkey={id} /> <FollowsYou followsMe={follows.includes(id)} />
</h2> </h2>
{user?.nip05 && <Nip05 nip05={user.nip05} pubkey={user.pubkey} />} {user?.nip05 && <Nip05 nip05={user.nip05} pubkey={user.pubkey} />}
<Copy text={params.id || ""} /> <Copy text={params.id || ""} />
@ -211,29 +215,22 @@ export default function ProfilePage() {
} }
case FOLLOWS: { case FOLLOWS: {
if (isMe) { return (
return ( <div className="main-content">
<div className="main-content"> {follows.map(a => (
<h4> <ProfilePreview key={a} pubkey={a.toLowerCase()} options={{ about: !isMe }} />
<FormattedMessage {...messages.Following} values={{ n: follows.length }} /> ))}
</h4> </div>
{follows.map(a => ( );
<ProfilePreview key={a} pubkey={a.toLowerCase()} options={{ about: false }} />
))}
</div>
);
} else {
return <FollowsList pubkey={id} />;
}
} }
case FOLLOWERS: { case FOLLOWERS: {
return <FollowersList pubkey={id} />; return <FollowsList pubkeys={followers} />;
} }
case MUTED: { case MUTED: {
return isMe ? <BlockList variant="muted" /> : <MutedList pubkey={id} />; return <MutedList pubkeys={muted} />;
} }
case BLOCKED: { case BLOCKED: {
return isMe ? <BlockList variant="blocked" /> : null; return <BlockList />;
} }
case RELAYS: { case RELAYS: {
return <RelaysMetadata relays={relays} />; return <RelaysMetadata relays={relays} />;
@ -308,20 +305,23 @@ export default function ProfilePage() {
} }
const w = window.document.querySelector(".page")?.clientWidth; const w = window.document.querySelector(".page")?.clientWidth;
const bannerStyle = user?.banner ? ({ "--img-url": `url(${user.banner})` } as CSSProperties) : {};
return ( return (
<> <>
<div className="profile flex"> <div className="profile flex">
{user?.banner && <div className="banner-bg" style={bannerStyle} />}
{user?.banner && <ProxyImg alt="banner" className="banner" src={user.banner} size={w} />} {user?.banner && <ProxyImg alt="banner" className="banner" src={user.banner} size={w} />}
<div className="profile-wrapper flex"> <div className="profile-wrapper flex">
{avatar()} {avatar()}
{userDetails()} {userDetails()}
</div> </div>
</div> </div>
<div className="tabs main-content" ref={horizontalScroll}> <div className="main-content">
{[ProfileTab.Notes, ProfileTab.Followers, ProfileTab.Follows, ProfileTab.Muted].map(renderTab)} <div className="tabs" ref={horizontalScroll}>
{[ProfileTab.Notes, ProfileTab.Followers, ProfileTab.Follows].map(renderTab)}
{optionalTabs.map(renderTab)} {optionalTabs.map(renderTab)}
{isMe && renderTab(ProfileTab.Blocked)} {isMe && blocked.length > 0 && renderTab(ProfileTab.Blocked)}
</div>
</div> </div>
{tabContent()} {tabContent()}
</> </>

View File

@ -10,10 +10,15 @@ export default defineMessages({
Notes: { defaultMessage: "Notes" }, Notes: { defaultMessage: "Notes" },
Reactions: { defaultMessage: "Reactions" }, Reactions: { defaultMessage: "Reactions" },
Followers: { defaultMessage: "Followers" }, Followers: { defaultMessage: "Followers" },
Follows: { defaultMessage: "Follows" }, FollowersCount: { defaultMessage: "{n} Followers" },
Follows: { defaultMessage: "Following" },
FollowsCount: { defaultMessage: "{n} Following" },
Zaps: { defaultMessage: "Zaps" }, Zaps: { defaultMessage: "Zaps" },
ZapsCount: { defaultMessage: "{n} Zaps" },
Muted: { defaultMessage: "Muted" }, Muted: { defaultMessage: "Muted" },
MutedCount: { defaultMessage: "{n} Muted" },
Blocked: { defaultMessage: "Blocked" }, Blocked: { defaultMessage: "Blocked" },
BlockedCount: { defaultMessage: "{n} Blocked" },
Sats: { defaultMessage: "{n} {n, plural, =1 {sat} other {sats}}" }, Sats: { defaultMessage: "{n} {n, plural, =1 {sat} other {sats}}" },
Following: { defaultMessage: "Following {n}" }, Following: { defaultMessage: "Following {n}" },
Settings: { defaultMessage: "Settings" }, Settings: { defaultMessage: "Settings" },
@ -36,6 +41,9 @@ export default defineMessages({
Relays: { Relays: {
defaultMessage: "Relays", defaultMessage: "Relays",
}, },
RelaysCount: {
defaultMessage: "{n} Relays",
},
Bookmarks: { defaultMessage: "Bookmarks" }, Bookmarks: { defaultMessage: "Bookmarks" },
BookmarksCount: { defaultMessage: "Bookmarks ({n})" }, BookmarksCount: { defaultMessage: "{n} Bookmarks" },
}); });

View File

@ -3,6 +3,7 @@
--font-color: #fff; --font-color: #fff;
--font-secondary-color: #a7a7a7; --font-secondary-color: #a7a7a7;
--font-tertiary-color: #a3a3a3; --font-tertiary-color: #a3a3a3;
--border-color: rgba(163, 163, 163, 0.3);
--font-size: 16px; --font-size: 16px;
--font-size-small: 14px; --font-size-small: 14px;
--font-size-tiny: 12px; --font-size-tiny: 12px;
@ -39,10 +40,11 @@
} }
html.light { html.light {
--bg-color: #f1f1f1; --bg-color: #f8f8f8;
--font-color: #57534e; --font-color: #27272a;
--font-secondary-color: #7b7b7b; --font-secondary-color: #71717a;
--font-tertiary-color: #a7a7a7; --font-tertiary-color: #71717a;
--border-color: rgba(167, 167, 167, 0.3);
--highlight-light: #16aac1; --highlight-light: #16aac1;
--highlight: #0284c7; --highlight: #0284c7;
@ -215,9 +217,8 @@ button.icon:hover {
cursor: pointer; cursor: pointer;
color: var(--font-color); color: var(--font-color);
user-select: none; user-select: none;
background-color: var(--bg-color); background: none;
color: var(--font-color); border: none;
border: 1px solid;
display: inline-block; display: inline-block;
} }
@ -230,18 +231,16 @@ button.icon:hover {
} }
.btn.active { .btn.active {
border: 2px solid;
background-color: var(--gray-secondary);
color: var(--font-color); color: var(--font-color);
font-weight: 700; font-weight: 700;
} }
.btn.disabled { .btn.disabled {
color: var(--gray-light); opacity: 0.3;
} }
.btn:hover { .btn:hover {
background-color: var(--gray); color: var(--highlight);
} }
.btn-sm { .btn-sm {