-
{i18n()('config.profile.profile')}
-
+
+
-
+
);
};
@@ -98,23 +147,10 @@ const RelayConfig = () => {
return (
<>
-
-
{i18n()('config.relays.relays')}
+
{i18n()('config.relays.numOfRelays', { count: config().relayUrls.length })}
-
-
- {(relayUrl: string) => (
- -
-
{relayUrl}
-
-
- )}
-
-
-
-
-
{i18n()('config.relays.importRelays')}
+
+
+ {(relayUrl: string) => (
+ -
+
{relayUrl}
+
+
+ )}
+
+
+
+
-
+
>
);
};
@@ -166,14 +213,13 @@ const ColorThemeConfig = () => {
};
return (
-
-
{i18n()('config.display.colorTheme')}
+
{(colorTheme) => (
-
+
);
};
@@ -221,8 +267,7 @@ const DateFormatConfig = () => {
};
return (
-
-
{i18n()('config.display.timeNotation')}
+
{({ id, name, example }) => (
@@ -245,29 +290,10 @@ const DateFormatConfig = () => {
)}
-
+
);
};
-const ToggleButton = (props: {
- value: boolean;
- onClick: JSX.EventHandler
;
-}) => (
-
-);
-
const ReactionConfig = () => {
const i18n = useTranslation();
const { config, setConfig } = useConfig();
@@ -287,8 +313,7 @@ const ReactionConfig = () => {
};
return (
-
-
{i18n()('config.display.reaction')}
+
{i18n()('config.display.enableEmojiReaction')}
@@ -305,7 +330,7 @@ const ReactionConfig = () => {
/>
-
+
);
};
@@ -325,21 +350,7 @@ const EmojiConfig = () => {
};
return (
-
-
{i18n()('config.customEmoji.customEmoji')}
-
-
- {({ shortcode, url }) => (
- -
-
- {shortcode}
-
-
- )}
-
-
+
+
+
);
};
@@ -400,8 +454,7 @@ const EmojiImport = () => {
};
return (
-
-
{i18n()('config.customEmoji.emojiImport')}
+
{i18n()('config.customEmoji.emojiImportDescription')}
-
+
);
};
@@ -437,14 +490,13 @@ const MuteConfig = () => {
return (
<>
-
-
{i18n()('config.mute.mutedUsers')}
-
-
-
{i18n()('config.mute.mutedKeywords')}
-
-
- {(keyword) => (
- -
-
{keyword}
-
-
- )}
-
-
+
+
+
+
>
);
};
@@ -500,8 +551,7 @@ const EmbeddingConfig = () => {
};
return (
-
-
{i18n()('config.display.embedding')}
+
{i18n()('config.display.embeddingDescription')}
@@ -517,7 +567,7 @@ const EmbeddingConfig = () => {
toggle('ogp')} />
-
+
);
};
@@ -547,8 +597,7 @@ const OtherConfig = () => {
};
return (
-
-
{i18n()('config.display.others')}
+
{i18n()('config.display.keepOpenPostForm')}
@@ -576,7 +625,7 @@ const OtherConfig = () => {
*/}
-
+
);
};
diff --git a/src/components/utils/usePopup.tsx b/src/components/utils/usePopup.tsx
new file mode 100644
index 0000000..4a05f98
--- /dev/null
+++ b/src/components/utils/usePopup.tsx
@@ -0,0 +1,112 @@
+import {
+ createSignal,
+ createEffect,
+ createMemo,
+ onCleanup,
+ children,
+ Show,
+ type JSX,
+} from 'solid-js';
+
+import { Portal } from 'solid-js/web';
+
+export type UsePopupProps = {
+ target?: HTMLElement;
+ popup?: JSX.Element;
+ position?: 'left' | 'bottom' | 'right' | 'top';
+};
+
+type UsePopup = {
+ open: () => void;
+ close: () => void;
+ toggle: () => void;
+ popup: () => JSX.Element;
+};
+
+const usePopup = (propsProvider: () => UsePopupProps): UsePopup => {
+ const props = createMemo(propsProvider);
+
+ const [popupRef, setPopupRef] = createSignal();
+ const [style, setStyle] = createSignal({});
+ const [isOpen, setIsOpen] = createSignal(false);
+
+ const resolvedChildren = children(() => props().popup);
+
+ const open = () => setIsOpen(true);
+ const close = () => setIsOpen(false);
+ const toggle = () => setIsOpen((current) => !current);
+
+ const handleClickOutside = (ev: MouseEvent) => {
+ const target = ev.target as HTMLElement;
+ ev.preventDefault();
+ ev.stopImmediatePropagation();
+ if (target != null && !popupRef()?.contains(target)) {
+ close();
+ }
+ };
+
+ const addClickOutsideHandler = () => {
+ document.addEventListener('mousedown', handleClickOutside);
+ };
+ const removeClickOutsideHandler = () => {
+ document.removeEventListener('mousedown', handleClickOutside);
+ };
+
+ const adjustPosition = () => {
+ const { target } = props();
+ const popupElem = popupRef();
+ if (target == null || popupElem == null) return;
+
+ const targetRect = target.getBoundingClientRect();
+ const popupRect = popupElem.getBoundingClientRect();
+
+ let { top, left } = targetRect;
+
+ if (props().position === 'left') {
+ left -= targetRect.width;
+ } else if (props().position === 'right') {
+ left += targetRect.width;
+ } else if (props().position === 'top') {
+ top -= targetRect.height;
+ left -= targetRect.left + targetRect.width / 2;
+ } else {
+ top += targetRect.height;
+ left += targetRect.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()) {
+ addClickOutsideHandler();
+ adjustPosition();
+ } else {
+ removeClickOutsideHandler();
+ }
+ });
+
+ onCleanup(() => removeClickOutsideHandler());
+
+ const popup = () => (
+
+
+
+ {resolvedChildren()}
+
+
+
+ );
+
+ return {
+ open,
+ close,
+ toggle,
+ popup,
+ };
+};
+
+export default usePopup;
diff --git a/src/locales/en.ts b/src/locales/en.ts
index 96b92c6..80d64a2 100644
--- a/src/locales/en.ts
+++ b/src/locales/en.ts
@@ -165,6 +165,7 @@ export default {
shortcode: 'Name',
url: 'URL',
addEmoji: 'Add',
+ removeEmoji: 'Remove',
emojiImport: 'Emoji import',
emojiImportDescription: 'Paste a JSON where the keys are names and the values are image URLs',
importEmoji: 'Import',
diff --git a/src/locales/ja.ts b/src/locales/ja.ts
index 1f8abe7..9dbe8cb 100644
--- a/src/locales/ja.ts
+++ b/src/locales/ja.ts
@@ -161,6 +161,7 @@ export default {
shortcode: '名前',
url: 'URL',
addEmoji: '追加',
+ removeEmoji: '削除',
emojiImport: '絵文字のインポート',
emojiImportDescription:
'絵文字の名前をキー、画像のURLを値とするJSONを読み込むことができます。',