mirror of
https://github.com/aljazceru/rabbit.git
synced 2025-12-17 22:14:26 +01:00
fix: ignore invalid event id and invalid pubkey
This commit is contained in:
@@ -1,4 +1,6 @@
|
||||
import { createSignal, onCleanup, createEffect, For, type Component, type JSX } from 'solid-js';
|
||||
import { For, type Component, type JSX } from 'solid-js';
|
||||
|
||||
import Popup, { PopupRef } from '@/components/utils/Popup';
|
||||
|
||||
export type MenuItem = {
|
||||
content: () => JSX.Element;
|
||||
@@ -32,60 +34,24 @@ const MenuItemDisplay: Component<MenuItemDisplayProps> = (props) => {
|
||||
};
|
||||
|
||||
const ContextMenu: Component<ContextMenuProps> = (props) => {
|
||||
let menuRef: HTMLUListElement | undefined;
|
||||
let popupRef: PopupRef | undefined;
|
||||
|
||||
const [isOpen, setIsOpen] = createSignal(false);
|
||||
|
||||
const handleClickOutside = (ev: MouseEvent) => {
|
||||
const target = ev.target as HTMLElement;
|
||||
if (target != null && !menuRef?.contains(target)) {
|
||||
setIsOpen(false);
|
||||
}
|
||||
};
|
||||
const addClickOutsideHandler = () => {
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
};
|
||||
const removeClickOutsideHandler = () => {
|
||||
document.removeEventListener('mousedown', handleClickOutside);
|
||||
};
|
||||
|
||||
const open = () => setIsOpen(true);
|
||||
const close = () => setIsOpen(false);
|
||||
|
||||
const handleClick: JSX.EventHandler<HTMLButtonElement, MouseEvent> = (ev) => {
|
||||
if (menuRef == null) return;
|
||||
|
||||
const buttonRect = ev.currentTarget.getBoundingClientRect();
|
||||
// const menuRect = menuRef.getBoundingClientRect();
|
||||
menuRef.style.left = `${buttonRect.left - buttonRect.width}px`;
|
||||
menuRef.style.top = `${buttonRect.top + buttonRect.height}px`;
|
||||
|
||||
open();
|
||||
};
|
||||
|
||||
createEffect(() => {
|
||||
if (isOpen()) {
|
||||
addClickOutsideHandler();
|
||||
} else {
|
||||
removeClickOutsideHandler();
|
||||
}
|
||||
});
|
||||
|
||||
onCleanup(() => removeClickOutsideHandler());
|
||||
const close = () => popupRef?.close();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<button onClick={handleClick}>{props.children}</button>
|
||||
<ul
|
||||
ref={menuRef}
|
||||
class="absolute z-20 min-w-[48px] rounded border bg-white shadow-md"
|
||||
classList={{ hidden: !isOpen(), block: isOpen() }}
|
||||
>
|
||||
<Popup
|
||||
ref={(e) => {
|
||||
popupRef = e;
|
||||
}}
|
||||
button={props.children}
|
||||
position="bottom"
|
||||
>
|
||||
<ul class="min-w-[96px] rounded border bg-white shadow-md">
|
||||
<For each={props.menu.filter((e) => e.when == null || e.when())}>
|
||||
{(item: MenuItem) => <MenuItemDisplay item={item} onClose={close} />}
|
||||
</For>
|
||||
</ul>
|
||||
</div>
|
||||
</Popup>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -18,14 +18,14 @@ export type ProfileEditProps = {
|
||||
};
|
||||
|
||||
const LNURLRegexString = '(LNURL1[AC-HJ-NP-Z02-9]+|lnurl1[ac-hj-np-z02-9]+)';
|
||||
const InternetIdentiferRegexString = '[-_a-zA-Z0-9.]+@[-a-zA-Z0-9.]+';
|
||||
const LUDAddressRegexString = `^(${LNURLRegexString}|${InternetIdentiferRegexString})$`;
|
||||
const InternetIdentifierRegexString = '[-_a-zA-Z0-9.]+@[-a-zA-Z0-9.]+';
|
||||
const LUDAddressRegexString = `^(${LNURLRegexString}|${InternetIdentifierRegexString})$`;
|
||||
|
||||
const LNURLRegex = new RegExp(`^${LNURLRegexString}$`);
|
||||
const InternetIdentiferRegex = new RegExp(`^${InternetIdentiferRegexString}$`);
|
||||
const InternetIdentifierRegex = new RegExp(`^${InternetIdentifierRegexString}$`);
|
||||
|
||||
const isLNURL = (s: string) => LNURLRegex.test(s);
|
||||
const isInternetIdentifier = (s: string) => InternetIdentiferRegex.test(s);
|
||||
const isInternetIdentifier = (s: string) => InternetIdentifierRegex.test(s);
|
||||
|
||||
const ProfileEdit: Component<ProfileEditProps> = (props) => {
|
||||
const pubkey = usePubkey();
|
||||
@@ -260,7 +260,7 @@ const ProfileEdit: Component<ProfileEditProps> = (props) => {
|
||||
name="nip05"
|
||||
value={nip05()}
|
||||
placeholder="yourname@domain.example.com"
|
||||
pattern={InternetIdentiferRegex.source}
|
||||
pattern={InternetIdentifierRegex.source}
|
||||
disabled={disabled()}
|
||||
onChange={(ev) => setNIP05(ev.currentTarget.value)}
|
||||
onKeyDown={ignoreEnter}
|
||||
|
||||
@@ -10,6 +10,7 @@ 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;
|
||||
@@ -18,14 +19,18 @@ type ReactionProps = {
|
||||
const Reaction: Component<ReactionProps> = (props) => {
|
||||
const { showProfile } = useModalState();
|
||||
const event = () => eventWrapper(props.event);
|
||||
const eventId = () => event().taggedEvents()[0].id;
|
||||
const eventId = () => event().lastTaggedEventId();
|
||||
|
||||
const { profile } = useProfile(() => ({
|
||||
pubkey: props.event.pubkey,
|
||||
}));
|
||||
const { event: reactedEvent, query: reactedEventQuery } = useEvent(() => ({
|
||||
eventId: eventId(),
|
||||
}));
|
||||
|
||||
const { event: reactedEvent, query: reactedEventQuery } = useEvent(() =>
|
||||
ensureNonNull([eventId()] as const)(([eventIdNonNull]) => ({
|
||||
eventId: eventIdNonNull,
|
||||
})),
|
||||
);
|
||||
|
||||
const isRemoved = () => reactedEventQuery.isSuccess && reactedEvent() == null;
|
||||
|
||||
return (
|
||||
@@ -67,7 +72,7 @@ const Reaction: Component<ReactionProps> = (props) => {
|
||||
<div class="notification-event py-1">
|
||||
<Show
|
||||
when={reactedEvent()}
|
||||
fallback={<div class="truncate">loading {eventId()}</div>}
|
||||
fallback={<div class="truncate">読み込み中 {eventId()}</div>}
|
||||
keyed
|
||||
>
|
||||
{(ev) => <TextNoteDisplay event={ev} />}
|
||||
|
||||
@@ -37,8 +37,52 @@ export type TextNoteDisplayProps = {
|
||||
actions?: boolean;
|
||||
};
|
||||
|
||||
type EmojiReactionsProps = {
|
||||
reactionsGroupedByContent: Map<string, NostrEvent[]>;
|
||||
onReaction: (emoji: string) => void;
|
||||
};
|
||||
|
||||
const { noteEncode } = nip19;
|
||||
|
||||
const EmojiReactions: Component<EmojiReactionsProps> = (props) => {
|
||||
const { config } = useConfig();
|
||||
const pubkey = usePubkey();
|
||||
|
||||
return (
|
||||
<div class="flex gap-2 pt-1">
|
||||
<For each={[...props.reactionsGroupedByContent.entries()]}>
|
||||
{([content, events]) => {
|
||||
const isReactedByMeWithThisContent =
|
||||
events.findIndex((ev) => ev.pubkey === pubkey()) >= 0;
|
||||
|
||||
return (
|
||||
<button
|
||||
class="flex items-center rounded border px-1"
|
||||
classList={{
|
||||
'text-zinc-400': !isReactedByMeWithThisContent,
|
||||
'bg-rose-50': isReactedByMeWithThisContent,
|
||||
'border-rose-200': isReactedByMeWithThisContent,
|
||||
'text-rose-400': isReactedByMeWithThisContent,
|
||||
}}
|
||||
type="button"
|
||||
onClick={() => props.onReaction(content)}
|
||||
>
|
||||
<Show when={content === '+'} fallback={<span class="text-xs">{content}</span>}>
|
||||
<span class="inline-block h-3 w-3 pt-[1px] text-rose-400">
|
||||
<HeartSolid />
|
||||
</span>
|
||||
</Show>
|
||||
<Show when={!config().hideCount}>
|
||||
<span class="ml-1 text-sm">{events.length}</span>
|
||||
</Show>
|
||||
</button>
|
||||
);
|
||||
}}
|
||||
</For>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const TextNoteDisplay: Component<TextNoteDisplayProps> = (props) => {
|
||||
let contentRef: HTMLDivElement | undefined;
|
||||
|
||||
@@ -349,40 +393,10 @@ const TextNoteDisplay: Component<TextNoteDisplayProps> = (props) => {
|
||||
</Show>
|
||||
<Show when={actions()}>
|
||||
<Show when={config().showEmojiReaction && reactions().length > 0}>
|
||||
<div class="flex gap-2 pt-1">
|
||||
<For each={[...reactionsGroupedByContent().entries()]}>
|
||||
{([content, events]) => {
|
||||
const isReactedByMeWithThisContent =
|
||||
events.findIndex((ev) => ev.pubkey === pubkey()) >= 0;
|
||||
|
||||
return (
|
||||
<button
|
||||
class="flex items-center rounded border px-1"
|
||||
classList={{
|
||||
'text-zinc-400': !isReactedByMeWithThisContent,
|
||||
'bg-rose-50': isReactedByMeWithThisContent,
|
||||
'border-rose-200': isReactedByMeWithThisContent,
|
||||
'text-rose-400': isReactedByMeWithThisContent,
|
||||
}}
|
||||
type="button"
|
||||
onClick={() => doReaction(content)}
|
||||
>
|
||||
<Show
|
||||
when={content === '+'}
|
||||
fallback={<span class="text-xs">{content}</span>}
|
||||
>
|
||||
<span class="inline-block h-3 w-3 pt-[1px] text-rose-400">
|
||||
<HeartSolid />
|
||||
</span>
|
||||
</Show>
|
||||
<Show when={!config().hideCount}>
|
||||
<span class="ml-1 text-sm">{events.length}</span>
|
||||
</Show>
|
||||
</button>
|
||||
);
|
||||
}}
|
||||
</For>
|
||||
</div>
|
||||
<EmojiReactions
|
||||
reactionsGroupedByContent={reactionsGroupedByContent()}
|
||||
onReaction={doReaction}
|
||||
/>
|
||||
</Show>
|
||||
<div class="actions flex w-48 items-center justify-between gap-8 pt-1">
|
||||
<button
|
||||
@@ -419,26 +433,15 @@ const TextNoteDisplay: Component<TextNoteDisplayProps> = (props) => {
|
||||
'text-rose-400': isReactedByMe() || publishReactionMutation.isLoading,
|
||||
}}
|
||||
>
|
||||
<Show
|
||||
when={!config().useEmojiReaction}
|
||||
fallback={
|
||||
<EmojiPicker onEmojiSelect={(emoji) => doReaction(emoji)}>
|
||||
<span class="inline-block h-4 w-4">
|
||||
<Plus />
|
||||
</span>
|
||||
</EmojiPicker>
|
||||
}
|
||||
<button
|
||||
class="h-4 w-4"
|
||||
onClick={handleReaction}
|
||||
disabled={publishReactionMutation.isLoading}
|
||||
>
|
||||
<button
|
||||
class="h-4 w-4"
|
||||
onClick={handleReaction}
|
||||
disabled={publishReactionMutation.isLoading}
|
||||
>
|
||||
<Show when={isReactedByMe()} fallback={<HeartOutlined />}>
|
||||
<HeartSolid />
|
||||
</Show>
|
||||
</button>
|
||||
</Show>
|
||||
<Show when={isReactedByMe()} fallback={<HeartOutlined />}>
|
||||
<HeartSolid />
|
||||
</Show>
|
||||
</button>
|
||||
<Show
|
||||
when={
|
||||
!config().hideCount && !config().showEmojiReaction && reactions().length > 0
|
||||
@@ -447,6 +450,15 @@ const TextNoteDisplay: Component<TextNoteDisplayProps> = (props) => {
|
||||
<div class="text-sm text-zinc-400">{reactions().length}</div>
|
||||
</Show>
|
||||
</div>
|
||||
<Show when={config().useEmojiReaction}>
|
||||
<div class="shrink-0">
|
||||
<EmojiPicker onEmojiSelect={(emoji) => doReaction(emoji)}>
|
||||
<span class="inline-block h-4 w-4">
|
||||
<Plus />
|
||||
</span>
|
||||
</EmojiPicker>
|
||||
</div>
|
||||
</Show>
|
||||
<div>
|
||||
<ContextMenu menu={menu}>
|
||||
<span class="inline-block h-4 w-4 text-zinc-400">
|
||||
|
||||
@@ -27,12 +27,16 @@ const Popup: Component<PopupProps> = (props) => {
|
||||
let popupRef: HTMLDivElement | undefined;
|
||||
|
||||
const [isOpen, setIsOpen] = createSignal(false);
|
||||
const [style, setStyle] = createSignal<JSX.CSSProperties>({});
|
||||
const resolvedChildren = children(() => props.children);
|
||||
|
||||
const close = () => setIsOpen(false);
|
||||
const toggleOpened = () => setIsOpen((current) => !current);
|
||||
|
||||
const handleClickOutside = (ev: MouseEvent) => {
|
||||
const target = ev.target as HTMLElement;
|
||||
if (target != null && !popupRef?.contains(target)) {
|
||||
setIsOpen(false);
|
||||
close();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -43,10 +47,31 @@ const Popup: Component<PopupProps> = (props) => {
|
||||
document.removeEventListener('mousedown', handleClickOutside);
|
||||
};
|
||||
|
||||
const close = () => setIsOpen(false);
|
||||
const toggle = () => setIsOpen((current) => !current);
|
||||
const adjustPosition = () => {
|
||||
if (buttonRef == null || popupRef == null) return;
|
||||
|
||||
const handleClick: JSX.EventHandler<HTMLButtonElement, MouseEvent> = () => toggle();
|
||||
const buttonRect = buttonRef?.getBoundingClientRect();
|
||||
const popupRect = popupRef?.getBoundingClientRect();
|
||||
|
||||
let { top, left } = buttonRect;
|
||||
|
||||
if (props.position === 'left') {
|
||||
left -= buttonRect.width;
|
||||
} else if (props.position === 'right') {
|
||||
left += buttonRect.width;
|
||||
} else if (props.position === 'top') {
|
||||
top -= buttonRect.height;
|
||||
left -= buttonRect.left + buttonRect.width / 2;
|
||||
} else {
|
||||
top += buttonRect.height;
|
||||
left += buttonRect.width / 2;
|
||||
}
|
||||
|
||||
top = Math.min(top, window.innerHeight - popupRect.height);
|
||||
left = Math.min(left, window.innerWidth - popupRect.width);
|
||||
|
||||
setStyle({ left: `${left}px`, top: `${top}px` });
|
||||
};
|
||||
|
||||
createEffect(() => {
|
||||
if (isOpen()) {
|
||||
@@ -60,34 +85,16 @@ const Popup: Component<PopupProps> = (props) => {
|
||||
|
||||
createEffect(
|
||||
on(resolvedChildren, () => {
|
||||
if (buttonRef == null || popupRef == null) return;
|
||||
|
||||
const buttonRect = buttonRef?.getBoundingClientRect();
|
||||
const popupRect = popupRef?.getBoundingClientRect();
|
||||
|
||||
let { top, left } = buttonRect;
|
||||
|
||||
if (props.position === 'left') {
|
||||
left -= buttonRect.width;
|
||||
} else if (props.position === 'right') {
|
||||
left += buttonRect.width;
|
||||
} else if (props.position === 'top') {
|
||||
top -= buttonRect.height;
|
||||
left -= buttonRect.left + buttonRect.width / 2;
|
||||
} else {
|
||||
top += buttonRect.height;
|
||||
left += buttonRect.width / 2;
|
||||
}
|
||||
|
||||
top = Math.min(top, window.innerHeight - popupRect.height);
|
||||
left = Math.min(left, window.innerWidth - popupRect.width);
|
||||
console.log(popupRect);
|
||||
|
||||
popupRef.style.left = `${left}px`;
|
||||
popupRef.style.top = `${top}px`;
|
||||
adjustPosition();
|
||||
}),
|
||||
);
|
||||
|
||||
createEffect(() => {
|
||||
if (isOpen()) {
|
||||
adjustPosition();
|
||||
}
|
||||
});
|
||||
|
||||
onMount(() => {
|
||||
props.ref?.({ close });
|
||||
});
|
||||
@@ -96,10 +103,22 @@ const Popup: Component<PopupProps> = (props) => {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<button ref={buttonRef} class="flex items-center" onClick={handleClick}>
|
||||
<button
|
||||
ref={buttonRef}
|
||||
class="flex items-center"
|
||||
onClick={() => {
|
||||
toggleOpened();
|
||||
adjustPosition();
|
||||
}}
|
||||
>
|
||||
{props.button}
|
||||
</button>
|
||||
<div ref={popupRef} class="absolute z-20" classList={{ hidden: !isOpen(), block: isOpen() }}>
|
||||
<div
|
||||
ref={popupRef}
|
||||
class="absolute z-20"
|
||||
classList={{ hidden: !isOpen(), block: isOpen() }}
|
||||
style={style()}
|
||||
>
|
||||
{resolvedChildren()}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
23
src/nostr/event.test.ts
Normal file
23
src/nostr/event.test.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import assert from 'assert';
|
||||
|
||||
import { describe, it } from 'vitest';
|
||||
|
||||
import { isValidId } from '@/nostr/event';
|
||||
|
||||
describe('isValidId', () => {
|
||||
it.each([
|
||||
'59cd96d24acd0679080679330a06ae9dcb5026ba080528fb93762c49050be8c9',
|
||||
'005c079e4c7c103168e0cb359270ac96a6a46e5ff4ce8f4643e0831f6d1c2450',
|
||||
])('should return true if valid id is given (%s)', (id) => {
|
||||
assert.deepStrictEqual(true, isValidId(id));
|
||||
});
|
||||
|
||||
it.each([
|
||||
'd9553d75bb38da004d380168221fa8cb7c5c55f5242397c1459c7013a2a1264z',
|
||||
'a8e947afb422bbc44577a107147f684ab5c97538b505d2699181e8815160950z',
|
||||
'a8e947a',
|
||||
'',
|
||||
])('should return false if invalid id is given (%s)', (id) => {
|
||||
assert.deepStrictEqual(false, isValidId(id));
|
||||
});
|
||||
});
|
||||
@@ -1,22 +1,37 @@
|
||||
import uniq from 'lodash/uniq';
|
||||
|
||||
import type { Event as NostrEvent } from 'nostr-tools';
|
||||
import { Kind, Event as NostrEvent } from 'nostr-tools';
|
||||
|
||||
export type EventMarker = 'reply' | 'root' | 'mention';
|
||||
|
||||
export type TaggedEvent = {
|
||||
// NIP-10
|
||||
export type MarkedEventTag = {
|
||||
id: string;
|
||||
relayUrl?: string;
|
||||
index: number;
|
||||
marker?: EventMarker;
|
||||
};
|
||||
|
||||
export type ContactPubkeyTag = {
|
||||
pubkey: string;
|
||||
relayUrl?: string;
|
||||
petname?: string;
|
||||
};
|
||||
|
||||
export type ContentWarning = {
|
||||
contentWarning: boolean;
|
||||
reason?: string;
|
||||
};
|
||||
|
||||
const IdRegex = /^[0-9a-fA-f]{64}$/;
|
||||
export const isValidId = (s: string): boolean => {
|
||||
const result = typeof s === 'string' && s.length === 64 && IdRegex.test(s);
|
||||
if (!result) console.warn('invalid id is ignored: ', s);
|
||||
return result;
|
||||
};
|
||||
|
||||
const eventWrapper = (event: NostrEvent) => {
|
||||
let memoizedMarkedEventTags: MarkedEventTag[] | undefined;
|
||||
|
||||
return {
|
||||
get rawEvent(): NostrEvent {
|
||||
return event;
|
||||
@@ -33,22 +48,36 @@ const eventWrapper = (event: NostrEvent) => {
|
||||
get content(): string {
|
||||
return event.content;
|
||||
},
|
||||
get tags(): string[][] {
|
||||
return event.tags;
|
||||
},
|
||||
createdAtAsDate(): Date {
|
||||
return new Date(event.created_at * 1000);
|
||||
},
|
||||
taggedUsers(): string[] {
|
||||
const pubkeys = new Set<string>();
|
||||
event.tags.forEach(([tagName, pubkey]) => {
|
||||
if (tagName === 'p') {
|
||||
pubkeys.add(pubkey);
|
||||
}
|
||||
});
|
||||
return Array.from(pubkeys);
|
||||
pTags(): string[][] {
|
||||
return event.tags.filter(([tagName, pubkey]) => tagName === 'p' && isValidId(pubkey));
|
||||
},
|
||||
taggedEvents(): TaggedEvent[] {
|
||||
eTags(): string[][] {
|
||||
return event.tags.filter(([tagName, eventId]) => tagName === 'e' && isValidId(eventId));
|
||||
},
|
||||
taggedEventIds(): string[] {
|
||||
return this.eTags().map(([, eventId]) => eventId);
|
||||
},
|
||||
lastTaggedEventId(): string | undefined {
|
||||
// for compatibility. some clients include additional event ids for reaction (kind:7).
|
||||
const ids = this.taggedEventIds();
|
||||
if (ids.length === 0) return undefined;
|
||||
return ids[ids.length - 1];
|
||||
},
|
||||
markedEventTags(): MarkedEventTag[] {
|
||||
if (event.kind !== Kind.Text) throw new Error('kind should be 1');
|
||||
|
||||
if (memoizedMarkedEventTags != null) return memoizedMarkedEventTags;
|
||||
|
||||
// 'eTags' cannot be used here because it does not preserve originalIndex.
|
||||
const events = event.tags
|
||||
.map((tag, originalIndex) => [tag, originalIndex] as const)
|
||||
.filter(([[tagName]]) => tagName === 'e');
|
||||
.filter(([[tagName, eventId]]) => tagName === 'e' && isValidId(eventId));
|
||||
|
||||
// NIP-10: Positional "e" tags (DEPRECATED)
|
||||
const positionToMarker = (marker: string, index: number): EventMarker | undefined => {
|
||||
@@ -68,24 +97,28 @@ const eventWrapper = (event: NostrEvent) => {
|
||||
return 'mention';
|
||||
};
|
||||
|
||||
return events.map(([[, eventId, relayUrl, marker], originalIndex], eTagIndex) => ({
|
||||
id: eventId,
|
||||
relayUrl,
|
||||
marker: positionToMarker(marker, eTagIndex),
|
||||
index: originalIndex,
|
||||
}));
|
||||
memoizedMarkedEventTags = events.map(
|
||||
([[, eventId, relayUrl, marker], originalIndex], eTagIndex) => ({
|
||||
id: eventId,
|
||||
relayUrl,
|
||||
marker: positionToMarker(marker, eTagIndex),
|
||||
index: originalIndex,
|
||||
}),
|
||||
);
|
||||
|
||||
return memoizedMarkedEventTags;
|
||||
},
|
||||
replyingToEvent(): TaggedEvent | undefined {
|
||||
return this.taggedEvents().find(({ marker }) => marker === 'reply');
|
||||
replyingToEvent(): MarkedEventTag | undefined {
|
||||
return this.markedEventTags().find(({ marker }) => marker === 'reply');
|
||||
},
|
||||
rootEvent(): TaggedEvent | undefined {
|
||||
return this.taggedEvents().find(({ marker }) => marker === 'root');
|
||||
rootEvent(): MarkedEventTag | undefined {
|
||||
return this.markedEventTags().find(({ marker }) => marker === 'root');
|
||||
},
|
||||
mentionedEvents(): TaggedEvent[] {
|
||||
return this.taggedEvents().filter(({ marker }) => marker === 'mention');
|
||||
mentionedEvents(): MarkedEventTag[] {
|
||||
return this.markedEventTags().filter(({ marker }) => marker === 'mention');
|
||||
},
|
||||
mentionedPubkeys(): string[] {
|
||||
return uniq(event.tags.filter(([tagName]) => tagName === 'p').map((e) => e[1]));
|
||||
return uniq(this.pTags().map(([, pubkey]) => pubkey));
|
||||
},
|
||||
mentionedPubkeysWithoutAuthor(): string[] {
|
||||
return this.mentionedPubkeys().filter((pubkey) => pubkey !== event.pubkey);
|
||||
|
||||
@@ -231,7 +231,7 @@ describe('resolveTagReference', () => {
|
||||
content: '',
|
||||
tags: [
|
||||
['p', '80d3a41d8a00679c0105faac2cdf7643c9ba26835cff096bf7f9c7a0eee8c8fc'],
|
||||
['e', 'b9cefcb857fa487d5794156e85b30a7f98cb21721040631210262091d86ff6f212', '', 'reply'],
|
||||
['e', 'b9cefcb857fa487d5794156e85b30a7f98cb21721040631210262091d86ff6f2', '', 'reply'],
|
||||
],
|
||||
created_at: 1678377182,
|
||||
pubkey: '9366708117c4a7edf9178acdce538c95059b9eb3394808cdd90564094172d972',
|
||||
@@ -242,7 +242,7 @@ describe('resolveTagReference', () => {
|
||||
tagIndex: 1,
|
||||
marker: 'reply',
|
||||
content: '#[1]',
|
||||
eventId: 'b9cefcb857fa487d5794156e85b30a7f98cb21721040631210262091d86ff6f212',
|
||||
eventId: 'b9cefcb857fa487d5794156e85b30a7f98cb21721040631210262091d86ff6f2',
|
||||
};
|
||||
assert.deepStrictEqual(result, expected);
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { nip19, type Event as NostrEvent } from 'nostr-tools';
|
||||
|
||||
import eventWrapper from '@/nostr/event';
|
||||
import eventWrapper, { isValidId } from '@/nostr/event';
|
||||
|
||||
type ProfilePointer = nip19.ProfilePointer;
|
||||
type EventPointer = nip19.EventPointer;
|
||||
@@ -125,7 +125,7 @@ const parseTextNote = (textNoteContent: string) => {
|
||||
};
|
||||
result.push(bech32Entity);
|
||||
} catch (e) {
|
||||
console.warn(`failed to parse Bech32 entity (NIP-19): ${match[0]}`);
|
||||
console.warn(`ignored invalid bech32 entity: ${match[0]}`);
|
||||
pushPlainText(index + match[0].length);
|
||||
}
|
||||
} else if (match.groups?.hashtag) {
|
||||
@@ -157,7 +157,7 @@ export const resolveTagReference = (
|
||||
|
||||
const tagName = tag[0];
|
||||
|
||||
if (tagName === 'p') {
|
||||
if (tagName === 'p' && isValidId(tag[1])) {
|
||||
return {
|
||||
type: 'MentionedUser',
|
||||
tagIndex,
|
||||
@@ -166,9 +166,9 @@ export const resolveTagReference = (
|
||||
} satisfies MentionedUser;
|
||||
}
|
||||
|
||||
if (tagName === 'e') {
|
||||
if (tagName === 'e' && isValidId(tag[1])) {
|
||||
const mention = eventWrapper(event)
|
||||
.taggedEvents()
|
||||
.markedEventTags()
|
||||
.find((ev) => ev.index === tagIndex);
|
||||
|
||||
return {
|
||||
|
||||
@@ -237,24 +237,23 @@ const { exec } = useBatch<TaskArg, TaskRes>(() => ({
|
||||
const registeredTasks = textNoteTasks.get(event.id) ?? [];
|
||||
resolveTasks(registeredTasks, event);
|
||||
} else if (event.kind === Kind.Reaction) {
|
||||
const eventTags = eventWrapper(event).taggedEvents();
|
||||
eventTags.forEach((eventTag) => {
|
||||
const taggedEventId = eventTag.id;
|
||||
const registeredTasks = reactionsTasks.get(taggedEventId) ?? [];
|
||||
// Use the last event id
|
||||
const id = eventWrapper(event).lastTaggedEventId();
|
||||
if (id != null) {
|
||||
const registeredTasks = reactionsTasks.get(id) ?? [];
|
||||
resolveTasks(registeredTasks, event);
|
||||
});
|
||||
}
|
||||
} else if ((event.kind as number) === 6) {
|
||||
const eventTags = eventWrapper(event).taggedEvents();
|
||||
eventTags.forEach((eventTag) => {
|
||||
const taggedEventId = eventTag.id;
|
||||
const registeredTasks = repostsTasks.get(taggedEventId) ?? [];
|
||||
// Use the last event id
|
||||
const id = eventWrapper(event).lastTaggedEventId();
|
||||
if (id != null) {
|
||||
const registeredTasks = repostsTasks.get(id) ?? [];
|
||||
resolveTasks(registeredTasks, event);
|
||||
});
|
||||
}
|
||||
} else if (event.kind === Kind.Zap) {
|
||||
const eventTags = eventWrapper(event).taggedEvents();
|
||||
eventTags.forEach((eventTag) => {
|
||||
const taggedEventId = eventTag.id;
|
||||
const registeredTasks = repostsTasks.get(taggedEventId) ?? [];
|
||||
const eTags = eventWrapper(event).eTags();
|
||||
eTags.forEach(([, id]) => {
|
||||
const registeredTasks = repostsTasks.get(id) ?? [];
|
||||
resolveTasks(registeredTasks, event);
|
||||
});
|
||||
} else if (event.kind === Kind.Contacts) {
|
||||
@@ -298,7 +297,7 @@ export const useProfile = (propsProvider: () => UseProfileProps | null): UseProf
|
||||
try {
|
||||
queryClient.setQueryData(queryKey, latestEvent());
|
||||
} catch (err) {
|
||||
console.error('updating profile error: ', err);
|
||||
console.error('error occurred while updating profile cache: ', err);
|
||||
}
|
||||
});
|
||||
return latestEvent();
|
||||
@@ -472,7 +471,7 @@ export const useFollowings = (propsProvider: () => UseFollowingsProps | null): U
|
||||
try {
|
||||
queryClient.setQueryData(queryKey, latestEvent());
|
||||
} catch (err) {
|
||||
console.error('updating followings error: ', err);
|
||||
console.error('error occurred while updating followings cache: ', err);
|
||||
}
|
||||
});
|
||||
return latestEvent();
|
||||
@@ -491,14 +490,12 @@ export const useFollowings = (propsProvider: () => UseFollowingsProps | null): U
|
||||
const followings = () => {
|
||||
if (query.data == null) return [];
|
||||
|
||||
const event = query.data;
|
||||
|
||||
const result: Following[] = [];
|
||||
event.tags.forEach((tag) => {
|
||||
// TODO zodにする
|
||||
const [tagName, followingPubkey, mainRelayUrl, petname] = tag;
|
||||
if (!tag.every((e) => typeof e === 'string')) return;
|
||||
if (tagName !== 'p') return;
|
||||
|
||||
// TODO zodにする
|
||||
const event = eventWrapper(query.data);
|
||||
event.pTags().forEach((tag) => {
|
||||
const [, followingPubkey, mainRelayUrl, petname] = tag;
|
||||
|
||||
const following: Following = { pubkey: followingPubkey, petname };
|
||||
if (mainRelayUrl != null && mainRelayUrl.length > 0) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { createEffect, onMount, type Component } from 'solid-js';
|
||||
import { createEffect, onMount, type Component, onError } from 'solid-js';
|
||||
|
||||
import { useNavigate } from '@solidjs/router';
|
||||
|
||||
@@ -44,6 +44,10 @@ const Home: Component = () => {
|
||||
}
|
||||
});
|
||||
|
||||
onError((err) => {
|
||||
console.error('uncaught error: ', err);
|
||||
});
|
||||
|
||||
return (
|
||||
<div class="absolute inset-0 flex w-screen touch-manipulation flex-row overflow-hidden">
|
||||
<SideBar />
|
||||
|
||||
Reference in New Issue
Block a user