This commit is contained in:
Shusui MOYATANI
2023-05-14 20:36:54 +09:00
parent 9ed589dcd2
commit 55768db83e
21 changed files with 256 additions and 197 deletions

View File

@@ -1,14 +1,15 @@
import { Component } from 'solid-js'; import { Show, type Component } from 'solid-js';
import { nip19 } from 'nostr-tools'; import { Kind, nip19 } from 'nostr-tools';
const { noteEncode } = nip19; const { noteEncode, neventEncode } = nip19;
type EventLinkProps = { type EventLinkProps = {
eventId: string; eventId: string;
kind?: Kind;
}; };
const tryEncode = (eventId: string) => { const tryEncodeNote = (eventId: string) => {
try { try {
return noteEncode(eventId); return noteEncode(eventId);
} catch (err) { } catch (err) {
@@ -17,8 +18,26 @@ const tryEncode = (eventId: string) => {
} }
}; };
const tryEncodeNevent = (eventId: string) => {
try {
return neventEncode({ id: eventId });
} catch (err) {
console.error('failed to encode event id into Bech32 entity (NIP-19) but ignore', eventId, err);
return eventId;
}
};
const EventLink: Component<EventLinkProps> = (props) => { const EventLink: Component<EventLinkProps> = (props) => {
return <button class="text-blue-500 underline">{tryEncode(props.eventId)}</button>; return (
<button class="text-blue-500 underline">
<Show
when={props.kind == null || props.kind === Kind.Text}
fallback={tryEncodeNevent(props.eventId)}
>
{tryEncodeNote(props.eventId)}
</Show>
</button>
);
}; };
export default EventLink; export default EventLink;

View File

@@ -151,8 +151,10 @@ const NotePostForm: Component<NotePostFormProps> = (props) => {
}, },
}); });
const mentionedPubkeys: Accessor<string[]> = createMemo( const mentionedPubkeys = createMemo(() => replyTo()?.mentionedPubkeysWithoutAuthor() ?? []);
() => replyTo()?.mentionedPubkeysWithoutAuthor() ?? [],
const mentionedPubkeysWithoutMe = createMemo(() =>
mentionedPubkeys().filter((pubkey) => pubkey !== getPubkey()),
); );
const notifyPubkeys = (pubkey: string, pubkeyReferences: string[]): string[] => { const notifyPubkeys = (pubkey: string, pubkeyReferences: string[]): string[] => {
@@ -160,10 +162,8 @@ const NotePostForm: Component<NotePostFormProps> = (props) => {
return uniq([ return uniq([
// 返信先を先頭に // 返信先を先頭に
props.replyTo.pubkey, props.replyTo.pubkey,
// 自分も通知欄に表示するために表示(他アプリとの互換性)
pubkey,
// その他の返信先 // その他の返信先
...mentionedPubkeys(), ...mentionedPubkeysWithoutMe(),
// 本文中の公開鍵npub) // 本文中の公開鍵npub)
...pubkeyReferences, ...pubkeyReferences,
]); ]);

View File

@@ -0,0 +1,42 @@
import { Switch, Match, Component } from 'solid-js';
import { Kind, type Event as NostrEvent } from 'nostr-tools';
// eslint-disable-next-line import/no-cycle
import Repost from '@/components/event/Repost';
// eslint-disable-next-line import/no-cycle
import TextNote from '@/components/event/TextNote';
import EventLink from '@/components/EventLink';
export type EventDisplayProps = {
event: NostrEvent;
embedding?: boolean;
actions?: boolean;
kinds?: Kind[];
};
const EventDisplay: Component<EventDisplayProps> = (props) => {
const isAllowedKind = () =>
props.kinds == null || props.kinds.length === 0 || props.kinds.includes(props.event.kind);
return (
<Switch
fallback={
<span>
<span>{props.event.kind}</span>
<EventLink eventId={props.event.id} kind={props.event.kind} />
</span>
}
>
<Match when={!isAllowedKind()}>{null}</Match>
<Match when={props.event.kind === Kind.Text}>
<TextNote event={props.event} embedding={props.actions} actions={props.actions} />
</Match>
<Match when={(props.event.kind as number) === 6}>
<Repost event={props.event} />
</Match>
</Switch>
);
};
export default EventDisplay;

View File

@@ -1,22 +1,23 @@
import { Switch, Match, type Component, Show } from 'solid-js'; import { Switch, Match, type Component, splitProps } from 'solid-js';
import { Kind } from 'nostr-tools';
import EventLink from '@/components/EventLink';
// eslint-disable-next-line import/no-cycle // eslint-disable-next-line import/no-cycle
import TextNoteDisplay, { type TextNoteDisplayProps } from '@/components/textNote/TextNoteDisplay'; import EventDisplay from '@/components/event/EventDisplay';
import { type EventDisplayProps } from '@/components/event/EventDisplay';
import EventLink from '@/components/EventLink';
import useConfig from '@/core/useConfig'; import useConfig from '@/core/useConfig';
import useEvent from '@/nostr/useEvent'; import useEvent from '@/nostr/useEvent';
import ensureNonNull from '@/utils/ensureNonNull'; import ensureNonNull from '@/utils/ensureNonNull';
type TextNoteDisplayByIdProps = Omit<TextNoteDisplayProps, 'event'> & { type EventDisplayByIdProps = Omit<EventDisplayProps, 'event'> & {
eventId: string | undefined; eventId: string | undefined;
}; };
const TextNoteDisplayById: Component<TextNoteDisplayByIdProps> = (props) => { const EventDisplayById: Component<EventDisplayByIdProps> = (props) => {
const [localProps, restProps] = splitProps(props, ['eventId']);
const { shouldMuteEvent } = useConfig(); const { shouldMuteEvent } = useConfig();
const { event: fetchedEvent, query: eventQuery } = useEvent(() => const { event: fetchedEvent, query: eventQuery } = useEvent(() =>
ensureNonNull([props.eventId] as const)(([eventIdNonNull]) => ({ ensureNonNull([localProps.eventId] as const)(([eventIdNonNull]) => ({
eventId: eventIdNonNull, eventId: eventIdNonNull,
})), })),
); );
@@ -30,16 +31,9 @@ const TextNoteDisplayById: Component<TextNoteDisplayByIdProps> = (props) => {
<Switch fallback="投稿が見つかりません"> <Switch fallback="投稿が見つかりません">
<Match when={hidden()}>{null}</Match> <Match when={hidden()}>{null}</Match>
<Match when={fetchedEvent()} keyed> <Match when={fetchedEvent()} keyed>
{(event) => ( {(event) => <EventDisplay event={event} {...restProps} />}
<Show
when={event.kind === Kind.Text}
fallback={<div>{event.kind}</div>}
>
<TextNoteDisplay event={event} {...props} />
</Show>
)}
</Match> </Match>
<Match when={eventQuery.isLoading && props.eventId} keyed> <Match when={eventQuery.isLoading && localProps.eventId} keyed>
{(id) => ( {(id) => (
<div class="truncate"> <div class="truncate">
{'読み込み中 '} {'読み込み中 '}
@@ -51,4 +45,4 @@ const TextNoteDisplayById: Component<TextNoteDisplayByIdProps> = (props) => {
); );
}; };
export default TextNoteDisplayById; export default EventDisplayById;

View File

@@ -0,0 +1,85 @@
import { Switch, Match, type Component, Show } from 'solid-js';
import HeartSolid from 'heroicons/24/solid/heart.svg';
import { type Event as NostrEvent } from 'nostr-tools';
import TextNoteDisplay from '@/components/event/textNote/TextNoteDisplay';
import UserDisplayName from '@/components/UserDisplayName';
import useConfig from '@/core/useConfig';
import useModalState from '@/hooks/useModalState';
import eventWrapper from '@/nostr/event';
import useEvent from '@/nostr/useEvent';
import useProfile from '@/nostr/useProfile';
import ensureNonNull from '@/utils/ensureNonNull';
type ReactionProps = {
event: NostrEvent;
};
const Reaction: Component<ReactionProps> = (props) => {
const { shouldMuteEvent } = useConfig();
const { showProfile } = useModalState();
const event = () => eventWrapper(props.event);
const eventId = () => event().lastTaggedEventId();
const { profile } = useProfile(() => ({
pubkey: props.event.pubkey,
}));
const { event: reactedEvent, query: reactedEventQuery } = useEvent(() =>
ensureNonNull([eventId()] as const)(([eventIdNonNull]) => ({
eventId: eventIdNonNull,
})),
);
const isRemoved = () => reactedEventQuery.isSuccess && reactedEvent() == null;
return (
// if the reacted event is not found, it should be a removed event
<Show when={!isRemoved() || shouldMuteEvent(props.event)}>
<div class="flex gap-1 px-1 text-sm">
<div class="notification-icon flex place-items-center">
<Switch fallback={props.event.content}>
<Match when={props.event.content === '+'}>
<span class="h-4 w-4 pt-[1px] text-rose-400">
<HeartSolid />
</span>
</Match>
</Switch>
</div>
<div class="notification-user flex gap-1 overflow-hidden">
<div class="author-icon h-5 w-5 shrink-0 overflow-hidden object-cover">
<Show when={profile()?.picture != null}>
<img
src={profile()?.picture}
alt="icon"
// TODO autofit
class="rounded"
/>
</Show>
</div>
<div class="flex-1 overflow-hidden">
<button
class="truncate font-bold hover:text-blue-500 hover:underline"
onClick={() => showProfile(props.event.pubkey)}
>
<UserDisplayName pubkey={props.event.pubkey} />
</button>
{' がリアクション'}
</div>
</div>
</div>
<div class="notification-event py-1">
<Show
when={reactedEvent()}
fallback={<div class="truncate"> {eventId()}</div>}
keyed
>
{(ev) => <TextNoteDisplay event={ev} />}
</Show>
</div>
</Show>
);
};
export default Reaction;

View File

@@ -1,28 +1,28 @@
// NIP-18 (DEPRECATED) // NIP-18
import { type Component, createMemo } from 'solid-js'; import { type Component, createMemo } from 'solid-js';
import ArrowPathRoundedSquare from 'heroicons/24/outline/arrow-path-rounded-square.svg'; import ArrowPathRoundedSquare from 'heroicons/24/outline/arrow-path-rounded-square.svg';
import { Event as NostrEvent } from 'nostr-tools'; import { Event as NostrEvent } from 'nostr-tools';
import ColumnItem from '@/components/ColumnItem'; // eslint-disable-next-line import/no-cycle
import TextNoteDisplayById from '@/components/textNote/TextNoteDisplayById'; import EventDisplayById from '@/components/event/EventDisplayById';
import UserDisplayName from '@/components/UserDisplayName'; import UserDisplayName from '@/components/UserDisplayName';
import useFormatDate from '@/hooks/useFormatDate'; import useFormatDate from '@/hooks/useFormatDate';
import useModalState from '@/hooks/useModalState'; import useModalState from '@/hooks/useModalState';
import eventWrapper from '@/nostr/event'; import eventWrapper from '@/nostr/event';
export type DeprecatedRepostProps = { export type RepostProps = {
event: NostrEvent; event: NostrEvent;
}; };
const DeprecatedRepost: Component<DeprecatedRepostProps> = (props) => { const Repost: Component<RepostProps> = (props) => {
const { showProfile } = useModalState(); const { showProfile } = useModalState();
const formatDate = useFormatDate(); const formatDate = useFormatDate();
const repostedId = () => props.event.tags.find(([tagName]) => tagName === 'e')?.[1];
const event = createMemo(() => eventWrapper(props.event)); const event = createMemo(() => eventWrapper(props.event));
const eventId = () => event().lastTaggedEventId();
return ( return (
<ColumnItem> <div>
<div class="flex content-center text-xs"> <div class="flex content-center text-xs">
<div class="h-5 w-5 shrink-0 pr-1 text-green-500" aria-hidden="true"> <div class="h-5 w-5 shrink-0 pr-1 text-green-500" aria-hidden="true">
<ArrowPathRoundedSquare /> <ArrowPathRoundedSquare />
@@ -39,10 +39,10 @@ const DeprecatedRepost: Component<DeprecatedRepostProps> = (props) => {
<div>{formatDate(event().createdAtAsDate())}</div> <div>{formatDate(event().createdAtAsDate())}</div>
</div> </div>
<div class="pt-1"> <div class="pt-1">
<TextNoteDisplayById eventId={repostedId()} /> <EventDisplayById eventId={eventId()} />
</div> </div>
</ColumnItem> </div>
); );
}; };
export default DeprecatedRepost; export default Repost;

View File

@@ -1,7 +1,8 @@
import { Show, type Component } from 'solid-js'; import { Show, type Component } from 'solid-js';
import ColumnItem from '@/components/ColumnItem'; import ColumnItem from '@/components/ColumnItem';
import TextNoteDisplay, { TextNoteDisplayProps } from '@/components/textNote/TextNoteDisplay'; // eslint-disable-next-line import/no-cycle
import TextNoteDisplay, { TextNoteDisplayProps } from '@/components/event/textNote/TextNoteDisplay';
import useConfig from '@/core/useConfig'; import useConfig from '@/core/useConfig';
export type TextNoteProps = TextNoteDisplayProps; export type TextNoteProps = TextNoteDisplayProps;
@@ -11,9 +12,7 @@ const TextNote: Component<TextNoteProps> = (props) => {
return ( return (
<Show when={!shouldMuteEvent(props.event)}> <Show when={!shouldMuteEvent(props.event)}>
<ColumnItem> <TextNoteDisplay {...props} />
<TextNoteDisplay {...props} />
</ColumnItem>
</Show> </Show>
); );
}; };

View File

@@ -1,8 +1,8 @@
import { Show } from 'solid-js'; import { Show } from 'solid-js';
import EventLink from '@/components/EventLink';
// eslint-disable-next-line import/no-cycle // eslint-disable-next-line import/no-cycle
import TextNoteDisplayById from '@/components/textNote/TextNoteDisplayById'; import EventDisplayById from '@/components/event/EventDisplayById';
import EventLink from '@/components/EventLink';
import { type MentionedEvent } from '@/nostr/parseTextNote'; import { type MentionedEvent } from '@/nostr/parseTextNote';
export type MentionedEventDisplayProps = { export type MentionedEventDisplayProps = {
@@ -16,7 +16,7 @@ const MentionedEventDisplay = (props: MentionedEventDisplayProps) => {
fallback={<EventLink eventId={props.mentionedEvent.eventId} />} fallback={<EventLink eventId={props.mentionedEvent.eventId} />}
> >
<div class="my-1 rounded border p-1"> <div class="my-1 rounded border p-1">
<TextNoteDisplayById <EventDisplayById
eventId={props.mentionedEvent.eventId} eventId={props.mentionedEvent.eventId}
embedding={false} embedding={false}
actions={false} actions={false}

View File

@@ -1,4 +1,4 @@
import GeneralUserMentionDisplay from '@/components/textNote/GeneralUserMentionDisplay'; import GeneralUserMentionDisplay from '@/components/event/textNote/GeneralUserMentionDisplay';
import useModalState from '@/hooks/useModalState'; import useModalState from '@/hooks/useModalState';
import type { MentionedUser } from '@/nostr/parseTextNote'; import type { MentionedUser } from '@/nostr/parseTextNote';

View File

@@ -1,12 +1,12 @@
import { For } from 'solid-js'; import { For } from 'solid-js';
import EventLink from '@/components/EventLink';
import ImageDisplay from '@/components/textNote/ImageDisplay';
// eslint-disable-next-line import/no-cycle // eslint-disable-next-line import/no-cycle
import MentionedEventDisplay from '@/components/textNote/MentionedEventDisplay'; import EventDisplayById from '@/components/event/EventDisplayById';
import MentionedUserDisplay from '@/components/textNote/MentionedUserDisplay'; import ImageDisplay from '@/components/event/textNote/ImageDisplay';
import PlainTextDisplay from '@/components/textNote/PlainTextDisplay'; import MentionedEventDisplay from '@/components/event/textNote/MentionedEventDisplay';
import TextNoteDisplayById from '@/components/textNote/TextNoteDisplayById'; import MentionedUserDisplay from '@/components/event/textNote/MentionedUserDisplay';
import PlainTextDisplay from '@/components/event/textNote/PlainTextDisplay';
import EventLink from '@/components/EventLink';
import SafeLink from '@/components/utils/SafeLink'; import SafeLink from '@/components/utils/SafeLink';
import { createSearchColumn } from '@/core/column'; import { createSearchColumn } from '@/core/column';
import useConfig from '@/core/useConfig'; import useConfig from '@/core/useConfig';
@@ -57,18 +57,14 @@ const TextNoteContentDisplay = (props: TextNoteContentDisplayProps) => {
if (item.data.type === 'note' && props.embedding) { if (item.data.type === 'note' && props.embedding) {
return ( return (
<div class="my-1 rounded border p-1"> <div class="my-1 rounded border p-1">
<TextNoteDisplayById eventId={item.data.data} actions={false} embedding={false} /> <EventDisplayById eventId={item.data.data} actions={false} embedding={false} />
</div> </div>
); );
} }
if (item.data.type === 'nevent' && props.embedding) { if (item.data.type === 'nevent' && props.embedding) {
return ( return (
<div class="my-1 rounded border p-1"> <div class="my-1 rounded border p-1">
<TextNoteDisplayById <EventDisplayById eventId={item.data.data.id} actions={false} embedding={false} />
eventId={item.data.data.id}
actions={false}
embedding={false}
/>
</div> </div>
); );
} }

View File

@@ -11,12 +11,12 @@ import { nip19, type Event as NostrEvent } from 'nostr-tools';
import ContextMenu, { MenuItem } from '@/components/ContextMenu'; import ContextMenu, { MenuItem } from '@/components/ContextMenu';
import EmojiPicker from '@/components/EmojiPicker'; import EmojiPicker from '@/components/EmojiPicker';
import NotePostForm from '@/components/NotePostForm';
import ContentWarningDisplay from '@/components/textNote/ContentWarningDisplay';
import GeneralUserMentionDisplay from '@/components/textNote/GeneralUserMentionDisplay';
// eslint-disable-next-line import/no-cycle // eslint-disable-next-line import/no-cycle
import TextNoteContentDisplay from '@/components/textNote/TextNoteContentDisplay'; import EventDisplayById from '@/components/event/EventDisplayById';
import TextNoteDisplayById from '@/components/textNote/TextNoteDisplayById'; import ContentWarningDisplay from '@/components/event/textNote/ContentWarningDisplay';
import GeneralUserMentionDisplay from '@/components/event/textNote/GeneralUserMentionDisplay';
import TextNoteContentDisplay from '@/components/event/textNote/TextNoteContentDisplay';
import NotePostForm from '@/components/NotePostForm';
import { useTimelineContext } from '@/components/timeline/TimelineContext'; import { useTimelineContext } from '@/components/timeline/TimelineContext';
import useConfig from '@/core/useConfig'; import useConfig from '@/core/useConfig';
import useFormatDate from '@/hooks/useFormatDate'; import useFormatDate from '@/hooks/useFormatDate';
@@ -355,7 +355,7 @@ const TextNoteDisplay: Component<TextNoteDisplayProps> = (props) => {
<Show when={showReplyEvent()} keyed> <Show when={showReplyEvent()} keyed>
{(id) => ( {(id) => (
<div class="mt-1 rounded border p-1"> <div class="mt-1 rounded border p-1">
<TextNoteDisplayById eventId={id} actions={false} embedding={false} /> <EventDisplayById eventId={id} actions={false} embedding={false} />
</div> </div>
)} )}
</Show> </Show>

View File

@@ -30,12 +30,6 @@ const isInternetIdentifier = (s: string) => InternetIdentifierRegex.test(s);
const ProfileEdit: Component<ProfileEditProps> = (props) => { const ProfileEdit: Component<ProfileEditProps> = (props) => {
const pubkey = usePubkey(); const pubkey = usePubkey();
const { config } = useConfig(); const { config } = useConfig();
const { profile, invalidateProfile, query } = useProfile(() =>
ensureNonNull([pubkey()] as const)(([pubkeyNonNull]) => ({
pubkey: pubkeyNonNull,
})),
);
const { updateProfile } = useCommands();
const [picture, setPicture] = createSignal(''); const [picture, setPicture] = createSignal('');
const [banner, setBanner] = createSignal(''); const [banner, setBanner] = createSignal('');
@@ -46,6 +40,13 @@ const ProfileEdit: Component<ProfileEditProps> = (props) => {
const [nip05, setNIP05] = createSignal(''); const [nip05, setNIP05] = createSignal('');
const [lightningAddress, setLightningAddress] = createSignal(''); const [lightningAddress, setLightningAddress] = createSignal('');
const { profile, invalidateProfile, query } = useProfile(() =>
ensureNonNull([pubkey()] as const)(([pubkeyNonNull]) => ({
pubkey: pubkeyNonNull,
})),
);
const { updateProfile } = useCommands();
const mutation = createMutation({ const mutation = createMutation({
mutationKey: ['updateProfile'], mutationKey: ['updateProfile'],
mutationFn: (...params: Parameters<typeof updateProfile>) => mutationFn: (...params: Parameters<typeof updateProfile>) =>
@@ -71,7 +72,9 @@ const ProfileEdit: Component<ProfileEditProps> = (props) => {
}, },
}); });
const disabled = () => query.isLoading || query.isError || mutation.isLoading; const loading = () => query.isLoading || mutation.isLoading;
const disabled = () => loading();
const otherProperties = () => const otherProperties = () =>
omit(profile(), [ omit(profile(), [
'picture', 'picture',
@@ -141,7 +144,7 @@ const ProfileEdit: Component<ProfileEditProps> = (props) => {
return ( return (
<BasicModal closeButton={() => <ArrowLeft />} onClose={props.onClose}> <BasicModal closeButton={() => <ArrowLeft />} onClose={props.onClose}>
<div> <div>
<Show when={banner().length > 0} fallback={<div class="h-12 shrink-0" />} keyed> <Show when={banner().length > 0} fallback={<div class="h-24 shrink-0" />} keyed>
<div class="h-40 w-full shrink-0 sm:h-52"> <div class="h-40 w-full shrink-0 sm:h-52">
<img src={banner()} alt="header" class="h-full w-full object-cover" /> <img src={banner()} alt="header" class="h-full w-full object-cover" />
</div> </div>
@@ -152,6 +155,9 @@ const ProfileEdit: Component<ProfileEditProps> = (props) => {
</Show> </Show>
</div> </div>
</div> </div>
<Show when={loading()}>
<div class="px-4 pt-4">...</div>
</Show>
<div> <div>
<form class="flex flex-col gap-4 p-4" onSubmit={handleSubmit}> <form class="flex flex-col gap-4 p-4" onSubmit={handleSubmit}>
<div class="flex flex-col items-start gap-1"> <div class="flex flex-col items-start gap-1">

View File

@@ -1,86 +0,0 @@
import { Switch, Match, type Component, Show } from 'solid-js';
import HeartSolid from 'heroicons/24/solid/heart.svg';
import { type Event as NostrEvent } from 'nostr-tools';
import ColumnItem from '@/components/ColumnItem';
import TextNoteDisplay from '@/components/textNote/TextNoteDisplay';
import UserDisplayName from '@/components/UserDisplayName';
import useModalState from '@/hooks/useModalState';
import eventWrapper from '@/nostr/event';
import useEvent from '@/nostr/useEvent';
import useProfile from '@/nostr/useProfile';
import ensureNonNull from '@/utils/ensureNonNull';
type ReactionProps = {
event: NostrEvent;
};
const Reaction: Component<ReactionProps> = (props) => {
const { showProfile } = useModalState();
const event = () => eventWrapper(props.event);
const eventId = () => event().lastTaggedEventId();
const { profile } = useProfile(() => ({
pubkey: props.event.pubkey,
}));
const { event: reactedEvent, query: reactedEventQuery } = useEvent(() =>
ensureNonNull([eventId()] as const)(([eventIdNonNull]) => ({
eventId: eventIdNonNull,
})),
);
const isRemoved = () => reactedEventQuery.isSuccess && reactedEvent() == null;
return (
// if the reacted event is not found, it should be a removed event
<Show when={!isRemoved()}>
<ColumnItem>
<div class="flex gap-1 px-1 text-sm">
<div class="notification-icon flex place-items-center">
<Switch fallback={props.event.content}>
<Match when={props.event.content === '+'}>
<span class="h-4 w-4 pt-[1px] text-rose-400">
<HeartSolid />
</span>
</Match>
</Switch>
</div>
<div class="notification-user flex gap-1 overflow-hidden">
<div class="author-icon h-5 w-5 shrink-0 overflow-hidden object-cover">
<Show when={profile()?.picture != null}>
<img
src={profile()?.picture}
alt="icon"
// TODO autofit
class="rounded"
/>
</Show>
</div>
<div class="flex-1 overflow-hidden">
<button
class="truncate font-bold hover:text-blue-500 hover:underline"
onClick={() => showProfile(props.event.pubkey)}
>
<UserDisplayName pubkey={props.event.pubkey} />
</button>
{' がリアクション'}
</div>
</div>
</div>
<div class="notification-event py-1">
<Show
when={reactedEvent()}
fallback={<div class="truncate"> {eventId()}</div>}
keyed
>
{(ev) => <TextNoteDisplay event={ev} />}
</Show>
</div>
</ColumnItem>
</Show>
);
};
export default Reaction;

View File

@@ -2,9 +2,9 @@ import { For, Switch, Match, type Component } from 'solid-js';
import { Kind, type Event as NostrEvent } from 'nostr-tools'; import { Kind, type Event as NostrEvent } from 'nostr-tools';
import DeprecatedRepost from '@/components/DeprecatedRepost'; import Reaction from '@/components/event/Reaction';
import Reaction from '@/components/notification/Reaction'; import Repost from '@/components/event/Repost';
import TextNote from '@/components/TextNote'; import TextNote from '@/components/event/TextNote';
export type NotificationProps = { export type NotificationProps = {
events: NostrEvent[]; events: NostrEvent[];
@@ -23,7 +23,7 @@ const Notification: Component<NotificationProps> = (props) => {
</Match> </Match>
{/* TODO ちゃんとnotification用のコンポーネント使う */} {/* TODO ちゃんとnotification用のコンポーネント使う */}
<Match when={(event.kind as number) === 6}> <Match when={(event.kind as number) === 6}>
<DeprecatedRepost event={event} /> <Repost event={event} />
</Match> </Match>
</Switch> </Switch>
)} )}

View File

@@ -1,26 +1,26 @@
import { For, Switch, Match, type Component } from 'solid-js'; import { For, type Component, Show } from 'solid-js';
import { Kind, type Event as NostrEvent } from 'nostr-tools'; import { type Event as NostrEvent } from 'nostr-tools';
import DeprecatedRepost from '@/components/DeprecatedRepost'; import ColumnItem from '@/components/ColumnItem';
import TextNote from '@/components/TextNote'; import EventDisplay from '@/components/event/EventDisplay';
import useConfig from '@/core/useConfig';
export type TimelineProps = { export type TimelineProps = {
events: NostrEvent[]; events: NostrEvent[];
}; };
const Timeline: Component<TimelineProps> = (props) => { const Timeline: Component<TimelineProps> = (props) => {
const { shouldMuteEvent } = useConfig();
return ( return (
<For each={props.events}> <For each={props.events}>
{(event) => ( {(event) => (
<Switch fallback={<div>{event.kind}</div>}> <Show when={!shouldMuteEvent(event)}>
<Match when={event.kind === Kind.Text}> <ColumnItem>
<TextNote event={event} /> <EventDisplay event={event} />
</Match> </ColumnItem>
<Match when={(event.kind as number) === 6}> </Show>
<DeprecatedRepost event={event} />
</Match>
</Switch>
)} )}
</For> </For>
); );

View File

@@ -12,7 +12,7 @@ import timeout from '@/utils/timeout';
type TaskArg = type TaskArg =
| { type: 'Profile'; pubkey: string } | { type: 'Profile'; pubkey: string }
| { type: 'TextNote'; eventId: string } | { type: 'Event'; eventId: string }
| { type: 'Reactions'; mentionedEventId: string } | { type: 'Reactions'; mentionedEventId: string }
| { type: 'ZapReceipts'; mentionedEventId: string } | { type: 'ZapReceipts'; mentionedEventId: string }
| { type: 'Reposts'; mentionedEventId: string } | { type: 'Reposts'; mentionedEventId: string }
@@ -56,12 +56,12 @@ type UseProfile = {
query: CreateQueryResult<NostrEvent | null>; query: CreateQueryResult<NostrEvent | null>;
}; };
// Textnote // Event
export type UseTextNoteProps = { export type UseEventProps = {
eventId: string; eventId: string;
}; };
export type UseTextNote = { export type UseEvent = {
event: () => NostrEvent | null; event: () => NostrEvent | null;
query: CreateQueryResult<NostrEvent | null>; query: CreateQueryResult<NostrEvent | null>;
}; };
@@ -128,19 +128,19 @@ const { exec } = useBatch<TaskArg, TaskRes>(() => ({
batchSize: 150, batchSize: 150,
executor: (tasks) => { executor: (tasks) => {
const profileTasks = new Map<string, Task<TaskArg, TaskRes>[]>(); const profileTasks = new Map<string, Task<TaskArg, TaskRes>[]>();
const textNoteTasks = new Map<string, Task<TaskArg, TaskRes>[]>(); const eventTasks = new Map<string, Task<TaskArg, TaskRes>[]>();
const reactionsTasks = new Map<string, Task<TaskArg, TaskRes>[]>(); const reactionsTasks = new Map<string, Task<TaskArg, TaskRes>[]>();
const repostsTasks = new Map<string, Task<TaskArg, TaskRes>[]>(); const repostsTasks = new Map<string, Task<TaskArg, TaskRes>[]>();
const zapReceiptsTasks = new Map<string, Task<TaskArg, TaskRes>[]>(); const zapReceiptsTasks = new Map<string, Task<TaskArg, TaskRes>[]>();
const followingsTasks = new Map<string, Task<TaskArg, TaskRes>[]>(); const followingsTasks = new Map<string, Task<TaskArg, TaskRes>[]>();
tasks.forEach((task) => { tasks.forEach((task) => {
if (task.args.type === 'Profile') { if (task.args.type === 'Event') {
const current = eventTasks.get(task.args.eventId) ?? [];
eventTasks.set(task.args.eventId, [...current, task]);
} else if (task.args.type === 'Profile') {
const current = profileTasks.get(task.args.pubkey) ?? []; const current = profileTasks.get(task.args.pubkey) ?? [];
profileTasks.set(task.args.pubkey, [...current, task]); profileTasks.set(task.args.pubkey, [...current, task]);
} else if (task.args.type === 'TextNote') {
const current = textNoteTasks.get(task.args.eventId) ?? [];
textNoteTasks.set(task.args.eventId, [...current, task]);
} else if (task.args.type === 'Reactions') { } else if (task.args.type === 'Reactions') {
const current = reactionsTasks.get(task.args.mentionedEventId) ?? []; const current = reactionsTasks.get(task.args.mentionedEventId) ?? [];
reactionsTasks.set(task.args.mentionedEventId, [...current, task]); reactionsTasks.set(task.args.mentionedEventId, [...current, task]);
@@ -156,8 +156,8 @@ const { exec } = useBatch<TaskArg, TaskRes>(() => ({
} }
}); });
const eventIds = [...eventTasks.keys()];
const profilePubkeys = [...profileTasks.keys()]; const profilePubkeys = [...profileTasks.keys()];
const textNoteIds = [...textNoteTasks.keys()];
const reactionsIds = [...reactionsTasks.keys()]; const reactionsIds = [...reactionsTasks.keys()];
const repostsIds = [...repostsTasks.keys()]; const repostsIds = [...repostsTasks.keys()];
const zapReceiptsIds = [...zapReceiptsTasks.keys()]; const zapReceiptsIds = [...zapReceiptsTasks.keys()];
@@ -165,12 +165,12 @@ const { exec } = useBatch<TaskArg, TaskRes>(() => ({
const filters: Filter[] = []; const filters: Filter[] = [];
if (eventIds.length > 0) {
filters.push({ ids: eventIds });
}
if (profilePubkeys.length > 0) { if (profilePubkeys.length > 0) {
filters.push({ kinds: [Kind.Metadata], authors: profilePubkeys }); filters.push({ kinds: [Kind.Metadata], authors: profilePubkeys });
} }
if (textNoteIds.length > 0) {
filters.push({ kinds: [Kind.Text], ids: textNoteIds });
}
if (reactionsIds.length > 0) { if (reactionsIds.length > 0) {
filters.push({ kinds: [Kind.Reaction], '#e': reactionsIds }); filters.push({ kinds: [Kind.Reaction], '#e': reactionsIds });
} }
@@ -229,10 +229,7 @@ const { exec } = useBatch<TaskArg, TaskRes>(() => ({
if (shouldMuteEvent(event)) return; if (shouldMuteEvent(event)) return;
if (event.kind === Kind.Text) { if (event.kind === Kind.Reaction) {
const registeredTasks = textNoteTasks.get(event.id) ?? [];
resolveTasks(registeredTasks, event);
} else if (event.kind === Kind.Reaction) {
// Use the last event id // Use the last event id
const id = eventWrapper(event).lastTaggedEventId(); const id = eventWrapper(event).lastTaggedEventId();
if (id != null) { if (id != null) {
@@ -255,6 +252,13 @@ const { exec } = useBatch<TaskArg, TaskRes>(() => ({
} else if (event.kind === Kind.Contacts) { } else if (event.kind === Kind.Contacts) {
const registeredTasks = followingsTasks.get(event.pubkey) ?? []; const registeredTasks = followingsTasks.get(event.pubkey) ?? [];
resolveTasks(registeredTasks, event); resolveTasks(registeredTasks, event);
} else {
const registeredTasks = eventTasks.get(event.id) ?? [];
if (registeredTasks.length > 0) {
resolveTasks(registeredTasks, event);
} else {
console.warn('unknown event received');
}
} }
}); });
@@ -328,21 +332,21 @@ export const useProfile = (propsProvider: () => UseProfileProps | null): UseProf
return { profile, invalidateProfile, query }; return { profile, invalidateProfile, query };
}; };
export const useTextNote = (propsProvider: () => UseTextNoteProps | null): UseTextNote => { export const useEvent = (propsProvider: () => UseEventProps | null): UseEvent => {
const props = createMemo(propsProvider); const props = createMemo(propsProvider);
const query = createQuery( const query = createQuery(
() => ['useTextNote', props()] as const, () => ['useEvent', props()] as const,
({ queryKey, signal }) => { ({ queryKey, signal }) => {
const [, currentProps] = queryKey; const [, currentProps] = queryKey;
if (currentProps == null) return null; if (currentProps == null) return null;
const { eventId } = currentProps; const { eventId } = currentProps;
const promise = exec({ type: 'TextNote', eventId }, signal).then((batchedEvents) => { const promise = exec({ type: 'Event', eventId }, signal).then((batchedEvents) => {
const event = batchedEvents().events[0]; const event = batchedEvents().events[0];
if (event == null) throw new Error(`event not found: ${eventId}`); if (event == null) throw new Error(`event not found: ${eventId}`);
return event; return event;
}); });
return timeout(15000, `useTextNote: ${eventId}`)(promise); return timeout(15000, `useEvent: ${eventId}`)(promise);
}, },
{ {
// Text notes never change, so they can be stored for a long time. // Text notes never change, so they can be stored for a long time.

View File

@@ -1,3 +1,3 @@
import { useTextNote } from '@/nostr/useBatchedEvents'; import { useEvent } from '@/nostr/useBatchedEvents';
export default useTextNote; export default useEvent;