Merge branch 'dev'
This commit is contained in:
commit
50bd6b238b
24 changed files with 498 additions and 209 deletions
|
|
@ -1,3 +0,0 @@
|
|||
# AutoJump
|
||||
|
||||
Jumps to the first message in new messages for a channel
|
||||
|
|
@ -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 });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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(() => {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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),"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}),"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -7,4 +7,4 @@
|
|||
|
||||
.vc-media-playback-speed-icon:hover {
|
||||
color: var(--interactive-active);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);",
|
||||
}
|
||||
}
|
||||
|
|
|
|||
212
src/equicordplugins/showMessageEmbeds/index.tsx
Normal file
212
src/equicordplugins/showMessageEmbeds/index.tsx
Normal 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
|
||||
}
|
||||
});
|
||||
|
|
@ -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}",
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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[][] = [];
|
||||
|
|
|
|||
|
|
@ -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, {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -101,7 +101,6 @@ function ComplexPicker({ voice, voices }: PickerProps) {
|
|||
);
|
||||
}
|
||||
|
||||
|
||||
function VoiceSetting() {
|
||||
const voices = useMemo(() => window.speechSynthesis?.getVoices() ?? [], []);
|
||||
const { voice } = settings.use(["voice"]);
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue