mirror of
https://github.com/aljazceru/rabbit.git
synced 2025-12-18 14:34:25 +01:00
feat: enable to copy event info
This commit is contained in:
@@ -42,7 +42,7 @@ const Column: Component<ColumnProps> = (props) => {
|
||||
<TimelineContext.Provider value={timelineState}>
|
||||
<div
|
||||
ref={columnDivRef}
|
||||
class="relative flex w-[80vw] shrink-0 snap-center snap-always flex-col border-r sm:snap-align-none"
|
||||
class="flex w-[80vw] shrink-0 snap-center snap-always flex-col border-r sm:snap-align-none"
|
||||
classList={{
|
||||
'sm:w-[500px]': width() === 'widest',
|
||||
'sm:w-[350px]': width() === 'wide',
|
||||
@@ -50,14 +50,21 @@ const Column: Component<ColumnProps> = (props) => {
|
||||
'sm:w-[270px]': width() === 'narrow',
|
||||
}}
|
||||
>
|
||||
<div class="flex h-8 shrink-0 items-center border-b bg-white px-2">
|
||||
{/* <span class="column-icon">🏠</span> */}
|
||||
<span class="column-name">{props.name}</span>
|
||||
</div>
|
||||
<div class="flex flex-col overflow-y-scroll scroll-smooth">{props.children}</div>
|
||||
<Show when={timelineState.timelineState.content} keyed>
|
||||
<Show
|
||||
when={timelineState.timelineState.content}
|
||||
keyed
|
||||
fallback={
|
||||
<>
|
||||
<div class="flex h-8 shrink-0 items-center border-b bg-white px-2">
|
||||
{/* <span class="column-icon">🏠</span> */}
|
||||
<span class="column-name">{props.name}</span>
|
||||
</div>
|
||||
<div class="flex flex-col overflow-y-scroll scroll-smooth">{props.children}</div>
|
||||
</>
|
||||
}
|
||||
>
|
||||
{(timeline) => (
|
||||
<div class="absolute h-full w-full bg-white">
|
||||
<div class="h-full w-full bg-white">
|
||||
<div class="flex h-8 shrink-0 items-center border-b bg-white px-2">
|
||||
<button
|
||||
class="flex w-full items-center gap-1"
|
||||
|
||||
100
src/components/ContextMenu.tsx
Normal file
100
src/components/ContextMenu.tsx
Normal file
@@ -0,0 +1,100 @@
|
||||
import { createSignal, onCleanup, createEffect, For, type Component, type JSX } from 'solid-js';
|
||||
|
||||
export type MenuItem = {
|
||||
content: () => JSX.Element;
|
||||
onSelect?: () => void;
|
||||
};
|
||||
|
||||
export type ContextMenuProps = {
|
||||
menu: MenuItem[];
|
||||
children: JSX.Element;
|
||||
};
|
||||
|
||||
export type MenuDisplayProps = {
|
||||
menu: MenuItem[];
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
export type MenuItemDisplayProps = {
|
||||
item: MenuItem;
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
const MenuItemDisplay: Component<MenuItemDisplayProps> = (props) => {
|
||||
const handleClick = () => {
|
||||
props.item?.onSelect?.();
|
||||
props.onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<li class="border-b hover:bg-stone-200">
|
||||
<button class="px-4 py-1" onClick={handleClick}>
|
||||
{props.item.content()}
|
||||
</button>
|
||||
</li>
|
||||
);
|
||||
};
|
||||
|
||||
const MenuDisplay: Component<MenuDisplayProps> = (props) => {
|
||||
return (
|
||||
<ul>
|
||||
<For each={props.menu}>
|
||||
{(item) => <MenuItemDisplay item={item} onClose={props.onClose} />}
|
||||
</For>
|
||||
</ul>
|
||||
);
|
||||
};
|
||||
|
||||
const ContextMenu: Component<ContextMenuProps> = (props) => {
|
||||
let menuRef: HTMLDivElement | 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 handleClick: JSX.EventHandler<HTMLButtonElement, MouseEvent> = (ev) => {
|
||||
if (menuRef == null) return;
|
||||
|
||||
const buttonRect = ev.target.getBoundingClientRect();
|
||||
menuRef.style.left = `${buttonRect.left}px`;
|
||||
menuRef.style.top = `${buttonRect.top + buttonRect.height}px`;
|
||||
|
||||
setIsOpen(true);
|
||||
};
|
||||
|
||||
createEffect(() => {
|
||||
if (isOpen()) {
|
||||
addClickOutsideHandler();
|
||||
} else {
|
||||
removeClickOutsideHandler();
|
||||
}
|
||||
});
|
||||
|
||||
onCleanup(() => removeClickOutsideHandler());
|
||||
|
||||
return (
|
||||
<div>
|
||||
<button onClick={handleClick}>{props.children}</button>
|
||||
<div
|
||||
ref={menuRef}
|
||||
class="absolute z-20 min-w-[48px] rounded border bg-white shadow-md"
|
||||
classList={{ hidden: !isOpen(), block: isOpen() }}
|
||||
>
|
||||
<MenuDisplay menu={props.menu} onClose={() => setIsOpen(false)} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ContextMenu;
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Show, For, createSignal, createMemo, onMount, type JSX, type Component } from 'solid-js';
|
||||
import type { Event as NostrEvent } from 'nostr-tools';
|
||||
import { nip19, type Event as NostrEvent } from 'nostr-tools';
|
||||
import { createMutation } from '@tanstack/solid-query';
|
||||
|
||||
import HeartOutlined from 'heroicons/24/outline/heart.svg';
|
||||
@@ -31,6 +31,7 @@ import NotePostForm from '@/components/NotePostForm';
|
||||
import ensureNonNull from '@/utils/ensureNonNull';
|
||||
import npubEncodeFallback from '@/utils/npubEncodeFallback';
|
||||
import useSubscription from '@/nostr/useSubscription';
|
||||
import ContextMenu, { MenuItem } from '../ContextMenu';
|
||||
|
||||
export type TextNoteDisplayProps = {
|
||||
event: NostrEvent;
|
||||
@@ -38,9 +39,26 @@ export type TextNoteDisplayProps = {
|
||||
actions?: boolean;
|
||||
};
|
||||
|
||||
const { noteEncode } = nip19;
|
||||
|
||||
const TextNoteDisplay: Component<TextNoteDisplayProps> = (props) => {
|
||||
let contentRef: HTMLDivElement | undefined;
|
||||
|
||||
const menu: MenuItem[] = [
|
||||
{
|
||||
content: () => 'IDをコピー',
|
||||
onSelect: () => {
|
||||
navigator.clipboard.writeText(noteEncode(props.event.id)).catch((err) => window.alert(err));
|
||||
},
|
||||
},
|
||||
{
|
||||
content: () => 'JSONとしてコピー',
|
||||
onSelect: () => {
|
||||
navigator.clipboard.writeText(JSON.stringify(props.event)).catch((err) => window.alert(err));
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const { config } = useConfig();
|
||||
const formatDate = useFormatDate();
|
||||
const pubkey = usePubkey();
|
||||
@@ -51,7 +69,6 @@ const TextNoteDisplay: Component<TextNoteDisplayProps> = (props) => {
|
||||
const closeReplyForm = () => setShowReplyForm(false);
|
||||
const [showOverflow, setShowOverflow] = createSignal(false);
|
||||
const [overflow, setOverflow] = createSignal(false);
|
||||
const [showMenu, setShowMenu] = createSignal(false);
|
||||
|
||||
const event = createMemo(() => eventWrapper(props.event));
|
||||
|
||||
@@ -88,13 +105,11 @@ const TextNoteDisplay: Component<TextNoteDisplayProps> = (props) => {
|
||||
mutationKey: ['publishDeprecatedRepost', event().id],
|
||||
mutationFn: commands.publishDeprecatedRepost.bind(commands),
|
||||
onSuccess: () => {
|
||||
console.log('succeeded to publish deprecated reposts');
|
||||
invalidateDeprecatedReposts().catch((err) =>
|
||||
console.error('failed to refetch deprecated reposts', err),
|
||||
);
|
||||
console.log('succeeded to publish reposts');
|
||||
invalidateDeprecatedReposts().catch((err) => console.error('failed to refetch reposts', err));
|
||||
},
|
||||
onError: (err) => {
|
||||
console.error('failed to publish deprecated repost: ', err);
|
||||
console.error('failed to publish repost: ', err);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -205,10 +220,12 @@ const TextNoteDisplay: Component<TextNoteDisplayProps> = (props) => {
|
||||
</div>
|
||||
</button>
|
||||
<div class="created-at shrink-0">
|
||||
<button
|
||||
<a
|
||||
href={`nostr:${noteEncode(event().id)}`}
|
||||
type="button"
|
||||
class="hover:underline"
|
||||
onClick={() => {
|
||||
onClick={(ev) => {
|
||||
ev.preventDefault();
|
||||
timelineContext?.setTimeline({
|
||||
type: 'Replies',
|
||||
event: props.event,
|
||||
@@ -216,7 +233,7 @@ const TextNoteDisplay: Component<TextNoteDisplayProps> = (props) => {
|
||||
}}
|
||||
>
|
||||
{createdAt()}
|
||||
</button>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
@@ -318,15 +335,11 @@ const TextNoteDisplay: Component<TextNoteDisplayProps> = (props) => {
|
||||
</Show>
|
||||
</div>
|
||||
<div>
|
||||
<button
|
||||
class="h-4 w-4 text-zinc-400"
|
||||
onClick={(ev) => {
|
||||
ev.stopPropagation();
|
||||
setShowMenu((current) => !current);
|
||||
}}
|
||||
>
|
||||
<EllipsisHorizontal />
|
||||
</button>
|
||||
<ContextMenu menu={menu}>
|
||||
<span class="inline-block h-4 w-4 text-zinc-400">
|
||||
<EllipsisHorizontal />
|
||||
</span>
|
||||
</ContextMenu>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
@@ -20,7 +20,7 @@ const eventWrapper = (event: NostrEvent) => {
|
||||
get rawEvent(): NostrEvent {
|
||||
return event;
|
||||
},
|
||||
get id(): string | undefined {
|
||||
get id(): string {
|
||||
return event.id;
|
||||
},
|
||||
get pubkey(): string {
|
||||
|
||||
Reference in New Issue
Block a user