mirror of
https://github.com/aljazceru/rabbit.git
synced 2025-12-17 14:04:21 +01:00
fix
This commit is contained in:
54
src/components/event/ChannelInfo.tsx
Normal file
54
src/components/event/ChannelInfo.tsx
Normal 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;
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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 }) => {
|
||||||
// すでに設定されている場合は終了
|
// すでに設定されている場合は終了
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user