This commit is contained in:
Shusui MOYATANI
2023-05-16 02:35:53 +09:00
parent da354c713f
commit 9567016206
7 changed files with 99 additions and 19 deletions

View File

@@ -0,0 +1,54 @@
import { Component, Show } from 'solid-js';
import ChatBubbleLeftRight from 'heroicons/24/outline/chat-bubble-left-right.svg';
import { Event as NostrEvent } from 'nostr-tools';
import { z } from 'zod';
import EventLink from '@/components/EventLink';
import { isImageUrl } from '@/utils/imageUrl';
export type ChannelInfoProps = {
event: NostrEvent;
};
const ChannelMetaSchema = z.object({
name: z.string(),
about: z.string().optional(),
picture: z
.string()
.url()
.refine((url) => isImageUrl(url), { message: 'not an image url' })
.optional(),
});
export type ChannelMeta = z.infer<typeof ChannelMetaSchema>;
const parseContent = (content: string): ChannelMeta | null => {
try {
return ChannelMetaSchema.parse(JSON.parse(content));
} catch (err) {
console.warn('failed to parse chat channel schema: ', err);
return null;
}
};
const ChannelInfo: Component<ChannelInfoProps> = (props) => {
const parsedContent = () => parseContent(props.event.content);
return (
<Show when={parsedContent()} keyed>
{(meta) => (
<button class="flex flex-col gap-1 px-1">
<div class="flex items-center gap-1">
<span class="inline-block h-4 w-4 text-purple-400">
<ChatBubbleLeftRight />
</span>
<span>{meta.name}</span>
</div>
</button>
)}
</Show>
);
};
export default ChannelInfo;

View File

@@ -2,6 +2,7 @@ import { Switch, Match, Component } from 'solid-js';
import { Kind, type Event as NostrEvent } from 'nostr-tools'; import { Kind, type Event as NostrEvent } from 'nostr-tools';
import ChannelInfo from '@/components/event/ChannelInfo';
// eslint-disable-next-line import/no-cycle // eslint-disable-next-line import/no-cycle
import Repost from '@/components/event/Repost'; import Repost from '@/components/event/Repost';
// eslint-disable-next-line import/no-cycle // eslint-disable-next-line import/no-cycle
@@ -12,12 +13,16 @@ export type EventDisplayProps = {
event: NostrEvent; event: NostrEvent;
embedding?: boolean; embedding?: boolean;
actions?: boolean; actions?: boolean;
kinds?: Kind[]; ensureKinds?: Kind[];
}; };
const EventDisplay: Component<EventDisplayProps> = (props) => { const EventDisplay: Component<EventDisplayProps> = (props) => {
// noteの場合は kind:1 であることを保証するために利用できる
// タイムラインで表示されるべきでないイベントが表示されてしまうのを防ぐ
const isAllowedKind = () => const isAllowedKind = () =>
props.kinds == null || props.kinds.length === 0 || props.kinds.includes(props.event.kind); props.ensureKinds == null ||
props.ensureKinds.length === 0 ||
props.ensureKinds.includes(props.event.kind);
return ( return (
<Switch <Switch
@@ -28,7 +33,12 @@ const EventDisplay: Component<EventDisplayProps> = (props) => {
</span> </span>
} }
> >
<Match when={!isAllowedKind()}>{null}</Match> <Match when={!isAllowedKind()} keyed>
<span>
{props.event.kind}
<EventLink eventId={props.event.id} kind={props.event.kind} />
</span>
</Match>
<Match when={props.event.kind === Kind.Text}> <Match when={props.event.kind === Kind.Text}>
<TextNote event={props.event} embedding={props.actions} actions={props.actions} /> <TextNote event={props.event} embedding={props.actions} actions={props.actions} />
</Match> </Match>
@@ -37,6 +47,11 @@ const EventDisplay: Component<EventDisplayProps> = (props) => {
</Match> </Match>
</Switch> </Switch>
); );
/*
<Match when={props.event.kind === Kind.ChannelCreation}>
<ChannelInfo event={props.event} />
</Match>
*/
}; };
export default EventDisplay; export default EventDisplay;

View File

@@ -1,5 +1,7 @@
import { For } from 'solid-js'; import { For } from 'solid-js';
import { Kind, Event as NostrEvent } from 'nostr-tools';
// eslint-disable-next-line import/no-cycle // eslint-disable-next-line import/no-cycle
import EventDisplayById from '@/components/event/EventDisplayById'; import EventDisplayById from '@/components/event/EventDisplayById';
import ImageDisplay from '@/components/event/textNote/ImageDisplay'; import ImageDisplay from '@/components/event/textNote/ImageDisplay';
@@ -15,8 +17,6 @@ import eventWrapper from '@/nostr/event';
import parseTextNote, { resolveTagReference, type ParsedTextNoteNode } from '@/nostr/parseTextNote'; import parseTextNote, { resolveTagReference, type ParsedTextNoteNode } from '@/nostr/parseTextNote';
import { isImageUrl } from '@/utils/imageUrl'; import { isImageUrl } from '@/utils/imageUrl';
import type { Event as NostrEvent } from 'nostr-tools';
export type TextNoteContentDisplayProps = { export type TextNoteContentDisplayProps = {
event: NostrEvent; event: NostrEvent;
embedding: boolean; embedding: boolean;
@@ -57,7 +57,12 @@ 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">
<EventDisplayById eventId={item.data.data} actions={false} embedding={false} /> <EventDisplayById
eventId={item.data.data}
actions={false}
embedding={false}
ensureKinds={[Kind.Text]}
/>
</div> </div>
); );
} }

View File

@@ -49,7 +49,7 @@ const EmojiReactions: Component<EmojiReactionsProps> = (props) => {
const pubkey = usePubkey(); const pubkey = usePubkey();
return ( return (
<div class="flex gap-2 pt-1"> <div class="flex gap-2 py-1">
<For each={[...props.reactionsGroupedByContent.entries()]}> <For each={[...props.reactionsGroupedByContent.entries()]}>
{([content, events]) => { {([content, events]) => {
const isReactedByMeWithThisContent = const isReactedByMeWithThisContent =
@@ -57,9 +57,10 @@ const EmojiReactions: Component<EmojiReactionsProps> = (props) => {
return ( return (
<button <button
class="flex items-center rounded border px-1" class="flex h-6 items-center rounded border px-1"
classList={{ classList={{
'text-zinc-400': !isReactedByMeWithThisContent, 'text-zinc-400': !isReactedByMeWithThisContent,
'hover:bg-zinc-50': !isReactedByMeWithThisContent,
'bg-rose-50': isReactedByMeWithThisContent, 'bg-rose-50': isReactedByMeWithThisContent,
'border-rose-200': isReactedByMeWithThisContent, 'border-rose-200': isReactedByMeWithThisContent,
'text-rose-400': isReactedByMeWithThisContent, 'text-rose-400': isReactedByMeWithThisContent,
@@ -67,7 +68,7 @@ const EmojiReactions: Component<EmojiReactionsProps> = (props) => {
type="button" type="button"
onClick={() => props.onReaction(content)} onClick={() => props.onReaction(content)}
> >
<Show when={content === '+'} fallback={<span class="text-xs">{content}</span>}> <Show when={content === '+'} fallback={<span class="text-base">{content}</span>}>
<span class="inline-block h-3 w-3 pt-[1px] text-rose-400"> <span class="inline-block h-3 w-3 pt-[1px] text-rose-400">
<HeartSolid /> <HeartSolid />
</span> </span>
@@ -403,9 +404,9 @@ const TextNoteDisplay: Component<TextNoteDisplayProps> = (props) => {
onReaction={doReaction} onReaction={doReaction}
/> />
</Show> </Show>
<div class="actions flex w-48 items-center justify-between gap-8 pt-1"> <div class="actions flex w-52 items-center justify-between gap-8 pt-1">
<button <button
class="h-4 w-4 shrink-0 text-zinc-400" class="h-4 w-4 shrink-0 text-zinc-400 hover:text-zinc-500"
onClick={(ev) => { onClick={(ev) => {
ev.stopPropagation(); ev.stopPropagation();
setShowReplyForm((current) => !current); setShowReplyForm((current) => !current);
@@ -417,6 +418,7 @@ const TextNoteDisplay: Component<TextNoteDisplayProps> = (props) => {
class="flex shrink-0 items-center gap-1" class="flex shrink-0 items-center gap-1"
classList={{ classList={{
'text-zinc-400': !isRepostedByMe(), 'text-zinc-400': !isRepostedByMe(),
'hover:text-green-400': !isRepostedByMe(),
'text-green-400': isRepostedByMe() || publishRepostMutation.isLoading, 'text-green-400': isRepostedByMe() || publishRepostMutation.isLoading,
}} }}
> >
@@ -435,6 +437,7 @@ const TextNoteDisplay: Component<TextNoteDisplayProps> = (props) => {
class="flex shrink-0 items-center gap-1" class="flex shrink-0 items-center gap-1"
classList={{ classList={{
'text-zinc-400': !isReactedByMe() || isReactedByMeWithEmoji(), 'text-zinc-400': !isReactedByMe() || isReactedByMeWithEmoji(),
'hover:text-rose-400': !isReactedByMe() || isReactedByMeWithEmoji(),
'text-rose-400': 'text-rose-400':
(isReactedByMe() && !isReactedByMeWithEmoji()) || (isReactedByMe() && !isReactedByMeWithEmoji()) ||
publishReactionMutation.isLoading, publishReactionMutation.isLoading,
@@ -465,6 +468,7 @@ const TextNoteDisplay: Component<TextNoteDisplayProps> = (props) => {
class="flex shrink-0 items-center gap-1" class="flex shrink-0 items-center gap-1"
classList={{ classList={{
'text-zinc-400': !isReactedByMe() || !isReactedByMeWithEmoji(), 'text-zinc-400': !isReactedByMe() || !isReactedByMeWithEmoji(),
'hover:text-rose-400': !isReactedByMe() || !isReactedByMeWithEmoji(),
'text-rose-400': 'text-rose-400':
(isReactedByMe() && isReactedByMeWithEmoji()) || (isReactedByMe() && isReactedByMeWithEmoji()) ||
publishReactionMutation.isLoading, publishReactionMutation.isLoading,
@@ -479,7 +483,7 @@ const TextNoteDisplay: Component<TextNoteDisplayProps> = (props) => {
</Show> </Show>
<div> <div>
<ContextMenu menu={menu}> <ContextMenu menu={menu}>
<span class="inline-block h-4 w-4 text-zinc-400"> <span class="inline-block h-4 w-4 text-zinc-400 hover:text-zinc-500">
<EllipsisHorizontal /> <EllipsisHorizontal />
</span> </span>
</ContextMenu> </ContextMenu>

View File

@@ -16,6 +16,7 @@ import {
createStorageWithSerializer, createStorageWithSerializer,
createStoreWithStorage, createStoreWithStorage,
} from '@/hooks/createSignalWithStorage'; } from '@/hooks/createSignalWithStorage';
import eventWrapper from '@/nostr/event';
export type Config = { export type Config = {
relayUrls: string[]; relayUrls: string[];
@@ -149,8 +150,14 @@ const useConfig = (): UseConfig => {
return false; return false;
}; };
const shouldMuteEvent = (event: NostrEvent) => const shouldMuteEvent = (event: NostrEvent) => {
isPubkeyMuted(event.pubkey) || hasMutedKeyword(event); const ev = eventWrapper(event);
return (
isPubkeyMuted(event.pubkey) ||
ev.mentionedPubkeys().some(isPubkeyMuted) ||
hasMutedKeyword(event)
);
};
const initializeColumns = ({ pubkey }: { pubkey: string }) => { const initializeColumns = ({ pubkey }: { pubkey: string }) => {
// すでに設定されている場合は終了 // すでに設定されている場合は終了

View File

@@ -227,8 +227,6 @@ const { exec } = useBatch<TaskArg, TaskRes>(() => ({
return; return;
} }
if (shouldMuteEvent(event)) return;
if (event.kind === Kind.Reaction) { 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();

View File

@@ -71,9 +71,6 @@ const useSubscription = (propsProvider: () => UseSubscriptionProps | null) => {
if (onEvent != null) { if (onEvent != null) {
onEvent(event as NostrEvent & { id: string }); onEvent(event as NostrEvent & { id: string });
} }
if (shouldMuteEvent(event)) {
return;
}
if (props.clientEventFilter != null && !props.clientEventFilter(event)) { if (props.clientEventFilter != null && !props.clientEventFilter(event)) {
return; return;
} }