mirror of
https://github.com/aljazceru/rabbit.git
synced 2025-12-17 14:04:21 +01:00
refactor
This commit is contained in:
@@ -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;
|
||||||
|
|||||||
@@ -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,
|
||||||
]);
|
]);
|
||||||
|
|||||||
42
src/components/event/EventDisplay.tsx
Normal file
42
src/components/event/EventDisplay.tsx
Normal 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;
|
||||||
@@ -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;
|
||||||
85
src/components/event/Reaction.tsx
Normal file
85
src/components/event/Reaction.tsx
Normal 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;
|
||||||
@@ -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;
|
||||||
@@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -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}
|
||||||
@@ -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';
|
||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -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>
|
||||||
@@ -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">
|
||||||
|
|||||||
@@ -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;
|
|
||||||
@@ -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>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
import { useTextNote } from '@/nostr/useBatchedEvents';
|
import { useEvent } from '@/nostr/useBatchedEvents';
|
||||||
|
|
||||||
export default useTextNote;
|
export default useEvent;
|
||||||
|
|||||||
Reference in New Issue
Block a user