import { Show, For, createSignal, createMemo, type JSX, type Component } from 'solid-js'; import type { Event as NostrEvent } from 'nostr-tools'; import { createMutation } from '@tanstack/solid-query'; import HeartOutlined from 'heroicons/24/outline/heart.svg'; import HeartSolid from 'heroicons/24/solid/heart.svg'; import ArrowPathRoundedSquare from 'heroicons/24/outline/arrow-path-rounded-square.svg'; import ChatBubbleLeft from 'heroicons/24/outline/chat-bubble-left.svg'; import EllipsisHorizontal from 'heroicons/24/outline/ellipsis-horizontal.svg'; import ColumnItem from '@/components/ColumnItem'; import GeneralUserMentionDisplay from '@/components/textNote/GeneralUserMentionDisplay'; import ContentWarningDisplay from '@/components/textNote/ContentWarningDisplay'; import TextNoteContentDisplay from '@/components/textNote/TextNoteContentDisplay'; import NotePostForm from '@/components/NotePostForm'; import eventWrapper from '@/core/event'; import useProfile from '@/nostr/useProfile'; import useConfig from '@/nostr/useConfig'; import usePubkey from '@/nostr/usePubkey'; import useCommands from '@/nostr/useCommands'; import useReactions from '@/nostr/useReactions'; import useDeprecatedReposts from '@/nostr/useDeprecatedReposts'; import useFormatDate from '@/hooks/useFormatDate'; import ensureNonNull from '@/utils/ensureNonNull'; import { npubEncode } from 'nostr-tools/nip19'; import UserNameDisplay from '../UserDisplayName'; import TextNoteDisplayById from './TextNoteDisplayById'; export type TextNoteDisplayProps = { event: NostrEvent; embedding?: boolean; actions?: boolean; }; const TextNoteDisplay: Component = (props) => { const { config } = useConfig(); const formatDate = useFormatDate(); const pubkey = usePubkey(); const [showReplyForm, setShowReplyForm] = createSignal(false); const closeReplyForm = () => setShowReplyForm(false); const [showMenu, setShowMenu] = createSignal(false); const event = createMemo(() => eventWrapper(props.event)); const embedding = () => props.embedding ?? true; const actions = () => props.actions ?? true; const { profile: author } = useProfile(() => ({ pubkey: props.event.pubkey, })); const { reactions, isReactedBy, invalidateReactions } = useReactions(() => ({ eventId: props.event.id as string, // TODO いつかなおす })); const { reposts, isRepostedBy, invalidateDeprecatedReposts } = useDeprecatedReposts(() => ({ eventId: props.event.id as string, // TODO いつかなおす })); const commands = useCommands(); const publishReactionMutation = createMutation({ mutationKey: ['publishReaction', event().id], mutationFn: commands.publishReaction.bind(commands), onSuccess: () => { console.log('succeeded to publish reaction'); invalidateReactions().catch((err) => console.error('failed to refetch reactions', err)); }, onError: (err) => { console.error('failed to publish reaction: ', err); }, }); const publishDeprecatedRepostMutation = createMutation({ mutationKey: ['publishDeprecatedRepost', event().id], mutationFn: commands.publishDeprecatedRepost.bind(commands), onSuccess: () => { console.log('succeeded to publish deprecated reposts'); invalidateDeprecatedReposts().catch((err) => console.error('failed to refetch deprecated reposts', err), ); }, onError: (err) => { console.error('failed to publish deprecated repost: ', err); }, }); const isReactedByMe = createMemo(() => isReactedBy(pubkey())); const isRepostedByMe = createMemo(() => isRepostedBy(pubkey())); const showReplyEvent = (): string | undefined => { const replyingToEvent = event().replyingToEvent(); if ( embedding() && replyingToEvent != null && !event().containsEventMentionIndex(replyingToEvent.index) ) { return replyingToEvent.id; } return undefined; }; const createdAt = () => formatDate(event().createdAtAsDate()); const handleRepost: JSX.EventHandler = (ev) => { if (isRepostedByMe()) { // TODO remove reaction return; } ensureNonNull([pubkey(), props.event.id] as const)(([pubkeyNonNull, eventIdNonNull]) => { publishDeprecatedRepostMutation.mutate({ relayUrls: config().relayUrls, pubkey: pubkeyNonNull, eventId: eventIdNonNull, notifyPubkey: props.event.pubkey, }); }); }; const handleReaction: JSX.EventHandler = (ev) => { if (isReactedByMe()) { // TODO remove reaction return; } ensureNonNull([pubkey(), props.event.id] as const)(([pubkeyNonNull, eventIdNonNull]) => { publishReactionMutation.mutate({ relayUrls: config().relayUrls, pubkey: pubkeyNonNull, content: '+', eventId: eventIdNonNull, notifyPubkey: props.event.pubkey, }); }); }; return (
{/* TODO 画像は脆弱性回避のためにimgじゃない方法で読み込みたい */} icon
{/* TODO link to author */} 0}>
{author()?.display_name}
@{author()?.name} {/* TODO @{author()?.nip05} */}
{createdAt()}
{(id) => (
)}
0}>
{(replyToPubkey: string) => ( )} {'への返信'}
0}>
{reposts().length}
0}>
{reactions().length}
); }; export default TextNoteDisplay;