fix: ignore invalid event id and invalid pubkey

This commit is contained in:
Shusui MOYATANI
2023-05-14 03:26:25 +09:00
parent dd64ef1724
commit 1b15989faf
11 changed files with 258 additions and 199 deletions

View File

@@ -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>
);
};

View File

@@ -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}

View File

@@ -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} />}

View File

@@ -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">

View File

@@ -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>