mirror of
https://github.com/aljazceru/rabbit.git
synced 2025-12-18 14:34:25 +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>
|
||||
|
||||
Reference in New Issue
Block a user