From b8df443f95008257139ead198803ee763cf2542d Mon Sep 17 00:00:00 2001 From: Silberengel Date: Mon, 16 Mar 2026 14:03:11 +0100 Subject: [PATCH] correct profile page --- src/components/DiscussionNote/index.tsx | 2 +- src/components/Profile/index.tsx | 207 ++++++++++++------ src/components/ProfileZapButton/index.tsx | 16 +- .../DiscussionsPage/CreateThreadDialog.tsx | 26 +-- .../primary/DiscussionsPage/ThreadCard.tsx | 2 +- .../DiscussionsPage/discussionTopics.ts | 44 ++++ src/pages/primary/DiscussionsPage/index.tsx | 2 +- 7 files changed, 196 insertions(+), 103 deletions(-) create mode 100644 src/pages/primary/DiscussionsPage/discussionTopics.ts diff --git a/src/components/DiscussionNote/index.tsx b/src/components/DiscussionNote/index.tsx index c75bef20..7fde8c5d 100644 --- a/src/components/DiscussionNote/index.tsx +++ b/src/components/DiscussionNote/index.tsx @@ -4,7 +4,7 @@ import { MessageCircle, Hash, Users } from 'lucide-react' import { Event } from 'nostr-tools' import { cn } from '@/lib/utils' import { useTranslation } from 'react-i18next' -import { DISCUSSION_TOPICS } from '@/pages/primary/DiscussionsPage/CreateThreadDialog' +import { DISCUSSION_TOPICS } from '@/pages/primary/DiscussionsPage/discussionTopics' import { extractGroupInfo } from '@/lib/discussion-topics' interface DiscussionNoteProps { diff --git a/src/components/Profile/index.tsx b/src/components/Profile/index.tsx index 383128e1..2dd59cc6 100644 --- a/src/components/Profile/index.tsx +++ b/src/components/Profile/index.tsx @@ -48,16 +48,92 @@ import ProfileMedia from './ProfileMedia' import ProfileInteractions from './ProfileInteractions' import ProfileNotes from './ProfileNotes' import { toFollowPacks } from '@/lib/link' +import ZapDialog from '@/components/ZapDialog' +import type { TProfile } from '@/types' type ProfileTabValue = 'posts' | 'pins' | 'bookmarks' | 'interests' | 'articles' | 'media' | 'you' | 'notes' +/** Normalize authority for deduplication (e.g. lightning addresses case-insensitive) */ +function normalizePaymentAuthority(type: string, authority: string): string { + if (type === 'lightning' && authority) return authority.toLowerCase().trim() + return authority.trim() +} + +type MergedPaymentMethod = { + type: string + authority: string + payto?: string + displayType: string + currency?: string + minAmount?: number + maxAmount?: number +} + +/** Merge payment methods from kind 10133 and profile (kind 0 lightning), deduplicated */ +function mergePaymentMethods( + paymentInfo: ReturnType | null, + profile: TProfile | null +): MergedPaymentMethod[] { + const seen = new Set() + const out: MergedPaymentMethod[] = [] + + const add = (type: string, authority: string, payto?: string, displayType?: string, extra?: { currency?: string; minAmount?: number; maxAmount?: number }) => { + const key = `${type}:${normalizePaymentAuthority(type, authority)}` + if (!authority || seen.has(key)) return + seen.add(key) + out.push({ + type, + authority, + payto: payto || (type && authority ? `payto://${type}/${authority}` : undefined), + displayType: displayType || (type === 'lightning' ? 'Lightning Network' : type === 'bitcoin' ? 'Bitcoin' : type || 'Payment'), + ...extra + }) + } + + // From kind 10133 + if (paymentInfo?.methods?.length) { + paymentInfo.methods.forEach((m) => { + const authority = m.authority || m.address || '' + add( + (m.type || 'lightning').toLowerCase(), + authority, + m.payto, + m.displayType, + { currency: m.currency, minAmount: m.minAmount, maxAmount: m.maxAmount } + ) + }) + } else if (paymentInfo?.payto) { + const type = (paymentInfo.type || 'lightning').toLowerCase() + const authority = paymentInfo.authority || paymentInfo.payto.replace(/^payto:\/\/[^/]+\//, '') || '' + add(type, authority, paymentInfo.payto, type === 'lightning' ? 'Lightning Network' : paymentInfo.type || 'Payment') + } + + // From profile (kind 0) lightning addresses + const fromProfile = profile?.lightningAddressList?.length + ? profile.lightningAddressList + : profile?.lightningAddress + ? [profile.lightningAddress] + : [] + fromProfile.forEach((addr) => { + if (addr) add('lightning', addr, `payto://lightning/${addr}`, 'Lightning Network') + }) + + return out +} + export default function Profile({ id }: { id?: string }) { const { t } = useTranslation() const { push } = useSecondaryPage() const { profile, isFetching } = useFetchProfile(id) const { pubkey: accountPubkey } = useNostr() const [paymentInfo, setPaymentInfo] = useState | null>(null) - + const [openZapDialog, setOpenZapDialog] = useState(false) + + const mergedPaymentMethods = useMemo( + () => mergePaymentMethods(paymentInfo, profile ?? null), + [paymentInfo, profile] + ) + // Fetch payment info (kind 10133) for this profile useEffect(() => { if (!profile?.pubkey) { @@ -295,7 +371,7 @@ export default function Profile({ id }: { id?: string }) { } if (!profile) return - const { banner, username, about, avatar, pubkey, website, websiteList, lightningAddress, lightningAddressList, nip05List } = profile + const { banner, username, about, avatar, pubkey, website, websiteList, nip05List } = profile logger.component('Profile', 'Profile data loaded', { pubkey, @@ -338,7 +414,9 @@ export default function Profile({ id }: { id?: string }) { ) : ( <> - {!!lightningAddress && } + {mergedPaymentMethods.some((m) => m.type === 'lightning') && ( + + )} )} @@ -357,23 +435,6 @@ export default function Profile({ id }: { id?: string }) { {nip05List && nip05List.length > 1 && ( )} - {/* Display lightning addresses - show first one prominently, others below */} - {lightningAddress && ( -
- -
{lightningAddress}
-
- )} - {lightningAddressList && lightningAddressList.length > 1 && ( -
- {lightningAddressList.slice(1).map((addr, idx) => ( -
- - {addr} -
- ))} -
- )}
@@ -415,69 +476,69 @@ export default function Profile({ id }: { id?: string }) { ))}
)} - {/* Display payment info from kind 10133 */} - {paymentInfo && ((paymentInfo.methods && paymentInfo.methods.length > 0) || paymentInfo.payto) && ( + {/* Payment methods: merged from kind 10133 + profile lightning, deduplicated */} + {mergedPaymentMethods.length > 0 && (
Payment Methods
- {paymentInfo.methods && paymentInfo.methods.length > 0 ? ( - paymentInfo.methods.map((method, idx) => { - // NIP-A3: type is in method.type, authority is in method.authority - const displayType = method.displayType || method.type || 'Payment' - const authority = method.authority || method.address || '' - const paytoUri = method.payto || (method.type && authority ? `payto://${method.type}/${authority}` : undefined) - - return ( -
-
{displayType}
- {authority && ( -
- {method.type === 'lightning' && } + {mergedPaymentMethods.map((method, idx) => { + const authority = method.authority + const paytoUri = method.payto + const isLightning = method.type === 'lightning' + return ( +
+
{method.displayType}
+ {authority && ( +
+ {isLightning && } + {isLightning && pubkey ? ( + + ) : paytoUri ? ( + e.stopPropagation()} + > + {authority} + + ) : ( {authority} -
- )} - {paytoUri && ( - e.stopPropagation()} - > - {paytoUri} - - )} - {(method.currency || (method.minAmount !== undefined && method.maxAmount !== undefined)) && ( -
- {method.currency && ({method.currency})} - {method.minAmount !== undefined && method.maxAmount !== undefined && ( - - {method.minAmount}-{method.maxAmount} - - )} -
- )} -
- ) - }) - ) : ( - // Display payto from root level if methods array is empty - paymentInfo.payto && ( -
-
Lightning Network
-
- - {paymentInfo.payto} -
- {paymentInfo.currency && ( -
({paymentInfo.currency})
+ )} +
+ )} + {(method.currency || (method.minAmount !== undefined && method.maxAmount !== undefined)) && ( +
+ {method.currency && ({method.currency})} + {method.minAmount !== undefined && method.maxAmount !== undefined && ( + + {method.minAmount}-{method.maxAmount} + + )} +
)}
) - )} + })}
)} +
diff --git a/src/components/ProfileZapButton/index.tsx b/src/components/ProfileZapButton/index.tsx index 8372c51e..344c6b3d 100644 --- a/src/components/ProfileZapButton/index.tsx +++ b/src/components/ProfileZapButton/index.tsx @@ -4,9 +4,19 @@ import { Zap } from 'lucide-react' import { useState } from 'react' import ZapDialog from '../ZapDialog' -export default function ProfileZapButton({ pubkey }: { pubkey: string }) { +export default function ProfileZapButton({ + pubkey, + openZapDialog, + setOpenZapDialog +}: { + pubkey: string + openZapDialog?: boolean + setOpenZapDialog?: (open: boolean) => void +}) { const { checkLogin } = useNostr() - const [open, setOpen] = useState(false) + const [internalOpen, setInternalOpen] = useState(false) + const open = setOpenZapDialog ? (openZapDialog ?? false) : internalOpen + const setOpen = setOpenZapDialog ?? setInternalOpen return ( <> @@ -18,7 +28,7 @@ export default function ProfileZapButton({ pubkey }: { pubkey: string }) { > - + {!setOpenZapDialog && } ) } diff --git a/src/pages/primary/DiscussionsPage/CreateThreadDialog.tsx b/src/pages/primary/DiscussionsPage/CreateThreadDialog.tsx index db2d4e4a..7c83146d 100644 --- a/src/pages/primary/DiscussionsPage/CreateThreadDialog.tsx +++ b/src/pages/primary/DiscussionsPage/CreateThreadDialog.tsx @@ -9,7 +9,7 @@ import { Slider } from '@/components/ui/slider' import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs' import { Checkbox } from '@/components/ui/checkbox' import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover' -import { Hash, X, Users, Code, Coins, Newspaper, BookOpen, Scroll, Cpu, Trophy, Film, Heart, TrendingUp, Utensils, MapPin, Home, PawPrint, Shirt, Image, Zap, Settings, Book, Network, Car, Eye, Edit3, ChevronDown, Check, ImageUp, Smile } from 'lucide-react' +import { Hash, X, Users, Trophy, Film, Image, Zap, Settings, Book, Eye, Edit3, ChevronDown, Check, ImageUp, Smile } from 'lucide-react' import { useState, useEffect, useMemo, useRef } from 'react' import { useTranslation } from 'react-i18next' import { useNostr } from '@/providers/NostrProvider' @@ -23,6 +23,7 @@ import { simplifyUrl } from '@/lib/url' import relaySelectionService, { type RelaySourceType } from '@/services/relay-selection.service' import dayjs from 'dayjs' import { extractHashtagsFromContent, normalizeTopic } from '@/lib/discussion-topics' +import { DISCUSSION_TOPICS } from './discussionTopics' import MarkdownArticle from '@/components/Note/MarkdownArticle/MarkdownArticle' import RelayIcon from '@/components/RelayIcon' import GifPicker from '@/components/GifPicker' @@ -76,29 +77,6 @@ interface CreateThreadDialogProps { onThreadCreated: (publishedEvent?: NostrEvent) => void } -export const DISCUSSION_TOPICS = [ - { id: 'general', label: 'General', icon: Hash }, - { id: 'meetups', label: 'Meetups', icon: Users }, - { id: 'devs', label: 'Developers', icon: Code }, - { id: 'finance', label: 'Bitcoin, Finance & Economics', icon: Coins }, - { id: 'politics', label: 'Politics & Breaking News', icon: Newspaper }, - { id: 'literature', label: 'Literature & Art', icon: BookOpen }, - { id: 'philosophy', label: 'Philosophy & Theology', icon: Scroll }, - { id: 'tech', label: 'Technology & Science', icon: Cpu }, - { id: 'nostr', label: 'Nostr', icon: Network }, - { id: 'automotive', label: 'Automotive', icon: Car }, - { id: 'sports', label: 'Sports and Gaming', icon: Trophy }, - { id: 'entertainment', label: 'Entertainment & Pop Culture', icon: Film }, - { id: 'health', label: 'Health & Wellness', icon: Heart }, - { id: 'lifestyle', label: 'Lifestyle & Personal Development', icon: TrendingUp }, - { id: 'food', label: 'Food & Cooking', icon: Utensils }, - { id: 'travel', label: 'Travel & Adventure', icon: MapPin }, - { id: 'home', label: 'Home & Garden', icon: Home }, - { id: 'pets', label: 'Pets & Animals', icon: PawPrint }, - { id: 'fashion', label: 'Fashion & Beauty', icon: Shirt }, - { id: 'groups', label: 'Groups', icon: Users } -] - export default function CreateThreadDialog({ topic: initialTopic, availableRelays, diff --git a/src/pages/primary/DiscussionsPage/ThreadCard.tsx b/src/pages/primary/DiscussionsPage/ThreadCard.tsx index f083230d..a7902aa2 100644 --- a/src/pages/primary/DiscussionsPage/ThreadCard.tsx +++ b/src/pages/primary/DiscussionsPage/ThreadCard.tsx @@ -5,7 +5,7 @@ import { NostrEvent } from 'nostr-tools' import { formatDistanceToNow } from 'date-fns' import { useTranslation } from 'react-i18next' import { cn } from '@/lib/utils' -import { DISCUSSION_TOPICS } from './CreateThreadDialog' +import { DISCUSSION_TOPICS } from './discussionTopics' import Username from '@/components/Username' import UserAvatar from '@/components/UserAvatar' import { useScreenSize } from '@/providers/ScreenSizeProvider' diff --git a/src/pages/primary/DiscussionsPage/discussionTopics.ts b/src/pages/primary/DiscussionsPage/discussionTopics.ts new file mode 100644 index 00000000..41ecca59 --- /dev/null +++ b/src/pages/primary/DiscussionsPage/discussionTopics.ts @@ -0,0 +1,44 @@ +import { + Hash, + Users, + Code, + Coins, + Newspaper, + BookOpen, + Scroll, + Cpu, + Trophy, + Film, + Heart, + TrendingUp, + Utensils, + MapPin, + Home, + PawPrint, + Shirt, + Network, + Car +} from 'lucide-react' + +export const DISCUSSION_TOPICS = [ + { id: 'general', label: 'General', icon: Hash }, + { id: 'meetups', label: 'Meetups', icon: Users }, + { id: 'devs', label: 'Developers', icon: Code }, + { id: 'finance', label: 'Bitcoin, Finance & Economics', icon: Coins }, + { id: 'politics', label: 'Politics & Breaking News', icon: Newspaper }, + { id: 'literature', label: 'Literature & Art', icon: BookOpen }, + { id: 'philosophy', label: 'Philosophy & Theology', icon: Scroll }, + { id: 'tech', label: 'Technology & Science', icon: Cpu }, + { id: 'nostr', label: 'Nostr', icon: Network }, + { id: 'automotive', label: 'Automotive', icon: Car }, + { id: 'sports', label: 'Sports and Gaming', icon: Trophy }, + { id: 'entertainment', label: 'Entertainment & Pop Culture', icon: Film }, + { id: 'health', label: 'Health & Wellness', icon: Heart }, + { id: 'lifestyle', label: 'Lifestyle & Personal Development', icon: TrendingUp }, + { id: 'food', label: 'Food & Cooking', icon: Utensils }, + { id: 'travel', label: 'Travel & Adventure', icon: MapPin }, + { id: 'home', label: 'Home & Garden', icon: Home }, + { id: 'pets', label: 'Pets & Animals', icon: PawPrint }, + { id: 'fashion', label: 'Fashion & Beauty', icon: Shirt }, + { id: 'groups', label: 'Groups', icon: Users } +] diff --git a/src/pages/primary/DiscussionsPage/index.tsx b/src/pages/primary/DiscussionsPage/index.tsx index 2618973c..33571ce8 100644 --- a/src/pages/primary/DiscussionsPage/index.tsx +++ b/src/pages/primary/DiscussionsPage/index.tsx @@ -12,7 +12,7 @@ import { normalizeUrl } from '@/lib/url' import { BIG_RELAY_URLS, FAST_READ_RELAY_URLS, FAST_WRITE_RELAY_URLS } from '@/constants' import client from '@/services/client.service' import discussionFeedCache from '@/services/discussion-feed-cache.service' -import { DISCUSSION_TOPICS } from './CreateThreadDialog' +import { DISCUSSION_TOPICS } from './discussionTopics' import ThreadCard from './ThreadCard' import CreateThreadDialog from './CreateThreadDialog' import PrimaryPageLayout from '@/layouts/PrimaryPageLayout'