New plugin CopyStickerLinks: adds Copy/Open Link option to stickers (#3191)
Co-authored-by: V <vendicated@riseup.net>
This commit is contained in:
parent
7e028267f1
commit
4403aee3c1
13 changed files with 188 additions and 69 deletions
5
src/plugins/copyStickerLinks/README.MD
Normal file
5
src/plugins/copyStickerLinks/README.MD
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
# CopyStickerLinks
|
||||
|
||||
Adds "Copy Link" and "Open Link" options to the context menu of stickers!
|
||||
|
||||

|
||||
94
src/plugins/copyStickerLinks/index.tsx
Normal file
94
src/plugins/copyStickerLinks/index.tsx
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
/*
|
||||
* Vencord, a modification for Discord's desktop app
|
||||
* Copyright (c) 2025 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { findGroupChildrenByChildId, NavContextMenuPatchCallback } from "@api/ContextMenu";
|
||||
import { Devs } from "@utils/constants";
|
||||
import { copyWithToast } from "@utils/misc";
|
||||
import definePlugin from "@utils/types";
|
||||
import { Message, Sticker } from "@vencord/discord-types";
|
||||
import { Menu, React, StickersStore } from "@webpack/common";
|
||||
import ExpressionClonerPlugin from "plugins/expressionCloner";
|
||||
|
||||
const StickerExt = [, "png", "png", "json", "gif"] as const;
|
||||
|
||||
type PartialSticker = Pick<Sticker, "id" | "format_type">;
|
||||
|
||||
function getUrl(data: PartialSticker): string {
|
||||
if (data.format_type === 4)
|
||||
return `https:${window.GLOBAL_ENV.MEDIA_PROXY_ENDPOINT}/stickers/${data.id}.gif?size=512&lossless=true`;
|
||||
|
||||
return `https://${window.GLOBAL_ENV.CDN_HOST}/stickers/${data.id}.${StickerExt[data.format_type]}?size=512&lossless=true`;
|
||||
}
|
||||
|
||||
function buildMenuItem(sticker: PartialSticker, addBottomSeparator: boolean) {
|
||||
return (
|
||||
<>
|
||||
<Menu.MenuGroup>
|
||||
<Menu.MenuItem
|
||||
id="vc-copy-sticker-link"
|
||||
key="vc-copy-sticker-link"
|
||||
label="Copy Link"
|
||||
action={() => copyWithToast(getUrl(sticker), "Link copied!")}
|
||||
/>
|
||||
|
||||
<Menu.MenuItem
|
||||
id="vc-open-sticker-link"
|
||||
key="vc-open-sticker-link"
|
||||
label="Open Link"
|
||||
action={() => VencordNative.native.openExternal(getUrl(sticker))}
|
||||
/>
|
||||
</Menu.MenuGroup>
|
||||
{addBottomSeparator && <Menu.MenuSeparator />}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const messageContextMenuPatch: NavContextMenuPatchCallback = (
|
||||
children,
|
||||
{ favoriteableId, favoriteableType, message }: { favoriteableId: string; favoriteableType: string; message: Message; }
|
||||
) => {
|
||||
if (!favoriteableId || favoriteableType !== "sticker") return;
|
||||
|
||||
const sticker = message.stickerItems.find(s => s.id === favoriteableId);
|
||||
if (!sticker?.format_type) return;
|
||||
|
||||
const idx = children.findIndex(c => Array.isArray(c) && findGroupChildrenByChildId("vc-copy-sticker-url", c) != null);
|
||||
|
||||
children.splice(idx, 0, buildMenuItem(sticker, idx !== -1));
|
||||
};
|
||||
|
||||
const expressionPickerPatch: NavContextMenuPatchCallback = (children, props: { target: HTMLElement; }) => {
|
||||
const id = props?.target?.dataset?.id;
|
||||
if (!id) return;
|
||||
if (props.target.className?.includes("lottieCanvas")) return;
|
||||
|
||||
const sticker = StickersStore.getStickerById(id);
|
||||
if (sticker) {
|
||||
children.push(buildMenuItem(sticker, Vencord.Plugins.isPluginEnabled(ExpressionClonerPlugin.name)));
|
||||
}
|
||||
};
|
||||
|
||||
export default definePlugin({
|
||||
name: "CopyStickerLinks",
|
||||
description: "Adds the ability to copy & open Sticker links",
|
||||
authors: [Devs.Ven, Devs.Byeoon],
|
||||
contextMenus: {
|
||||
"message": messageContextMenuPatch,
|
||||
"expression-picker": expressionPickerPatch
|
||||
}
|
||||
});
|
||||
|
|
@ -25,25 +25,17 @@ import { Logger } from "@utils/Logger";
|
|||
import { Margins } from "@utils/margins";
|
||||
import { ModalContent, ModalHeader, ModalRoot, openModalLazy } from "@utils/modal";
|
||||
import definePlugin from "@utils/types";
|
||||
import { Guild } from "@vencord/discord-types";
|
||||
import { findByCodeLazy, findStoreLazy } from "@webpack";
|
||||
import { Constants, EmojiStore, FluxDispatcher, Forms, GuildStore, IconUtils, Menu, PermissionsBits, PermissionStore, React, RestAPI, Toasts, Tooltip, UserStore } from "@webpack/common";
|
||||
import { Guild, GuildSticker } from "@vencord/discord-types";
|
||||
import { findByCodeLazy } from "@webpack";
|
||||
import { Constants, EmojiStore, FluxDispatcher, Forms, GuildStore, IconUtils, Menu, PermissionsBits, PermissionStore, React, RestAPI, StickersStore, Toasts, Tooltip, UserStore } from "@webpack/common";
|
||||
import { Promisable } from "type-fest";
|
||||
|
||||
const StickersStore = findStoreLazy("StickersStore");
|
||||
const uploadEmoji = findByCodeLazy(".GUILD_EMOJIS(", "EMOJI_UPLOAD_START");
|
||||
|
||||
const getGuildMaxEmojiSlots = findByCodeLazy(".additionalEmojiSlots") as (guild: Guild) => number;
|
||||
|
||||
interface Sticker {
|
||||
interface Sticker extends GuildSticker {
|
||||
t: "Sticker";
|
||||
description: string;
|
||||
format_type: number;
|
||||
guild_id: string;
|
||||
id: string;
|
||||
name: string;
|
||||
tags: string;
|
||||
type: number;
|
||||
}
|
||||
|
||||
interface Emoji {
|
||||
|
|
|
|||
|
|
@ -24,17 +24,12 @@ import { getCurrentGuild, getEmojiURL } from "@utils/discord";
|
|||
import { Logger } from "@utils/Logger";
|
||||
import definePlugin, { OptionType, Patch } from "@utils/types";
|
||||
import type { Emoji, Message } from "@vencord/discord-types";
|
||||
import { StickerFormatType } from "@vencord/discord-types/enums";
|
||||
import { findByCodeLazy, findByPropsLazy, findStoreLazy, proxyLazyWebpack } from "@webpack";
|
||||
import { Alerts, ChannelStore, DraftType, EmojiStore, FluxDispatcher, Forms, GuildMemberStore, lodash, Parser, PermissionsBits, PermissionStore, UploadHandler, UserSettingsActionCreators, UserStore } from "@webpack/common";
|
||||
import { Alerts, ChannelStore, DraftType, EmojiStore, FluxDispatcher, Forms, GuildMemberStore, lodash, Parser, PermissionsBits, PermissionStore, StickersStore, UploadHandler, UserSettingsActionCreators, UserStore } from "@webpack/common";
|
||||
import { applyPalette, GIFEncoder, quantize } from "gifenc";
|
||||
import type { ReactElement, ReactNode } from "react";
|
||||
|
||||
const StickerStore = findStoreLazy("StickersStore") as {
|
||||
getPremiumPacks(): StickerPack[];
|
||||
getAllGuildStickers(): Map<string, Sticker[]>;
|
||||
getStickerById(id: string): Sticker | undefined;
|
||||
};
|
||||
|
||||
const UserSettingsProtoStore = findStoreLazy("UserSettingsProtoStore");
|
||||
|
||||
const BINARY_READ_OPTIONS = findByPropsLazy("readerFactory");
|
||||
|
|
@ -70,41 +65,6 @@ const enum EmojiIntentions {
|
|||
|
||||
const IS_BYPASSEABLE_INTENTION = `[${EmojiIntentions.CHAT},${EmojiIntentions.GUILD_STICKER_RELATED_EMOJI}].includes(fakeNitroIntention)`;
|
||||
|
||||
const enum StickerType {
|
||||
PNG = 1,
|
||||
APNG = 2,
|
||||
LOTTIE = 3,
|
||||
// don't think you can even have gif stickers but the docs have it
|
||||
GIF = 4
|
||||
}
|
||||
|
||||
interface BaseSticker {
|
||||
available: boolean;
|
||||
description: string;
|
||||
format_type: number;
|
||||
id: string;
|
||||
name: string;
|
||||
tags: string;
|
||||
type: number;
|
||||
}
|
||||
interface GuildSticker extends BaseSticker {
|
||||
guild_id: string;
|
||||
}
|
||||
interface DiscordSticker extends BaseSticker {
|
||||
pack_id: string;
|
||||
}
|
||||
type Sticker = GuildSticker | DiscordSticker;
|
||||
|
||||
interface StickerPack {
|
||||
id: string;
|
||||
name: string;
|
||||
sku_id: string;
|
||||
description: string;
|
||||
cover_sticker_id: string;
|
||||
banner_asset_id: string;
|
||||
stickers: Sticker[];
|
||||
}
|
||||
|
||||
const enum FakeNoticeType {
|
||||
Sticker,
|
||||
Emoji
|
||||
|
|
@ -549,8 +509,8 @@ export default definePlugin({
|
|||
|
||||
const gifMatch = child.props.href.match(fakeNitroGifStickerRegex);
|
||||
if (gifMatch) {
|
||||
// There is no way to differentiate a regular gif attachment from a fake nitro animated sticker, so we check if the StickerStore contains the id of the fake sticker
|
||||
if (StickerStore.getStickerById(gifMatch[1])) return null;
|
||||
// There is no way to differentiate a regular gif attachment from a fake nitro animated sticker, so we check if the StickersStore contains the id of the fake sticker
|
||||
if (StickersStore.getStickerById(gifMatch[1])) return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -638,7 +598,7 @@ export default definePlugin({
|
|||
url = new URL(item);
|
||||
} catch { }
|
||||
|
||||
const stickerName = StickerStore.getStickerById(imgMatch[1])?.name ?? url?.searchParams.get("name") ?? "FakeNitroSticker";
|
||||
const stickerName = StickersStore.getStickerById(imgMatch[1])?.name ?? url?.searchParams.get("name") ?? "FakeNitroSticker";
|
||||
stickers.push({
|
||||
format_type: 1,
|
||||
id: imgMatch[1],
|
||||
|
|
@ -651,9 +611,9 @@ export default definePlugin({
|
|||
|
||||
const gifMatch = item.match(fakeNitroGifStickerRegex);
|
||||
if (gifMatch) {
|
||||
if (!StickerStore.getStickerById(gifMatch[1])) continue;
|
||||
if (!StickersStore.getStickerById(gifMatch[1])) continue;
|
||||
|
||||
const stickerName = StickerStore.getStickerById(gifMatch[1])?.name ?? "FakeNitroSticker";
|
||||
const stickerName = StickersStore.getStickerById(gifMatch[1])?.name ?? "FakeNitroSticker";
|
||||
stickers.push({
|
||||
format_type: 2,
|
||||
id: gifMatch[1],
|
||||
|
|
@ -689,8 +649,8 @@ export default definePlugin({
|
|||
|
||||
const gifMatch = url.match(fakeNitroGifStickerRegex);
|
||||
if (gifMatch) {
|
||||
// There is no way to differentiate a regular gif attachment from a fake nitro animated sticker, so we check if the StickerStore contains the id of the fake sticker
|
||||
if (StickerStore.getStickerById(gifMatch[1])) return true;
|
||||
// There is no way to differentiate a regular gif attachment from a fake nitro animated sticker, so we check if the StickersStore contains the id of the fake sticker
|
||||
if (StickersStore.getStickerById(gifMatch[1])) return true;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -710,8 +670,8 @@ export default definePlugin({
|
|||
|
||||
const match = attachment.url.match(fakeNitroGifStickerRegex);
|
||||
if (match) {
|
||||
// There is no way to differentiate a regular gif attachment from a fake nitro animated sticker, so we check if the StickerStore contains the id of the fake sticker
|
||||
if (StickerStore.getStickerById(match[1])) return false;
|
||||
// There is no way to differentiate a regular gif attachment from a fake nitro animated sticker, so we check if the StickersStore contains the id of the fake sticker
|
||||
if (StickersStore.getStickerById(match[1])) return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
|
|
@ -866,7 +826,7 @@ export default definePlugin({
|
|||
if (!s.enableStickerBypass)
|
||||
break stickerBypass;
|
||||
|
||||
const sticker = StickerStore.getStickerById(extra.stickers?.[0]!);
|
||||
const sticker = StickersStore.getStickerById(extra.stickers?.[0]!);
|
||||
if (!sticker)
|
||||
break stickerBypass;
|
||||
|
||||
|
|
@ -883,11 +843,11 @@ export default definePlugin({
|
|||
// but will give us a normal non animated png for no reason
|
||||
// TODO: Remove this workaround when it's not needed anymore
|
||||
let link = this.getStickerLink(sticker.id);
|
||||
if (sticker.format_type === StickerType.GIF && link.includes(".png")) {
|
||||
if (sticker.format_type === StickerFormatType.GIF && link.includes(".png")) {
|
||||
link = link.replace(".png", ".gif");
|
||||
}
|
||||
|
||||
if (sticker.format_type === StickerType.APNG) {
|
||||
if (sticker.format_type === StickerFormatType.APNG) {
|
||||
if (!hasAttachmentPerms(channelId)) {
|
||||
Alerts.show({
|
||||
title: "Hold on!",
|
||||
|
|
|
|||
|
|
@ -482,7 +482,7 @@ export const Devs = /* #__PURE__*/ Object.freeze({
|
|||
name: "Sqaaakoi",
|
||||
id: 259558259491340288n
|
||||
},
|
||||
Byron: {
|
||||
Byeoon: {
|
||||
name: "byeoon",
|
||||
id: 1167275288036655133n
|
||||
},
|
||||
|
|
|
|||
|
|
@ -49,11 +49,11 @@ export let TypingStore: t.TypingStore;
|
|||
export let RelationshipStore: t.RelationshipStore;
|
||||
|
||||
export let EmojiStore: t.EmojiStore;
|
||||
export let StickersStore: t.StickersStore;
|
||||
export let ThemeStore: t.ThemeStore;
|
||||
export let WindowStore: t.WindowStore;
|
||||
export let DraftStore: t.DraftStore;
|
||||
|
||||
|
||||
/**
|
||||
* @see jsdoc of {@link t.useStateFromStores}
|
||||
*/
|
||||
|
|
@ -77,6 +77,7 @@ waitForStore("GuildRoleStore", m => GuildRoleStore = m);
|
|||
waitForStore("MessageStore", m => MessageStore = m);
|
||||
waitForStore("WindowStore", m => WindowStore = m);
|
||||
waitForStore("EmojiStore", m => EmojiStore = m);
|
||||
waitForStore("StickersStore", m => StickersStore = m);
|
||||
waitForStore("TypingStore", m => TypingStore = m);
|
||||
waitForStore("ThemeStore", m => {
|
||||
ThemeStore = m;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue