mirror of
https://github.com/aljazceru/rabbit.git
synced 2025-12-18 14:34:25 +01:00
feat: support posting custom emoji
This commit is contained in:
@@ -3,17 +3,34 @@ import { Component, JSX, createSignal } from 'solid-js';
|
|||||||
import { Picker } from 'emoji-mart';
|
import { Picker } from 'emoji-mart';
|
||||||
|
|
||||||
import Popup, { PopupRef } from '@/components/utils/Popup';
|
import Popup, { PopupRef } from '@/components/utils/Popup';
|
||||||
|
import useConfig from '@/core/useConfig';
|
||||||
|
|
||||||
type EmojiPickerProps = {
|
type EmojiPickerProps = {
|
||||||
onEmojiSelect?: (emoji: string) => void;
|
onEmojiSelect?: (emoji: string) => void;
|
||||||
|
customEmojis?: boolean;
|
||||||
children: JSX.Element;
|
children: JSX.Element;
|
||||||
};
|
};
|
||||||
|
|
||||||
const EmojiPicker: Component<EmojiPickerProps> = (props) => {
|
const EmojiPicker: Component<EmojiPickerProps> = (props) => {
|
||||||
let popupRef: PopupRef | undefined;
|
let popupRef: PopupRef | undefined;
|
||||||
|
|
||||||
|
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 () => {
|
||||||
@@ -24,11 +41,12 @@ 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',
|
||||||
onEmojiSelect: (emoji: { id: string; native: string }) => {
|
onEmojiSelect: (emoji: { id: string; native?: string }) => {
|
||||||
props.onEmojiSelect?.(emoji.native);
|
props.onEmojiSelect?.(emoji.native ?? `:${emoji.id}:`);
|
||||||
popupRef?.close();
|
popupRef?.close();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,21 +1,14 @@
|
|||||||
import {
|
import { createSignal, createMemo, onMount, Show, For, type Component, type JSX } from 'solid-js';
|
||||||
createSignal,
|
|
||||||
createMemo,
|
|
||||||
onMount,
|
|
||||||
Show,
|
|
||||||
For,
|
|
||||||
type Component,
|
|
||||||
type JSX,
|
|
||||||
type Accessor,
|
|
||||||
} from 'solid-js';
|
|
||||||
|
|
||||||
import { createMutation } from '@tanstack/solid-query';
|
import { createMutation } from '@tanstack/solid-query';
|
||||||
|
import FaceSmile from 'heroicons/24/outline/face-smile.svg';
|
||||||
import Photo from 'heroicons/24/outline/photo.svg';
|
import Photo from 'heroicons/24/outline/photo.svg';
|
||||||
import XMark from 'heroicons/24/outline/x-mark.svg';
|
import XMark from 'heroicons/24/outline/x-mark.svg';
|
||||||
import PaperAirplane from 'heroicons/24/solid/paper-airplane.svg';
|
import PaperAirplane from 'heroicons/24/solid/paper-airplane.svg';
|
||||||
import uniq from 'lodash/uniq';
|
import uniq from 'lodash/uniq';
|
||||||
import { Event as NostrEvent } from 'nostr-tools';
|
import { Event as NostrEvent } from 'nostr-tools';
|
||||||
|
|
||||||
|
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 { useHandleCommand } from '@/hooks/useCommandBus';
|
import { useHandleCommand } from '@/hooks/useCommandBus';
|
||||||
@@ -48,12 +41,13 @@ const extract = (parsed: ParsedTextNote) => {
|
|||||||
const pubkeyReferences: string[] = [];
|
const pubkeyReferences: string[] = [];
|
||||||
const eventReferences: string[] = [];
|
const eventReferences: string[] = [];
|
||||||
const urlReferences: string[] = [];
|
const urlReferences: string[] = [];
|
||||||
|
const emojis: string[] = [];
|
||||||
|
|
||||||
parsed.forEach((node) => {
|
parsed.forEach((node) => {
|
||||||
if (node.type === 'HashTag') {
|
if (node.type === 'URL') {
|
||||||
hashtags.push(node.tagName);
|
|
||||||
} else if (node.type === 'URL') {
|
|
||||||
urlReferences.push(node.content);
|
urlReferences.push(node.content);
|
||||||
|
} else if (node.type === 'HashTag') {
|
||||||
|
hashtags.push(node.tagName);
|
||||||
} else if (node.type === 'Bech32Entity') {
|
} else if (node.type === 'Bech32Entity') {
|
||||||
if (node.data.type === 'npub') {
|
if (node.data.type === 'npub') {
|
||||||
pubkeyReferences.push(node.data.data);
|
pubkeyReferences.push(node.data.data);
|
||||||
@@ -63,14 +57,17 @@ const extract = (parsed: ParsedTextNote) => {
|
|||||||
// TODO nevent can contain an event not only textnote (kind:1).
|
// TODO nevent can contain an event not only textnote (kind:1).
|
||||||
// In my understanding, it is not allowed to include other kinds of events in `tags`.
|
// In my understanding, it is not allowed to include other kinds of events in `tags`.
|
||||||
// It is needed to verify that the kind of the event is 1.
|
// It is needed to verify that the kind of the event is 1.
|
||||||
|
} else if (node.type === 'CustomEmoji' && !emojis.includes(node.shortcode)) {
|
||||||
|
emojis.push(node.shortcode);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
hashtags,
|
hashtags,
|
||||||
|
urlReferences,
|
||||||
pubkeyReferences,
|
pubkeyReferences,
|
||||||
eventReferences,
|
eventReferences,
|
||||||
urlReferences,
|
emojis,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -94,6 +91,8 @@ const NotePostForm: Component<NotePostFormProps> = (props) => {
|
|||||||
const [contentWarning, setContentWarning] = createSignal(false);
|
const [contentWarning, setContentWarning] = createSignal(false);
|
||||||
const [contentWarningReason, setContentWarningReason] = createSignal('');
|
const [contentWarningReason, setContentWarningReason] = createSignal('');
|
||||||
|
|
||||||
|
const appendText = (s: string) => setText((current) => `${current} ${s}`);
|
||||||
|
|
||||||
const clearText = () => {
|
const clearText = () => {
|
||||||
setText('');
|
setText('');
|
||||||
setContentWarningReason('');
|
setContentWarningReason('');
|
||||||
@@ -106,7 +105,7 @@ const NotePostForm: Component<NotePostFormProps> = (props) => {
|
|||||||
props.onClose();
|
props.onClose();
|
||||||
};
|
};
|
||||||
|
|
||||||
const { config } = useConfig();
|
const { config, getEmoji } = useConfig();
|
||||||
const getPubkey = usePubkey();
|
const getPubkey = usePubkey();
|
||||||
const commands = useCommands();
|
const commands = useCommands();
|
||||||
|
|
||||||
@@ -140,7 +139,7 @@ const NotePostForm: Component<NotePostFormProps> = (props) => {
|
|||||||
uploadResults.forEach((result) => {
|
uploadResults.forEach((result) => {
|
||||||
if (result.status === 'fulfilled') {
|
if (result.status === 'fulfilled') {
|
||||||
console.log('succeeded to upload', result);
|
console.log('succeeded to upload', result);
|
||||||
setText((current) => `${current} ${result.value.imageUrl}`);
|
appendText(result.value.imageUrl);
|
||||||
resizeTextArea();
|
resizeTextArea();
|
||||||
} else {
|
} else {
|
||||||
console.error('failed to upload', result);
|
console.error('failed to upload', result);
|
||||||
@@ -169,6 +168,19 @@ const NotePostForm: Component<NotePostFormProps> = (props) => {
|
|||||||
]);
|
]);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const buildEmojiTags = (emojis: string[]): string[][] => {
|
||||||
|
const emojiTags: string[][] = [];
|
||||||
|
|
||||||
|
emojis.forEach((shortcode) => {
|
||||||
|
const emoji = getEmoji(shortcode);
|
||||||
|
if (emoji != null) {
|
||||||
|
emojiTags.push(['emoji', shortcode, emoji.url]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return emojiTags;
|
||||||
|
};
|
||||||
|
|
||||||
const submit = () => {
|
const submit = () => {
|
||||||
if (text().length === 0) return;
|
if (text().length === 0) return;
|
||||||
if (publishTextNoteMutation.isLoading) return;
|
if (publishTextNoteMutation.isLoading) return;
|
||||||
@@ -180,8 +192,9 @@ const NotePostForm: Component<NotePostFormProps> = (props) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const parsed = parseTextNote(text());
|
const parsed = parseTextNote(text());
|
||||||
const { hashtags, pubkeyReferences, eventReferences, urlReferences } = extract(parsed);
|
const { hashtags, urlReferences, pubkeyReferences, eventReferences, emojis } = extract(parsed);
|
||||||
const formattedContent = format(parsed);
|
const formattedContent = format(parsed);
|
||||||
|
const emojiTags = buildEmojiTags(emojis);
|
||||||
|
|
||||||
let textNote: PublishTextNoteParams = {
|
let textNote: PublishTextNoteParams = {
|
||||||
relayUrls: config().relayUrls,
|
relayUrls: config().relayUrls,
|
||||||
@@ -191,6 +204,7 @@ const NotePostForm: Component<NotePostFormProps> = (props) => {
|
|||||||
mentionEventIds: eventReferences,
|
mentionEventIds: eventReferences,
|
||||||
hashtags,
|
hashtags,
|
||||||
urls: urlReferences,
|
urls: urlReferences,
|
||||||
|
tags: emojiTags,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (replyTo() != null) {
|
if (replyTo() != null) {
|
||||||
@@ -329,6 +343,13 @@ const NotePostForm: Component<NotePostFormProps> = (props) => {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
|
{/*
|
||||||
|
<EmojiPicker customEmojis={true} onEmojiSelect={(emoji) => appendText(emoji)}>
|
||||||
|
<span class="inline-block h-8 w-8 rounded bg-primary p-2 font-bold text-white">
|
||||||
|
<FaceSmile />
|
||||||
|
</span>
|
||||||
|
</EmojiPicker>
|
||||||
|
*/}
|
||||||
<button
|
<button
|
||||||
class="flex items-center justify-center rounded p-2 text-xs font-bold text-white"
|
class="flex items-center justify-center rounded p-2 text-xs font-bold text-white"
|
||||||
classList={{
|
classList={{
|
||||||
|
|||||||
@@ -13,6 +13,11 @@ type ConfigProps = {
|
|||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const BaseUrlRegex = (schemaRegex: string) =>
|
||||||
|
`^(?:${schemaRegex})://[-a-zA-Z0-9.]+(:\\d{1,5})?(?:/[-[\\]~!$&'()*+.,:;@&=%\\w]+|/)*(?:\\?[-[\\]~!$&'()*+.,/:;%@&=\\w?]+)?(?:#[-[\\]~!$&'()*+.,/:;%@\\w&=?#]+)?$`;
|
||||||
|
const HttpUrlRegex = BaseUrlRegex('https?');
|
||||||
|
const RelayUrlRegex = BaseUrlRegex('wss?');
|
||||||
|
|
||||||
const ProfileSection = () => {
|
const ProfileSection = () => {
|
||||||
const pubkey = usePubkey();
|
const pubkey = usePubkey();
|
||||||
const { showProfile, showProfileEdit } = useModalState();
|
const { showProfile, showProfileEdit } = useModalState();
|
||||||
@@ -77,6 +82,7 @@ const RelayConfig = () => {
|
|||||||
type="text"
|
type="text"
|
||||||
name="relayUrl"
|
name="relayUrl"
|
||||||
value={relayUrlInput()}
|
value={relayUrlInput()}
|
||||||
|
pattern={RelayUrlRegex}
|
||||||
onChange={(ev) => setRelayUrlInput(ev.currentTarget.value)}
|
onChange={(ev) => setRelayUrlInput(ev.currentTarget.value)}
|
||||||
/>
|
/>
|
||||||
<button type="submit" class="rounded bg-rose-300 p-2 font-bold text-white">
|
<button type="submit" class="rounded bg-rose-300 p-2 font-bold text-white">
|
||||||
@@ -166,7 +172,7 @@ const ToggleButton = (props: {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const EmojiConfig = () => {
|
const ReactionConfig = () => {
|
||||||
const { config, setConfig } = useConfig();
|
const { config, setConfig } = useConfig();
|
||||||
|
|
||||||
const toggleUseEmojiReaction = () => {
|
const toggleUseEmojiReaction = () => {
|
||||||
@@ -206,6 +212,68 @@ const EmojiConfig = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const EmojiConfig = () => {
|
||||||
|
const { config, saveEmoji, removeEmoji } = useConfig();
|
||||||
|
|
||||||
|
const [shortcodeInput, setShortcodeInput] = createSignal('');
|
||||||
|
const [urlInput, setUrlInput] = createSignal('');
|
||||||
|
|
||||||
|
const handleClickSaveEmoji: JSX.EventHandler<HTMLFormElement, SubmitEvent> = (ev) => {
|
||||||
|
ev.preventDefault();
|
||||||
|
if (shortcodeInput().length === 0 || urlInput().length === 0) return;
|
||||||
|
saveEmoji({ shortcode: shortcodeInput(), url: urlInput() });
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div class="py-2">
|
||||||
|
<h3 class="font-bold">カスタム絵文字</h3>
|
||||||
|
<ul class="flex flex-col gap-1 py-2">
|
||||||
|
<For each={Object.values(config().customEmojis)}>
|
||||||
|
{({ shortcode, url }) => (
|
||||||
|
<li class="flex items-center gap-2">
|
||||||
|
<img class="h-7 max-w-[128px]" src={url} alt={shortcode} />
|
||||||
|
<div class="flex-1 truncate">{shortcode}</div>
|
||||||
|
<button class="h-3 w-3 shrink-0" onClick={() => removeEmoji(shortcode)}>
|
||||||
|
<XMark />
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</ul>
|
||||||
|
<form class="flex gap-2" onSubmit={handleClickSaveEmoji}>
|
||||||
|
<label class="flex flex-1 items-center gap-1">
|
||||||
|
<div>名前</div>
|
||||||
|
<input
|
||||||
|
class="flex-1 rounded-md focus:border-rose-100 focus:ring-rose-300"
|
||||||
|
type="text"
|
||||||
|
name="shortcode"
|
||||||
|
value={shortcodeInput()}
|
||||||
|
pattern="^[a-zA-Z0-9]+$"
|
||||||
|
required
|
||||||
|
onChange={(ev) => setShortcodeInput(ev.currentTarget.value)}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label class="flex flex-1 items-center gap-1">
|
||||||
|
<div>URL</div>
|
||||||
|
<input
|
||||||
|
class="flex-1 rounded-md focus:border-rose-100 focus:ring-rose-300"
|
||||||
|
type="text"
|
||||||
|
name="url"
|
||||||
|
value={urlInput()}
|
||||||
|
placeholder="https://.../emoji.png"
|
||||||
|
pattern={HttpUrlRegex}
|
||||||
|
required
|
||||||
|
onChange={(ev) => setUrlInput(ev.currentTarget.value)}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<button type="submit" class="rounded bg-rose-300 p-2 font-bold text-white">
|
||||||
|
保存
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const MuteConfig = () => {
|
const MuteConfig = () => {
|
||||||
const { config, removeMutedPubkey, addMutedKeyword, removeMutedKeyword } = useConfig();
|
const { config, removeMutedPubkey, addMutedKeyword, removeMutedKeyword } = useConfig();
|
||||||
|
|
||||||
@@ -335,6 +403,7 @@ const ConfigUI = (props: ConfigProps) => {
|
|||||||
<ProfileSection />
|
<ProfileSection />
|
||||||
<RelayConfig />
|
<RelayConfig />
|
||||||
<DateFormatConfig />
|
<DateFormatConfig />
|
||||||
|
<ReactionConfig />
|
||||||
<EmojiConfig />
|
<EmojiConfig />
|
||||||
<OtherConfig />
|
<OtherConfig />
|
||||||
<MuteConfig />
|
<MuteConfig />
|
||||||
|
|||||||
@@ -104,6 +104,7 @@ const Popup: Component<PopupProps> = (props) => {
|
|||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
ref={buttonRef}
|
ref={buttonRef}
|
||||||
class="flex items-center"
|
class="flex items-center"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { type Accessor, type Setter } from 'solid-js';
|
import { type Accessor, type Setter } from 'solid-js';
|
||||||
|
|
||||||
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 {
|
||||||
@@ -18,9 +19,15 @@ import {
|
|||||||
} from '@/hooks/createSignalWithStorage';
|
} from '@/hooks/createSignalWithStorage';
|
||||||
import eventWrapper from '@/nostr/event';
|
import eventWrapper from '@/nostr/event';
|
||||||
|
|
||||||
|
export type CustomEmojiConfig = {
|
||||||
|
shortcode: string;
|
||||||
|
url: string;
|
||||||
|
};
|
||||||
|
|
||||||
export type Config = {
|
export type Config = {
|
||||||
relayUrls: string[];
|
relayUrls: string[];
|
||||||
columns: ColumnType[];
|
columns: ColumnType[];
|
||||||
|
customEmojis: Record<string, CustomEmojiConfig>;
|
||||||
dateFormat: 'relative' | 'absolute-long' | 'absolute-short';
|
dateFormat: 'relative' | 'absolute-long' | 'absolute-short';
|
||||||
keepOpenPostForm: boolean;
|
keepOpenPostForm: boolean;
|
||||||
useEmojiReaction: boolean;
|
useEmojiReaction: boolean;
|
||||||
@@ -37,19 +44,22 @@ type UseConfig = {
|
|||||||
// relay
|
// relay
|
||||||
addRelay: (url: string) => void;
|
addRelay: (url: string) => void;
|
||||||
removeRelay: (url: string) => void;
|
removeRelay: (url: string) => void;
|
||||||
|
// column
|
||||||
|
saveColumn: (column: ColumnType) => void;
|
||||||
|
moveColumn: (columnId: string, index: number) => void;
|
||||||
|
removeColumn: (columnId: string) => void;
|
||||||
|
initializeColumns: (param: { pubkey: string }) => void;
|
||||||
|
// emoji
|
||||||
|
saveEmoji: (emoji: CustomEmojiConfig) => void;
|
||||||
|
removeEmoji: (shortcode: string) => void;
|
||||||
|
getEmoji: (shortcode: string) => CustomEmojiConfig | undefined;
|
||||||
// mute
|
// mute
|
||||||
addMutedPubkey: (pubkey: string) => void;
|
addMutedPubkey: (pubkey: string) => void;
|
||||||
removeMutedPubkey: (pubkey: string) => void;
|
removeMutedPubkey: (pubkey: string) => void;
|
||||||
addMutedKeyword: (keyword: string) => void;
|
addMutedKeyword: (keyword: string) => void;
|
||||||
removeMutedKeyword: (keyword: string) => void;
|
removeMutedKeyword: (keyword: string) => void;
|
||||||
// column
|
|
||||||
saveColumn: (column: ColumnType) => void;
|
|
||||||
moveColumn: (columnId: string, index: number) => void;
|
|
||||||
removeColumn: (columnId: string) => void;
|
|
||||||
// functions
|
|
||||||
isPubkeyMuted: (pubkey: string) => boolean;
|
isPubkeyMuted: (pubkey: string) => boolean;
|
||||||
shouldMuteEvent: (event: NostrEvent) => boolean;
|
shouldMuteEvent: (event: NostrEvent) => boolean;
|
||||||
initializeColumns: (param: { pubkey: string }) => void;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const initialRelays = (): string[] => {
|
const initialRelays = (): string[] => {
|
||||||
@@ -63,6 +73,7 @@ const initialRelays = (): string[] => {
|
|||||||
const InitialConfig = (): Config => ({
|
const InitialConfig = (): Config => ({
|
||||||
relayUrls: initialRelays(),
|
relayUrls: initialRelays(),
|
||||||
columns: [],
|
columns: [],
|
||||||
|
customEmojis: {},
|
||||||
dateFormat: 'relative',
|
dateFormat: 'relative',
|
||||||
keepOpenPostForm: false,
|
keepOpenPostForm: false,
|
||||||
useEmojiReaction: false,
|
useEmojiReaction: false,
|
||||||
@@ -141,6 +152,17 @@ const useConfig = (): UseConfig => {
|
|||||||
setConfig('columns', (current) => current.filter((e) => e.id !== columnId));
|
setConfig('columns', (current) => current.filter((e) => e.id !== columnId));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const saveEmoji = (emoji: CustomEmojiConfig) => {
|
||||||
|
setConfig('customEmojis', (current) => ({ ...current, [emoji.shortcode]: emoji }));
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeEmoji = (shortcode: string) => {
|
||||||
|
setConfig('customEmojis', (current) => ({ ...current, [shortcode]: undefined }));
|
||||||
|
};
|
||||||
|
|
||||||
|
const getEmoji = (shortcode: string): CustomEmojiConfig | undefined =>
|
||||||
|
config.customEmojis[shortcode];
|
||||||
|
|
||||||
const isPubkeyMuted = (pubkey: string) => config.mutedPubkeys.includes(pubkey);
|
const isPubkeyMuted = (pubkey: string) => config.mutedPubkeys.includes(pubkey);
|
||||||
|
|
||||||
const hasMutedKeyword = (event: NostrEvent) => {
|
const hasMutedKeyword = (event: NostrEvent) => {
|
||||||
@@ -180,18 +202,25 @@ const useConfig = (): UseConfig => {
|
|||||||
return {
|
return {
|
||||||
config: () => config,
|
config: () => config,
|
||||||
setConfig,
|
setConfig,
|
||||||
|
// relay
|
||||||
addRelay,
|
addRelay,
|
||||||
removeRelay,
|
removeRelay,
|
||||||
|
// column
|
||||||
|
saveColumn,
|
||||||
|
moveColumn,
|
||||||
|
removeColumn,
|
||||||
|
initializeColumns,
|
||||||
|
// emoji
|
||||||
|
saveEmoji,
|
||||||
|
removeEmoji,
|
||||||
|
getEmoji,
|
||||||
|
// mute
|
||||||
addMutedPubkey,
|
addMutedPubkey,
|
||||||
removeMutedPubkey,
|
removeMutedPubkey,
|
||||||
addMutedKeyword,
|
addMutedKeyword,
|
||||||
removeMutedKeyword,
|
removeMutedKeyword,
|
||||||
saveColumn,
|
|
||||||
moveColumn,
|
|
||||||
removeColumn,
|
|
||||||
isPubkeyMuted,
|
isPubkeyMuted,
|
||||||
shouldMuteEvent,
|
shouldMuteEvent,
|
||||||
initializeColumns,
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -47,20 +47,15 @@ const eventSchema = z.object({
|
|||||||
const EmojiTagSchema = z.tuple([
|
const EmojiTagSchema = z.tuple([
|
||||||
z.literal('emoji'),
|
z.literal('emoji'),
|
||||||
z.string().regex(/^[a-zA-Z0-9]+$/, { message: 'shortcode should be alpahnumeric' }),
|
z.string().regex(/^[a-zA-Z0-9]+$/, { message: 'shortcode should be alpahnumeric' }),
|
||||||
z.string().url().refine(isImageUrl),
|
z.string().url(), // .refine(isImageUrl)
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export type EmojiTag = z.infer<typeof EmojiTagSchema>;
|
export type EmojiTag = z.infer<typeof EmojiTagSchema>;
|
||||||
|
|
||||||
const ensureSchema =
|
const ensureSchema =
|
||||||
<T>(schema: z.Schema<T>) =>
|
<T>(schema: z.Schema<T>) =>
|
||||||
(value: any): value is T => {
|
(value: any): value is T =>
|
||||||
const result = schema.safeParse(value);
|
schema.safeParse(value).success;
|
||||||
if (!result.success) {
|
|
||||||
console.warn('failed to parse value', value, schema);
|
|
||||||
}
|
|
||||||
return result.success;
|
|
||||||
};
|
|
||||||
|
|
||||||
const eventWrapper = (event: NostrEvent) => {
|
const eventWrapper = (event: NostrEvent) => {
|
||||||
let memoizedMarkedEventTags: MarkedEventTag[] | undefined;
|
let memoizedMarkedEventTags: MarkedEventTag[] | undefined;
|
||||||
|
|||||||
Reference in New Issue
Block a user