Merge branch 'dev'

This commit is contained in:
thororen1234 2025-09-06 10:59:23 -04:00
commit 50bd6b238b
No known key found for this signature in database
24 changed files with 498 additions and 209 deletions

View file

@ -1,3 +0,0 @@
# AutoJump
Jumps to the first message in new messages for a channel

View file

@ -1,69 +0,0 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { NavContextMenuPatchCallback } from "@api/ContextMenu";
import { definePluginSettings } from "@api/Settings";
import { EquicordDevs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types";
import { ChannelStore, Menu, MessageActions, NavigationRouter } from "@webpack/common";
interface ChannelSelectEvent {
type: "CHANNEL_SELECT";
channelId: string | null;
guildId: string | null;
}
let lastChannelId = "0";
function autoJump({ guild_id, id: channelId }) {
const guildId = guild_id ?? "@me";
lastChannelId = channelId;
NavigationRouter.transitionTo(`/channels/${guildId}/${channelId}`);
MessageActions.jumpToPresent(channelId, { limit: null });
}
const MenuPatch: NavContextMenuPatchCallback = (children, { channel }) => {
children.push(
<Menu.MenuItem
id="auto-jump"
label="Jump to Last Message"
action={() => {
autoJump(channel);
}}
/>
);
};
const settings = definePluginSettings({
autoJumping: {
type: OptionType.BOOLEAN,
description: "Automatically jump to the last message in the channel when switching channels",
default: false
}
});
export default definePlugin({
name: "AutoJump",
description: "Jumps to Last Message in Channel",
authors: [EquicordDevs.omaw],
settings,
contextMenus: {
"channel-context": MenuPatch,
"user-context": MenuPatch,
"thread-context": MenuPatch
},
flux: {
async CHANNEL_SELECT({ guildId, channelId }: ChannelSelectEvent) {
if (!settings.store.autoJumping || !channelId) return;
const channel = ChannelStore.getChannel(channelId);
if (!channel || channel.id === lastChannelId) return;
autoJump({ guild_id: guildId, id: channelId });
}
}
});

View file

@ -80,8 +80,8 @@ export default definePlugin({
{
find: "(this,\"handleMessageClick\"",
replacement: {
match: /(?<=(\i)\.isSearchHit\));(?=null!=(\i))/,
replace: ";if ($1.ctrlKey) return $self.open($2);"
match: /(\i)\.stopPropagation.{0,50}(?=null!=(\i))/,
replace: "$&if ($1.ctrlKey) return $self.open($2);"
}
},
// prevent issues with the pins/inbox popouts being too tall

View file

@ -8,7 +8,7 @@ import "./styles.css";
import { ChatBarButton } from "@api/ChatButtons";
import { DataStore } from "@api/index";
import { definePluginSettings, Settings } from "@api/Settings";
import { definePluginSettings, migratePluginSettings, Settings } from "@api/Settings";
import { EquicordDevs } from "@utils/constants";
import { getCurrentChannel, sendMessage } from "@utils/discord";
import { useForceUpdater } from "@utils/react";
@ -223,6 +223,7 @@ const settings = definePluginSettings({
}
});
migratePluginSettings("ChatButtonsPlus", "Meow", "Woof");
export default definePlugin({
name: "ChatButtonsPlus",
description: "Add custom chat buttons with personalized + messages and SVG icons",

View file

@ -7,12 +7,14 @@
import "./style.css";
import { definePluginSettings, useSettings } from "@api/Settings";
import ErrorBoundary from "@components/ErrorBoundary";
import { Link } from "@components/Link";
import { Devs, EquicordDevs } from "@utils/constants";
import { Margins } from "@utils/margins";
import { useForceUpdater } from "@utils/react";
import definePlugin, { OptionType } from "@utils/types";
import { Forms, moment, TextInput, useEffect, useState } from "@webpack/common";
import { findByCodeLazy, findComponentByCodeLazy } from "@webpack";
import { Forms, moment, TextInput, useEffect, useRef, UserStore, useState } from "@webpack/common";
type TimeFormat = {
name: string;
@ -66,9 +68,9 @@ const timeFormats: Record<string, TimeFormat> = {
},
lastWeekFormat: {
name: "Last week",
description: "[calendar] format for last week",
description: "[calendar] format for within the last week",
default: "ddd DD.MM.YYYY HH:mm:ss",
offset: -1000 * 60 * 60 * 24 * 7,
offset: -1000 * 60 * 60 * 24 * 6, // setting an offset of a week exactly pushes it into "older date" territory as soon as a second passes
},
sameElseFormat: {
name: "Older date",
@ -102,31 +104,80 @@ const format = (date: Date, formatTemplate: string): string => {
const TimeRow = (props: TimeRowProps) => {
const [state, setState] = useState(props.pluginSettings?.[props.id] || props.format.default);
const [preview, setPreview] = useState("");
const handleChange = (value: string) => {
setState(value);
props.onChange(props.id, value);
};
const updatePreview = () => setPreview(format(new Date(Date.now() + props.format.offset), state || props.format.default));
useEffect(() => {
updatePreview();
const interval = setInterval(updatePreview, 1000);
return () => clearInterval(interval);
}, [state]);
return (
<>
<Forms.FormTitle tag="h5">{props.format.name}</Forms.FormTitle>
<Forms.FormText>{props.format.description}</Forms.FormText>
<TextInput value={state} onChange={handleChange} />
<Forms.FormText className={"vc-cmt-preview-text"}>{preview}</Forms.FormText>
</>
);
};
const MessagePreview = findComponentByCodeLazy<{
author: any,
message: any,
compact: boolean,
isGroupStart: boolean,
className: string,
hideSimpleEmbedContent: boolean;
}>(/previewGuildId:\i,preview:\i,/);
const createBotMessage = findByCodeLazy('username:"Clyde"');
const populateMessagePrototype = findByCodeLazy("isProbablyAValidSnowflake", "messageReference:");
const DemoMessage = (props: { msgId, compact, message, date: Date | undefined, isGroupStart?: boolean; }) => {
const message = createBotMessage({ content: props.message, channelId: "1337", embeds: [] });
message.author = UserStore.getCurrentUser();
message.id = props.msgId;
message.timestamp = moment(props.date ?? new Date());
const user = UserStore.getCurrentUser();
const populatedMessage = message && populateMessagePrototype(message);
return populatedMessage ? (
<div className="vc-cmt-demo-message">
<MessagePreview
author={{ ...user, nick: user.globalName || user.username }}
message={populatedMessage}
compact={props.compact}
isGroupStart={props.isGroupStart || false}
className="vc-cmt-demo-message-preview"
hideSimpleEmbedContent={true}
/>
</div>
) : <div className="vc-cmt-demo-message">
<Forms.FormText>
{/* @ts-ignore */}
<b>Preview:</b> {Vencord.Plugins.plugins.CustomTimestamps.renderTimestamp(date, "cozy")}
</Forms.FormText>
</div>;
};
const DemoMessageContainer = ErrorBoundary.wrap(() => {
const [isCompact, setIsCompact] = useState(false);
const today = useRef<Date>(new Date());
const yesterday = useRef<Date>(new Date(Date.now() + timeFormats.lastDayFormat.offset));
const lastWeek = useRef<Date>(new Date(Date.now() + timeFormats.lastWeekFormat.offset));
const aMonthAgo = useRef<Date>(new Date(Date.now() + timeFormats.sameElseFormat.offset));
return (
<div className={"vc-cmt-demo-message-container"} onClick={() => setIsCompact(!isCompact)}>
<DemoMessage compact={isCompact} msgId={"1337"}
message={`Click me to switch to ${isCompact ? "cozy" : "compact"} mode`} isGroupStart={true}
date={aMonthAgo.current} />
<DemoMessage compact={isCompact} msgId={"1338"} message={"This message was sent in the last week"}
isGroupStart={true} date={lastWeek.current} />
<DemoMessage compact={isCompact} msgId={"1339"} message={"Hover over timestamps to see tooltip formats"}
isGroupStart={true} date={yesterday.current} />
<DemoMessage compact={isCompact} msgId={"1340"} message={"Edit the formats below to see them live update here"} isGroupStart={true}
date={today.current} />
</div>
);
}, { noop: true });
const settings = definePluginSettings({
formats: {
type: OptionType.COMPONENT,
@ -140,25 +191,29 @@ const settings = definePluginSettings({
componentProps.setValue(newSettings);
};
return Object.entries(timeFormats).map(([key, value]) => (
<Forms.FormSection key={key}>
{key === "sameDayFormat" && (
<div className={Margins.bottom20}>
<Forms.FormDivider style={{ marginBottom: "10px" }} />
<Forms.FormTitle tag="h1">Calendar formats</Forms.FormTitle>
<Forms.FormText>
How to format the [calendar] value if used in the above timestamps.
</Forms.FormText>
</div>
)}
<TimeRow
id={key}
format={value}
onChange={setNewValue}
pluginSettings={settingsState}
/>
</Forms.FormSection>
));
return (
<>
<DemoMessageContainer />
{Object.entries(timeFormats).map(([key, value]) => (
<Forms.FormSection key={key}>
{key === "sameDayFormat" && (
<div className={Margins.bottom20}>
<Forms.FormDivider style={{ marginBottom: "10px" }} />
<Forms.FormTitle tag="h1">Calendar formats</Forms.FormTitle>
<Forms.FormText>
How to format the [calendar] value if used in the above timestamps.
</Forms.FormText>
</div>
)}
<TimeRow
id={key}
format={value}
onChange={setNewValue}
pluginSettings={settingsState}
/>
</Forms.FormSection>
))}
</>);
}
}
}).withPrivateSettings<{
@ -230,16 +285,16 @@ export default definePlugin({
switch (type) {
case "cozy":
formatTemplate = settings.use(["formats"]).formats?.cozyFormat || timeFormats.cozyFormat.default;
formatTemplate = settings.store.formats?.cozyFormat || timeFormats.cozyFormat.default;
break;
case "compact":
formatTemplate = settings.use(["formats"]).formats?.compactFormat || timeFormats.compactFormat.default;
formatTemplate = settings.store.formats?.compactFormat || timeFormats.compactFormat.default;
break;
case "tooltip":
formatTemplate = settings.use(["formats"]).formats?.tooltipFormat || timeFormats.tooltipFormat.default;
formatTemplate = settings.store.formats?.tooltipFormat || timeFormats.tooltipFormat.default;
break;
case "ariaLabel":
formatTemplate = settings.use(["formats"]).formats?.ariaLabelFormat || timeFormats.ariaLabelFormat.default;
formatTemplate = settings.store.formats?.ariaLabelFormat || timeFormats.ariaLabelFormat.default;
}
useEffect(() => {

View file

@ -6,7 +6,12 @@
margin: 10px 0;
}
.vc-cmt-preview-text {
color: yellow;
margin: 8px 0;
.vc-cmt-demo-message-container{
position: sticky;
background: var(--background-base-lowest);
padding: 10px 0;
cursor: pointer;
top: -8px;
border: solid grey 1px;
border-radius: 5px;
}

View file

@ -152,9 +152,10 @@ export default definePlugin({
patches: [
{
find: '"M9 3v18"',
find: '?"BACK_FORWARD_NAVIGATION":',
replacement: {
match: /focusSectionProps:"HELP".{0,20},className:(\i\.button)\}\),/,
// TODO: (?:\.button) is for stable compat and should be removed soon:tm:
match: /focusSectionProps:"HELP".{0,20},className:(\i(?:\.button)?)\}\),/,
replace: "$& $self.renderVencordPopoutButton($1),"
}
}

View file

@ -52,7 +52,7 @@ export default definePlugin({
find: ".GUILD_CREATE_INVITE_SUGGESTION,defaultMaxAge",
replacement: [
{
match: /(?<=maxAge:)null!=\(\i=null!=\i\?\i:\i\)\?\i:\i.value/,
match: /(?<=maxAge:)null!=\(\i=null!=\i\?\i:\i\)\?\i:\i.\i/,
replace: "$self.settings.store.inviteDuration"
},
{

View file

@ -13,7 +13,7 @@ function SpeedIcon() {
fill="currentColor"
viewBox="0 -960 960 960"
>
<path d="M418-340q24 24 62 23.5t56-27.5l224-336-336 224q-27 18-28.5 55t22.5 61zm62-460q59 0 113.5 16.5T696-734l-76 48q-33-17-68.5-25.5T480-720q-133 0-226.5 93.5T160-400q0 42 11.5 83t32.5 77h552q23-38 33.5-79t10.5-85q0-36-8.5-70T766-540l48-76q30 47 47.5 100T880-406q1 57-13 109t-41 99q-11 18-30 28t-40 10H204q-21 0-40-10t-30-28q-26-45-40-95.5T80-400q0-83 31.5-155.5t86-127Q252-737 325-768.5T480-800zm7 313z"></path>
<path d="M418-340q24 24 62 23.5t56-27.5l224-336-336 224q-27 18-28.5 55t22.5 61zm62-460q59 0 113.5 16.5T696-734l-76 48q-33-17-68.5-25.5T480-720q-133 0-226.5 93.5T160-400q0 42 11.5 83t32.5 77h552q23-38 33.5-79t10.5-85q0-36-8.5-70T766-540l48-76q30 47 47.5 100T880-406q1 57-13 109t-41 99q-11 18-30 28t-40 10H204q-21 0-40-10t-30-28q-26-45-40-95.5T80-400q0-83 31.5-155.5t86-127Q252-737 325-768.5T480-800zm7 313z" />
</svg>
);
}

View file

@ -11,7 +11,7 @@ import { classNameFactory } from "@api/Styles";
import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants";
import definePlugin, { makeRange, OptionType } from "@utils/types";
import { ContextMenuApi, FluxDispatcher, Heading, Menu, React, Tooltip, useEffect } from "@webpack/common";
import { ContextMenuApi, FluxDispatcher, Menu, React, Tooltip, useEffect } from "@webpack/common";
import { RefObject } from "react";
import SpeedIcon from "./components/SpeedIcon";
@ -23,15 +23,6 @@ const max = 3.5;
const speeds = makeRange(min, max, 0.25);
const settings = definePluginSettings({
test: {
type: OptionType.COMPONENT,
description: "",
component() {
return <Heading variant="heading-lg/bold" selectable={false}>
Default playback speeds
</Heading>;
}
},
defaultVoiceMessageSpeed: {
type: OptionType.SLIDER,
default: 1,
@ -61,7 +52,7 @@ export default definePlugin({
settings,
PlaybackSpeedComponent({ mediaRef }: { mediaRef: MediaRef; }) {
renderPlaybackSpeedComponent: ErrorBoundary.wrap(({ mediaRef }: { mediaRef: MediaRef; }) => {
const changeSpeed = (speed: number) => {
const media = mediaRef?.current;
if (media) {
@ -70,11 +61,16 @@ export default definePlugin({
};
useEffect(() => {
if (!mediaRef?.current) return;
const media = mediaRef.current;
const media = mediaRef?.current;
if (!media) return;
if (media.tagName === "AUDIO") {
const isVoiceMessage = media.className.includes("audioElement_");
changeSpeed(isVoiceMessage ? settings.store.defaultVoiceMessageSpeed : settings.store.defaultAudioSpeed);
if (isVoiceMessage) {
// Workaround because Discord seems to override it somewhere
media.addEventListener("play", () => { changeSpeed(settings.store.defaultVoiceMessageSpeed); }, { once: true });
} else {
changeSpeed(settings.store.defaultAudioSpeed);
}
} else if (media.tagName === "VIDEO") {
changeSpeed(settings.store.defaultVideoSpeed);
}
@ -113,21 +109,15 @@ export default definePlugin({
)}
</Tooltip>
);
},
renderComponent(mediaRef: MediaRef) {
return <ErrorBoundary noop>
<this.PlaybackSpeedComponent mediaRef={mediaRef} />
</ErrorBoundary>;
},
}),
patches: [
// voice message embeds
// replace voice message embed speed control because ours provides more speeds
{
find: "\"--:--\"",
replacement: {
match: /onVolumeShow:\i,onVolumeHide:\i\}\)(?<=useCallback\(\(\)=>\{let \i=(\i).current;.+?)/,
replace: "$&,$self.renderComponent($1)"
match: /\(0,\i\.jsxs?\)\(.{0,50}\.playbackRateContainer.+?}\)}\)(?<=playbackCacheKey:\i\}=\i,(\i).+?)/,
replace: "$self.renderPlaybackSpeedComponent({mediaRef:$1})"
}
},
// audio & video embeds
@ -142,8 +132,8 @@ export default definePlugin({
{
find: "AUDIO:\"AUDIO\"",
replacement: {
match: /onVolumeHide:\i,iconClassName:\i.controlIcon,iconColor:"currentColor",sliderWrapperClassName:\i.volumeSliderWrapper\}\)\}\),/,
replace: "$&$self.renderComponent(this.props.mediaRef),"
match: /\i.volumeSliderWrapper\}\)\}\),/,
replace: "$&$self.renderPlaybackSpeedComponent({mediaRef:this?.props?.mediaRef}),"
}
}
]

View file

@ -7,4 +7,4 @@
.vc-media-playback-speed-icon:hover {
color: var(--interactive-active);
}
}

View file

@ -109,7 +109,7 @@ export default definePlugin({
{
find: '"LocalActivityStore"',
replacement: {
match: /\i\(\i\)\{.{0,25}activity:(\i)\}=\i;/,
match: /\i\(\i\)\{.{0,25}activity:(\i).*?\}=\i;/,
replace: "$&$self.patchActivity($1);",
}
}

View file

@ -0,0 +1,212 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { findGroupChildrenByChildId } from "@api/ContextMenu";
import { updateMessage } from "@api/MessageUpdater";
import { ImageInvisible, ImageVisible } from "@components/Icons";
import { EquicordDevs } from "@utils/constants";
import { Logger } from "@utils/Logger";
import { parseUrl } from "@utils/misc";
import definePlugin from "@utils/types";
import { Message } from "@vencord/discord-types";
import { findByCodeLazy } from "@webpack";
import { ChannelStore, Constants, Menu, MessageStore, React, RestAPI, showToast, Toasts } from "@webpack/common";
const logger = new Logger("ShowMessageEmbeds");
const addShowEmbedButton = (children, props) => {
if (props.itemSrc || !props.itemHref || !props.message) return; // itemSrc means the right clicked item is an image/attachment
const group = findGroupChildrenByChildId("copy-native-link", children);
if (!group) return;
const { message } = props;
addButton(group, message, props.itemHref);
};
const addShowAttachmentEmbedButton = (children, props) => {
if (!props.attachmentUrl) return;
const message = MessageStore.getMessage(props.channelId, props.messageId);
if (!message) return;
addButton(children, message, props.attachmentUrl);
};
const addButton = (children, message, url) => {
url = normaliseUrl(url);
if (!isEmbedInMessage(message, url)) {
children.splice(0, 0,
<Menu.MenuItem
id="vc-sme-show"
label="Show Embed"
action={_ => unfurlEmbed(url, message)}
icon={ImageVisible}
key="vc-sme-show" />);
} else if (isUrlInMessage(message, url)) { // check the url is actually in the message text so we know it's one people can actually add back
children.splice(0, 0,
<Menu.MenuItem
id="vc-sme-remove"
label="Remove Embed"
action={_ => removeEmbed(url, message)}
icon={ImageInvisible}
key="vc-sme-remove" />);
}
};
function isEmbedInMessage(message: Message, url: string): boolean {
return message?.embeds?.some((embed: any) => {
return embed?.url === url;
}) || message?.attachments?.some((attachment: any) => {
return attachment?.url === url;
});
}
function isUrlInMessage(message: Message, url: string): boolean {
const urls = message?.content?.match(/https?:\/\/[^\s]+/g) || [];
for (const u of urls) {
if (u === url || normaliseUrl(u) === url) {
return true;
}
}
for (const embed of message.embeds) {
// check if the embed content contains the url but not the embed.url property itself
if (embed.url !== url && (embed.rawDescription?.includes(url) || embed.fields.some((field: any) => field.value.includes(url)))) {
return true;
}
}
return false;
}
// special cases where the unfurl api endpoint returns an embed with a different url than the one we requested
// e.g. you request an embed for a youtu.be link and the returned object has { ... url: youtube.com }
// this is not an exhaustive list, may need to add more cases in the future
function normaliseUrl(url: string): string {
// normalise youtube urls to the /watch?v= format (t param is replaced with start, v always comes first)
const youtubeRegex = /(https?:\/\/)?(?:m\.|www\.)?(youtu\.be|youtube\.com)\/(embed\/|v\/|watch\?v=|watch\?.+&v=|shorts\/)?((\w|-){11})(?:\S+)?/;
if (youtubeRegex.test(url)) {
const urlObj = new URL(url);
const params = new URLSearchParams(urlObj.search);
let start = 0;
if (params.has("t") || params.has("start")) {
const startParam = params.get("start") || params.get("t");
if (startParam && startParam.match(/^(?:(\d+h)?(\d+m)?(\d+s)?(\d+)?)?$/)) {
const hours = startParam.match(/(\d+)h/);
const minutes = startParam.match(/(\d+)m/);
const seconds = startParam.match(/(\d+)s/);
const unqualifiedSeconds = startParam.match(/(\d+)$/);
if (hours) start += parseInt(hours[1]) * 3600;
if (minutes) start += parseInt(minutes[1]) * 60;
if (seconds) start += parseInt(seconds[1]);
if (unqualifiedSeconds) start += parseInt(unqualifiedSeconds[1]);
}
}
url = url.replace(youtubeRegex, `https://www.youtube.com/watch?v=$4${start ? "&start=" + start : ""}`);
}
// x.com links still return an embed with url twitter.com at the moment
const xDotComRegex = /(https?:\/\/(?:www\.)?)x\.com(\/.*)?/;
if (xDotComRegex.test(url)) {
url = url.replace(xDotComRegex, (match, p1, p2) => p1 + "twitter.com" + (p2 || ""));
}
// tiktok adds ?enable_tiktok_webview=true
const tiktokRegex = /(https?:\/\/(?:www\.)?)tiktok\.com(\/.*)?/;
const searchParams = url.includes("?") ? new URLSearchParams(url.split("?")[1]) : new URLSearchParams();
if (tiktokRegex.test(url) && !searchParams.has("enable_tiktok_webview")) {
searchParams.append("enable_tiktok_webview", "true");
const newSearch = searchParams.toString();
url = url.split("?")[0] + (newSearch ? "?" + newSearch : "");
}
return url;
}
const convertEmbed = findByCodeLazy(".uniqueId(\"embed_\")");
async function unfurlEmbed(url: string, message: Message) {
const channel = ChannelStore.getChannel(message.channel_id);
if (!parseUrl(url)) {
return;
}
const resp = await RestAPI.post({
url: Constants.Endpoints.UNFURL_EMBED_URLS,
body: {
urls: [url]
}
}).catch(e => {
showFailureToast("Failed to get embed");
logger.error("Failed to get embed", e);
});
if (!resp?.body?.embeds || resp.body.embeds.length === 0) {
showFailureToast("No embeds found");
return;
}
const { embeds } = resp.body;
const convertedEmbeds: any = [];
for (const embed of embeds) {
try {
const convertedEmbed = convertEmbed(channel.id, message.id, embed);
if (!convertedEmbed) {
showFailureToast("Failed to get embed");
logger.error("embed object couldn't be converted", embed);
continue;
}
convertedEmbeds.push(convertedEmbed);
} catch (e) {
showFailureToast("Failed to get embed");
logger.error("Failed to convert embed", e);
}
}
const newEmbeds = [...message.embeds, ...convertedEmbeds];
newEmbeds.sort((a: any, b: any) => {
return message.content.indexOf(a.url) - message.content.indexOf(b.url);
});
updateMessage(message.channel_id, message.id, { embeds: newEmbeds });
}
function removeEmbed(url: string, message: Message) {
const newEmbeds = message.embeds.filter((embed: any) => embed.url !== url);
updateMessage(message.channel_id, message.id, { embeds: newEmbeds });
}
function showFailureToast(message: string) {
showToast(message, Toasts.Type.FAILURE, { position: Toasts.Position.BOTTOM });
}
export default definePlugin({
name: "ShowMessageEmbeds",
description: "Adds a context menu option to show embeds for links that don't have one",
authors: [EquicordDevs.Suffocate],
patches: [
{
find: "className:\"attachmentLink\",",
replacement: {
match: /(?:(\i).noStyleAndInteraction.*?)attachmentName:\i.attachmentName/,
replace: "$&,channelId:$1.channelId,messageId:$1.messageId",
}
}
],
contextMenus: {
"message": addShowEmbedButton,
"attachment-link-context": addShowAttachmentEmbedButton
}
});

View file

@ -4,12 +4,11 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { definePluginSettings } from "@api/Settings";
import ErrorBoundary from "@components/ErrorBoundary";
import { EquicordDevs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types";
import definePlugin from "@utils/types";
import { findComponentByCodeLazy } from "@webpack";
import { Constants, React, RestAPI } from "@webpack/common";
import { Constants, React, RestAPI, useEffect, useState } from "@webpack/common";
const Button = findComponentByCodeLazy(".NONE,disabled:", ".PANEL_BUTTON");
@ -35,55 +34,54 @@ function makeSpotifyIcon(enabled: boolean) {
);
}
async function setSpotifyActivity(value: boolean) {
const { body } = await RestAPI.get({
url: Constants.Endpoints.CONNECTIONS
});
if (!body) return;
const spotifyId = body.find(conn => conn.type === "spotify")?.id;
await RestAPI.patch({
url: Constants.Endpoints.CONNECTION("spotify", spotifyId),
body: {
show_activity: value,
},
});
}
function SpotifyActivityToggleButton() {
const { spotifyConnection } = settings.store;
const [loading, setLoading] = useState(true);
const [id, setId] = useState<string | null>(null);
const [showActivity, setShowActivity] = useState<boolean | null>(null);
useEffect(() => {
(async () => {
const { body } = await RestAPI.get({
url: Constants.Endpoints.CONNECTIONS,
});
if (!body) return;
const spotifyConn = body.find(c => c.type === "spotify");
if (spotifyConn) {
setId(spotifyConn.id);
setShowActivity(spotifyConn.show_activity);
}
setLoading(false);
})();
}, []);
if (loading || !id || showActivity === null) return null;
return (
<Button
tooltipText={spotifyConnection ? "Turn off Spotify activity" : "Turn on Spotify activity"}
icon={makeSpotifyIcon(spotifyConnection)}
tooltipText={showActivity ? "Turn off Spotify activity" : "Turn on Spotify activity"}
icon={makeSpotifyIcon(showActivity)}
role="switch"
aria-checked={spotifyConnection}
redGlow={!spotifyConnection}
aria-checked={showActivity}
redGlow={!showActivity}
onClick={async () => {
const newValue = !spotifyConnection;
settings.store.spotifyConnection = newValue;
setSpotifyActivity(newValue);
const newValue = !showActivity;
setShowActivity(newValue);
await RestAPI.patch({
url: Constants.Endpoints.CONNECTION("spotify", id),
body: {
show_activity: newValue,
},
});
}}
/>
);
}
const settings = definePluginSettings({
spotifyConnection: {
type: OptionType.BOOLEAN,
description: "Show Spotify activity",
default: false,
onChange: async value => setSpotifyActivity(value),
}
});
export default definePlugin({
name: "SpotifyActivityToggle",
description: "Adds a toggle button for Spotify activity visibility.",
authors: [EquicordDevs.thororen],
settings,
patches: [
{
find: "#{intl::ACCOUNT_SPEAKING_WHILE_MUTED}",

View file

@ -68,7 +68,7 @@ export default definePlugin({
},
// Change top right chat toolbar button from the help one to the dev one
{
find: '"M9 3v18"',
find: '?"BACK_FORWARD_NAVIGATION":',
replacement: {
match: /hasBugReporterAccess:(\i)/,
replace: "_hasBugReporterAccess:$1=true"

View file

@ -167,7 +167,6 @@ function makeBypassPatches(): Omit<Patch, "plugin"> {
{ func: "canUseHighVideoUploadQuality", predicate: () => settings.store.enableStreamQualityBypass },
{ func: "canStreamQuality", predicate: () => settings.store.enableStreamQualityBypass },
{ func: "canUseClientThemes" },
{ func: "canUseCustomNotificationSounds" },
{ func: "canUsePremiumAppIcons" }
];
@ -184,7 +183,7 @@ function makeBypassPatches(): Omit<Patch, "plugin"> {
export default definePlugin({
name: "FakeNitro",
authors: [Devs.Arjix, Devs.D3SOX, Devs.Ven, Devs.fawn, Devs.captain, Devs.Nuckyz, Devs.AutumnVN],
description: "Allows you to stream in nitro quality, send fake emojis/stickers, use client themes and custom Discord notifications.",
description: "Allows you to stream in nitro quality, send fake emojis/stickers, and use client themes.",
dependencies: ["MessageEventsAPI"],
settings,

View file

@ -9,6 +9,41 @@ export interface DiffPart {
text: string;
}
function tokenizeMessage(text: string): string[] {
const tokens: string[] = [];
let i = 0;
while (i < text.length) {
// handle Discord custom emojis: <:name:id> or <a:name:id>
if (text[i] === "<" && (text.slice(i + 1, i + 3) === ":" || text.slice(i + 1, i + 3) === "a:")) {
const endIndex = text.indexOf(">", i);
if (endIndex !== -1) {
tokens.push(text.slice(i, endIndex + 1));
i = endIndex + 1;
continue;
}
}
// handle mentions: <@id>, <@!id>, <@&id>, <#id>
if (text[i] === "<" && text[i + 1] === "@" ||
(text[i] === "<" && text[i + 1] === "#")) {
const endIndex = text.indexOf(">", i);
if (endIndex !== -1) {
tokens.push(text.slice(i, endIndex + 1));
i = endIndex + 1;
continue;
}
}
// handle regular characters (including Unicode emojis)
const char = Array.from(text.slice(i))[0];
tokens.push(char);
i += char.length;
}
return tokens;
}
export function createWordDiff(oldText: string, newText: string): DiffPart[] {
// suffix shit, if oldText is shorter than newText and newText starts with oldText
if (oldText.length < newText.length && newText.startsWith(oldText)) {
@ -55,8 +90,8 @@ export function createWordDiff(oldText: string, newText: string): DiffPart[] {
}
// For complex cases, fall back to LCS algorithm
const oldChars = oldText.split("");
const newChars = newText.split("");
const oldChars = tokenizeMessage(oldText);
const newChars = tokenizeMessage(newText);
const result: DiffPart[] = [];
const dp: number[][] = [];

View file

@ -166,22 +166,32 @@ const patchChannelContextMenu: NavContextMenuPatchCallback = (
);
};
function renderDiffParts(diffParts: DiffPart[]) {
function renderDiffParts(diffParts: DiffPart[], message: Message) {
return diffParts.map((part, index) => {
const parsedContent = Parser.parse(part.text, true, {
channelId: message.channel_id,
messageId: message.id,
allowLinks: true,
allowHeading: true,
allowList: true,
allowEmojiLinks: true,
viewingChannelId: SelectedChannelStore.getChannelId(),
});
if (part.type === "unchanged") {
return React.createElement("span", { key: index }, part.text);
return React.createElement("span", { key: index }, parsedContent);
} else if (part.type === "added") {
return React.createElement("span", {
key: index,
className: "messagelogger-diff-added"
}, part.text);
}, parsedContent);
} else if (part.type === "removed") {
return React.createElement("span", {
key: index,
className: "messagelogger-diff-removed"
}, part.text);
}, parsedContent);
}
return React.createElement("span", { key: index }, part.text);
return React.createElement("span", { key: index }, parsedContent);
});
}
@ -189,7 +199,7 @@ export function parseEditContent(content: string, message: Message, previousCont
const perMessageDiffEnabled = !disabledDiffMessages.has(message.id);
if (previousContent && content !== previousContent && settings.store.showEditDiffs && perMessageDiffEnabled) {
const diffParts = createMessageDiff(content, previousContent);
return renderDiffParts(diffParts);
return renderDiffParts(diffParts, message);
}
return Parser.parse(content, true, {

View file

@ -54,8 +54,45 @@
color: #F47068;
}
.messagelogger-diff-added [class*="emojiContainer"],
.messagelogger-diff-added img.emoji {
position: relative;
}
.messagelogger-diff-added [class*="emojiContainer"]::after,
.messagelogger-diff-added img.emoji::after {
content: '';
position: absolute;
inset: 0;
background-color: rgb(133 232 157 / 60%);
border-radius: 3px;
pointer-events: none;
}
.messagelogger-diff-removed [class*="emojiContainer"],
.messagelogger-diff-removed img.emoji {
position: relative;
}
.messagelogger-diff-removed [class*="emojiContainer"]::after,
.messagelogger-diff-removed img.emoji::after {
content: '';
position: absolute;
inset: 0;
background-color: rgb(244 112 104 / 60%);
border-radius: 3px;
pointer-events: none;
}
/* when diffs are disabled for a specific message, remove diff colors */
.messagelogger-diff-disabled .messagelogger-diff-added,
.messagelogger-diff-disabled .messagelogger-diff-removed {
color: inherit;
}
.messagelogger-diff-disabled .messagelogger-diff-added [class*="emojiContainer"]::after,
.messagelogger-diff-disabled .messagelogger-diff-added img.emoji::after,
.messagelogger-diff-disabled .messagelogger-diff-removed [class*="emojiContainer"]::after,
.messagelogger-diff-disabled .messagelogger-diff-removed img.emoji::after {
display: none !important;
}

View file

@ -87,7 +87,7 @@ export default definePlugin({
replacement: {
// The two groups inside the first group grab the minified names of the variables,
// they are then referenced later to find unviewedTrialCount + unviewedDiscountCount.
match: /(?<=\{unviewedTrialCount:(\i),unviewedDiscountCount:(\i)\}.{0,300}\i=)\1\+\2/,
match: /(?<=\{unviewedTrialCount:(\i),unviewedDiscountCount:(\i)\}.{0,300}\.NONE\?)\1\+\2/,
replace: "0"
}
}

View file

@ -20,6 +20,7 @@ import { definePluginSettings } from "@api/Settings";
import { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types";
import type { Message } from "@vencord/discord-types";
import { ChannelStore, GuildMemberStore } from "@webpack/common";
const settings = definePluginSettings({
userList: {
@ -28,16 +29,22 @@ const settings = definePluginSettings({
type: OptionType.STRING,
default: "1234567890123445,1234567890123445",
},
roleList: {
description:
"List of roles to allow or exempt pings for (separated by commas or spaces)",
type: OptionType.STRING,
default: "1234567890123445,1234567890123445",
},
shouldPingListed: {
description: "Behaviour",
type: OptionType.SELECT,
options: [
{
label: "Do not ping the listed users",
label: "Do not ping the listed users / roles",
value: false,
},
{
label: "Only ping the listed users",
label: "Only ping the listed users / roles",
value: true,
default: true,
},
@ -57,7 +64,14 @@ export default definePlugin({
settings,
shouldMention(message: Message, isHoldingShift: boolean) {
const isListed = settings.store.userList.includes(message.author.id);
let isListed = settings.store.userList.includes(message.author.id);
const channel = ChannelStore.getChannel(message.channel_id);
if (channel?.guild_id && !isListed) {
const roles = GuildMemberStore.getMember(channel.guild_id, message.author.id)?.roles;
isListed = !!roles && roles.some(role => settings.store.roleList.includes(role));
}
const isExempt = settings.store.shouldPingListed ? isListed : !isListed;
return settings.store.inverseShiftReply ? isHoldingShift !== isExempt : !isHoldingShift && isExempt;
},

View file

@ -44,17 +44,21 @@ export default definePlugin({
description: "If unread messages are sent by a user in DMs multiple times, you'll only receive one audio ping. Read the messages to reset the limit",
authors: [Devs.ProffDea],
settings,
patches: [{
find: ".getDesktopType()===",
replacement: [{
match: /(\i\.\i\.getDesktopType\(\)===\i\.\i\.NEVER)\)/,
replace: "$&if(!$self.isPrivateChannelRead(arguments[0]?.message))return;else "
},
patches: [
{
match: /sound:(\i\?\i:void 0,soundpack:\i,volume:\i,onClick)/,
replace: "sound:!$self.isPrivateChannelRead(arguments[0]?.message)?undefined:$1"
}]
}],
find: ".getDesktopType()===",
replacement: [
{
match: /(\i\.\i\.getDesktopType\(\)===\i\.\i\.NEVER)\)/,
replace: "$&if(!$self.isPrivateChannelRead(arguments[0]?.message))return;else "
},
{
match: /sound:(\i\?\i:void 0,volume:\i,onClick)/,
replace: "sound:!$self.isPrivateChannelRead(arguments[0]?.message)?undefined:$1"
}
]
}
],
isPrivateChannelRead(message: MessageJSON) {
const ignoreList = settings.store.ignoreUsers.split(", ").filter(Boolean);
if (ignoreList.includes(message.author.id)) return true;

View file

@ -28,6 +28,7 @@ const Engines = {
Yandex: "https://yandex.com/images/search?rpt=imageview&url=",
SauceNAO: "https://saucenao.com/search.php?url=",
IQDB: "https://iqdb.org/?url=",
Bing: "https://www.bing.com/images/search?view=detailv2&iss=sbi&q=imgurl:",
TinEye: "https://www.tineye.com/search?url=",
ImgOps: "https://imgops.com/start?url="
} as const;

View file

@ -101,7 +101,6 @@ function ComplexPicker({ voice, voices }: PickerProps) {
);
}
function VoiceSetting() {
const voices = useMemo(() => window.speechSynthesis?.getVoices() ?? [], []);
const { voice } = settings.use(["voice"]);