solve conflicts

This commit is contained in:
Fernando Porazzi
2023-10-10 23:37:37 +02:00
88 changed files with 1401 additions and 461 deletions

View File

@ -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>

View File

@ -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;
}

View File

@ -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" />

View File

@ -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>

View File

@ -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 ? (

View File

@ -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);
}

View File

@ -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();

View File

@ -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);
}

View File

@ -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>
);

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

@ -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:

View File

@ -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} />