import { Component, createSignal, createMemo, Show, Switch, Match } from 'solid-js'; import { createMutation } from '@tanstack/solid-query'; import ArrowPath from 'heroicons/24/outline/arrow-path.svg'; import EllipsisHorizontal from 'heroicons/24/outline/ellipsis-horizontal.svg'; import GlobeAlt from 'heroicons/24/outline/globe-alt.svg'; import CheckCircle from 'heroicons/24/solid/check-circle.svg'; import ExclamationCircle from 'heroicons/24/solid/exclamation-circle.svg'; import ContextMenu, { MenuItem } from '@/components/ContextMenu'; import TextNoteContentDisplay from '@/components/event/textNote/TextNoteContentDisplay'; import BasicModal from '@/components/modal/BasicModal'; import UserList from '@/components/modal/UserList'; import Timeline from '@/components/timeline/Timeline'; import SafeLink from '@/components/utils/SafeLink'; import { createFollowingColumn, createPostsColumn } from '@/core/column'; import useConfig from '@/core/useConfig'; import { useRequestCommand } from '@/hooks/useCommandBus'; import useModalState from '@/hooks/useModalState'; import { useTranslation } from '@/i18n/useTranslation'; import { genericEvent } from '@/nostr/event'; import parseTextNote, { toResolved } from '@/nostr/parseTextNote'; import useCommands from '@/nostr/useCommands'; import useFollowers from '@/nostr/useFollowers'; import useFollowings, { fetchLatestFollowings } from '@/nostr/useFollowings'; import useProfile from '@/nostr/useProfile'; import usePubkey from '@/nostr/usePubkey'; import useSubscription from '@/nostr/useSubscription'; import useVerification from '@/nostr/useVerification'; import ensureNonNull from '@/utils/ensureNonNull'; import epoch from '@/utils/epoch'; import npubEncodeFallback from '@/utils/npubEncodeFallback'; import timeout from '@/utils/timeout'; export type ProfileDisplayProps = { pubkey: string; onClose?: () => void; }; const FollowersCount: Component<{ pubkey: string }> = (props) => { const { count } = useFollowers(() => ({ pubkey: props.pubkey, })); return <>{count()}; }; const ProfileDisplay: Component = (props) => { const i18n = useTranslation(); const { config, addMutedPubkey, removeMutedPubkey, isPubkeyMuted, saveColumn } = useConfig(); const request = useRequestCommand(); const commands = useCommands(); const myPubkey = usePubkey(); const { showProfileEdit } = useModalState(); const npub = createMemo(() => npubEncodeFallback(props.pubkey)); const [updatingContacts, setUpdatingContacts] = createSignal(false); const [hoverFollowButton, setHoverFollowButton] = createSignal(false); const [showFollowers, setShowFollowers] = createSignal(false); const [modal, setModal] = createSignal<'Following' | null>(null); const closeModal = () => setModal(null); const { profile, event: profileEvent, query: profileQuery, } = useProfile(() => ({ pubkey: props.pubkey, })); const { verification, query: verificationQuery } = useVerification(() => ensureNonNull([profile()?.nip05] as const)(([nip05]) => ({ nip05 })), ); const nip05Identifier = () => { const ident = profile()?.nip05; if (ident == null) return null; const [user, domain] = ident.split('@'); if (domain == null) return null; if (user === '_') return { domain, ident: domain }; return { user, domain, ident }; }; const isVerified = () => verification()?.pubkey === props.pubkey; const isMuted = () => isPubkeyMuted(props.pubkey); const aboutParsed = createMemo(() => { const ev = profileEvent(); const about = profile()?.about; if (ev == null || about == null) return undefined; const parsed = parseTextNote(about); const resolved = toResolved(parsed, genericEvent(ev)); return resolved; }); const { followingPubkeys: myFollowingPubkeys, invalidateFollowings: invalidateMyFollowings, query: myFollowingQuery, } = useFollowings(() => ensureNonNull([myPubkey()] as const)(([pubkeyNonNull]) => ({ pubkey: pubkeyNonNull, })), ); const following = () => myFollowingPubkeys().includes(props.pubkey); const { followingPubkeys: userFollowingPubkeys, query: userFollowingQuery } = useFollowings( () => ({ pubkey: props.pubkey }), ); const followed = () => { const p = myPubkey(); return p != null && userFollowingPubkeys().includes(p); }; const updateContactsMutation = createMutation(() => ({ mutationKey: ['updateContacts'], mutationFn: (...params: Parameters) => commands .updateContacts(...params) .then((promises) => Promise.allSettled(promises.map(timeout(5000)))), onSuccess: (results) => { const succeeded = results.filter((res) => res.status === 'fulfilled').length; const failed = results.length - succeeded; if (succeeded === results.length) { console.log('succeeded to update contacts'); } else if (succeeded > 0) { console.log( `succeeded to update contacts for ${succeeded} relays but failed for ${failed} relays`, ); } else { console.error('failed to update contacts'); } }, onError: (err) => { console.error('failed to update contacts: ', err); }, onSettled: () => { invalidateMyFollowings() .then(() => myFollowingQuery.refetch()) .catch((err) => console.error('failed to refetch contacts', err)); }, })); const updateContacts = async (op: 'follow' | 'unfollow', pubkey: string) => { try { const p = myPubkey(); if (p == null) return; setUpdatingContacts(true); const latest = await fetchLatestFollowings({ pubkey: p }); if ( (latest.data() == null || latest.followingPubkeys().length === 0) && !window.confirm(i18n()('profile.confirmUpdateEvenIfEmpty')) ) return; if ((latest?.data()?.created_at ?? 0) < (myFollowingQuery.data?.created_at ?? 0)) { window.alert(i18n()('profile.failedToFetchLatestFollowList')); return; } const latestTags = latest.data()?.tags ?? []; let updatedTags: string[][]; switch (op) { case 'follow': updatedTags = [...latestTags, ['p', pubkey]]; break; case 'unfollow': updatedTags = latestTags.filter(([name, value]) => !(name === 'p' && value === pubkey)); break; default: console.error('updateContacts: invalid operation', op); return; } await updateContactsMutation.mutateAsync({ relayUrls: config().relayUrls, pubkey: p, updatedTags, content: latest.data()?.content ?? '', }); } catch (err) { console.error('failed to update contact list', err); window.alert(i18n()('profile.failedToUpdateFollowList')); } finally { setUpdatingContacts(false); } }; const follow = () => { updateContacts('follow', props.pubkey).catch((err) => { console.log('failed to follow', err); }); }; const unfollow = () => { if (!window.confirm(i18n()('profile.confirmUnfollow'))) return; updateContacts('unfollow', props.pubkey).catch((err) => { console.log('failed to unfollow', err); }); }; const menu: MenuItem[] = [ /* { content: () => 'ユーザ宛に投稿', onSelect: () => { navigator.clipboard.writeText(npub()).catch((err) => window.alert(err)); }, }, */ { content: () => i18n()('profile.copyPubkey'), onSelect: () => { navigator.clipboard.writeText(npub()).catch((err) => window.alert(err)); }, }, { content: () => i18n()('profile.addUserColumn'), onSelect: () => { const columnName = profile()?.name ?? npub(); saveColumn(createPostsColumn({ name: columnName, pubkey: props.pubkey })); request({ command: 'moveToLastColumn' }).catch((err) => console.error(err)); props.onClose?.(); }, }, { content: () => i18n()('profile.addUserHomeColumn'), onSelect: () => { const columnName = `${i18n()('column.home')} / ${profile()?.name ?? npub()}`; saveColumn(createFollowingColumn({ name: columnName, pubkey: props.pubkey })); request({ command: 'moveToLastColumn' }).catch((err) => console.error(err)); props.onClose?.(); }, }, { content: () => (!isMuted() ? i18n()('profile.mute') : i18n()('profile.unmute')), onSelect: () => { if (!isMuted()) { addMutedPubkey(props.pubkey); } else { removeMutedPubkey(props.pubkey); } }, }, { when: () => props.pubkey === myPubkey(), content: () => !following() ? i18n()('profile.followMyself') : i18n()('profile.unfollowMyself'), onSelect: () => { if (!following()) { follow(); } else { unfollow(); } }, }, ]; const { events } = useSubscription(() => ({ relayUrls: config().relayUrls, filters: [ { kinds: [1, 6], authors: [props.pubkey], limit: 10, until: epoch(), }, ], continuous: false, })); return ( props.onClose?.()}> } keyed > {(bannerUrl) => (
header
)}
{(pictureUrl) => ( user icon )}
{i18n()('general.updating')} {i18n()('general.loading')}
{i18n()('general.loading')}
{i18n()('profile.followsYou')}
{i18n()('general.loading')} 0}>
{profile()?.display_name}
0}>
@{profile()?.name}
0}>
{nip05Identifier()?.ident} } >
{npub()}
{(parsed) => (
)}
{i18n()('profile.followers')}
setShowFollowers(true)} > {i18n()('profile.loadFollowers')} } keyed >
0}>
    {(website) => (
  • )}
e} onClose={closeModal} />
); }; export default ProfileDisplay;