diff --git a/.changeset/cuddly-bikes-drop.md b/.changeset/cuddly-bikes-drop.md
new file mode 100644
index 000000000..dc5c64731
--- /dev/null
+++ b/.changeset/cuddly-bikes-drop.md
@@ -0,0 +1,5 @@
+---
+"nostrudel": minor
+---
+
+Create about tab in profile view
diff --git a/.changeset/great-ties-compare.md b/.changeset/great-ties-compare.md
new file mode 100644
index 000000000..fd19080a6
--- /dev/null
+++ b/.changeset/great-ties-compare.md
@@ -0,0 +1,5 @@
+---
+"nostrudel": minor
+---
+
+Virtulize following and followers tabs in profile view
diff --git a/.changeset/nervous-otters-wink.md b/.changeset/nervous-otters-wink.md
new file mode 100644
index 000000000..59702ce53
--- /dev/null
+++ b/.changeset/nervous-otters-wink.md
@@ -0,0 +1,5 @@
+---
+"nostrudel": minor
+---
+
+Add profile stats from nostr.band
diff --git a/.changeset/soft-yaks-lie.md b/.changeset/soft-yaks-lie.md
new file mode 100644
index 000000000..6b63904b4
--- /dev/null
+++ b/.changeset/soft-yaks-lie.md
@@ -0,0 +1,5 @@
+---
+"nostrudel": patch
+---
+
+Fix redirect not working on login view
diff --git a/package.json b/package.json
index 40f050687..c24f1ac6b 100644
--- a/package.json
+++ b/package.json
@@ -30,16 +30,19 @@
"react-router-dom": "^6.11.2",
"react-singleton-hook": "^4.0.1",
"react-use": "^17.4.0",
+ "react-virtualized-auto-sizer": "^1.0.20",
+ "react-window": "^1.8.9",
"webln": "^0.3.2"
},
"devDependencies": {
- "@testing-library/cypress": "^9.0.0",
- "cypress": "^12.13.0",
"@changesets/cli": "^2.26.1",
+ "@testing-library/cypress": "^9.0.0",
"@types/identicon.js": "^2.3.1",
"@types/react": "^18.2.7",
"@types/react-dom": "^18.2.4",
+ "@types/react-window": "^1.8.5",
"@vitejs/plugin-react": "^4.0.0",
+ "cypress": "^12.13.0",
"prettier": "^2.8.8",
"typescript": "^5.0.4",
"vite": "^4.3.8",
diff --git a/src/app.tsx b/src/app.tsx
index f39a9d2a8..dcd3ada64 100644
--- a/src/app.tsx
+++ b/src/app.tsx
@@ -34,6 +34,7 @@ import appSettings from "./services/app-settings";
import UserMediaTab from "./views/user/media";
import ToolsHomeView from "./views/tools";
import Nip19ToolsView from "./views/tools/nip19";
+import UserAboutTab from "./views/user/about";
// code split search view because QrScanner library is 400kB
const SearchView = React.lazy(() => import("./views/search"));
@@ -64,7 +65,8 @@ const router = createHashRouter([
path: "/u/:pubkey",
element: ,
children: [
- { path: "", element: },
+ { path: "", element: },
+ { path: "about", element: },
{ path: "notes", element: },
{ path: "media", element: },
{ path: "zaps", element: },
diff --git a/src/components/embed-types/common.tsx b/src/components/embed-types/common.tsx
index bee8ff359..497d6730a 100644
--- a/src/components/embed-types/common.tsx
+++ b/src/components/embed-types/common.tsx
@@ -43,7 +43,7 @@ export function renderVideoUrl(match: URL) {
return ;
}
-export function renderDefaultUrl(match: URL) {
+export function renderGenericUrl(match: URL) {
return (
{match.toString()}
diff --git a/src/components/embed-types/nostr.tsx b/src/components/embed-types/nostr.tsx
index 05519d690..6907e9dec 100644
--- a/src/components/embed-types/nostr.tsx
+++ b/src/components/embed-types/nostr.tsx
@@ -9,7 +9,7 @@ import { Link as RouterLink } from "react-router-dom";
// nostr:nevent1qqsthg2qlxp9l7egtwa92t8lusm7pjknmjwa75ctrrpcjyulr9754fqpz3mhxue69uhhyetvv9ujuerpd46hxtnfduq36amnwvaz7tmwdaehgu3dwp6kytnhv4kxcmmjv3jhytnwv46q2qg5q9
// nostr:nevent1qqsq3wc73lqxd70lg43m5rul57d4mhcanttjat56e30yx5zla48qzlspz9mhxue69uhkummnw3e82efwvdhk6qgdwaehxw309ahx7uewd3hkcq5hsum
-export function embedNostrLinks(content: EmbedableContent, event: NostrEvent | DraftNostrEvent) {
+export function embedNostrLinks(content: EmbedableContent) {
return embedJSX(content, {
name: "nostr-link",
regexp: /(nostr:|@)?((npub|note|nprofile|nevent)1[qpzry9x8gf2tvdw0s3jn54khce6mua7l]{58,})/i,
diff --git a/src/components/embed-types/reddit.tsx b/src/components/embed-types/reddit.tsx
index ab92603ca..96190a019 100644
--- a/src/components/embed-types/reddit.tsx
+++ b/src/components/embed-types/reddit.tsx
@@ -1,6 +1,6 @@
import { replaceDomain } from "../../helpers/url";
import appSettings from "../../services/app-settings";
-import { renderDefaultUrl } from "./common";
+import { renderGenericUrl } from "./common";
// copied from https://github.com/SimonBrazell/privacy-redirect/blob/master/src/assets/javascripts/helpers/reddit.js
const REDDIT_DOMAINS = [
@@ -21,5 +21,5 @@ export function renderRedditUrl(match: URL) {
const { redditRedirect } = appSettings.value;
const fixed = redditRedirect ? replaceDomain(match, redditRedirect) : match;
- return renderDefaultUrl(fixed);
+ return renderGenericUrl(fixed);
}
diff --git a/src/components/embed-types/twitter.tsx b/src/components/embed-types/twitter.tsx
index a8323b746..ed274215a 100644
--- a/src/components/embed-types/twitter.tsx
+++ b/src/components/embed-types/twitter.tsx
@@ -1,21 +1,15 @@
import { replaceDomain } from "../../helpers/url";
import appSettings from "../../services/app-settings";
import { TweetEmbed } from "../tweet-embed";
-import { renderDefaultUrl } from "./common";
+import { renderGenericUrl } from "./common";
// copied from https://github.com/SimonBrazell/privacy-redirect/blob/master/src/assets/javascripts/helpers/twitter.js
-export const TWITTER_DOMAINS = [
- "twitter.com",
- "www.twitter.com",
- "mobile.twitter.com",
- "pbs.twimg.com",
- "video.twimg.com",
-];
+export const TWITTER_DOMAINS = ["twitter.com", "www.twitter.com", "mobile.twitter.com", "pbs.twimg.com"];
export function renderTwitterUrl(match: URL) {
if (!TWITTER_DOMAINS.includes(match.hostname)) return null;
const { twitterRedirect } = appSettings.value;
- if (twitterRedirect) return renderDefaultUrl(replaceDomain(match, twitterRedirect));
+ if (twitterRedirect) return renderGenericUrl(replaceDomain(match, twitterRedirect));
else return ;
}
diff --git a/src/components/icons.tsx b/src/components/icons.tsx
index 45892d421..41378382f 100644
--- a/src/components/icons.tsx
+++ b/src/components/icons.tsx
@@ -247,3 +247,15 @@ export const ToolsIcon = createIcon({
d: "M5.32894 3.27158C6.56203 2.8332 7.99181 3.10749 8.97878 4.09446C10.0997 5.21537 10.3014 6.90741 9.58382 8.23385L20.2925 18.9437L18.8783 20.3579L8.16933 9.64875C6.84277 10.3669 5.1502 10.1654 4.02903 9.04421C3.04178 8.05696 2.76761 6.62665 3.20652 5.39332L5.44325 7.63C6.02903 8.21578 6.97878 8.21578 7.56457 7.63C8.15035 7.04421 8.15035 6.09446 7.56457 5.50868L5.32894 3.27158ZM15.6963 5.15512L18.8783 3.38736L20.2925 4.80157L18.5247 7.98355L16.757 8.3371L14.6356 10.4584L13.2214 9.04421L15.3427 6.92289L15.6963 5.15512ZM8.97878 13.2868L10.393 14.7011L5.08969 20.0044C4.69917 20.3949 4.066 20.3949 3.67548 20.0044C3.31285 19.6417 3.28695 19.0699 3.59777 18.6774L3.67548 18.5902L8.97878 13.2868Z",
defaultProps,
});
+
+export const EditIcon = createIcon({
+ displayName: "EditIcon",
+ d: "M6.41421 15.89L16.5563 5.74786L15.1421 4.33365L5 14.4758V15.89H6.41421ZM7.24264 17.89H3V13.6474L14.435 2.21233C14.8256 1.8218 15.4587 1.8218 15.8492 2.21233L18.6777 5.04075C19.0682 5.43128 19.0682 6.06444 18.6777 6.45497L7.24264 17.89ZM3 19.89H21V21.89H3V19.89Z",
+ defaultProps,
+});
+
+export const AtIcon = createIcon({
+ displayName: "AtIcon",
+ d: "M20 12C20 7.58172 16.4183 4 12 4C7.58172 4 4 7.58172 4 12C4 16.4183 7.58172 20 12 20C13.6418 20 15.1681 19.5054 16.4381 18.6571L17.5476 20.3214C15.9602 21.3818 14.0523 22 12 22C6.47715 22 2 17.5228 2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12V13.5C22 15.433 20.433 17 18.5 17C17.2958 17 16.2336 16.3918 15.6038 15.4659C14.6942 16.4115 13.4158 17 12 17C9.23858 17 7 14.7614 7 12C7 9.23858 9.23858 7 12 7C13.1258 7 14.1647 7.37209 15.0005 8H17V13.5C17 14.3284 17.6716 15 18.5 15C19.3284 15 20 14.3284 20 13.5V12ZM12 9C10.3431 9 9 10.3431 9 12C9 13.6569 10.3431 15 12 15C13.6569 15 15 13.6569 15 12C15 10.3431 13.6569 9 12 9Z",
+ defaultProps,
+});
diff --git a/src/components/note/note-contents.tsx b/src/components/note/note-contents.tsx
index 289e59435..c4bb64a6b 100644
--- a/src/components/note/note-contents.tsx
+++ b/src/components/note/note-contents.tsx
@@ -11,7 +11,7 @@ import {
embedNostrHashtags,
renderWavlakeUrl,
renderYoutubeUrl,
- renderDefaultUrl,
+ renderGenericUrl,
renderImageUrl,
renderTwitterUrl,
renderAppleMusicUrl,
@@ -37,14 +37,14 @@ function buildContents(event: NostrEvent | DraftNostrEvent, trusted = false) {
renderTidalUrl,
renderImageUrl,
renderVideoUrl,
- renderDefaultUrl,
+ renderGenericUrl,
]);
// bitcoin
content = embedLightningInvoice(content);
// nostr
- content = embedNostrLinks(content, event);
+ content = embedNostrLinks(content);
content = embedNostrMentions(content, event);
content = embedNostrHashtags(content, event);
diff --git a/src/components/page/account-switcher.tsx b/src/components/page/account-switcher.tsx
index a598825ab..935b106e0 100644
--- a/src/components/page/account-switcher.tsx
+++ b/src/components/page/account-switcher.tsx
@@ -18,7 +18,7 @@ import { useUserMetadata } from "../../hooks/use-user-metadata";
import accountService from "../../services/account";
import { AddIcon } from "../icons";
import { UserAvatar } from "../user-avatar";
-import { useNavigate } from "react-router-dom";
+import { useLocation, useNavigate } from "react-router-dom";
function AccountItem({ pubkey }: { pubkey: string }) {
const metadata = useUserMetadata(pubkey, []);
@@ -53,6 +53,7 @@ export function AccountSwitcherList() {
const navigate = useNavigate();
const accounts = useSubject(accountService.accounts);
const current = useSubject(accountService.current);
+ const location = useLocation();
const otherAccounts = accounts.filter((acc) => acc.pubkey !== current?.pubkey);
diff --git a/src/components/page/profile-link.tsx b/src/components/page/profile-link.tsx
index 16cf9b555..4a00bee6c 100644
--- a/src/components/page/profile-link.tsx
+++ b/src/components/page/profile-link.tsx
@@ -1,5 +1,5 @@
import { Box, Button, LinkBox, Text } from "@chakra-ui/react";
-import { Link as RouterLink } from "react-router-dom";
+import { Link as RouterLink, useLocation } from "react-router-dom";
import { UserAvatar } from "../user-avatar";
import { useUserMetadata } from "../../hooks/use-user-metadata";
import { Bech32Prefix, normalizeToBech32 } from "../../helpers/nip19";
@@ -29,6 +29,7 @@ function ProfileButton() {
export default function ProfileLink() {
const account = useCurrentAccount();
+ const location = useLocation();
if (account) return ;
else
diff --git a/src/components/user-dns-identity-icon.tsx b/src/components/user-dns-identity-icon.tsx
index 17fb5c758..2b496670c 100644
--- a/src/components/user-dns-identity-icon.tsx
+++ b/src/components/user-dns-identity-icon.tsx
@@ -1,4 +1,4 @@
-import { Spinner, Tooltip } from "@chakra-ui/react";
+import { Spinner, Text, Tooltip } from "@chakra-ui/react";
import { useDnsIdentity } from "../hooks/use-dns-identity";
import { useUserMetadata } from "../hooks/use-user-metadata";
import { VerificationFailed, VerificationMissing, VerifiedIcon } from "./icons";
@@ -29,8 +29,8 @@ export const UserDnsIdentityIcon = ({ pubkey, onlyIcon }: { pubkey: string; only
return {renderIcon()};
}
return (
-
+
{metadata.nip05} {renderIcon()}
-
+
);
};
diff --git a/src/components/user-follow-button.tsx b/src/components/user-follow-button.tsx
index 555c1a40b..41d186a93 100644
--- a/src/components/user-follow-button.tsx
+++ b/src/components/user-follow-button.tsx
@@ -2,8 +2,9 @@ import { Button, ButtonProps } from "@chakra-ui/react";
import { useCurrentAccount } from "../hooks/use-current-account";
import useSubject from "../hooks/use-subject";
import clientFollowingService from "../services/client-following";
-import clientRelaysService from "../services/client-relays";
import { useUserContacts } from "../hooks/use-user-contacts";
+import { useReadRelayUrls } from "../hooks/use-client-relays";
+import { useAdditionalRelayContext } from "../providers/additional-relay-context";
export const UserFollowButton = ({
pubkey,
@@ -13,9 +14,11 @@ export const UserFollowButton = ({
const following = useSubject(clientFollowingService.following) ?? [];
const savingDraft = useSubject(clientFollowingService.savingDraft);
- const isFollowing = following.some((t) => t[1] === pubkey);
+ const readRelays = useReadRelayUrls(useAdditionalRelayContext());
+ const userContacts = useUserContacts(pubkey, readRelays);
- const userContacts = useUserContacts(pubkey, clientRelaysService.getReadUrls());
+ const isFollowing = following.some((t) => t[1] === pubkey);
+ const isFollowingMe = account && userContacts?.contacts.includes(account.pubkey);
const toggleFollow = async () => {
if (isFollowing) {
@@ -29,13 +32,13 @@ export const UserFollowButton = ({
return (
);
};
diff --git a/src/helpers/user-metadata.ts b/src/helpers/user-metadata.ts
index b9aca891d..4201bad70 100644
--- a/src/helpers/user-metadata.ts
+++ b/src/helpers/user-metadata.ts
@@ -22,6 +22,9 @@ export function parseKind0Event(event: NostrEvent): Kind0ParsedContent {
// ensure nip05 is a string
if (metadata.nip05 && typeof metadata.nip05 !== "string") metadata.nip05 = String(metadata.nip05);
+ // fix user website
+ if (metadata.website) metadata.website = fixWebsiteUrl(metadata.website);
+
return metadata;
} catch (e) {}
return {};
diff --git a/src/services/user-trusted-stats.ts b/src/services/user-trusted-stats.ts
new file mode 100644
index 000000000..398a353ab
--- /dev/null
+++ b/src/services/user-trusted-stats.ts
@@ -0,0 +1,102 @@
+export type NostrBandProfileStats = {
+ pubkey: string;
+ pub_note_count: number;
+ pub_post_count: number;
+ pub_reply_count: number;
+ pub_reaction_count: number;
+ pub_repost_count: number;
+ pub_report_count: number;
+ pub_badge_definition_count: number;
+ pub_long_note_count: number;
+ pub_note_ref_event_count: number;
+ pub_note_ref_pubkey_count: number;
+ pub_reaction_ref_event_count: number;
+ pub_reaction_ref_pubkey_count: number;
+ pub_repost_ref_event_count: number;
+ pub_repost_ref_pubkey_count: number;
+ pub_report_ref_event_count: number;
+ pub_report_ref_pubkey_count: number;
+ pub_mute_ref_pubkey_count: number;
+ pub_bookmark_ref_event_count: number;
+ pub_badge_award_ref_pubkey_count: number;
+ pub_profile_badge_ref_event_count: number;
+ pub_following_pubkey_count: number;
+ reaction_count: number;
+ reaction_pubkey_count: number;
+ repost_count: number;
+ repost_pubkey_count: number;
+ reply_count: number;
+ reply_pubkey_count: number;
+ report_count: number;
+ report_pubkey_count: number;
+ mute_pubkey_count: number;
+ followers_pubkey_count: number;
+ zaps_sent: {
+ count: number;
+ zapper_count: number;
+ target_event_count: number;
+ target_pubkey_count: number;
+ provider_count: number;
+ msats: number;
+ min_msats: number;
+ max_msats: number;
+ avg_msats: number;
+ median_msats: number;
+ };
+ zaps_received: {
+ count: number;
+ zapper_count: number;
+ target_event_count: number;
+ target_pubkey_count: number;
+ provider_count: number;
+ msats: number;
+ min_msats: number;
+ max_msats: number;
+ avg_msats: number;
+ median_msats: number;
+ };
+ zaps_processed: {
+ count: number;
+ zapper_count: number;
+ target_event_count: number;
+ target_pubkey_count: number;
+ provider_count: number;
+ msats: number;
+ min_msats: number;
+ max_msats: number;
+ avg_msats: number;
+ median_msats: number;
+ };
+};
+
+class UserTrustedStatsService {
+ private userStats = new Map();
+
+ async fetchUserStats(pubkey: string) {
+ try {
+ const stats = await fetch(`https://api.nostr.band/v0/stats/profile/${pubkey}`).then(
+ (res) => res.json() as Promise<{ stats: Record }>
+ );
+
+ if (stats?.stats[pubkey]) {
+ this.userStats.set(pubkey, stats?.stats[pubkey]);
+ return stats?.stats[pubkey];
+ }
+ } catch (e) {}
+ }
+
+ private dedupe = new Map>();
+ async getUserStats(pubkey: string, alwaysFetch = false) {
+ if (this.userStats.has(pubkey) && !alwaysFetch) return this.userStats.get(pubkey)!;
+
+ if (this.dedupe.has(pubkey)) this.dedupe.get(pubkey)!;
+ const p = this.fetchUserStats(pubkey);
+ this.dedupe.set(pubkey, p);
+ p.then(() => this.dedupe.delete(pubkey));
+ return p;
+ }
+}
+
+const userTrustedStatsService = new UserTrustedStatsService();
+
+export default userTrustedStatsService;
diff --git a/src/views/dm/chat.tsx b/src/views/dm/chat.tsx
index 93e855b88..b144cb474 100644
--- a/src/views/dm/chat.tsx
+++ b/src/views/dm/chat.tsx
@@ -18,7 +18,7 @@ import directMessagesService, { getMessageRecipient } from "../../services/direc
import { DraftNostrEvent, NostrEvent } from "../../types/nostr-event";
import DecryptPlaceholder from "./decrypt-placeholder";
import { EmbedableContent, embedUrls } from "../../helpers/embeds";
-import { embedNostrLinks, renderDefaultUrl, renderImageUrl, renderVideoUrl } from "../../components/embed-types";
+import { embedNostrLinks, renderGenericUrl, renderImageUrl, renderVideoUrl } from "../../components/embed-types";
import RequireCurrentAccount from "../../providers/require-current-account";
function MessageContent({ event, text }: { event: NostrEvent; text: string }) {
@@ -26,7 +26,7 @@ function MessageContent({ event, text }: { event: NostrEvent; text: string }) {
content = embedNostrLinks(content, event);
- content = embedUrls(content, [renderImageUrl, renderVideoUrl, renderDefaultUrl]);
+ content = embedUrls(content, [renderImageUrl, renderVideoUrl, renderGenericUrl]);
return {content};
}
diff --git a/src/views/user/about.tsx b/src/views/user/about.tsx
new file mode 100644
index 000000000..e620627ee
--- /dev/null
+++ b/src/views/user/about.tsx
@@ -0,0 +1,248 @@
+import React from "react";
+import { useNavigate, useOutletContext, Link as RouterLink } from "react-router-dom";
+import moment from "moment";
+import {
+ Accordion,
+ AccordionButton,
+ AccordionIcon,
+ AccordionItem,
+ AccordionPanel,
+ Box,
+ Flex,
+ IconButton,
+ Image,
+ Link,
+ Stat,
+ StatArrow,
+ StatGroup,
+ StatHelpText,
+ StatLabel,
+ StatNumber,
+ Text,
+ useDisclosure,
+} from "@chakra-ui/react";
+import { useAdditionalRelayContext } from "../../providers/additional-relay-context";
+import { useUserMetadata } from "../../hooks/use-user-metadata";
+import { embedNostrLinks, renderGenericUrl } from "../../components/embed-types";
+import { EmbedableContent, embedUrls } from "../../helpers/embeds";
+import { useCurrentAccount } from "../../hooks/use-current-account";
+import { ArrowDownSIcon, ArrowUpSIcon, AtIcon, ExternalLinkIcon, KeyIcon } from "../../components/icons";
+import { normalizeToBech32 } from "../../helpers/nip19";
+import { Bech32Prefix } from "../../helpers/nip19";
+import { truncatedId } from "../../helpers/nostr-event";
+import { CopyIconButton } from "../../components/copy-icon-button";
+import { QrIconButton } from "./components/share-qr-button";
+import { UserDnsIdentityIcon } from "../../components/user-dns-identity-icon";
+import { useUserContacts } from "../../hooks/use-user-contacts";
+import { convertTimestampToDate } from "../../helpers/date";
+import { useAsync } from "react-use";
+import userTrustedStatsService from "../../services/user-trusted-stats";
+import { readablizeSats } from "../../helpers/bolt11";
+import { useSharableProfileId } from "../../hooks/use-shareable-profile-id";
+
+function buildDescriptionContent(description: string) {
+ let content: EmbedableContent = [description.trim()];
+
+ content = embedNostrLinks(content);
+ content = embedUrls(content, [renderGenericUrl]);
+
+ return content;
+}
+
+export default function UserAboutTab() {
+ const navigate = useNavigate();
+ const expanded = useDisclosure();
+ const { pubkey } = useOutletContext() as { pubkey: string };
+ const contextRelays = useAdditionalRelayContext();
+
+ const metadata = useUserMetadata(pubkey, contextRelays);
+ const contacts = useUserContacts(pubkey, contextRelays);
+ const npub = normalizeToBech32(pubkey, Bech32Prefix.Pubkey);
+
+ const { value: stats } = useAsync(() => userTrustedStatsService.getUserStats(pubkey), [pubkey]);
+
+ const account = useCurrentAccount();
+ const isSelf = pubkey === account?.pubkey;
+
+ const aboutContent = metadata?.about && buildDescriptionContent(metadata?.about);
+
+ return (
+
+ {metadata?.banner && (
+
+ {expanded.isOpen && }
+ : }
+ aria-label="expand"
+ onClick={expanded.onToggle}
+ top="2"
+ right="2"
+ variant="solid"
+ position="absolute"
+ />
+
+ )}
+ {aboutContent && (
+
+ {aboutContent.map((part, i) =>
+ typeof part === "string" ? (
+
+ {part}
+
+ ) : (
+ React.cloneElement(part, { key: "part-" + i })
+ )
+ )}
+
+ )}
+
+
+ {metadata?.nip05 && (
+
+
+
+
+ )}
+ {metadata?.website && (
+
+
+
+ {metadata.website}
+
+
+ )}
+ {npub && (
+
+
+ {truncatedId(npub, 10)}
+
+
+
+ )}
+
+
+
+
+
+
+
+ Network Stats
+
+
+
+
+
+
+
+ Following
+
+ {contacts ? readablizeSats(contacts.contacts.length) : "Unknown"}
+
+ {contacts && (
+ Updated {moment(convertTimestampToDate(contacts.created_at)).fromNow()}
+ )}
+
+
+ {stats && (
+ <>
+
+ Followers
+
+ {readablizeSats(stats.followers_pubkey_count)}
+
+
+
+
+ Published Notes
+
+ {readablizeSats(stats.pub_post_count)}
+
+
+
+
+ Reactions
+ {readablizeSats(stats.pub_reaction_count)}
+
+ >
+ )}
+
+
+
+
+ {stats && (
+
+
+
+
+ Zap Stats
+
+
+
+
+
+
+
+ Zap Sent
+ {stats.zaps_sent.count}
+
+
+ Total Sats Sent
+ {readablizeSats(stats.zaps_sent.msats / 1000)}
+
+
+ Avg Zap Sent
+ {readablizeSats(stats.zaps_sent.avg_msats / 1000)}
+
+
+ Biggest Zap Sent
+ {readablizeSats(stats.zaps_sent.max_msats / 1000)}
+
+
+
+ Zap Received
+ {stats.zaps_received.count}
+
+
+ Total Sats Received
+ {readablizeSats(stats.zaps_received.msats / 1000)}
+
+
+ Avg Zap Received
+ {readablizeSats(stats.zaps_received.avg_msats / 1000)}
+
+
+ Biggest Zap Received
+ {readablizeSats(stats.zaps_received.max_msats / 1000)}
+
+
+
+ Stats from{" "}
+
+ nostr.band
+
+
+
+
+ )}
+
+
+ );
+}
diff --git a/src/views/user/components/header.tsx b/src/views/user/components/header.tsx
index 8e8afbb52..66257066e 100644
--- a/src/views/user/components/header.tsx
+++ b/src/views/user/components/header.tsx
@@ -1,7 +1,7 @@
-import { Flex, Heading, SkeletonText, Text, Link, IconButton } from "@chakra-ui/react";
+import { Flex, Heading, SkeletonText, Text, Link, IconButton, Spacer } from "@chakra-ui/react";
import { useNavigate, Link as RouterLink } from "react-router-dom";
import { CopyIconButton } from "../../../components/copy-icon-button";
-import { ChatIcon, ExternalLinkIcon, KeyIcon, SettingsIcon } from "../../../components/icons";
+import { ChatIcon, EditIcon, ExternalLinkIcon, KeyIcon, SettingsIcon } from "../../../components/icons";
import { QrIconButton } from "./share-qr-button";
import { UserAvatar } from "../../../components/user-avatar";
import { UserDnsIdentityIcon } from "../../../components/user-dns-identity-icon";
@@ -15,7 +15,7 @@ import { useIsMobile } from "../../../hooks/use-is-mobile";
import { useUserMetadata } from "../../../hooks/use-user-metadata";
import { UserProfileMenu } from "./user-profile-menu";
import { embedUrls } from "../../../helpers/embeds";
-import { renderDefaultUrl } from "../../../components/embed-types";
+import { renderGenericUrl } from "../../../components/embed-types";
export default function Header({
pubkey,
@@ -34,56 +34,23 @@ export default function Header({
return (
-
-
-
-
-
- {getUserDisplayName(metadata, pubkey)}
-
-
-
-
-
-
-
- {metadata?.about && {embedUrls([metadata.about], [renderDefaultUrl])}}
-
-
-
- {metadata?.website && (
-
- {" "}
-
- {metadata.website}
-
-
+
+
+ {getUserDisplayName(metadata, pubkey)}
+
+
+ {isSelf && (
+ }
+ aria-label="Edit profile"
+ title="Edit profile"
+ size="sm"
+ onClick={() => navigate("/profile")}
+ />
)}
- {npub && (
-
-
- {truncatedId(npub, 10)}
-
-
-
- )}
-
- {isMobile && isSelf && (
- }
- aria-label="Settings"
- title="Settings"
- size="sm"
- onClick={() => navigate("/settings")}
- />
- )}
- {!isSelf && (
+ {!isSelf && (
+ <>
+
- )}
- {!isSelf && }
-
+
+ >
+ )}
+
);
diff --git a/src/views/user/components/user-card.tsx b/src/views/user/components/user-card.tsx
index b1b878cca..e2e757beb 100644
--- a/src/views/user/components/user-card.tsx
+++ b/src/views/user/components/user-card.tsx
@@ -1,4 +1,4 @@
-import { Box, Flex, Heading, Link, Text } from "@chakra-ui/react";
+import { Box, Code, Flex, Heading, Input, Link, Spacer, Text } from "@chakra-ui/react";
import { Link as ReactRouterLink } from "react-router-dom";
import { useUserMetadata } from "../../../hooks/use-user-metadata";
@@ -6,22 +6,37 @@ import { getUserDisplayName } from "../../../helpers/user-metadata";
import { UserAvatar } from "../../../components/user-avatar";
import { Bech32Prefix, normalizeToBech32 } from "../../../helpers/nip19";
import { UserDnsIdentityIcon } from "../../../components/user-dns-identity-icon";
+import { UserFollowButton } from "../../../components/user-follow-button";
+import { useIsMobile } from "../../../hooks/use-is-mobile";
export const UserCard = ({ pubkey, relay }: { pubkey: string; relay?: string }) => {
+ const isMobile = useIsMobile();
const metadata = useUserMetadata(pubkey, relay ? [relay] : []);
return (
-
-
-
-
-
- {getUserDisplayName(metadata, pubkey)}
-
-
- {relay && {relay}}
-
+
+
+
+
+
+ {getUserDisplayName(metadata, pubkey)}
+
+
+
+ {relay && !isMobile && }
+
);
};
diff --git a/src/views/user/components/user-profile-menu.tsx b/src/views/user/components/user-profile-menu.tsx
index aec7dcef1..26fcdb1ab 100644
--- a/src/views/user/components/user-profile-menu.tsx
+++ b/src/views/user/components/user-profile-menu.tsx
@@ -10,6 +10,7 @@ import { RelayMode } from "../../../classes/relay";
import UserDebugModal from "../../../components/debug-modals/user-debug-modal";
import { useCopyToClipboard } from "react-use";
import { useSharableProfileId } from "../../../hooks/use-shareable-profile-id";
+import { truncatedId } from "../../../helpers/nostr-event";
export const UserProfileMenu = ({
pubkey,
@@ -39,7 +40,7 @@ export const UserProfileMenu = ({
<>
} onClick={() => loginAsUser()}>
- Login as {getUserDisplayName(metadata, pubkey)}
+ Login as {truncatedId(getUserDisplayName(metadata, pubkey))}