From 02ffe7440a887c3bf66d5f3a6cefde09534c4684 Mon Sep 17 00:00:00 2001 From: busyboxkitty Date: Tue, 6 Jan 2026 15:52:42 +0000 Subject: [PATCH] feat: DragFavoriteEmotes plugin (#614) * feat: DragFavoriteEmotes plugin * bleh * Update index.tsx * classnames * Update index.tsx --------- Co-authored-by: thororen1234 <78185467+thororen1234@users.noreply.github.com> --- .../dragFavoriteEmotes/index.tsx | 163 ++++++++++++++++++ .../dragFavoriteEmotes/styles.css | 25 +++ src/utils/constants.ts | 8 + 3 files changed, 196 insertions(+) create mode 100644 src/equicordplugins/dragFavoriteEmotes/index.tsx create mode 100644 src/equicordplugins/dragFavoriteEmotes/styles.css diff --git a/src/equicordplugins/dragFavoriteEmotes/index.tsx b/src/equicordplugins/dragFavoriteEmotes/index.tsx new file mode 100644 index 000000000..9c0cad5e7 --- /dev/null +++ b/src/equicordplugins/dragFavoriteEmotes/index.tsx @@ -0,0 +1,163 @@ +/* + * Vencord, a modification for Discord's desktop app + * Copyright (c) 2024 Vendicated and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . +*/ + +import "./styles.css"; + +import { EquicordDevs } from "@utils/constants"; +import { classNameFactory } from "@utils/css"; +import { classes } from "@utils/misc"; +import definePlugin from "@utils/types"; +import { findByPropsLazy } from "@webpack"; +import { useDrag, useDrop, useLayoutEffect, useRef, UserSettingsActionCreators } from "@webpack/common"; + +const UserSettingsDelay = findByPropsLazy("INFREQUENT_USER_ACTION"); +const imgCls = findByPropsLazy("image", "imageLoading"); +const dndCls = findByPropsLazy("wrapper", "target", "dragOver"); + +const cl = classNameFactory("vc-drag-favorite-emotes-"); + +type EmojiData = { + uniqueName: string | undefined; + id: string; +}; + +type EmojiDescriptor = { + category: string; + emoji: EmojiData; +}; + +export default definePlugin({ + name: "DragFavoriteEmotes", + authors: [EquicordDevs.PWall], + description: "Adds the ability to change the order of your favourite emotes", + patches: [ + { + find: "#{intl::EMOJI_FAVORITE_TOOLTIP}", + replacement: [ + { + match: /(ref:\i,children:)(\(0,\i.jsx\)\(\i.\i)/, + replace: "$1arguments[0]?.collected?.isDragging?$self.dragItem():$2", + }, + { + match: /\[(\i\.emojiItemSelected)\]/, + replace: '[arguments[0].collected.isDragging?"":$1]', + }, + { + match: /(\[\i,\i\]=\i\.useState\(""\))/, + replace: "$1,[collected,drag]=$self.drag(arguments[0])", + }, + { + match: /(onFocus".{0,50}\(\i,{ref:)(\i),/, + replace: '$1arguments[0]?.descriptor?.emoji?.category==="FAVORITES"?drag:$2,collected:collected,', + }, + { + match: /(,\{key:\i,ref:\i)(?=\}\),)/, + replace: '$1,style:{position:"relative"}', + }, + { + match: /(delay:200,children:)(\i)/, + replace: "$1[$2,$self.wrapper(arguments[0]?.descriptor?.emoji)]", + }, + { + match: /(delay:200,children:.{0,100}\}\):)(\i)\)/, + replace: "$1[$2,$self.wrapper(arguments[0]?.descriptor?.emoji)])", + }, + ], + }, + ], + drag(e: { descriptor: EmojiDescriptor; }) { + return useDrag( + () => ({ + type: "emoji", + collect: monitor => ({ + isDragging: !!monitor.isDragging(), + }), + canDrag: () => (e?.descriptor?.category === "FAVORITES"), + item: { id: e?.descriptor?.emoji?.uniqueName ?? e?.descriptor?.emoji?.id } + }), + [e?.descriptor],); + }, + drop({ emoji, category }: EmojiDescriptor) { + return useDrop(() => ({ + accept: "emoji", + canDrop() { + return category === "FAVORITES"; + }, + collect: monitor => ({ + canDrop: !!monitor.canDrop(), + isOver: !!monitor.isOver(), + }), + drop(item: { id: string; }) { + const source = item.id; + const target = emoji?.uniqueName ?? emoji?.id; + function update(this: { source: string; target: string; }, e: { emojis: string[]; }) { + if (this.source === this.target) { + return false; + } + const sourceIndex = e.emojis.findIndex(emoji => emoji === this.source); + const targetIndex = e.emojis.findIndex(emoji => emoji === this.target); + // Adjust final index to account for removal of source emoji + const finalIndex = targetIndex < sourceIndex ? targetIndex : targetIndex - 1; + if (sourceIndex === finalIndex) { + return false; + } + e.emojis.splice(sourceIndex, 1); + e.emojis.splice(finalIndex, 0, this.source); + } + UserSettingsActionCreators.FrecencyUserSettingsActionCreators.updateAsync("favoriteEmojis", update.bind({ source, target }), UserSettingsDelay.INFREQUENT_USER_ACTION); + } + }), [emoji]); + }, + dragItem() { + return ( + + ); + }, + wrapper(emoji: EmojiDescriptor) { + const [collected, drop] = this.drop(emoji); + const ref: React.RefObject = useRef(null); + useLayoutEffect(() => { + if (emoji?.category !== "FAVORITES") return; + const frame = requestAnimationFrame(() => { + if (!ref.current) { + return; + } + if (collected.canDrop) { + ref.current.classList.add(dndCls.autoPointerEvents); + return; + } + ref.current.classList.remove(dndCls.autoPointerEvents); + } + ); + return () => cancelAnimationFrame(frame); + }, [collected, ref]); + + if (emoji?.category !== "FAVORITES") return; + + return ( + + ); + } +}); diff --git a/src/equicordplugins/dragFavoriteEmotes/styles.css b/src/equicordplugins/dragFavoriteEmotes/styles.css new file mode 100644 index 000000000..e55b5da9f --- /dev/null +++ b/src/equicordplugins/dragFavoriteEmotes/styles.css @@ -0,0 +1,25 @@ +.vc-drag-favorite-emotes-indicator::before { + content: ""; + position: absolute; + z-index: 1; + top: 4px; + right: 0; + left: 2px; + height: calc(100% - (var(--custom-emoji-picker-constants-emoji-container-padding-vertical) * 2)); + width: 4px; + transform: translateX(-4px); + background-color: var(--green-360); + border-radius: 2px; +} + +.vc-drag-favorite-emotes-wrapper { + inset: 0; + flex-direction: row; +} + +.vc-drag-favorite-emotes-item { + background-size: 40px; + height: 40px; + width: 40px; + display: block; +} \ No newline at end of file diff --git a/src/utils/constants.ts b/src/utils/constants.ts index 5b9b20f69..448ce2b8d 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -1233,6 +1233,14 @@ export const EquicordDevs = Object.freeze({ name: "Key", id: 1230319937155760131n }, + PWall: { + name: "PWall", + id: 0n + }, + busyboxkitty: { + name: "busyboxkitty", + id: 1312185484159483948n + }, } satisfies Record); // iife so #__PURE__ works correctly