solve conflicts
This commit is contained in:
@ -94,13 +94,13 @@ const DonatePage = () => {
|
||||
<h2>
|
||||
<FormattedMessage
|
||||
defaultMessage="Help fund the development of {site}"
|
||||
values={{ site: process.env.APP_NAME_CAPITALIZED }}
|
||||
values={{ site: CONFIG.appNameCapitalized }}
|
||||
/>
|
||||
</h2>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
defaultMessage="{site} is an open source project built by passionate people in their free time"
|
||||
values={{ site: process.env.APP_NAME_CAPITALIZED }}
|
||||
values={{ site: CONFIG.appNameCapitalized }}
|
||||
/>
|
||||
</p>
|
||||
<p>
|
||||
|
@ -90,3 +90,22 @@ header {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.stalker {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
box-shadow: 0px 0px 26px 0px rgba(139, 92, 246, 0.7) inset;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.stalker button {
|
||||
position: absolute;
|
||||
top: 50px;
|
||||
right: 50px;
|
||||
color: black;
|
||||
background-color: var(--btn-color);
|
||||
padding: 12px;
|
||||
pointer-events: all !important;
|
||||
}
|
||||
|
@ -23,10 +23,12 @@ import { useLoginRelays } from "Hooks/useLoginRelays";
|
||||
import { useNoteCreator } from "State/NoteCreator";
|
||||
import { LoginUnlock } from "Element/PinPrompt";
|
||||
import useKeyboardShortcut from "Hooks/useKeyboardShortcut";
|
||||
import { LoginStore } from "Login";
|
||||
|
||||
export default function Layout() {
|
||||
const location = useLocation();
|
||||
const [pageClass, setPageClass] = useState("page");
|
||||
const { id, stalker } = useLogin(s => ({ id: s.id, stalker: s.stalker ?? false }));
|
||||
|
||||
useLoginFeed();
|
||||
useTheme();
|
||||
@ -71,6 +73,17 @@ export default function Layout() {
|
||||
<Toaster />
|
||||
</div>
|
||||
<LoginUnlock />
|
||||
{stalker && (
|
||||
<div
|
||||
className="stalker"
|
||||
onClick={() => {
|
||||
LoginStore.removeSession(id);
|
||||
}}>
|
||||
<button type="button" className="btn btn-rnd">
|
||||
<Icon name="close" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@ -240,7 +253,7 @@ function LogoHeader() {
|
||||
|
||||
return (
|
||||
<Link to="/" className="logo">
|
||||
<h1>{process.env.APP_NAME}</h1>
|
||||
<h1>{CONFIG.appName}</h1>
|
||||
{currentSubscription && (
|
||||
<small className="flex">
|
||||
<Icon name="diamond" size={10} className="mr5" />
|
||||
|
@ -143,7 +143,7 @@ export default function LoginPage() {
|
||||
|
||||
function generateNip46() {
|
||||
const meta = {
|
||||
name: process.env.APP_NAME_CAPITALIZED,
|
||||
name: CONFIG.appNameCapitalized,
|
||||
url: window.location.href,
|
||||
};
|
||||
|
||||
@ -287,7 +287,7 @@ export default function LoginPage() {
|
||||
<div>
|
||||
<div className="login-container">
|
||||
<h1 className="logo" onClick={() => navigate("/")}>
|
||||
{process.env.APP_NAME}
|
||||
{CONFIG.appName}
|
||||
</h1>
|
||||
<h1 dir="auto">
|
||||
<FormattedMessage defaultMessage="Login" description="Login header" />
|
||||
@ -342,7 +342,7 @@ export default function LoginPage() {
|
||||
<FormattedMessage
|
||||
defaultMessage="Secure your private key with a PIN, ensuring enhanced protection on {site}. You'll be prompted to enter this PIN each time you access the site."
|
||||
values={{
|
||||
site: process.env.APP_NAME_CAPITALIZED,
|
||||
site: CONFIG.appNameCapitalized,
|
||||
}}
|
||||
/>
|
||||
</p>
|
||||
|
@ -1,32 +1,33 @@
|
||||
import { NostrPrefix, tryParseNostrLink } from "@snort/system";
|
||||
import { useEffect, useState } from "react";
|
||||
import FormattedMessage from "Element/FormattedMessage";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import { useParams } from "react-router-dom";
|
||||
|
||||
import Spinner from "Icons/Spinner";
|
||||
import { profileLink } from "SnortUtils";
|
||||
import { getNip05PubKey } from "Pages/LoginPage";
|
||||
import ProfilePage from "Pages/Profile/ProfilePage";
|
||||
import { ThreadRoute } from "Element/Event/Thread";
|
||||
|
||||
export default function NostrLinkHandler() {
|
||||
const params = useParams();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [renderComponent, setRenderComponent] = useState<React.ReactNode | null>(null);
|
||||
|
||||
const link = decodeURIComponent(params["*"] ?? "").toLowerCase();
|
||||
|
||||
async function handleLink(link: string) {
|
||||
const nav = tryParseNostrLink(link);
|
||||
if (nav) {
|
||||
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) {
|
||||
navigate(`/p/${nav.encode()}`);
|
||||
setRenderComponent(<ProfilePage id={nav.encode()} />); // Directly render ProfilePage
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
const pubkey = await getNip05PubKey(`${link}@${process.env.NIP05_DOMAIN}`);
|
||||
const pubkey = await getNip05PubKey(`${link}@${CONFIG.nip05Domain}`);
|
||||
if (pubkey) {
|
||||
navigate(profileLink(pubkey));
|
||||
setRenderComponent(<ProfilePage id={pubkey} />); // Directly render ProfilePage
|
||||
}
|
||||
} catch {
|
||||
//ignored
|
||||
@ -41,6 +42,10 @@ export default function NostrLinkHandler() {
|
||||
}
|
||||
}, [link]);
|
||||
|
||||
if (renderComponent) {
|
||||
return renderComponent;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex f-center">
|
||||
{loading ? (
|
||||
|
@ -52,3 +52,29 @@
|
||||
max-width: 100%;
|
||||
max-height: 300px; /* Cap images in notifications to 300px height */
|
||||
}
|
||||
|
||||
.summary-icon {
|
||||
padding: 4px;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
color: var(--gray-light) !important;
|
||||
}
|
||||
|
||||
.summary-icon:not(.active):hover {
|
||||
background-color: var(--gray-dark);
|
||||
}
|
||||
|
||||
.summary-icon.active {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.summary-tooltip {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
padding: 12px 16px;
|
||||
border-radius: 16px;
|
||||
background: var(--gray-superdark);
|
||||
box-shadow: 0px 2px 4px 0px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
@ -1,16 +1,17 @@
|
||||
import "./Notifications.css";
|
||||
import { useEffect, useMemo, useSyncExternalStore } from "react";
|
||||
import { useEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
|
||||
import { EventExt, EventKind, NostrEvent, NostrLink, NostrPrefix, TaggedNostrEvent, parseZap } from "@snort/system";
|
||||
import { unwrap } from "@snort/shared";
|
||||
import { unixNow, unwrap } from "@snort/shared";
|
||||
import { useUserProfile } from "@snort/system-react";
|
||||
import { useInView } from "react-intersection-observer";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { Bar, BarChart, Tooltip, XAxis, YAxis } from "recharts";
|
||||
|
||||
import useLogin from "Hooks/useLogin";
|
||||
import { markNotificationsRead } from "Login";
|
||||
import { Notifications, UserCache } from "Cache";
|
||||
import { dedupe, findTag, orderDescending } from "SnortUtils";
|
||||
import { dedupe, findTag, orderAscending, orderDescending } from "SnortUtils";
|
||||
import Icon from "Icons/Icon";
|
||||
import ProfileImage from "Element/User/ProfileImage";
|
||||
import useModeration from "Hooks/useModeration";
|
||||
@ -20,6 +21,8 @@ import { formatShort } from "Number";
|
||||
import { LiveEvent } from "Element/LiveEvent";
|
||||
import ProfilePreview from "Element/User/ProfilePreview";
|
||||
import { getDisplayName } from "Element/User/DisplayName";
|
||||
import { Day } from "Const";
|
||||
import Tabs, { Tab } from "Element/Tabs";
|
||||
|
||||
function notificationContext(ev: TaggedNostrEvent) {
|
||||
switch (ev.kind) {
|
||||
@ -77,28 +80,195 @@ export default function NotificationsPage({ onClick }: { onClick?: (link: NostrL
|
||||
return onHour.toString();
|
||||
};
|
||||
|
||||
const myNotifications = useMemo(() => {
|
||||
return orderDescending([...notifications]).filter(
|
||||
a => !isMuted(a.pubkey) && a.tags.some(b => b[0] === "p" && b[1] === login.publicKey),
|
||||
);
|
||||
}, [notifications, login.publicKey]);
|
||||
|
||||
const timeGrouped = useMemo(() => {
|
||||
return orderDescending([...notifications])
|
||||
.filter(a => !isMuted(a.pubkey) && a.tags.some(b => b[0] === "p" && b[1] === login.publicKey))
|
||||
.reduce((acc, v) => {
|
||||
const key = `${timeKey(v)}:${notificationContext(v as TaggedNostrEvent)?.encode()}:${v.kind}`;
|
||||
if (acc.has(key)) {
|
||||
unwrap(acc.get(key)).push(v as TaggedNostrEvent);
|
||||
} else {
|
||||
acc.set(key, [v as TaggedNostrEvent]);
|
||||
}
|
||||
return acc;
|
||||
}, new Map<string, Array<TaggedNostrEvent>>());
|
||||
}, [notifications]);
|
||||
return myNotifications.reduce((acc, v) => {
|
||||
const key = `${timeKey(v)}:${notificationContext(v as TaggedNostrEvent)?.encode()}:${v.kind}`;
|
||||
if (acc.has(key)) {
|
||||
unwrap(acc.get(key)).push(v as TaggedNostrEvent);
|
||||
} else {
|
||||
acc.set(key, [v as TaggedNostrEvent]);
|
||||
}
|
||||
return acc;
|
||||
}, new Map<string, Array<TaggedNostrEvent>>());
|
||||
}, [myNotifications]);
|
||||
|
||||
return (
|
||||
<div className="main-content">
|
||||
<NotificationSummary evs={myNotifications as TaggedNostrEvent[]} />
|
||||
|
||||
{login.publicKey &&
|
||||
[...timeGrouped.entries()].map(([k, g]) => <NotificationGroup key={k} evs={g} onClick={onClick} />)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface StatSlot {
|
||||
time: string;
|
||||
reactions: number;
|
||||
reposts: number;
|
||||
quotes: number;
|
||||
mentions: number;
|
||||
zaps: number;
|
||||
}
|
||||
|
||||
const enum NotificationSummaryPeriod {
|
||||
Daily,
|
||||
Weekly,
|
||||
}
|
||||
|
||||
const enum NotificationSummaryFilter {
|
||||
Reactions = 1,
|
||||
Reposts = 2,
|
||||
Mentions = 4,
|
||||
Zaps = 8,
|
||||
All = 255,
|
||||
}
|
||||
|
||||
function NotificationSummary({ evs }: { evs: Array<TaggedNostrEvent> }) {
|
||||
const ref = useRef<HTMLDivElement | null>(null);
|
||||
const [period, setPeriod] = useState(NotificationSummaryPeriod.Daily);
|
||||
const [filter, setFilter] = useState(NotificationSummaryFilter.All);
|
||||
|
||||
const periodTabs = [
|
||||
{
|
||||
value: NotificationSummaryPeriod.Daily,
|
||||
text: <FormattedMessage defaultMessage="Daily" />,
|
||||
},
|
||||
{
|
||||
value: NotificationSummaryPeriod.Weekly,
|
||||
text: <FormattedMessage defaultMessage="Weekly" />,
|
||||
},
|
||||
] as Array<Tab>;
|
||||
|
||||
const hasFlag = (v: number, f: NotificationSummaryFilter) => {
|
||||
return (v & f) > 0;
|
||||
};
|
||||
|
||||
const getWeek = (d: Date) => {
|
||||
const onejan = new Date(d.getFullYear(), 0, 1);
|
||||
const today = new Date(d.getFullYear(), d.getMonth(), d.getDate());
|
||||
const dayOfYear = (today.getTime() - onejan.getTime() + 86400000) / 86400000;
|
||||
return Math.ceil(dayOfYear / 7);
|
||||
};
|
||||
|
||||
const stats = useMemo(() => {
|
||||
return orderAscending(evs)
|
||||
.filter(a => (period === NotificationSummaryPeriod.Daily ? a.created_at > unixNow() - 14 * Day : true))
|
||||
.reduce(
|
||||
(acc, v) => {
|
||||
const date = new Date(v.created_at * 1000);
|
||||
const key =
|
||||
period === NotificationSummaryPeriod.Daily
|
||||
? `${date.getMonth() + 1}/${date.getDate()}`
|
||||
: getWeek(date).toString();
|
||||
acc[key] ??= {
|
||||
time: key,
|
||||
reactions: 0,
|
||||
reposts: 0,
|
||||
quotes: 0,
|
||||
mentions: 0,
|
||||
zaps: 0,
|
||||
};
|
||||
|
||||
if (v.kind === EventKind.Reaction) {
|
||||
acc[key].reactions++;
|
||||
} else if (v.kind === EventKind.Repost) {
|
||||
acc[key].reposts++;
|
||||
} else if (v.kind === EventKind.ZapReceipt) {
|
||||
acc[key].zaps++;
|
||||
}
|
||||
if (v.kind === EventKind.TextNote) {
|
||||
acc[key].mentions++;
|
||||
}
|
||||
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, StatSlot>,
|
||||
);
|
||||
}, [evs, period]);
|
||||
|
||||
const filterIcon = (f: NotificationSummaryFilter, icon: string, iconActiveClass?: string) => {
|
||||
const active = hasFlag(filter, f);
|
||||
return (
|
||||
<div className={`summary-icon${active ? " active" : ""}`} onClick={() => setFilter(v => v ^ f)}>
|
||||
<Icon name={icon} className={active ? iconActiveClass : undefined} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex-column g12 p bb">
|
||||
<div className="flex f-space">
|
||||
<h2>
|
||||
<FormattedMessage defaultMessage="Summary" description="Notifications summary" />
|
||||
</h2>
|
||||
<div className="flex g8">
|
||||
{filterIcon(NotificationSummaryFilter.Reactions, "heart-solid", "text-heart")}
|
||||
{filterIcon(NotificationSummaryFilter.Zaps, "zap-solid", "text-zap")}
|
||||
{filterIcon(NotificationSummaryFilter.Reposts, "reverse-left", "text-repost")}
|
||||
{filterIcon(NotificationSummaryFilter.Mentions, "at-sign", "text-mention")}
|
||||
</div>
|
||||
</div>
|
||||
<Tabs tabs={periodTabs} tab={unwrap(periodTabs.find(a => a.value === period))} setTab={t => setPeriod(t.value)} />
|
||||
<div ref={ref}>
|
||||
<BarChart
|
||||
width={ref.current?.clientWidth}
|
||||
height={200}
|
||||
data={Object.values(stats)}
|
||||
margin={{ left: 0, right: 0 }}
|
||||
style={{ userSelect: "none" }}>
|
||||
<XAxis dataKey="time" />
|
||||
<YAxis />
|
||||
{hasFlag(filter, NotificationSummaryFilter.Reactions) && (
|
||||
<Bar dataKey="reactions" fill="var(--heart)" stackId="" />
|
||||
)}
|
||||
{hasFlag(filter, NotificationSummaryFilter.Reposts) && (
|
||||
<Bar dataKey="reposts" fill="var(--repost)" stackId="" />
|
||||
)}
|
||||
{hasFlag(filter, NotificationSummaryFilter.Mentions) && (
|
||||
<Bar dataKey="mentions" fill="var(--mention)" stackId="" />
|
||||
)}
|
||||
{hasFlag(filter, NotificationSummaryFilter.Zaps) && <Bar dataKey="zaps" fill="var(--zap)" stackId="" />}
|
||||
<Tooltip
|
||||
cursor={{ fill: "rgba(255,255,255,0.2)" }}
|
||||
content={({ active, payload }) => {
|
||||
if (active && payload && payload.length) {
|
||||
return (
|
||||
<div className="summary-tooltip">
|
||||
<div className="flex-column g12">
|
||||
<Icon name="heart-solid" className="text-heart" />
|
||||
{formatShort(payload.find(a => a.name === "reactions")?.value as number)}
|
||||
</div>
|
||||
<div className="flex-column g12">
|
||||
<Icon name="zap-solid" className="text-zap" />
|
||||
{formatShort(payload.find(a => a.name === "zaps")?.value as number)}
|
||||
</div>
|
||||
<div className="flex-column g12">
|
||||
<Icon name="reverse-left" className="text-repost" />
|
||||
{formatShort(payload.find(a => a.name === "reposts")?.value as number)}
|
||||
</div>
|
||||
<div className="flex-column g12">
|
||||
<Icon name="at-sign" className="text-mention" />
|
||||
{formatShort(payload.find(a => a.name === "mentions")?.value as number)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}}
|
||||
/>
|
||||
</BarChart>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function NotificationGroup({ evs, onClick }: { evs: Array<TaggedNostrEvent>; onClick?: (link: NostrLink) => void }) {
|
||||
const { ref, inView } = useInView({ triggerOnce: true });
|
||||
const { formatMessage } = useIntl();
|
||||
|
@ -138,14 +138,6 @@
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.profile .website a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.profile .website a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.profile .link svg {
|
||||
color: var(--highlight);
|
||||
}
|
@ -2,36 +2,19 @@ import "./ProfilePage.css";
|
||||
import { useEffect, useState } from "react";
|
||||
import FormattedMessage from "Element/FormattedMessage";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import {
|
||||
encodeTLV,
|
||||
encodeTLVEntries,
|
||||
EventKind,
|
||||
HexKey,
|
||||
NostrLink,
|
||||
NostrPrefix,
|
||||
TLVEntryType,
|
||||
tryParseNostrLink,
|
||||
} from "@snort/system";
|
||||
import { encodeTLV, encodeTLVEntries, EventKind, NostrPrefix, TLVEntryType, tryParseNostrLink } from "@snort/system";
|
||||
import { LNURL } from "@snort/shared";
|
||||
import { useUserProfile } from "@snort/system-react";
|
||||
|
||||
import { findTag, getReactions, unwrap } from "SnortUtils";
|
||||
import { formatShort } from "Number";
|
||||
import Note from "Element/Event/Note";
|
||||
import Bookmarks from "Element/Bookmarks";
|
||||
import RelaysMetadata from "Element/Relay/RelaysMetadata";
|
||||
import { Tab, TabElement } from "Element/Tabs";
|
||||
import Icon from "Icons/Icon";
|
||||
import useMutedFeed from "Feed/MuteList";
|
||||
import useRelaysFeed from "Feed/RelaysFeed";
|
||||
import usePinnedFeed from "Feed/PinnedFeed";
|
||||
import useBookmarkFeed from "Feed/BookmarkFeed";
|
||||
import useFollowersFeed from "Feed/FollowersFeed";
|
||||
import useFollowsFeed from "Feed/FollowsFeed";
|
||||
import useProfileBadges from "Feed/BadgesFeed";
|
||||
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 { parseId, hexToBech32 } from "SnortUtils";
|
||||
import Avatar from "Element/User/Avatar";
|
||||
@ -57,61 +40,24 @@ import useLogin from "Hooks/useLogin";
|
||||
import { ZapTarget } from "Zapper";
|
||||
import { useStatusFeed } from "Feed/StatusFeed";
|
||||
|
||||
import messages from "./messages";
|
||||
import messages from "../messages";
|
||||
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";
|
||||
|
||||
const NOTES = 0;
|
||||
const REACTIONS = 1;
|
||||
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>
|
||||
);
|
||||
interface ProfilePageProps {
|
||||
id?: string;
|
||||
}
|
||||
|
||||
function FollowersTab({ id }: { id: HexKey }) {
|
||||
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() {
|
||||
export default function ProfilePage({ id: propId }: ProfilePageProps) {
|
||||
const params = useParams();
|
||||
const navigate = useNavigate();
|
||||
const [id, setId] = useState<string>();
|
||||
@ -135,8 +81,6 @@ export default function ProfilePage() {
|
||||
const showBadges = login.preferences.showBadges ?? false;
|
||||
const showStatus = login.preferences.showStatus ?? true;
|
||||
|
||||
const website_url =
|
||||
user?.website && !user.website.startsWith("http") ? "https://" + user.website : user?.website || "";
|
||||
// feeds
|
||||
const { blocked } = useModeration();
|
||||
const pinned = usePinnedFeed(id);
|
||||
@ -146,89 +90,6 @@ export default function ProfilePage() {
|
||||
const status = useStatusFeed(showStatus ? id : undefined, true);
|
||||
|
||||
// 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 optionalTabs = [ProfileTab.Zaps, ProfileTab.Relays, ProfileTab.Bookmarks, ProfileTab.Muted].filter(a =>
|
||||
unwrap(a),
|
||||
@ -236,21 +97,22 @@ export default function ProfilePage() {
|
||||
const horizontalScroll = useHorizontalScroll();
|
||||
|
||||
useEffect(() => {
|
||||
if (params.id?.match(EmailRegex)) {
|
||||
getNip05PubKey(params.id).then(a => {
|
||||
const resolvedId = propId || params.id;
|
||||
if (resolvedId?.match(EmailRegex)) {
|
||||
getNip05PubKey(resolvedId).then(a => {
|
||||
setId(a);
|
||||
});
|
||||
} else {
|
||||
const nav = tryParseNostrLink(params.id ?? "");
|
||||
const nav = tryParseNostrLink(resolvedId ?? "");
|
||||
if (nav?.type === NostrPrefix.PublicKey || nav?.type === NostrPrefix.Profile) {
|
||||
// todo: use relays if any for nprofile
|
||||
setId(nav.id);
|
||||
} else {
|
||||
setId(parseId(params.id ?? ""));
|
||||
setId(parseId(resolvedId ?? ""));
|
||||
}
|
||||
}
|
||||
setTab(ProfileTab.Notes);
|
||||
}, [params]);
|
||||
}, [propId, params]);
|
||||
|
||||
function musicStatus() {
|
||||
if (!status.music) return;
|
||||
@ -275,12 +137,21 @@ export default function ProfilePage() {
|
||||
return inner();
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (user?.nip05 && user?.isNostrAddressValid) {
|
||||
if (user.nip05.endsWith(`@${CONFIG.nip05Domain}`)) {
|
||||
const username = user.nip05?.replace(`@${CONFIG.nip05Domain}`, "");
|
||||
navigate(`/${username}`, { replace: true });
|
||||
}
|
||||
}
|
||||
}, [user?.isNostrAddressValid, user?.nip05]);
|
||||
|
||||
function username() {
|
||||
return (
|
||||
<>
|
||||
<div className="flex-column g4">
|
||||
<h2 className="flex g4">
|
||||
{user?.display_name || user?.name || "Nostrich"}
|
||||
<DisplayName user={user} pubkey={user?.pubkey ?? ""} />
|
||||
<FollowsYou followsMe={follows.includes(loginPubKey ?? "")} />
|
||||
</h2>
|
||||
{user?.nip05 && <Nip05 nip05={user.nip05} pubkey={user.pubkey} />}
|
||||
@ -295,28 +166,10 @@ export default function ProfilePage() {
|
||||
);
|
||||
}
|
||||
|
||||
function tryFormatWebsite(url: string) {
|
||||
try {
|
||||
const u = new URL(url);
|
||||
return `${u.hostname}${u.pathname !== "/" ? u.pathname : ""}`;
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
return url;
|
||||
}
|
||||
|
||||
function links() {
|
||||
return (
|
||||
<>
|
||||
{user?.website && (
|
||||
<div className="link website f-ellipsis">
|
||||
<Icon name="link-02" size={16} />
|
||||
<a href={website_url} target="_blank" rel="noreferrer">
|
||||
{tryFormatWebsite(user.website)}
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<UserWebsiteLink user={user} />
|
||||
{lnurl && (
|
||||
<div className="link lnurl f-ellipsis" onClick={() => setShowLnQr(true)}>
|
||||
<Icon name="zapCircle" size={16} />
|
||||
@ -369,7 +222,7 @@ export default function ProfilePage() {
|
||||
if (!id) return null;
|
||||
|
||||
switch (tab.value) {
|
||||
case NOTES:
|
||||
case ProfileTabType.NOTES:
|
||||
return (
|
||||
<>
|
||||
{pinned
|
||||
@ -399,29 +252,29 @@ export default function ProfilePage() {
|
||||
/>
|
||||
</>
|
||||
);
|
||||
case ZAPS: {
|
||||
case ProfileTabType.ZAPS: {
|
||||
return <ZapsProfileTab id={id} />;
|
||||
}
|
||||
case FOLLOWS: {
|
||||
case ProfileTabType.FOLLOWS: {
|
||||
if (isMe) {
|
||||
return <FollowsList pubkeys={follows} showFollowAll={!isMe} showAbout={false} className="p" />;
|
||||
} else {
|
||||
return <FollowsTab id={id} />;
|
||||
}
|
||||
}
|
||||
case FOLLOWERS: {
|
||||
case ProfileTabType.FOLLOWERS: {
|
||||
return <FollowersTab id={id} />;
|
||||
}
|
||||
case MUTED: {
|
||||
case ProfileTabType.MUTED: {
|
||||
return <MutedList pubkeys={muted} />;
|
||||
}
|
||||
case BLOCKED: {
|
||||
case ProfileTabType.BLOCKED: {
|
||||
return <BlockList />;
|
||||
}
|
||||
case RELAYS: {
|
||||
case ProfileTabType.RELAYS: {
|
||||
return <RelaysTab id={id} />;
|
||||
}
|
||||
case BOOKMARKS: {
|
||||
case ProfileTabType.BOOKMARKS: {
|
||||
return <BookMarksTab id={id} />;
|
||||
}
|
||||
}
|
||||
@ -433,7 +286,7 @@ export default function ProfilePage() {
|
||||
<Avatar pubkey={id ?? ""} user={user} onClick={() => setModalImage(user?.picture || "")} className="pointer" />
|
||||
<div className="profile-actions">
|
||||
{renderIcons()}
|
||||
{!isMe && id && <FollowButton className="primary" pubkey={id} />}
|
||||
{!isMe && id && <FollowButton pubkey={id} />}
|
||||
</div>
|
||||
</div>
|
||||
);
|
154
packages/app/src/Pages/Profile/ProfileTab.tsx
Normal file
154
packages/app/src/Pages/Profile/ProfileTab.tsx
Normal 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;
|
@ -14,11 +14,11 @@ export default defineMessages({
|
||||
KeysSaved: { defaultMessage: "I have saved my keys, continue" },
|
||||
WhatIsSnort: {
|
||||
defaultMessage: "What is {site} and how does it work?",
|
||||
values: { site: process.env.APP_NAME_CAPITALIZED },
|
||||
values: { site: CONFIG.appNameCapitalized },
|
||||
},
|
||||
WhatIsSnortIntro: {
|
||||
defaultMessage: `{site} is a Nostr UI, nostr is a decentralised protocol for saving and distributing "notes".`,
|
||||
values: { site: process.env.APP_NAME_CAPITALIZED },
|
||||
values: { site: CONFIG.appNameCapitalized },
|
||||
},
|
||||
WhatIsSnortNotes: {
|
||||
defaultMessage: `Notes hold text content, the most popular usage of these notes is to store "tweet like" messages.`,
|
||||
@ -26,7 +26,7 @@ export default defineMessages({
|
||||
|
||||
WhatIsSnortExperience: {
|
||||
defaultMessage: "{site} is designed to have a similar experience to Twitter.",
|
||||
values: { site: process.env.APP_NAME_CAPITALIZED },
|
||||
values: { site: CONFIG.appNameCapitalized },
|
||||
},
|
||||
HowKeysWork: { defaultMessage: "How do keys work?" },
|
||||
DigitalSignatures: {
|
||||
@ -70,9 +70,9 @@ export default defineMessages({
|
||||
NameSquatting: {
|
||||
defaultMessage:
|
||||
"Name-squatting and impersonation is not allowed. {site} and our partners reserve the right to terminate your handle (not your account - nobody can take that away) for violating this rule.",
|
||||
values: { site: process.env.APP_NAME_CAPITALIZED },
|
||||
values: { site: CONFIG.appNameCapitalized },
|
||||
},
|
||||
PreviewOnSnort: { defaultMessage: "Preview on {site}", values: { site: process.env.APP_NAME_CAPITALIZED } },
|
||||
PreviewOnSnort: { defaultMessage: "Preview on {site}", values: { site: CONFIG.appNameCapitalized } },
|
||||
GetSnortId: { defaultMessage: "Get a Snort identifier" },
|
||||
GetSnortIdHelp: {
|
||||
defaultMessage:
|
||||
|
@ -6,9 +6,9 @@ import Icon from "Icons/Icon";
|
||||
import { LoginStore, logout } from "Login";
|
||||
import useLogin from "Hooks/useLogin";
|
||||
import { getCurrentSubscription } from "Subscription";
|
||||
import usePageWidth from "Hooks/usePageWidth";
|
||||
|
||||
import messages from "./messages";
|
||||
import usePageWidth from "Hooks/usePageWidth";
|
||||
|
||||
const SettingsIndex = () => {
|
||||
const login = useLogin();
|
||||
@ -61,11 +61,13 @@ const SettingsIndex = () => {
|
||||
<FormattedMessage defaultMessage="Nostr Adddress" />
|
||||
<Icon name="arrowFront" size={16} />
|
||||
</div>
|
||||
<div className="settings-row" onClick={() => navigate("/subscribe/manage")}>
|
||||
<Icon name="diamond" size={24} />
|
||||
<FormattedMessage defaultMessage="Subscription" />
|
||||
<Icon name="arrowFront" size={16} />
|
||||
</div>
|
||||
{CONFIG.features.subscriptions && (
|
||||
<div className="settings-row" onClick={() => navigate("/subscribe/manage")}>
|
||||
<Icon name="diamond" size={24} />
|
||||
<FormattedMessage defaultMessage="Subscription" />
|
||||
<Icon name="arrowFront" size={16} />
|
||||
</div>
|
||||
)}
|
||||
{sub && (
|
||||
<div className="settings-row" onClick={() => navigate("accounts")}>
|
||||
<Icon name="code-circle" size={24} />
|
||||
|
Reference in New Issue
Block a user