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

View File

@@ -1,5 +1,7 @@
import { For } from 'solid-js';
import { Kind, Event as NostrEvent } from 'nostr-tools';
// eslint-disable-next-line import/no-cycle
import EventDisplayById from '@/components/event/EventDisplayById';
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 { isImageUrl } from '@/utils/imageUrl';
import type { Event as NostrEvent } from 'nostr-tools';
export type TextNoteContentDisplayProps = {
event: NostrEvent;
embedding: boolean;
@@ -57,7 +57,12 @@ const TextNoteContentDisplay = (props: TextNoteContentDisplayProps) => {
if (item.data.type === 'note' && props.embedding) {
return (
<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>
);
}

View File

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

View File

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

View File

@@ -227,8 +227,6 @@ const { exec } = useBatch<TaskArg, TaskRes>(() => ({
return;
}
if (shouldMuteEvent(event)) return;
if (event.kind === Kind.Reaction) {
// Use the last event id
const id = eventWrapper(event).lastTaggedEventId();

View File

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