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:
oma 2026-03-30 10:49:51 -04:00 • committed by GitHub
parent 21a66d0aa0
commit f2895288e7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 175 additions and 84 deletions

View file

@ -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>
);

View file

@ -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;

View file

@ -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;
}