Browse Source

correct profile page

imwald
Silberengel 2 months ago
parent
commit
b8df443f95
  1. 2
      src/components/DiscussionNote/index.tsx
  2. 205
      src/components/Profile/index.tsx
  3. 16
      src/components/ProfileZapButton/index.tsx
  4. 26
      src/pages/primary/DiscussionsPage/CreateThreadDialog.tsx
  5. 2
      src/pages/primary/DiscussionsPage/ThreadCard.tsx
  6. 44
      src/pages/primary/DiscussionsPage/discussionTopics.ts
  7. 2
      src/pages/primary/DiscussionsPage/index.tsx

2
src/components/DiscussionNote/index.tsx

@ -4,7 +4,7 @@ import { MessageCircle, Hash, Users } from 'lucide-react' @@ -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 {

205
src/components/Profile/index.tsx

@ -48,15 +48,91 @@ import ProfileMedia from './ProfileMedia' @@ -48,15 +48,91 @@ 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<typeof getPaymentInfoFromEvent> | null,
profile: TProfile | null
): MergedPaymentMethod[] {
const seen = new Set<string>()
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<ReturnType<typeof getPaymentInfoFromEvent> | 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(() => {
@ -295,7 +371,7 @@ export default function Profile({ id }: { id?: string }) { @@ -295,7 +371,7 @@ export default function Profile({ id }: { id?: string }) {
}
if (!profile) return <NotFound />
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 }) { @@ -338,7 +414,9 @@ export default function Profile({ id }: { id?: string }) {
</div>
) : (
<>
{!!lightningAddress && <ProfileZapButton pubkey={pubkey} />}
{mergedPaymentMethods.some((m) => m.type === 'lightning') && (
<ProfileZapButton pubkey={pubkey} openZapDialog={openZapDialog} setOpenZapDialog={setOpenZapDialog} />
)}
<FollowButton pubkey={pubkey} />
</>
)}
@ -357,23 +435,6 @@ export default function Profile({ id }: { id?: string }) { @@ -357,23 +435,6 @@ export default function Profile({ id }: { id?: string }) {
{nip05List && nip05List.length > 1 && (
<Nip05List nip05List={nip05List.slice(1)} pubkey={pubkey} />
)}
{/* Display lightning addresses - show first one prominently, others below */}
{lightningAddress && (
<div className="text-sm text-yellow-400 flex gap-1 items-center select-text">
<Zap className="size-4 shrink-0" />
<div className="flex-1 max-w-fit w-0 truncate">{lightningAddress}</div>
</div>
)}
{lightningAddressList && lightningAddressList.length > 1 && (
<div className="text-sm text-yellow-400/70 flex flex-wrap gap-2 mt-1">
{lightningAddressList.slice(1).map((addr, idx) => (
<div key={idx} className="flex gap-1 items-center select-text">
<Zap className="size-3 shrink-0" />
<span className="truncate">{addr}</span>
</div>
))}
</div>
)}
<div className="flex gap-1 mt-1">
<PubkeyCopy pubkey={pubkey} />
<NpubQrCode pubkey={pubkey} />
@ -415,69 +476,69 @@ export default function Profile({ id }: { id?: string }) { @@ -415,69 +476,69 @@ export default function Profile({ id }: { id?: string }) {
))}
</div>
)}
{/* 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 && (
<div className="mt-2 p-2 border rounded-lg bg-muted/50 min-w-0 overflow-hidden">
<div className="text-xs font-semibold text-muted-foreground mb-2">Payment Methods</div>
<div className="space-y-2 min-w-0">
{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 (
<div key={idx} className="text-sm min-w-0">
<div className="font-medium">{displayType}</div>
{authority && (
<div className="text-muted-foreground mt-1 flex items-center gap-2 min-w-0">
{method.type === 'lightning' && <Zap className="size-3 text-yellow-400 shrink-0" />}
{mergedPaymentMethods.map((method, idx) => {
const authority = method.authority
const paytoUri = method.payto
const isLightning = method.type === 'lightning'
return (
<div key={idx} className="text-sm min-w-0">
<div className="font-medium">{method.displayType}</div>
{authority && (
<div className="text-muted-foreground mt-1 flex items-center gap-2 min-w-0">
{isLightning && <Zap className="size-3 text-yellow-400 shrink-0" />}
{isLightning && pubkey ? (
<button
type="button"
className="text-left hover:underline break-all min-w-0 text-primary"
onClick={(e) => {
e.preventDefault()
e.stopPropagation()
setOpenZapDialog(true)
}}
>
{authority}
</button>
) : paytoUri ? (
<a
href={paytoUri}
target="_blank"
rel="noopener noreferrer"
className="hover:underline break-all min-w-0 text-primary"
onClick={(e) => e.stopPropagation()}
>
{authority}
</a>
) : (
<span className="select-text min-w-0 break-all">{authority}</span>
</div>
)}
{paytoUri && (
<a
href={paytoUri}
target="_blank"
rel="noopener noreferrer"
className="text-muted-foreground text-xs mt-1 hover:underline block break-all min-w-0"
onClick={(e) => e.stopPropagation()}
>
{paytoUri}
</a>
)}
{(method.currency || (method.minAmount !== undefined && method.maxAmount !== undefined)) && (
<div className="text-muted-foreground text-xs mt-1">
{method.currency && <span>({method.currency})</span>}
{method.minAmount !== undefined && method.maxAmount !== undefined && (
<span className="ml-2">
{method.minAmount}-{method.maxAmount}
</span>
)}
</div>
)}
</div>
)
})
) : (
// Display payto from root level if methods array is empty
paymentInfo.payto && (
<div className="text-sm min-w-0">
<div className="font-medium">Lightning Network</div>
<div className="text-muted-foreground mt-1 flex items-center gap-2 min-w-0">
<Zap className="size-3 text-yellow-400 shrink-0" />
<span className="select-text min-w-0 break-all">{paymentInfo.payto}</span>
</div>
{paymentInfo.currency && (
<div className="text-muted-foreground text-xs mt-1">({paymentInfo.currency})</div>
)}
</div>
)}
{(method.currency || (method.minAmount !== undefined && method.maxAmount !== undefined)) && (
<div className="text-muted-foreground text-xs mt-1">
{method.currency && <span>({method.currency})</span>}
{method.minAmount !== undefined && method.maxAmount !== undefined && (
<span className="ml-2">
{method.minAmount}-{method.maxAmount}
</span>
)}
</div>
)}
</div>
)
)}
})}
</div>
</div>
)}
<ZapDialog
open={openZapDialog}
setOpen={setOpenZapDialog}
pubkey={pubkey}
/>
<div className="flex justify-between items-center mt-2 text-sm">
<div className="flex gap-4 items-center">
<SmartFollowings pubkey={pubkey} />

16
src/components/ProfileZapButton/index.tsx

@ -4,9 +4,19 @@ import { Zap } from 'lucide-react' @@ -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 }) { @@ -18,7 +28,7 @@ export default function ProfileZapButton({ pubkey }: { pubkey: string }) {
>
<Zap className="text-yellow-400" />
</Button>
<ZapDialog open={open} setOpen={setOpen} pubkey={pubkey} />
{!setOpenZapDialog && <ZapDialog open={open} setOpen={setOpen} pubkey={pubkey} />}
</>
)
}

26
src/pages/primary/DiscussionsPage/CreateThreadDialog.tsx

@ -9,7 +9,7 @@ import { Slider } from '@/components/ui/slider' @@ -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' @@ -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 { @@ -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,

2
src/pages/primary/DiscussionsPage/ThreadCard.tsx

@ -5,7 +5,7 @@ import { NostrEvent } from 'nostr-tools' @@ -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'

44
src/pages/primary/DiscussionsPage/discussionTopics.ts

@ -0,0 +1,44 @@ @@ -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 }
]

2
src/pages/primary/DiscussionsPage/index.tsx

@ -12,7 +12,7 @@ import { normalizeUrl } from '@/lib/url' @@ -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'

Loading…
Cancel
Save