MessageClickActions fix EmojiSelector (#941)
* MessageClickActions fix EmojiSelector * Simplify custom emoji preview fallback * Simplify MessageClickActions emoji settings * Move emoji trigger styles to stylesheet * Fix MessageClickActions emoji picker typing * Fix message click actions emoji selector
This commit is contained in:
parent
21a66d0aa0
commit
f2895288e7
3 changed files with 175 additions and 84 deletions
|
|
@ -7,50 +7,146 @@
|
|||
import "./styles.css";
|
||||
|
||||
import { Heading } from "@components/Heading";
|
||||
import { classNameFactory } from "@utils/css";
|
||||
import type { IPluginOptionComponentProps } from "@utils/types";
|
||||
import type { Channel } from "@vencord/discord-types";
|
||||
import { findComponentByCodeLazy } from "@webpack";
|
||||
import { TextInput, useState } from "@webpack/common";
|
||||
import { ChannelStore, IconUtils, Popout, SelectedChannelStore, TextInput, useRef, useState, useStateFromStores } from "@webpack/common";
|
||||
|
||||
import { settings } from ".";
|
||||
|
||||
type EmojiData = {
|
||||
id?: string | null;
|
||||
name: string;
|
||||
};
|
||||
export const MAX_ADDITIONAL_REACT_EMOJIS = 8;
|
||||
const cl = classNameFactory("vc-message-click-actions-");
|
||||
|
||||
type EmojiSelectPayload = {
|
||||
id: string | null;
|
||||
name: string;
|
||||
id?: string | null;
|
||||
name?: string | null;
|
||||
optionallyDiverseSequence?: string;
|
||||
animated?: boolean;
|
||||
};
|
||||
|
||||
const StatusEmojiPicker = findComponentByCodeLazy("setCustomStatusEmoji", "setIsEmojiPickerOpen", "selectedDefaultStatus");
|
||||
export const MAX_ADDITIONAL_REACT_EMOJIS = 8;
|
||||
type ReactionEmojiPickerProps = {
|
||||
channel?: Channel | null;
|
||||
closePopout(): void;
|
||||
onSelectEmoji(selection: {
|
||||
emoji: EmojiSelectPayload | null;
|
||||
willClose: boolean;
|
||||
}): void;
|
||||
};
|
||||
|
||||
function getEmojiValue(emoji: EmojiData) {
|
||||
return emoji.id ? `${emoji.name}:${emoji.id}` : emoji.name;
|
||||
const ReactionEmojiPicker = findComponentByCodeLazy<ReactionEmojiPickerProps>(
|
||||
"showAddEmojiButton:",
|
||||
"pickerIntention:",
|
||||
"messageId:"
|
||||
);
|
||||
|
||||
function parseCustomEmoji(value: string) {
|
||||
return value.match(/^(?:<(?:(a):)?|:)?([\w-]+?)(?:~\d+)?:([0-9]+)>?$/);
|
||||
}
|
||||
|
||||
function parseEmojiValue(value: string): EmojiSelectPayload | null {
|
||||
function getEmojiValue(emoji: EmojiSelectPayload | null | undefined) {
|
||||
if (!emoji) return "";
|
||||
if (emoji.id && emoji.name) return `${emoji.name}:${emoji.id}`;
|
||||
if (emoji.optionallyDiverseSequence?.trim()) return emoji.optionallyDiverseSequence;
|
||||
return emoji.name?.trim() ?? "";
|
||||
}
|
||||
|
||||
function toRenderedEmoji(value: string) {
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed) return null;
|
||||
|
||||
const customEmoji = trimmed.match(/^:?([\w-]+):(\d+)$/);
|
||||
const customEmoji = parseCustomEmoji(trimmed);
|
||||
if (customEmoji) {
|
||||
return {
|
||||
id: customEmoji[2],
|
||||
name: customEmoji[1],
|
||||
animated: false
|
||||
kind: "custom" as const,
|
||||
id: customEmoji[3],
|
||||
name: customEmoji[2],
|
||||
animated: customEmoji[1] === "a"
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
id: null,
|
||||
kind: "unicode" as const,
|
||||
name: trimmed,
|
||||
animated: false
|
||||
};
|
||||
}
|
||||
|
||||
function getCustomEmojiSources(id: string, animated: boolean) {
|
||||
const staticUrl = IconUtils.getEmojiURL({ id, animated: false, size: 48 });
|
||||
if (!animated) return [staticUrl];
|
||||
|
||||
const animatedUrl = IconUtils.getEmojiURL({ id, animated: true, size: 48 });
|
||||
return animatedUrl === staticUrl ? [staticUrl] : [animatedUrl, staticUrl];
|
||||
}
|
||||
|
||||
function CustomEmojiPreview({
|
||||
id,
|
||||
name,
|
||||
animated
|
||||
}: {
|
||||
id: string;
|
||||
name: string;
|
||||
animated: boolean;
|
||||
}) {
|
||||
const sources = getCustomEmojiSources(id, animated);
|
||||
const [srcIndex, setSrcIndex] = useState(0);
|
||||
|
||||
return (
|
||||
<img
|
||||
src={sources[srcIndex]}
|
||||
alt={name}
|
||||
width={34}
|
||||
height={34}
|
||||
className={cl("emoji-preview")}
|
||||
onError={() => setSrcIndex(current => current < sources.length - 1 ? current + 1 : current)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function EmojiPickerButton({
|
||||
onSelect,
|
||||
children
|
||||
}: {
|
||||
onSelect(value: string): void;
|
||||
children?: React.ReactNode;
|
||||
}) {
|
||||
const triggerRef = useRef<HTMLDivElement>(null);
|
||||
const channel = useStateFromStores([SelectedChannelStore, ChannelStore], () => {
|
||||
const channelId = SelectedChannelStore.getChannelId();
|
||||
return channelId ? ChannelStore.getChannel(channelId) : null;
|
||||
});
|
||||
|
||||
return (
|
||||
<Popout
|
||||
position="bottom"
|
||||
align="left"
|
||||
targetElementRef={triggerRef}
|
||||
renderPopout={({ closePopout }) => (
|
||||
<ReactionEmojiPicker
|
||||
channel={channel}
|
||||
closePopout={closePopout}
|
||||
onSelectEmoji={({ emoji, willClose }) => {
|
||||
const nextValue = getEmojiValue(emoji);
|
||||
if (nextValue) onSelect(nextValue);
|
||||
if (willClose) closePopout();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
>
|
||||
{popoutProps => (
|
||||
<div
|
||||
{...popoutProps}
|
||||
ref={triggerRef}
|
||||
className={cl("emoji-trigger")}
|
||||
>
|
||||
{children ?? "Pick Emoji"}
|
||||
</div>
|
||||
)}
|
||||
</Popout>
|
||||
);
|
||||
}
|
||||
|
||||
function parseEmojiList(value: string) {
|
||||
return Array.from(new Set(
|
||||
value
|
||||
|
|
@ -60,37 +156,47 @@ function parseEmojiList(value: string) {
|
|||
)).slice(0, MAX_ADDITIONAL_REACT_EMOJIS);
|
||||
}
|
||||
|
||||
function addEmojiToList(list: string, emoji: string) {
|
||||
const parsedList = parseEmojiList(list);
|
||||
if (parsedList.includes(emoji)) return parsedList.join(", ");
|
||||
|
||||
return [...parsedList, emoji]
|
||||
.slice(0, MAX_ADDITIONAL_REACT_EMOJIS)
|
||||
.join(", ");
|
||||
}
|
||||
|
||||
function EmojiPreview({ value }: { value: string; }) {
|
||||
const renderedEmoji = toRenderedEmoji(value);
|
||||
if (!renderedEmoji) return <>Pick Emoji</>;
|
||||
|
||||
if (renderedEmoji.kind === "custom") {
|
||||
return (
|
||||
<CustomEmojiPreview
|
||||
id={renderedEmoji.id}
|
||||
name={renderedEmoji.name}
|
||||
animated={renderedEmoji.animated}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return <span className={cl("unicode-preview")}>{renderedEmoji.name}</span>;
|
||||
}
|
||||
|
||||
export function ReactEmojiSetting({ setValue }: IPluginOptionComponentProps) {
|
||||
const [emoji, setEmoji] = useState(settings.store.reactEmoji ?? "💀");
|
||||
const [isEmojiPickerOpen, setIsEmojiPickerOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Heading>Select Emoji For Reactions</Heading>
|
||||
<div
|
||||
className="vc-message-click-actions-primary-emoji-picker"
|
||||
onClick={event => event.stopPropagation()}
|
||||
onMouseDown={event => event.stopPropagation()}
|
||||
>
|
||||
<StatusEmojiPicker
|
||||
customStatusEmoji={parseEmojiValue(emoji)}
|
||||
setCustomStatusEmoji={(selectedEmoji: EmojiSelectPayload | null) => {
|
||||
if (!selectedEmoji) {
|
||||
setEmoji("");
|
||||
setValue("");
|
||||
setIsEmojiPickerOpen(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const value = getEmojiValue(selectedEmoji);
|
||||
setEmoji(value);
|
||||
setValue(value);
|
||||
setIsEmojiPickerOpen(false);
|
||||
<div className={cl("primary-emoji-picker")}>
|
||||
<EmojiPickerButton
|
||||
onSelect={newValue => {
|
||||
setEmoji(newValue);
|
||||
setValue(newValue);
|
||||
}}
|
||||
selectedDefaultStatus={isEmojiPickerOpen ? null : "online"}
|
||||
setIsEmojiPickerOpen={setIsEmojiPickerOpen}
|
||||
defaultStatusVariant="small"
|
||||
/>
|
||||
>
|
||||
<EmojiPreview value={emoji} />
|
||||
</EmojiPickerButton>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
@ -99,8 +205,6 @@ export function ReactEmojiSetting({ setValue }: IPluginOptionComponentProps) {
|
|||
export function AdditionalReactEmojisSetting({ setValue }: IPluginOptionComponentProps) {
|
||||
const { addAdditionalReacts } = settings.use(["addAdditionalReacts"]);
|
||||
const [emojiList, setEmojiList] = useState(settings.store.additionalReactEmojis ?? "");
|
||||
const [isEmojiPickerOpen, setIsEmojiPickerOpen] = useState(false);
|
||||
const [pickerEmoji, setPickerEmoji] = useState<EmojiSelectPayload | null>(parseEmojiValue("😀"));
|
||||
|
||||
if (!addAdditionalReacts) return null;
|
||||
|
||||
|
|
@ -122,8 +226,6 @@ export function AdditionalReactEmojisSetting({ setValue }: IPluginOptionComponen
|
|||
onChange={newValue => {
|
||||
setEmojiList(newValue);
|
||||
setValue(newValue);
|
||||
const [first] = parseEmojiList(newValue);
|
||||
setPickerEmoji(parseEmojiValue(first ?? "😀"));
|
||||
}}
|
||||
onClick={event => event.stopPropagation()}
|
||||
onMouseDown={event => event.stopPropagation()}
|
||||
|
|
@ -131,39 +233,13 @@ export function AdditionalReactEmojisSetting({ setValue }: IPluginOptionComponen
|
|||
flex: 1
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
onClick={event => event.stopPropagation()}
|
||||
onMouseDown={event => event.stopPropagation()}
|
||||
style={{
|
||||
display: "inline-flex",
|
||||
transform: "scale(1.2)",
|
||||
transformOrigin: "center"
|
||||
<EmojiPickerButton
|
||||
onSelect={newValue => {
|
||||
const nextValue = addEmojiToList(emojiList, newValue);
|
||||
setEmojiList(nextValue);
|
||||
setValue(nextValue);
|
||||
}}
|
||||
>
|
||||
<StatusEmojiPicker
|
||||
customStatusEmoji={pickerEmoji}
|
||||
setCustomStatusEmoji={(selectedEmoji: EmojiSelectPayload | null) => {
|
||||
if (!selectedEmoji) {
|
||||
setIsEmojiPickerOpen(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const value = getEmojiValue(selectedEmoji);
|
||||
const parsed = parseEmojiList(emojiList);
|
||||
const merged = parsed.includes(value)
|
||||
? parsed
|
||||
: [...parsed, value].slice(0, MAX_ADDITIONAL_REACT_EMOJIS);
|
||||
const nextValue = merged.join(", ");
|
||||
setEmojiList(nextValue);
|
||||
setValue(nextValue);
|
||||
setPickerEmoji(selectedEmoji);
|
||||
setIsEmojiPickerOpen(false);
|
||||
}}
|
||||
selectedDefaultStatus={isEmojiPickerOpen ? null : "online"}
|
||||
setIsEmojiPickerOpen={setIsEmojiPickerOpen}
|
||||
defaultStatusVariant="small"
|
||||
/>
|
||||
</div>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -287,9 +287,9 @@ function normalizeEmoji(emoji: string): string | null {
|
|||
const trimmed = emoji.trim();
|
||||
if (!trimmed) return null;
|
||||
|
||||
const customMatch = trimmed.match(/^:?([\w-]+):(\d+)$/);
|
||||
const customMatch = trimmed.match(/^(?:<(?:(a):)?|:)?([\w-]+?)(?:~\d+)?:([0-9]+)>?$/);
|
||||
if (customMatch) {
|
||||
return `${customMatch[1]}:${customMatch[2]}`;
|
||||
return `${customMatch[2]}:${customMatch[3]}`;
|
||||
}
|
||||
|
||||
return trimmed;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,22 @@
|
|||
.vc-message-click-actions-primary-emoji-picker {
|
||||
margin-top: 8px;
|
||||
margin-left: 6px;
|
||||
.vc-message-click-actions-emoji-trigger {
|
||||
min-width: 46px;
|
||||
height: 46px;
|
||||
padding: 0 8px;
|
||||
display: inline-flex;
|
||||
transform: scale(1.3);
|
||||
transform-origin: top left;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 8px;
|
||||
background: var(--background-secondary-alt);
|
||||
border: 1px solid var(--input-border);
|
||||
cursor: pointer;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.vc-message-click-actions-emoji-preview {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.vc-message-click-actions-unicode-preview {
|
||||
font-size: 28px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue