diff --git a/src/App.tsx b/src/App.tsx index ee33745..09dec90 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -27,9 +27,9 @@ const App: Component = () => { return ( - } /> - } /> - } /> + } /> + } /> + } /> ); diff --git a/src/components/ColumnItem.tsx b/src/components/ColumnItem.tsx index 737cafe..8a172f5 100644 --- a/src/components/ColumnItem.tsx +++ b/src/components/ColumnItem.tsx @@ -5,7 +5,7 @@ type ColumnItemProps = { }; const ColumnItem: Component = (props) => { - return
  • {props.children}
  • ; + return
    {props.children}
    ; }; export default ColumnItem; diff --git a/src/components/ProfileDisplay.tsx b/src/components/ProfileDisplay.tsx deleted file mode 100644 index 9aa987b..0000000 --- a/src/components/ProfileDisplay.tsx +++ /dev/null @@ -1,390 +0,0 @@ -import { Component, createSignal, createMemo, Show, Switch, Match, createEffect } 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 XMark from 'heroicons/24/outline/x-mark.svg'; -import CheckCircle from 'heroicons/24/solid/check-circle.svg'; -import ExclamationCircle from 'heroicons/24/solid/exclamation-circle.svg'; -import uniq from 'lodash/uniq'; - -import Modal from '@/components/Modal'; -import Timeline from '@/components/Timeline'; -import Copy from '@/components/utils/Copy'; -import SafeLink from '@/components/utils/SafeLink'; -import useConfig from '@/core/useConfig'; -import useModalState from '@/hooks/useModalState'; -import useCommands from '@/nostr/useCommands'; -import useFollowers from '@/nostr/useFollowers'; -import useFollowings 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'; - -import ContextMenu, { MenuItem } from './ContextMenu'; - -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 { config, addMutedPubkey, removeMutedPubkey, isPubkeyMuted } = useConfig(); - const commands = useCommands(); - const myPubkey = usePubkey(); - const { showProfileEdit } = useModalState(); - - const npub = createMemo(() => npubEncodeFallback(props.pubkey)); - - const [hoverFollowButton, setHoverFollowButton] = createSignal(false); - const [showFollowers, setShowFollowers] = createSignal(false); - - const { profile, 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 { - 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 follow = () => { - const p = myPubkey(); - if (p == null) return; - if (!myFollowingQuery.isFetched) return; - - updateContactsMutation.mutate({ - relayUrls: config().relayUrls, - pubkey: p, - content: myFollowingQuery.data?.content ?? '', - followingPubkeys: uniq([...myFollowingPubkeys(), props.pubkey]), - }); - }; - - const unfollow = () => { - const p = myPubkey(); - if (p == null) return; - if (!myFollowingQuery.isFetched) return; - - if (!window.confirm('本当にフォロー解除しますか?')) return; - - updateContactsMutation.mutate({ - relayUrls: config().relayUrls, - pubkey: p, - content: myFollowingQuery.data?.content ?? '', - followingPubkeys: myFollowingPubkeys().filter((k) => k !== props.pubkey), - }); - }; - - const menu: MenuItem[] = [ - { - content: () => 'IDをコピー', - onSelect: () => { - navigator.clipboard.writeText(npub()).catch((err) => window.alert(err)); - }, - }, - { - content: () => (!isMuted() ? 'ミュート' : 'ミュート解除'), - onSelect: () => { - if (!isMuted()) { - addMutedPubkey(props.pubkey); - } else { - removeMutedPubkey(props.pubkey); - } - }, - }, - { - when: () => props.pubkey === myPubkey(), - content: () => (!following() ? '自分をフォロー' : '自分をフォロー解除'), - 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?.()}> -
    - -
    - loading}> - } keyed> - {(bannerUrl) => ( -
    - header -
    - )} -
    -
    -
    -
    - - {(pictureUrl) => ( - user icon - )} - -
    -
    -
    -
    - - - - - - - 読み込み中 - - - - - 更新中 - - - - - - - - - - - - -
    - -
    フォローされています
    -
    -
    -
    -
    -
    - 0}> -
    {profile()?.display_name}
    -
    -
    - 0}> -
    @{profile()?.name}
    -
    - 0}> -
    - {nip05Identifier()?.ident} - - - - } - > - - - - - - - - - - - -
    -
    -
    -
    -
    {npub()}
    -
    -
    -
    - 0}> -
    - {profile()?.about} -
    -
    -
    -
    -
    フォロー
    -
    - 読み込み中} - > - {userFollowingPubkeys().length} - -
    -
    - -
    -
    フォロワー
    -
    - setShowFollowers(true)} - > - 読み込む - - } - keyed - > - - -
    -
    -
    -
    - 0}> -
      - - {(website) => ( -
    • - - - - -
    • - )} -
      -
    -
    -
    -
      - -
    -
    -
    -
    - ); -}; - -export default ProfileDisplay; diff --git a/src/components/ProfileEdit.tsx b/src/components/ProfileEdit.tsx deleted file mode 100644 index 45d1392..0000000 --- a/src/components/ProfileEdit.tsx +++ /dev/null @@ -1,340 +0,0 @@ -import { createSignal, type Component, batch, onMount, For, JSX, Show } from 'solid-js'; - -import { createMutation } from '@tanstack/solid-query'; -import ArrowLeft from 'heroicons/24/outline/arrow-left.svg'; -import omit from 'lodash/omit'; -import omitBy from 'lodash/omitBy'; - -import Modal from '@/components/Modal'; -import useConfig from '@/core/useConfig'; -import { Profile, useProfile } from '@/nostr/useBatchedEvents'; -import useCommands from '@/nostr/useCommands'; -import usePubkey from '@/nostr/usePubkey'; -import ensureNonNull from '@/utils/ensureNonNull'; -import timeout from '@/utils/timeout'; - -export type ProfileEditProps = { - onClose: () => void; -}; - -const LNURLRegexString = 'LNURL1[AC-HJ-NP-Zac-hj-np-z02-9]+'; -const InternetIdentiferRegexString = '[-_a-zA-Z0-9.]+@[-a-zA-Z0-9.]+'; -const LUDAddressRegexString = `^(${LNURLRegexString}|${InternetIdentiferRegexString})$`; - -const LNURLRegex = new RegExp(`^${LNURLRegexString}$`); -const InternetIdentiferRegex = new RegExp(`^${InternetIdentiferRegexString}$`); - -const isLNURL = (s: string) => LNURLRegex.test(s); -const isInternetIdentifier = (s: string) => InternetIdentiferRegex.test(s); - -const ProfileEdit: Component = (props) => { - const pubkey = usePubkey(); - const { config } = useConfig(); - const { profile, invalidateProfile, query } = useProfile(() => - ensureNonNull([pubkey()] as const)(([pubkeyNonNull]) => ({ - pubkey: pubkeyNonNull, - })), - ); - const { updateProfile } = useCommands(); - - const [picture, setPicture] = createSignal(''); - const [banner, setBanner] = createSignal(''); - const [name, setName] = createSignal(''); - const [displayName, setDisplayName] = createSignal(''); - const [about, setAbout] = createSignal(''); - const [website, setWebsite] = createSignal(''); - const [nip05, setNIP05] = createSignal(''); - const [lightningAddress, setLightningAddress] = createSignal(''); - - const mutation = createMutation({ - mutationKey: ['updateProfile'], - mutationFn: (...params: Parameters) => - updateProfile(...params).then((promeses) => Promise.allSettled(promeses.map(timeout(10000)))), - onSuccess: (results) => { - const succeeded = results.filter((res) => res.status === 'fulfilled').length; - const failed = results.length - succeeded; - if (succeeded === results.length) { - window.alert('更新しました'); - } else if (succeeded > 0) { - window.alert(`${failed}個のリレーで更新に失敗しました`); - } else { - window.alert('すべてのリレーで更新に失敗しました'); - } - invalidateProfile() - .then(() => query.refetch()) - .catch((err) => console.error(err)); - - props.onClose(); - }, - onError: (err) => { - console.error('failed to delete', err); - }, - }); - - const disabled = () => query.isLoading || query.isError || mutation.isLoading; - const otherProperties = () => - omit(profile(), [ - 'picture', - 'banner', - 'name', - 'display_name', - 'about', - 'website', - 'nip05', - 'lud06', - 'lud16', - ]); - - const handleSubmit: JSX.EventHandler = (ev) => { - ev.preventDefault(); - - const p = pubkey(); - if (p == null) return; - - const newProfile: Profile = omitBy( - { - picture: picture(), - banner: banner(), - name: name(), - display_name: displayName(), - about: about(), - website: website(), - nip05: nip05(), - lud06: isLNURL(lightningAddress()) ? lightningAddress() : null, - lud16: isInternetIdentifier(lightningAddress()) ? lightningAddress() : null, - }, - (v) => v == null || v.length === 0, - ); - - mutation.mutate({ - relayUrls: config().relayUrls, - pubkey: p, - profile: newProfile, - otherProperties: otherProperties(), - }); - }; - - const ignoreEnter = (ev: KeyboardEvent) => ev.key === 'Enter' && ev.preventDefault(); - - onMount(() => { - const currentProfile = profile(); - if (currentProfile == null) return; - - query.refetch().catch((err) => console.error(err)); - - batch(() => { - setPicture((current) => currentProfile.picture ?? current); - setBanner((current) => currentProfile.banner ?? current); - setName((current) => currentProfile.name ?? current); - setDisplayName((current) => currentProfile.display_name ?? current); - setAbout((current) => currentProfile.about ?? current); - setWebsite((current) => currentProfile.website ?? current); - setNIP05((current) => currentProfile.nip05 ?? current); - if (currentProfile.lud16 != null) { - setLightningAddress(currentProfile.lud16); - } else if (currentProfile.lud06 != null) { - setLightningAddress(currentProfile.lud06); - } - }); - }); - - return ( - -
    - -
    -
    - 0} fallback={
    } keyed> -
    - header -
    - -
    - 0}> - user icon - -
    -
    -
    -
    -
    - - setPicture(ev.currentTarget.value)} - onKeyDown={ignoreEnter} - /> -
    -
    - - setBanner(ev.currentTarget.value)} - onKeyDown={ignoreEnter} - /> -
    -
    - -
    - @ - setName(ev.currentTarget.value)} - onKeyDown={ignoreEnter} - /> -
    -
    -
    - - setDisplayName(ev.currentTarget.value)} - onKeyDown={ignoreEnter} - /> -
    -
    - -