mirror of
https://github.com/aljazceru/rabbit.git
synced 2025-12-19 06:54:23 +01:00
feat: emoji autocomplete
This commit is contained in:
81
package-lock.json
generated
81
package-lock.json
generated
@@ -16,6 +16,8 @@
|
|||||||
"@tanstack/query-sync-storage-persister": "^4.29.19",
|
"@tanstack/query-sync-storage-persister": "^4.29.19",
|
||||||
"@tanstack/solid-query": "^4.29.19",
|
"@tanstack/solid-query": "^4.29.19",
|
||||||
"@tanstack/solid-virtual": "^3.0.0-beta.6",
|
"@tanstack/solid-virtual": "^3.0.0-beta.6",
|
||||||
|
"@textcomplete/core": "^0.1.12",
|
||||||
|
"@textcomplete/textarea": "^0.1.12",
|
||||||
"@thisbeyond/solid-dnd": "^0.7.4",
|
"@thisbeyond/solid-dnd": "^0.7.4",
|
||||||
"@types/lodash": "^4.14.195",
|
"@types/lodash": "^4.14.195",
|
||||||
"emoji-mart": "^5.5.2",
|
"emoji-mart": "^5.5.2",
|
||||||
@@ -1308,6 +1310,32 @@
|
|||||||
"url": "https://github.com/sponsors/tannerlinsley"
|
"url": "https://github.com/sponsors/tannerlinsley"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@textcomplete/core": {
|
||||||
|
"version": "0.1.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/@textcomplete/core/-/core-0.1.12.tgz",
|
||||||
|
"integrity": "sha512-37Q8Wic3IGpZHtknlJ/ODKMyvaBhVMM56Vl7aoBfno2Qq099fFqoCL0VzKhTn8qvTf1Z/8ymlHwPCBzPvW7KSQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"eventemitter3": "^4.0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@textcomplete/textarea": {
|
||||||
|
"version": "0.1.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/@textcomplete/textarea/-/textarea-0.1.12.tgz",
|
||||||
|
"integrity": "sha512-E05H4wXr1Q50CrCFBAHewyZqvQEX681V5zleDw/31tr8vl5PDFl6TyFmS1W0jQjlrQfxa5uVvgHCx+gpfICBDQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"@textcomplete/utils": "^0.1.11",
|
||||||
|
"textarea-caret": "^3.1.0",
|
||||||
|
"undate": "^0.3.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@textcomplete/core": "^0.1.9"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@textcomplete/utils": {
|
||||||
|
"version": "0.1.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/@textcomplete/utils/-/utils-0.1.12.tgz",
|
||||||
|
"integrity": "sha512-llHhD1FAVwFaaHzs7PU0BZYTpNLDzTccDWbw+5cj0TiB2NOXZGjPm6l7PJrJwN/yUuPDxOHip/3I+kF6OBkBAg=="
|
||||||
|
},
|
||||||
"node_modules/@thisbeyond/solid-dnd": {
|
"node_modules/@thisbeyond/solid-dnd": {
|
||||||
"version": "0.7.4",
|
"version": "0.7.4",
|
||||||
"resolved": "https://registry.npmjs.org/@thisbeyond/solid-dnd/-/solid-dnd-0.7.4.tgz",
|
"resolved": "https://registry.npmjs.org/@thisbeyond/solid-dnd/-/solid-dnd-0.7.4.tgz",
|
||||||
@@ -3476,6 +3504,11 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/eventemitter3": {
|
||||||
|
"version": "4.0.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz",
|
||||||
|
"integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw=="
|
||||||
|
},
|
||||||
"node_modules/execa": {
|
"node_modules/execa": {
|
||||||
"version": "7.1.1",
|
"version": "7.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/execa/-/execa-7.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/execa/-/execa-7.1.1.tgz",
|
||||||
@@ -6734,6 +6767,11 @@
|
|||||||
"integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=",
|
"integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/textarea-caret": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/textarea-caret/-/textarea-caret-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-cXAvzO9pP5CGa6NKx0WYHl+8CHKZs8byMkt3PCJBCmq2a34YA9pO1NrQET5pzeqnBjBdToF5No4rrmkDUgQC2Q=="
|
||||||
|
},
|
||||||
"node_modules/thenify": {
|
"node_modules/thenify": {
|
||||||
"version": "3.3.1",
|
"version": "3.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz",
|
||||||
@@ -6962,6 +7000,11 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/undate": {
|
||||||
|
"version": "0.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/undate/-/undate-0.3.0.tgz",
|
||||||
|
"integrity": "sha512-ssH8QTNBY6B+2fRr3stSQ+9m2NT8qTaun3ExTx5ibzYQvP7yX4+BnX0McNxFCvh6S5ia/DYu6bsCKQx/U4nb/Q=="
|
||||||
|
},
|
||||||
"node_modules/update-browserslist-db": {
|
"node_modules/update-browserslist-db": {
|
||||||
"version": "1.0.10",
|
"version": "1.0.10",
|
||||||
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.10.tgz",
|
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.10.tgz",
|
||||||
@@ -8228,6 +8271,29 @@
|
|||||||
"@reach/observe-rect": "^1.1.0"
|
"@reach/observe-rect": "^1.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"@textcomplete/core": {
|
||||||
|
"version": "0.1.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/@textcomplete/core/-/core-0.1.12.tgz",
|
||||||
|
"integrity": "sha512-37Q8Wic3IGpZHtknlJ/ODKMyvaBhVMM56Vl7aoBfno2Qq099fFqoCL0VzKhTn8qvTf1Z/8ymlHwPCBzPvW7KSQ==",
|
||||||
|
"requires": {
|
||||||
|
"eventemitter3": "^4.0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@textcomplete/textarea": {
|
||||||
|
"version": "0.1.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/@textcomplete/textarea/-/textarea-0.1.12.tgz",
|
||||||
|
"integrity": "sha512-E05H4wXr1Q50CrCFBAHewyZqvQEX681V5zleDw/31tr8vl5PDFl6TyFmS1W0jQjlrQfxa5uVvgHCx+gpfICBDQ==",
|
||||||
|
"requires": {
|
||||||
|
"@textcomplete/utils": "^0.1.11",
|
||||||
|
"textarea-caret": "^3.1.0",
|
||||||
|
"undate": "^0.3.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@textcomplete/utils": {
|
||||||
|
"version": "0.1.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/@textcomplete/utils/-/utils-0.1.12.tgz",
|
||||||
|
"integrity": "sha512-llHhD1FAVwFaaHzs7PU0BZYTpNLDzTccDWbw+5cj0TiB2NOXZGjPm6l7PJrJwN/yUuPDxOHip/3I+kF6OBkBAg=="
|
||||||
|
},
|
||||||
"@thisbeyond/solid-dnd": {
|
"@thisbeyond/solid-dnd": {
|
||||||
"version": "0.7.4",
|
"version": "0.7.4",
|
||||||
"resolved": "https://registry.npmjs.org/@thisbeyond/solid-dnd/-/solid-dnd-0.7.4.tgz",
|
"resolved": "https://registry.npmjs.org/@thisbeyond/solid-dnd/-/solid-dnd-0.7.4.tgz",
|
||||||
@@ -9799,6 +9865,11 @@
|
|||||||
"integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
|
"integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"eventemitter3": {
|
||||||
|
"version": "4.0.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz",
|
||||||
|
"integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw=="
|
||||||
|
},
|
||||||
"execa": {
|
"execa": {
|
||||||
"version": "7.1.1",
|
"version": "7.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/execa/-/execa-7.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/execa/-/execa-7.1.1.tgz",
|
||||||
@@ -12138,6 +12209,11 @@
|
|||||||
"integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=",
|
"integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"textarea-caret": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/textarea-caret/-/textarea-caret-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-cXAvzO9pP5CGa6NKx0WYHl+8CHKZs8byMkt3PCJBCmq2a34YA9pO1NrQET5pzeqnBjBdToF5No4rrmkDUgQC2Q=="
|
||||||
|
},
|
||||||
"thenify": {
|
"thenify": {
|
||||||
"version": "3.3.1",
|
"version": "3.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz",
|
||||||
@@ -12313,6 +12389,11 @@
|
|||||||
"which-boxed-primitive": "^1.0.2"
|
"which-boxed-primitive": "^1.0.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"undate": {
|
||||||
|
"version": "0.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/undate/-/undate-0.3.0.tgz",
|
||||||
|
"integrity": "sha512-ssH8QTNBY6B+2fRr3stSQ+9m2NT8qTaun3ExTx5ibzYQvP7yX4+BnX0McNxFCvh6S5ia/DYu6bsCKQx/U4nb/Q=="
|
||||||
|
},
|
||||||
"update-browserslist-db": {
|
"update-browserslist-db": {
|
||||||
"version": "1.0.10",
|
"version": "1.0.10",
|
||||||
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.10.tgz",
|
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.10.tgz",
|
||||||
|
|||||||
@@ -53,6 +53,8 @@
|
|||||||
"@tanstack/query-sync-storage-persister": "^4.29.19",
|
"@tanstack/query-sync-storage-persister": "^4.29.19",
|
||||||
"@tanstack/solid-query": "^4.29.19",
|
"@tanstack/solid-query": "^4.29.19",
|
||||||
"@tanstack/solid-virtual": "^3.0.0-beta.6",
|
"@tanstack/solid-virtual": "^3.0.0-beta.6",
|
||||||
|
"@textcomplete/core": "^0.1.12",
|
||||||
|
"@textcomplete/textarea": "^0.1.12",
|
||||||
"@thisbeyond/solid-dnd": "^0.7.4",
|
"@thisbeyond/solid-dnd": "^0.7.4",
|
||||||
"@types/lodash": "^4.14.195",
|
"@types/lodash": "^4.14.195",
|
||||||
"emoji-mart": "^5.5.2",
|
"emoji-mart": "^5.5.2",
|
||||||
|
|||||||
@@ -17,20 +17,6 @@ const EmojiPicker: Component<EmojiPickerProps> = (props) => {
|
|||||||
const { config } = useConfig();
|
const { config } = useConfig();
|
||||||
const [pickerElement, setPickerElement] = createSignal<HTMLElement | undefined>(undefined);
|
const [pickerElement, setPickerElement] = createSignal<HTMLElement | undefined>(undefined);
|
||||||
|
|
||||||
/*
|
|
||||||
const buildCustom = () => {
|
|
||||||
const emojis = Object.values(config().customEmojis).map(({ shortcode, url }) => ({
|
|
||||||
id: shortcode,
|
|
||||||
name: shortcode,
|
|
||||||
keywords: [shortcode],
|
|
||||||
skins: [{ src: url }],
|
|
||||||
}));
|
|
||||||
|
|
||||||
console.log(emojis);
|
|
||||||
return [{ id: 'custom_rabbit', name: 'カスタム絵文字', emojis }];
|
|
||||||
};
|
|
||||||
*/
|
|
||||||
|
|
||||||
const handleOpen = () => {
|
const handleOpen = () => {
|
||||||
const picker = new Picker({
|
const picker = new Picker({
|
||||||
data: async () => {
|
data: async () => {
|
||||||
@@ -41,7 +27,6 @@ const EmojiPicker: Component<EmojiPickerProps> = (props) => {
|
|||||||
const response = await fetch('https://cdn.jsdelivr.net/npm/@emoji-mart/data/i18n/ja.json');
|
const response = await fetch('https://cdn.jsdelivr.net/npm/@emoji-mart/data/i18n/ja.json');
|
||||||
return response.json();
|
return response.json();
|
||||||
},
|
},
|
||||||
// custom: props.customEmojis ? buildCustom() : [],
|
|
||||||
autoFocus: false,
|
autoFocus: false,
|
||||||
locale: 'ja',
|
locale: 'ja',
|
||||||
theme: 'light',
|
theme: 'light',
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { Event as NostrEvent } from 'nostr-tools';
|
|||||||
import EmojiPicker from '@/components/EmojiPicker';
|
import EmojiPicker from '@/components/EmojiPicker';
|
||||||
import UserNameDisplay from '@/components/UserDisplayName';
|
import UserNameDisplay from '@/components/UserDisplayName';
|
||||||
import useConfig from '@/core/useConfig';
|
import useConfig from '@/core/useConfig';
|
||||||
|
import useEmojiComplete from '@/hooks/useEmojiComplete';
|
||||||
import usePersistStatus from '@/hooks/usePersistStatus';
|
import usePersistStatus from '@/hooks/usePersistStatus';
|
||||||
import { textNote } from '@/nostr/event';
|
import { textNote } from '@/nostr/event';
|
||||||
import parseTextNote, { ParsedTextNote } from '@/nostr/parseTextNote';
|
import parseTextNote, { ParsedTextNote } from '@/nostr/parseTextNote';
|
||||||
@@ -88,6 +89,7 @@ const NotePostForm: Component<NotePostFormProps> = (props) => {
|
|||||||
let textAreaRef: HTMLTextAreaElement | undefined;
|
let textAreaRef: HTMLTextAreaElement | undefined;
|
||||||
let fileInputRef: HTMLInputElement | undefined;
|
let fileInputRef: HTMLInputElement | undefined;
|
||||||
|
|
||||||
|
const { elementRef: emojiTextAreaRef } = useEmojiComplete();
|
||||||
const [text, setText] = createSignal<string>('');
|
const [text, setText] = createSignal<string>('');
|
||||||
const [contentWarning, setContentWarning] = createSignal(false);
|
const [contentWarning, setContentWarning] = createSignal(false);
|
||||||
const [contentWarningReason, setContentWarningReason] = createSignal('');
|
const [contentWarningReason, setContentWarningReason] = createSignal('');
|
||||||
@@ -364,6 +366,7 @@ const NotePostForm: Component<NotePostFormProps> = (props) => {
|
|||||||
ref={(el) => {
|
ref={(el) => {
|
||||||
textAreaRef = el;
|
textAreaRef = el;
|
||||||
props.textAreaRef?.(el);
|
props.textAreaRef?.(el);
|
||||||
|
emojiTextAreaRef(el);
|
||||||
}}
|
}}
|
||||||
name="text"
|
name="text"
|
||||||
class="min-h-[40px] rounded-md border-none focus:ring-rose-300"
|
class="min-h-[40px] rounded-md border-none focus:ring-rose-300"
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { type Accessor, type Setter } from 'solid-js';
|
import { type Accessor, type Setter } from 'solid-js';
|
||||||
|
|
||||||
|
import { sortBy } from 'lodash';
|
||||||
import uniq from 'lodash/uniq';
|
import uniq from 'lodash/uniq';
|
||||||
import uniqBy from 'lodash/uniqBy';
|
|
||||||
import { Kind, type Event as NostrEvent } from 'nostr-tools';
|
import { Kind, type Event as NostrEvent } from 'nostr-tools';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -54,6 +54,7 @@ type UseConfig = {
|
|||||||
saveEmojis: (emojis: CustomEmojiConfig[]) => void;
|
saveEmojis: (emojis: CustomEmojiConfig[]) => void;
|
||||||
removeEmoji: (shortcode: string) => void;
|
removeEmoji: (shortcode: string) => void;
|
||||||
getEmoji: (shortcode: string) => CustomEmojiConfig | undefined;
|
getEmoji: (shortcode: string) => CustomEmojiConfig | undefined;
|
||||||
|
searchEmojis: (term: string) => CustomEmojiConfig[];
|
||||||
// mute
|
// mute
|
||||||
addMutedPubkey: (pubkey: string) => void;
|
addMutedPubkey: (pubkey: string) => void;
|
||||||
removeMutedPubkey: (pubkey: string) => void;
|
removeMutedPubkey: (pubkey: string) => void;
|
||||||
@@ -171,6 +172,12 @@ const useConfig = (): UseConfig => {
|
|||||||
const getEmoji = (shortcode: string): CustomEmojiConfig | undefined =>
|
const getEmoji = (shortcode: string): CustomEmojiConfig | undefined =>
|
||||||
config.customEmojis[shortcode];
|
config.customEmojis[shortcode];
|
||||||
|
|
||||||
|
const searchEmojis = (term: string): CustomEmojiConfig[] =>
|
||||||
|
sortBy(
|
||||||
|
Object.values(config.customEmojis).filter(({ shortcode }) => shortcode.includes(term)),
|
||||||
|
[(e) => e.shortcode.length],
|
||||||
|
);
|
||||||
|
|
||||||
const isPubkeyMuted = (pubkey: string) => config.mutedPubkeys.includes(pubkey);
|
const isPubkeyMuted = (pubkey: string) => config.mutedPubkeys.includes(pubkey);
|
||||||
|
|
||||||
const hasMutedKeyword = (event: NostrEvent) => {
|
const hasMutedKeyword = (event: NostrEvent) => {
|
||||||
@@ -223,6 +230,7 @@ const useConfig = (): UseConfig => {
|
|||||||
saveEmojis,
|
saveEmojis,
|
||||||
removeEmoji,
|
removeEmoji,
|
||||||
getEmoji,
|
getEmoji,
|
||||||
|
searchEmojis,
|
||||||
// mute
|
// mute
|
||||||
addMutedPubkey,
|
addMutedPubkey,
|
||||||
removeMutedPubkey,
|
removeMutedPubkey,
|
||||||
|
|||||||
54
src/hooks/useEmojiComplete.tsx
Normal file
54
src/hooks/useEmojiComplete.tsx
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import { createEffect, createSignal, onCleanup } from 'solid-js';
|
||||||
|
|
||||||
|
import { Textcomplete } from '@textcomplete/core';
|
||||||
|
import { TextareaEditor } from '@textcomplete/textarea';
|
||||||
|
|
||||||
|
import useConfig, { CustomEmojiConfig } from '@/core/useConfig';
|
||||||
|
|
||||||
|
const useEmojiComplete = () => {
|
||||||
|
const { searchEmojis } = useConfig();
|
||||||
|
|
||||||
|
const [elementRef, setElementRef] = createSignal<HTMLTextAreaElement | undefined>();
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
const el = elementRef();
|
||||||
|
if (el == null) return;
|
||||||
|
|
||||||
|
const editor = new TextareaEditor(el);
|
||||||
|
const textcomplete = new Textcomplete(
|
||||||
|
editor,
|
||||||
|
[
|
||||||
|
{
|
||||||
|
id: 'customEmoji',
|
||||||
|
match: /\B:(\w+)$/,
|
||||||
|
search: (term, callback) => {
|
||||||
|
callback(searchEmojis(term));
|
||||||
|
},
|
||||||
|
template: (config: CustomEmojiConfig) => {
|
||||||
|
const e = (
|
||||||
|
<div class="flex gap-1 pb-1">
|
||||||
|
<img class="h-6 max-w-[3rem]" src={config.url} alt={config.shortcode} />
|
||||||
|
<div>{config.shortcode}</div>
|
||||||
|
</div>
|
||||||
|
) as HTMLElement;
|
||||||
|
return e.outerHTML;
|
||||||
|
},
|
||||||
|
replace: (result: CustomEmojiConfig) => `:${result.shortcode}: `,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
{
|
||||||
|
dropdown: {
|
||||||
|
className: 'px-2 pt-2 pb-1 bg-white shadow rounded',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
onCleanup(() => {
|
||||||
|
textcomplete.destroy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return { elementRef: setElementRef };
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useEmojiComplete;
|
||||||
Reference in New Issue
Block a user