mirror of
https://github.com/aljazceru/rabbit.git
synced 2025-12-18 22:44:26 +01:00
enhance batching
This commit is contained in:
@@ -47,7 +47,7 @@ const Column: Component<ColumnProps> = (props) => {
|
||||
{/* <span class="column-icon">🏠</span> */}
|
||||
<span class="column-name">{props.name}</span>
|
||||
</div>
|
||||
<ul class="block flex flex-col overflow-y-scroll scroll-smooth">{props.children}</ul>
|
||||
<ul class="flex flex-col overflow-y-scroll scroll-smooth">{props.children}</ul>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import useConfig, { type Config } from '@/nostr/useConfig';
|
||||
import { createSignal, For, type JSX } from 'solid-js';
|
||||
import XMark from 'heroicons/24/outline/x-mark.svg';
|
||||
|
||||
import Modal from '@/components/Modal';
|
||||
|
||||
type ConfigProps = {
|
||||
onClose: () => void;
|
||||
@@ -24,9 +27,11 @@ const RelayConfig = () => {
|
||||
<For each={config().relayUrls}>
|
||||
{(relayUrl: string) => {
|
||||
return (
|
||||
<li class="flex">
|
||||
<div class="flex-1">{relayUrl}</div>
|
||||
<button onClick={() => removeRelay(relayUrl)}>x</button>
|
||||
<li class="flex items-center">
|
||||
<div class="flex-1 truncate">{relayUrl}</div>
|
||||
<button class="h-3 w-3 shrink-0" onClick={() => removeRelay(relayUrl)}>
|
||||
<XMark />
|
||||
</button>
|
||||
</li>
|
||||
);
|
||||
}}
|
||||
@@ -154,33 +159,22 @@ const OtherConfig = () => {
|
||||
};
|
||||
|
||||
const ConfigUI = (props: ConfigProps) => {
|
||||
let containerRef: HTMLDivElement | undefined;
|
||||
|
||||
const handleClickContainer: JSX.EventHandler<HTMLDivElement, MouseEvent> = (ev) => {
|
||||
if (ev.target === containerRef) {
|
||||
props.onClose();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
class="absolute top-0 left-0 flex h-screen w-screen cursor-default place-content-center place-items-center bg-black/25"
|
||||
role="button"
|
||||
onClick={handleClickContainer}
|
||||
>
|
||||
<Modal title="設定" onClose={props.onClose}>
|
||||
<div class="max-h-[90vh] w-[640px] max-w-[100vw] overflow-y-scroll rounded bg-white p-4 shadow">
|
||||
<div class="relative">
|
||||
<h2 class="flex-1 text-center font-bold">設定</h2>
|
||||
<button class="absolute top-1 right-0" onClick={() => props.onClose()}>
|
||||
X
|
||||
</button>
|
||||
<div class="flex flex-col gap-1">
|
||||
<h2 class="flex-1 text-center font-bold">設定</h2>
|
||||
<button class="absolute top-1 right-0 h-4 w-4" onClick={() => props.onClose?.()}>
|
||||
<XMark />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<RelayConfig />
|
||||
<DateFormatConfig />
|
||||
<OtherConfig />
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
28
src/components/Modal.tsx
Normal file
28
src/components/Modal.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
import { type Component, type JSX } from 'solid-js';
|
||||
|
||||
export type ModalProps = {
|
||||
onClose?: () => void;
|
||||
children?: JSX.Element;
|
||||
};
|
||||
|
||||
const Modal: Component<ModalProps> = (props) => {
|
||||
let containerRef: HTMLDivElement | undefined;
|
||||
|
||||
const handleClickContainer: JSX.EventHandler<HTMLDivElement, MouseEvent> = (ev) => {
|
||||
if (ev.target === containerRef) {
|
||||
props.onClose?.();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
class="absolute top-0 left-0 flex h-screen w-screen cursor-default place-content-center place-items-center bg-black/25"
|
||||
onClick={handleClickContainer}
|
||||
>
|
||||
{props.children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Modal;
|
||||
@@ -14,8 +14,6 @@ import uniq from 'lodash/uniq';
|
||||
|
||||
import PaperAirplane from 'heroicons/24/solid/paper-airplane.svg';
|
||||
import Photo from 'heroicons/24/outline/photo.svg';
|
||||
import Eye from 'heroicons/24/solid/eye.svg';
|
||||
import EyeSlash from 'heroicons/24/outline/eye-slash.svg';
|
||||
import XMark from 'heroicons/24/outline/x-mark.svg';
|
||||
|
||||
import UserNameDisplay from '@/components/UserDisplayName';
|
||||
@@ -61,6 +59,11 @@ const NotePostForm: Component<NotePostFormProps> = (props) => {
|
||||
setContentWarning(false);
|
||||
};
|
||||
|
||||
const close = () => {
|
||||
textAreaRef?.blur();
|
||||
props.onClose();
|
||||
};
|
||||
|
||||
const { config } = useConfig();
|
||||
const getPubkey = usePubkey();
|
||||
const commands = useCommands();
|
||||
@@ -160,7 +163,7 @@ const NotePostForm: Component<NotePostFormProps> = (props) => {
|
||||
submit();
|
||||
} else if (ev.key === 'Escape') {
|
||||
textAreaRef?.blur();
|
||||
props.onClose();
|
||||
close();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -168,6 +171,7 @@ const NotePostForm: Component<NotePostFormProps> = (props) => {
|
||||
ev.preventDefault();
|
||||
const files = [...(ev.currentTarget.files ?? [])];
|
||||
uploadFilesMutation.mutate(files);
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
ev.currentTarget.value = '';
|
||||
};
|
||||
|
||||
@@ -184,7 +188,6 @@ const NotePostForm: Component<NotePostFormProps> = (props) => {
|
||||
|
||||
const submitDisabled = () =>
|
||||
text().trim().length === 0 ||
|
||||
(contentWarning() && contentWarningReason().length === 0) ||
|
||||
publishTextNoteMutation.isLoading ||
|
||||
uploadFilesMutation.isLoading;
|
||||
|
||||
@@ -192,6 +195,7 @@ const NotePostForm: Component<NotePostFormProps> = (props) => {
|
||||
|
||||
onMount(() => {
|
||||
setTimeout(() => {
|
||||
textAreaRef?.click();
|
||||
textAreaRef?.focus();
|
||||
}, 50);
|
||||
});
|
||||
@@ -239,7 +243,7 @@ const NotePostForm: Component<NotePostFormProps> = (props) => {
|
||||
<div class="flex items-end justify-end gap-1">
|
||||
<Show when={mode() === 'reply'}>
|
||||
<div class="flex-1">
|
||||
<button class="h-5 w-5 text-stone-500" onClick={() => props.onClose()}>
|
||||
<button class="h-5 w-5 text-stone-500" onClick={() => close()}>
|
||||
<XMark />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
83
src/components/Profile.tsx
Normal file
83
src/components/Profile.tsx
Normal file
@@ -0,0 +1,83 @@
|
||||
import { Component, createMemo, Show } from 'solid-js';
|
||||
import { npubEncode } from 'nostr-tools/nip19';
|
||||
|
||||
import GlobeAlt from 'heroicons/24/outline/globe-alt.svg';
|
||||
import XMark from 'heroicons/24/outline/x-mark.svg';
|
||||
|
||||
import Modal from '@/components/Modal';
|
||||
import Copy from '@/components/utils/Copy';
|
||||
|
||||
import useProfile from '@/nostr/useProfile';
|
||||
import useConfig from '@/nostr/useConfig';
|
||||
|
||||
export type ProfileDisplayProps = {
|
||||
pubkey: string;
|
||||
};
|
||||
|
||||
const ProfileDisplay: Component<ProfileDisplayProps> = (props) => {
|
||||
const { config } = useConfig();
|
||||
const { profile, query } = useProfile(() => ({
|
||||
relayUrls: config().relayUrls,
|
||||
pubkey: props.pubkey,
|
||||
}));
|
||||
|
||||
const npub = createMemo(() => npubEncode(props.pubkey));
|
||||
|
||||
return (
|
||||
<Modal>
|
||||
<div class="max-h-full w-[640px] max-w-full overflow-scroll">
|
||||
<div class="flex justify-end">
|
||||
<button class="h-8 w-8 text-stone-700">
|
||||
<XMark />
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex w-full flex-col overflow-hidden rounded-2xl border bg-white text-stone-700 shadow-lg">
|
||||
<Show when={query.isFetched} fallback={<>loading</>}>
|
||||
<div class="h-40 w-full sm:h-52">
|
||||
<Show when={profile()?.banner} keyed>
|
||||
{(bannerUrl) => (
|
||||
<img src={bannerUrl} alt="header" class="h-full w-full object-cover" />
|
||||
)}
|
||||
</Show>
|
||||
</div>
|
||||
<div class="flex h-[64px] items-center gap-2 px-2">
|
||||
<div class="mt-[-64px] h-28 w-28 shrink-0 rounded-lg border-2 object-cover">
|
||||
<Show when={profile()?.picture} keyed>
|
||||
{(pictureUrl) => <img src={pictureUrl} alt="user icon" class="h-full w-full" />}
|
||||
</Show>
|
||||
</div>
|
||||
<div>
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="truncate text-xl font-bold">{profile()?.display_name}</div>
|
||||
<div class="shrink-0 text-sm">@{profile()?.name}</div>
|
||||
</div>
|
||||
<div class="flex gap-1">
|
||||
<div class="truncate text-xs">{npub()}</div>
|
||||
<Copy class="h-4 w-4 text-stone-500 hover:text-stone-700" text={npub()} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="max-h-32 overflow-scroll whitespace-pre-wrap px-4 pt-1 text-sm">
|
||||
{profile()?.about}
|
||||
</div>
|
||||
<ul class="px-4 py-2 text-xs">
|
||||
<Show when={profile()?.website}>
|
||||
<li class="flex items-center gap-1">
|
||||
<span class="inline-block h-4 w-4" area-label="website" title="website">
|
||||
<GlobeAlt />
|
||||
</span>
|
||||
<a href={profile()?.website} target="_blank" rel="noreferrer noopener">
|
||||
{profile()?.website}
|
||||
</a>
|
||||
</li>
|
||||
</Show>
|
||||
</ul>
|
||||
</Show>
|
||||
<div class="h-16 border" />
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProfileDisplay;
|
||||
@@ -16,14 +16,21 @@ const SideBar: Component = () => {
|
||||
const [formOpened, setFormOpened] = createSignal(false);
|
||||
const [configOpened, setConfigOpened] = createSignal(false);
|
||||
|
||||
const focusTextArea = () => {
|
||||
textAreaRef?.focus();
|
||||
textAreaRef?.click();
|
||||
};
|
||||
const openForm = () => setFormOpened(true);
|
||||
const closeForm = () => setFormOpened(false);
|
||||
const toggleForm = () => setFormOpened((current) => !current);
|
||||
|
||||
useHandleCommand(() => ({
|
||||
commandType: 'openPostForm',
|
||||
handler: () => {
|
||||
openForm();
|
||||
setTimeout(() => textAreaRef?.focus?.(), 100);
|
||||
if (textAreaRef != null) {
|
||||
setTimeout(() => focusTextArea(), 100);
|
||||
}
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -32,8 +39,8 @@ const SideBar: Component = () => {
|
||||
<div class="flex w-14 flex-auto flex-col items-center gap-3 border-r border-rose-200 pt-5">
|
||||
<div class="flex flex-col items-center gap-3">
|
||||
<button
|
||||
class={`h-9 w-9 rounded-full border border-primary bg-primary p-2 text-2xl font-bold text-white`}
|
||||
onClick={() => setFormOpened((current) => !current)}
|
||||
class="h-9 w-9 rounded-full border border-primary bg-primary p-2 text-2xl font-bold text-white"
|
||||
onClick={() => toggleForm()}
|
||||
>
|
||||
<PencilSquare />
|
||||
</button>
|
||||
@@ -55,14 +62,19 @@ const SideBar: Component = () => {
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<Show when={formOpened() || config().keepOpenPostForm}>
|
||||
<div
|
||||
classList={{
|
||||
static: formOpened() || config().keepOpenPostForm,
|
||||
hidden: !(formOpened() || config().keepOpenPostForm),
|
||||
}}
|
||||
>
|
||||
<NotePostForm
|
||||
textAreaRef={(el) => {
|
||||
textAreaRef = el;
|
||||
}}
|
||||
onClose={closeForm}
|
||||
/>
|
||||
</Show>
|
||||
</div>
|
||||
<Show when={configOpened()}>
|
||||
<Config onClose={() => setConfigOpened(false)} />
|
||||
</Show>
|
||||
|
||||
42
src/components/utils/Copy.tsx
Normal file
42
src/components/utils/Copy.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
import { createSignal, Show, type Component } from 'solid-js';
|
||||
|
||||
import ClipboardDocument from 'heroicons/24/outline/clipboard-document.svg';
|
||||
|
||||
type CopyProps = {
|
||||
class: string;
|
||||
text: string;
|
||||
};
|
||||
|
||||
const Copy: Component<CopyProps> = (props) => {
|
||||
const [showPopup, setShowPopup] = createSignal(false);
|
||||
|
||||
const handleClick = () => {
|
||||
navigator.clipboard
|
||||
.writeText(props.text)
|
||||
.then((e) => {
|
||||
setShowPopup(true);
|
||||
setTimeout(() => setShowPopup(false), 1000);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error('failed to copy', err);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div class="relative inline-block">
|
||||
<button type="button" class={props.class} onClick={handleClick}>
|
||||
<ClipboardDocument />
|
||||
</button>
|
||||
<Show when={showPopup()}>
|
||||
<div
|
||||
class="absolute left-[-1rem] top-[-1.5rem] rounded-lg
|
||||
bg-rose-300 p-1 text-xs font-bold text-white shadow"
|
||||
>
|
||||
Copied!
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Copy;
|
||||
Reference in New Issue
Block a user