Merge remote-tracking branch 'upstream/dev' into dev

This commit is contained in:
thororen1234 2026-07-15 21:37:37 -04:00
commit a0004a5b49
No known key found for this signature in database
9 changed files with 345 additions and 28 deletions

View file

@ -1,7 +1,7 @@
{
"name": "equicord",
"private": "true",
"version": "1.14.15.2",
"version": "1.14.16.0",
"description": "The other cutest Discord client mod",
"homepage": "https://github.com/Equicord/Equicord#readme",
"bugs": {

View file

@ -12,6 +12,7 @@ type PolicyMap = Record<string, string[]>;
export const ConnectSrc = ["connect-src"];
export const ImageSrc = [...ConnectSrc, "img-src"];
export const CssSrc = ["style-src", "font-src"];
export const ImageAndMediaSrc = [...ImageSrc, "media-src"];
export const ImageAndCssSrc = [...ImageSrc, ...CssSrc];
export const ImageScriptsAndCssSrc = [...ImageAndCssSrc, "script-src", "worker-src"];
export const CSPSrc = ["style-src", "connect-src", "img-src", "frame-src", "font-src", "media-src", "worker-src"];
@ -41,7 +42,6 @@ export const CspPolicies: PolicyMap = {
"i.imgur.com": ImageSrc, // Imgur, used by some themes
"i.ibb.co": ImageSrc, // ImgBB, used by some themes
"i.pinimg.com": ImageSrc, // Pinterest, used by some themes
"*.tenor.com": ImageSrc, // Tenor, used by some themes
"files.catbox.moe": ImageAndCssSrc, // Catbox, used by some themes
"cdn.discordapp.com": ImageAndCssSrc, // Discord CDN, used by Vencord and some themes to load media
@ -64,6 +64,10 @@ export const CspPolicies: PolicyMap = {
"dearrow-thumb.ajay.app": ImageSrc, // Dearrow Thumbnail CDN
"usrbg.is-hardly.online": ImageSrc, // USRBG API
"icons.duckduckgo.com": ImageSrc, // DuckDuckGo Favicon API (Reverse Image Search)
// Tenor, used by TenorSearch plugin and some themes
"*.tenor.com": ImageAndMediaSrc,
"*.tenor.co": ImageAndMediaSrc,
};
const findHeader = (headers: PolicyMap, headerName: Lowercase<string>) => {

View file

@ -219,8 +219,8 @@ export default definePlugin({
// If we are rendering the normal GuildsBar sidebar, we make Discord think the folder is always collapsed to show better icons (the mini guild icons) and avoid transitions
{
predicate: () => settings.store.keepIcons,
match: /(?<=let ?(?:\i,)*?{folderNode:\i,setNodeRef:\i,.+?expanded:(\i),.+?;)(?=let)/,
replace: (_, isExpanded) => `${isExpanded}=!!arguments[0]?.isBetterFolders&&${isExpanded};`
match: /let ?(?:\i,)*?{folderNode:\i,setNodeRef:\i,.+?expanded:(\i),.+?;(?=let)/,
replace: (m, isExpanded) => `${m}${isExpanded}=!!arguments[0]?.isBetterFolders&&${isExpanded};`
},
// Disable expanding and collapsing folders transition in the normal GuildsBar sidebar
{

View file

@ -110,8 +110,8 @@ export default definePlugin({
find: "this.renderArtisanalHack()",
replacement: [
{
match: /class (\i)(?= extends \i\.PureComponent.+?static contextType=.+?jsx\)\(\1,\{mode:)/,
replace: "var $1=$self.Layer;class VencordPatchedOldFadeLayer",
match: /class (\i)( extends \i\.PureComponent.+?jsx\)\(\1,\{mode:)/,
replace: "var $1=$self.Layer;class VencordPatchedOldFadeLayer$2",
predicate: () => settings.store.disableFade
},
{ // Lazy-load contents

View file

@ -20,9 +20,9 @@ import { getIntlMessage } from "@utils/discord";
import { Logger } from "@utils/Logger";
import { classes } from "@utils/misc";
import definePlugin, { OptionType } from "@utils/types";
import { Message } from "@vencord/discord-types";
import { Message, MessageAttachment } from "@vencord/discord-types";
import { findCssClassesLazy } from "@webpack";
import { ChannelStore, FluxDispatcher, Menu, MessageStore, Parser, SelectedChannelStore, Timestamp, UserStore, useStateFromStores } from "@webpack/common";
import { AuthenticationStore, ChannelStore, FluxDispatcher, Menu, MessageStore, Parser, SelectedChannelStore, Timestamp, UserStore, useStateFromStores } from "@webpack/common";
import overlayStyle from "./deleteStyleOverlay.css?managed";
import textStyle from "./deleteStyleText.css?managed";
@ -36,6 +36,15 @@ interface MLMessage extends Message {
diffViewDisabled?: boolean;
}
interface MLAttachment extends MessageAttachment {
/**
* if the attachment was deleted
*
* a non-deleted {@link MLMessage|Message} can have deleted attachments
*/
deleted?: boolean;
}
const MessageClasses = findCssClassesLazy("edited", "communicationDisabled", "isSystemMessage");
// track messages where the user disabled diffs for this session
@ -418,7 +427,7 @@ export default definePlugin({
name: "MessageLogger",
description: "Temporarily logs deleted and edited messages.",
tags: ["Chat", "Utility"],
authors: [Devs.rushii, Devs.Ven, Devs.AutumnVN, Devs.Nickyux, Devs.Kyuuhachi, EquicordDevs.justjxke],
authors: [Devs.rushii, Devs.Ven, Devs.AutumnVN, Devs.Nickyux, Devs.Kyuuhachi, Devs.sadan, EquicordDevs.justjxke],
dependencies: ["MessageUpdaterAPI"],
isModified: true,
settings,
@ -550,11 +559,29 @@ export default definePlugin({
};
},
handleDelete(
cache: any,
data: { ids: string[]; id: string; mlDeleted?: boolean; },
isBulk: boolean,
) {
handleUpdateAttachments(newMessage: MLMessage): MLAttachment[] {
const oldMessage = MessageStore.getMessage(newMessage.channel_id, newMessage.id) as MLMessage | undefined;
// if oldMessage is undefined, this is a new message and we shouldn't touch the attachments
if (!oldMessage || this.shouldIgnore(newMessage, true)) {
return newMessage.attachments;
}
// not sure if it's ever actually null after an edit but discord does a null check here
if (!newMessage.attachments?.length) {
return oldMessage.attachments.map((a): MLAttachment => ({ ...a, deleted: true }));
}
const attachments: MLAttachment[] = [];
for (const oldAttachment of oldMessage.attachments) {
const wasDeleted = newMessage.attachments.every(a => a.id !== oldAttachment.id);
if (wasDeleted) {
attachments.push({ ...oldAttachment, deleted: true });
} else {
attachments.push(oldAttachment);
}
}
return attachments;
},
handleDelete(cache: any, data: { ids: string[], id: string; mlDeleted?: boolean; }, isBulk: boolean) {
try {
if (cache == null || (!isBulk && !cache.has(data.id))) return cache;
@ -624,6 +651,24 @@ export default definePlugin({
}
},
// It is possible to replace a message in place by creating a new message with the same nonce as an existing one.
// This is not considered an edit since it's a new message. Thus it bypasses our edit logging and can be used to "delete" a message by replacing it with an empty one.
// This fixes that bypass
normalizeNonce(msg: Message) {
try {
if (!msg.nonce || msg.author.id === AuthenticationStore.getId()) return;
const prevMsg = MessageStore.getMessage(msg.channel_id, msg.nonce);
if (!prevMsg || prevMsg.state !== "SENT") return;
if (prevMsg.id !== msg.id) {
delete msg.nonce;
}
} catch (e) {
console.error("[MessageLogger] Error normalizing nonce");
}
},
EditMarker({ message, className, children, ...props }: any) {
return (
<span
@ -728,17 +773,21 @@ export default definePlugin({
},
{
// Updated message transformer(?)
// Updated message transformer
find: ".PREMIUM_REFERRAL&&(",
replacement: [
{
// Pass through editHistory & deleted & original attachments to the "edited message" transformer
match:
/(?<=null!=\i\.edited_timestamp\)return )\i\(\i,\{reactions:(\i)\.reactions.{0,50}\}\)/,
// Pass through editHistory & deleted to the "edited message" transformer
match: /(?<=null!=\i\.edited_timestamp\)return )\i\(\i,\{reactions:(\i)\.reactions.{0,50}\}\)/,
replace:
"Object.assign($&,{ deleted:$1.deleted, editHistory:$1.editHistory, firstEditTimestamp:$1.firstEditTimestamp, diffViewDisabled:$1.diffViewDisabled })",
},
],
// just mark deleted attachments as deleted on MESSAGE_UPDATE
{
match: /attachments:(\i)\.attachments\?\?\[\],/,
replace: "attachments: $self.handleUpdateAttachments($1),"
}
]
},
{
@ -751,8 +800,9 @@ export default definePlugin({
match: /\.SPOILER,(?=\[\i\.\i\]:)(?<=item:(\i),.{0,200}?)/,
replace: '$&"messagelogger-deleted-attachment": $1?.originalItem?.deleted,'
},
// dont allow deleting attachments from deleted messages
{
match: /(?<=function \i\(\i\)\{let\{[^}]*?item:(\i),\i:\i,)canRemoveItem:(\i)(?=,onRemoveItem:)/,
match: /(?<=\{let\{[^}]*?item:(\i),autoPlayGif:\i,)canRemoveItem:(\i)(?=,onRemoveItem:)/,
replace: "_canRemoveItem:$2 = arguments[0].canRemoveItem && !$1?.originalItem?.deleted",
}
]
@ -838,7 +888,15 @@ export default definePlugin({
replace: '$&$1.type==="MESSAGE_GROUP_DELETED"?$2=$self.DELETED_MESSAGE_COUNT:',
},
],
predicate: () => settings.store.collapseDeleted,
predicate: () => settings.store.collapseDeleted
},
],
{
find: "this.truncateTop",
replacement: {
match: /receiveMessage\((\i)\)\{/,
replace: "$& $self.normalizeNonce($1);"
}
}
]
});

View file

@ -2,8 +2,10 @@
display: none;
}
.messagelogger-deleted :is(.messagelogger-deleted-attachment,
.emoji,
/* non-deleted messages can have deleted attachments */
.messagelogger-deleted-attachment,
.messagelogger-deleted :is(.emoji,
[data-type="sticker"],
[class*="embedIframe"],
[class*="embedSpotify"],
@ -107,4 +109,4 @@
.messagelogger-diff-disabled .messagelogger-diff-removed [class*="emojiContainer"]::after,
.messagelogger-diff-disabled .messagelogger-diff-removed img.emoji::after {
display: none !important;
}
}

View file

@ -1,4 +1,4 @@
.vc-reply-timestamp {
width: fit-content !important;
margin-right: 0.25em;
}
margin-inline-end: 0.5rem;
width: unset !important;
}

View file

@ -0,0 +1,249 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2026 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { Devs } from "@utils/constants";
import { isNonNullish } from "@utils/guards";
import definePlugin from "@utils/types";
import { FluxDispatcher, LocaleStore } from "@webpack/common";
// API key is taken from the GBoard app on iOS
const TENOR_KEY = "3Z0688EVWYKH";
let cachedCategories: TrendingCategories | null = null;
interface TenorMedia {
url: string;
preview: string;
dims: [number, number];
}
interface TenorResult {
id: string;
media: Array<Record<string, TenorMedia>>;
itemurl: string;
}
interface TenorCategoryTag {
searchterm: string;
image: string;
}
interface DiscordGif {
id: string;
title: string;
url: string;
src: string;
gif_src: string;
width: number;
height: number;
preview: string;
}
interface TrendingCategories {
trendingCategories: Record<"name" | "src", string>[];
trendingGIFPreview: { src: string; };
}
function toDiscordGif(item: TenorResult): DiscordGif | null {
const { gif, webm } = item.media[0];
return {
id: item.id,
title: "", // discord always returns a blank string
url: item.itemurl,
gif_src: gif.url,
src: webm.url,
width: webm.dims[0],
height: webm.dims[1],
preview: webm.preview
};
}
function mapToDiscordGifs(items: TenorResult[]) {
return items.map(toDiscordGif).filter(isNonNullish);
}
async function tenorFetch<TResult>(path: string, params: Record<string, string>) {
const url = `https://api.tenor.com/v1${path}?` + new URLSearchParams({
key: TENOR_KEY,
locale: LocaleStore.locale.replace("-", "_").toLowerCase(),
...params
});
const res = await fetch(url);
if (!res.ok)
throw new Error(`GET ${path}: Tenor API request failed with status ${res.status}`);
return res.json() as Promise<TResult>;
}
// function contributed by taep96
async function fetchTenorResults(path: string, limit: number, extra: Record<string, string> = {}) {
const pageSize = Math.min(limit, 50);
const items: TenorResult[] = [];
const seen = new Set<string>();
let pos = "";
while (items.length < limit) {
const params: Record<string, string> = {
...extra,
limit: String(Math.min(limit - items.length, pageSize))
};
if (pos) params.pos = pos;
const { next, results: page } = await tenorFetch<{ next?: string; results: TenorResult[]; }>(path, params);
if (!page.length) break;
const previousLength = items.length;
for (const item of page) {
if (seen.has(item.id)) continue;
seen.add(item.id);
items.push(item);
if (items.length >= limit) break;
}
if (items.length === previousLength) break;
if (!next || next === pos) break;
pos = next;
}
return items;
}
async function fetchCategories(): Promise<TrendingCategories | null> {
return tenorFetch<{ tags?: TenorCategoryTag[]; }>("/categories", { type: "featured" })
.then(({ tags }) => {
if (!tags?.length) return null;
return {
trendingCategories: tags.map(t => ({ name: t.searchterm, src: t.image })),
trendingGIFPreview: { src: tags[0].image }
};
})
.catch(() => null);
}
export default definePlugin({
name: "TenorGifSearch",
description: "Restore Tenor GIF search",
authors: [Devs.Lunascape],
patches: [
{
find: "renderHeaderContent()",
replacement: {
match: /placeholder:(\i),"aria-label":(\i)/,
replace: 'placeholder:$1?.replace(/Giphy|Klipy/gi,"Tenor"),"aria-label":$2?.replace(/Giphy|Klipy/gi,"Tenor")'
}
},
{
find: '"GIF_PICKER_TRENDING_FETCH_SUCCESS",trendingCategories:',
replacement: [
{
match: /let \i=Date\.now\(\);\i\([^)]+\),\i\.\i\.get\(\{url:\i\.\i\.GIFS_SEARCH,query:\{q:(\i),/,
replace: "return $self.handleSearchFetch($1);$&"
},
{
match: /""!==(\i)&&null!=\1&&\i\.\i\.get\(\{url:\i\.\i\.GIFS_SUGGEST,/,
replace: "return $self.handleSuggestionsFetch($1);$&"
},
{
match: /\i\.\i\.get\(\{url:\i\.\i\.GIFS_TRENDING,/,
replace: "return $self.handleTrendingFetch();$&"
},
{
match: /let \i=Date\.now\(\);\i\([^)]+\),\i\.\i\.get\(\{url:\i\.\i\.GIFS_TRENDING_GIFS,/,
replace: "return $self.handleTrendingGifsFetch();$&"
},
{
match: /\i\.\i\.post\(\{url:\i\.\i\.GIFS_SELECT,body:\{id:(\i),q:(\i),provider:\i\}/,
replace: "return $self.handleGifSelect($1,$2);$&"
}
]
},
{
find: '"IntegrationQueryStore"',
replacement: {
match: /(?<=search\((\i),(\i)\)\{)null==\i\.getResults\(\1,\2\)&&/,
replace: "return $self.tenorIntegrationSearch($1,$2);null==void 0&&"
}
}
],
async start() {
cachedCategories = await fetchCategories() ?? cachedCategories;
},
handleSearchFetch(query: string) {
// discord has a 100 result limit for normal search
fetchTenorResults("/search", 100, { q: query })
.then(results => {
const items = mapToDiscordGifs(results);
FluxDispatcher.dispatch(
items.length
? { type: "GIF_PICKER_QUERY_SUCCESS", query, items }
: { type: "GIF_PICKER_QUERY_FAILURE", query }
);
})
.catch(() => {
FluxDispatcher.dispatch({ type: "GIF_PICKER_QUERY_FAILURE", query });
});
},
async handleSuggestionsFetch(query: string) {
if (!query) return;
const { results } = await tenorFetch<{ results?: string[]; }>("/search_suggestions", { q: query, limit: "5" });
FluxDispatcher.dispatch({ type: "GIF_PICKER_SUGGESTIONS_SUCCESS", query, items: results });
},
async handleTrendingFetch() {
if (!cachedCategories) {
cachedCategories = await fetchCategories();
if (!cachedCategories) return;
}
FluxDispatcher.dispatch({ type: "GIF_PICKER_TRENDING_FETCH_SUCCESS", ...cachedCategories });
},
handleGifSelect(id: string, query: string) {
tenorFetch("/registershare", { id, q: query });
},
handleTrendingGifsFetch() {
fetchTenorResults("/trending", 50)
.then(results => {
const items = mapToDiscordGifs(results);
FluxDispatcher.dispatch(
items.length
? { type: "GIF_PICKER_QUERY_SUCCESS", items }
: { type: "GIF_PICKER_QUERY_FAILURE" }
);
})
.catch(() => {
FluxDispatcher.dispatch({ type: "GIF_PICKER_QUERY_FAILURE" });
});
},
tenorIntegrationSearch(integration: string, query: string) {
FluxDispatcher.dispatch({ type: "INTEGRATION_QUERY", integration, query });
fetchTenorResults("/search", 20, { q: query })
.then(results => {
const items = mapToDiscordGifs(results);
FluxDispatcher.dispatch(
items.length
? { type: "INTEGRATION_QUERY_SUCCESS", integration, query, results: items }
: { type: "INTEGRATION_QUERY_FAILURE", integration, query }
);
})
.catch(() => {
FluxDispatcher.dispatch({ type: "INTEGRATION_QUERY_FAILURE", integration, query, results: [] });
});
}
});

View file

@ -666,6 +666,10 @@ export const Devs = /* #__PURE__*/ Object.freeze({
name: "NightmareSan",
id: 304239816466235392n
},
Lunascape: {
name: "Lunascape",
id: 383365021415243776n
}
} satisfies Record<string, Dev>);
export const EquicordDevs = Object.freeze({