Merge branch 'dev'
This commit is contained in:
commit
586b49b50b
13 changed files with 725 additions and 234 deletions
|
|
@ -87,6 +87,14 @@ const settings = definePluginSettings({
|
|||
type: OptionType.BOOLEAN,
|
||||
default: true,
|
||||
},
|
||||
posterSource: {
|
||||
description: "Choose which poster to display in Rich Presence",
|
||||
type: OptionType.SELECT,
|
||||
options: [
|
||||
{ label: "Jellyfin", value: "jellyfin", default: true },
|
||||
{ label: "TheMovieDB", value: "tmdb" },
|
||||
],
|
||||
},
|
||||
overrideRichPresenceType: {
|
||||
description: "Override the rich presence type",
|
||||
type: OptionType.SELECT,
|
||||
|
|
@ -310,7 +318,14 @@ export default definePlugin({
|
|||
}
|
||||
|
||||
const assets: ActivityAssets = {
|
||||
large_image: tmdbData?.posterPath ? await getApplicationAsset(tmdbData.posterPath) : mediaData.imageUrl ? await getApplicationAsset(mediaData.imageUrl) : undefined,
|
||||
large_image:
|
||||
settings.store.posterSource === "tmdb"
|
||||
? (tmdbData?.posterPath
|
||||
? await getApplicationAsset(tmdbData.posterPath)
|
||||
: undefined)
|
||||
: (mediaData.imageUrl
|
||||
? await getApplicationAsset(mediaData.imageUrl)
|
||||
: undefined),
|
||||
large_text: mediaData.seriesName || mediaData.album || undefined,
|
||||
};
|
||||
|
||||
|
|
|
|||
371
src/equicordplugins/listenBrainzRPC/index.tsx
Normal file
371
src/equicordplugins/listenBrainzRPC/index.tsx
Normal file
|
|
@ -0,0 +1,371 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2025 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { definePluginSettings } from "@api/Settings";
|
||||
import { Link } from "@components/Link";
|
||||
import { EquicordDevs } from "@utils/constants";
|
||||
import { Logger } from "@utils/Logger";
|
||||
import definePlugin, { OptionType } from "@utils/types";
|
||||
import { Activity, ActivityAssets, ActivityButton } from "@vencord/discord-types";
|
||||
import { ActivityFlags, ActivityType } from "@vencord/discord-types/enums";
|
||||
import { findByPropsLazy } from "@webpack";
|
||||
import { ApplicationAssetUtils, FluxDispatcher, Forms } from "@webpack/common";
|
||||
|
||||
interface TrackData {
|
||||
name: string;
|
||||
album: string;
|
||||
artist: string;
|
||||
durationMs?: number;
|
||||
recordingMBID?: string;
|
||||
url: string;
|
||||
imageUrl?: string;
|
||||
}
|
||||
|
||||
const enum NameFormat {
|
||||
StatusName = "status-name",
|
||||
ArtistFirst = "artist-first",
|
||||
SongFirst = "song-first",
|
||||
ArtistOnly = "artist",
|
||||
SongOnly = "song",
|
||||
AlbumName = "album",
|
||||
}
|
||||
|
||||
const applicationId = "1090155131007406132";
|
||||
const placeholderId = "2a96cbd8b46e442fc41c2b86b821562f";
|
||||
|
||||
const logger = new Logger("ListenBrainzRPC");
|
||||
|
||||
const PresenceStore = findByPropsLazy("getLocalPresence");
|
||||
|
||||
async function getApplicationAsset(key: string): Promise<string> {
|
||||
return (await ApplicationAssetUtils.fetchAssetIds(applicationId, [key]))[0];
|
||||
}
|
||||
|
||||
function setActivity(activity: Activity | null) {
|
||||
FluxDispatcher.dispatch({
|
||||
type: "LOCAL_ACTIVITY_UPDATE",
|
||||
activity,
|
||||
socketId: "ListenBrainz",
|
||||
});
|
||||
}
|
||||
|
||||
const settings = definePluginSettings({
|
||||
username: {
|
||||
description: "ListenBrainz username",
|
||||
type: OptionType.STRING,
|
||||
},
|
||||
mbContact: {
|
||||
description: "ListenBrainz contact",
|
||||
type: OptionType.STRING,
|
||||
},
|
||||
shareUsername: {
|
||||
description:
|
||||
"show link to ListenBrainz profile (may only be visible to other users)",
|
||||
type: OptionType.BOOLEAN,
|
||||
default: false,
|
||||
},
|
||||
shareSong: {
|
||||
description:
|
||||
"show link to song on ListenBrainz (may only be visible to other users)",
|
||||
type: OptionType.BOOLEAN,
|
||||
default: true,
|
||||
},
|
||||
hideWithSpotify: {
|
||||
description: "hide ListenBrainz presence if Spotify is running",
|
||||
type: OptionType.BOOLEAN,
|
||||
default: true,
|
||||
},
|
||||
hideWithActivity: {
|
||||
description: "hide ListenBrainz presence if you have any other presence",
|
||||
type: OptionType.BOOLEAN,
|
||||
default: false,
|
||||
},
|
||||
useTimeBar: {
|
||||
description:
|
||||
"use track duration to display a time bar (must be using Listening status)",
|
||||
type: OptionType.BOOLEAN,
|
||||
default: true,
|
||||
},
|
||||
statusName: {
|
||||
description: "custom status text",
|
||||
type: OptionType.STRING,
|
||||
default: "some music",
|
||||
},
|
||||
nameFormat: {
|
||||
description: "Show name of song and artist in status name",
|
||||
type: OptionType.SELECT,
|
||||
options: [
|
||||
{
|
||||
label: "Use custom status name",
|
||||
value: NameFormat.StatusName,
|
||||
default: true,
|
||||
},
|
||||
{
|
||||
label: "Use format 'artist - song'",
|
||||
value: NameFormat.ArtistFirst,
|
||||
},
|
||||
{
|
||||
label: "Use format 'song - artist'",
|
||||
value: NameFormat.SongFirst,
|
||||
},
|
||||
{
|
||||
label: "Use artist name only",
|
||||
value: NameFormat.ArtistOnly,
|
||||
},
|
||||
{
|
||||
label: "Use song name only",
|
||||
value: NameFormat.SongOnly,
|
||||
},
|
||||
{
|
||||
label:
|
||||
"Use album name (falls back to custom status text if song has no album)",
|
||||
value: NameFormat.AlbumName,
|
||||
},
|
||||
],
|
||||
},
|
||||
useListeningStatus: {
|
||||
description: 'show "Listening to" status instead of "Playing"',
|
||||
type: OptionType.BOOLEAN,
|
||||
default: true,
|
||||
},
|
||||
missingArt: {
|
||||
description: "When album or album art is missing",
|
||||
type: OptionType.SELECT,
|
||||
options: [
|
||||
{
|
||||
label: "Use large ListenBrainz logo",
|
||||
value: "listenbrainzLogo",
|
||||
default: true,
|
||||
},
|
||||
{
|
||||
label: "Use generic placeholder",
|
||||
value: "placeholder",
|
||||
},
|
||||
],
|
||||
},
|
||||
useLogo: {
|
||||
description: "Show ListenBrainz logo on album art",
|
||||
type: OptionType.BOOLEAN,
|
||||
default: true,
|
||||
},
|
||||
});
|
||||
|
||||
var currentRecordingMBID = "";
|
||||
var currentStart = 0;
|
||||
|
||||
export default definePlugin({
|
||||
name: "ListenBrainzRPC",
|
||||
description: "Little plugin for ListenBrainz rich presence",
|
||||
authors: [EquicordDevs.qouesm],
|
||||
|
||||
settingsAboutComponent: () => (
|
||||
<>
|
||||
<Forms.FormTitle tag="h3">About MusicBrainz API</Forms.FormTitle>
|
||||
<Forms.FormText>
|
||||
The MusicBrainz API does not require an API key, but it does require a{" "}
|
||||
<Link href="https://musicbrainz.org/doc/MusicBrainz_API/Rate_Limiting#Provide_meaningful_User-Agent_strings">
|
||||
{" "}
|
||||
meaningful user-agent string
|
||||
</Link>{" "}
|
||||
. For most, an email address should suffice.
|
||||
</Forms.FormText>
|
||||
</>
|
||||
),
|
||||
|
||||
settings,
|
||||
|
||||
start() {
|
||||
this.updatePresence();
|
||||
this.updateInterval = setInterval(() => {
|
||||
this.updatePresence();
|
||||
}, 16000);
|
||||
},
|
||||
|
||||
stop() {
|
||||
clearInterval(this.updateInterval);
|
||||
},
|
||||
|
||||
async fetchTrackData(): Promise<TrackData | null> {
|
||||
if (!settings.store.username) return null;
|
||||
|
||||
try {
|
||||
const lbRes = await fetch(
|
||||
`https://api.listenbrainz.org/1/user/${settings.store.username}/playing-now`,
|
||||
);
|
||||
if (!lbRes.ok) throw `${lbRes.status} ${lbRes.statusText}`;
|
||||
|
||||
const lbJson = await lbRes.json();
|
||||
if (lbJson.error) {
|
||||
logger.error(
|
||||
"Error from ListenBrainz API",
|
||||
`${lbJson.error}: ${lbJson.message}`,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
const listen = lbJson.payload?.listens?.[0];
|
||||
if (!listen?.playing_now) return null;
|
||||
|
||||
const trackMetadata = listen.track_metadata;
|
||||
const albumName = trackMetadata.release_name || "Unknown";
|
||||
const artistName = trackMetadata.artist_name || "Unknown";
|
||||
|
||||
const mbRes = await fetch(`https://musicbrainz.org/ws/2/release/?query=release:${encodeURIComponent(albumName)}%20AND%20artist:${encodeURIComponent(artistName)}&fmt=json`,);
|
||||
|
||||
if (!mbRes.ok) throw `${mbRes.status} ${mbRes.statusText}`;
|
||||
|
||||
const mbJson = await mbRes.json();
|
||||
const releases = mbJson.releases || [];
|
||||
|
||||
const releaseGroup = releases[0]["release-group"].id;
|
||||
|
||||
const caaRes = await fetch(
|
||||
`https://coverartarchive.org/release-group/${releaseGroup}`,
|
||||
);
|
||||
if (!caaRes.ok) throw `${caaRes.status} ${caaRes.statusText}`;
|
||||
const caaJson = await caaRes.json();
|
||||
|
||||
const url: string = caaJson.release;
|
||||
|
||||
const { images } = caaJson;
|
||||
let imageUrl: string = "";
|
||||
for (const image of images) {
|
||||
imageUrl = image.thumbnails.large || "";
|
||||
if (!imageUrl) continue;
|
||||
break;
|
||||
}
|
||||
|
||||
return {
|
||||
name: trackMetadata.track_name || "Unknown",
|
||||
album: albumName,
|
||||
artist: artistName,
|
||||
durationMs: trackMetadata.additional_info.duration_ms,
|
||||
recordingMBID: trackMetadata.additional_info.recording_mbid,
|
||||
url: url,
|
||||
imageUrl: imageUrl,
|
||||
};
|
||||
} catch (e) {
|
||||
logger.error("Failed to query ListenBrainz API", e);
|
||||
// will clear the rich presence if API fails
|
||||
return null;
|
||||
}
|
||||
},
|
||||
|
||||
async updatePresence() {
|
||||
setActivity(await this.getActivity());
|
||||
},
|
||||
|
||||
getLargeImage(track: TrackData): string | undefined {
|
||||
if (track.imageUrl && !track.imageUrl.includes(placeholderId))
|
||||
return track.imageUrl;
|
||||
|
||||
if (settings.store.missingArt === "placeholder") return "placeholder";
|
||||
},
|
||||
|
||||
async getActivity(): Promise<Activity | null> {
|
||||
if (settings.store.hideWithActivity) {
|
||||
if (
|
||||
PresenceStore.getActivities().some(
|
||||
a => a.application_id !== applicationId,
|
||||
)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
if (settings.store.hideWithSpotify) {
|
||||
if (
|
||||
PresenceStore.getActivities().some(
|
||||
a =>
|
||||
a.type === ActivityType.LISTENING &&
|
||||
a.application_id !== applicationId,
|
||||
)
|
||||
) {
|
||||
// there is already music status because of Spotify or richerCider (probably more)
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
const trackData = await this.fetchTrackData();
|
||||
if (!trackData) return null;
|
||||
|
||||
const largeImage = this.getLargeImage(trackData);
|
||||
const assets: ActivityAssets = largeImage
|
||||
? {
|
||||
large_image: await getApplicationAsset(largeImage),
|
||||
large_text: trackData.album || undefined,
|
||||
small_image: settings.store.useLogo
|
||||
? await getApplicationAsset("listenbrainz")
|
||||
: undefined,
|
||||
small_text: "ListenBrainz",
|
||||
}
|
||||
: {
|
||||
large_image: await getApplicationAsset("listenbrainz"),
|
||||
large_text: trackData.album || undefined,
|
||||
};
|
||||
|
||||
const buttons: ActivityButton[] = [];
|
||||
|
||||
if (settings.store.shareUsername)
|
||||
buttons.push({
|
||||
label: "ListenBrainz Profile",
|
||||
url: `https://www.listenbrainz.org/user/${settings.store.username}`,
|
||||
});
|
||||
|
||||
if (settings.store.shareSong)
|
||||
buttons.push({
|
||||
label: "View Song",
|
||||
url: trackData.url,
|
||||
});
|
||||
|
||||
const statusName = (() => {
|
||||
switch (settings.store.nameFormat) {
|
||||
case NameFormat.ArtistFirst:
|
||||
return trackData.artist + " - " + trackData.name;
|
||||
case NameFormat.SongFirst:
|
||||
return trackData.name + " - " + trackData.artist;
|
||||
case NameFormat.ArtistOnly:
|
||||
return trackData.artist;
|
||||
case NameFormat.SongOnly:
|
||||
return trackData.name;
|
||||
case NameFormat.AlbumName:
|
||||
return trackData.album || settings.store.statusName;
|
||||
default:
|
||||
return settings.store.statusName;
|
||||
}
|
||||
})();
|
||||
|
||||
if (trackData.recordingMBID && trackData.recordingMBID !== currentRecordingMBID) {
|
||||
currentRecordingMBID = trackData.recordingMBID;
|
||||
currentStart = Date.now();
|
||||
}
|
||||
|
||||
return {
|
||||
application_id: applicationId,
|
||||
name: statusName,
|
||||
|
||||
details: trackData.name,
|
||||
state: trackData.artist,
|
||||
assets,
|
||||
|
||||
timestamps: {
|
||||
start: settings.store.useTimeBar ? currentStart : undefined,
|
||||
end: settings.store.useTimeBar
|
||||
? currentStart + (trackData.durationMs ?? 0)
|
||||
: undefined,
|
||||
},
|
||||
buttons: buttons.length ? buttons.map(v => v.label) : undefined,
|
||||
metadata: {
|
||||
button_urls: buttons.map(v => v.url),
|
||||
},
|
||||
|
||||
type: settings.store.useListeningStatus
|
||||
? ActivityType.LISTENING
|
||||
: ActivityType.PLAYING,
|
||||
flags: ActivityFlags.INSTANCE,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
|
@ -16,7 +16,7 @@ export const GuildlessServerListItemComponent = findComponentByCodeLazy("tooltip
|
|||
export const GuildedServerListItemPillComponent = findComponentByCodeLazy('"pill":"empty"');
|
||||
export const ServerListItemLowerBadgeComponent = findComponentByCodeLazy("STATUS_DANGER,disableColor:");
|
||||
export const ServerListItemUpperBadgeComponent = findComponentByCodeLazy("STATUS_DANGER,shape:", "iconBadge,");
|
||||
export const RadioGroup = findComponentByCodeLazy("\"label\",\"description\"]);");
|
||||
export const RadioGroup = findComponentByCodeLazy('["label"', "description:");
|
||||
export const QuestTile = findComponentByCodeLazy(".rowIndex,trackGuildAndChannelMetadata");
|
||||
|
||||
export enum QuestRewardType {
|
||||
|
|
|
|||
|
|
@ -4,47 +4,14 @@
|
|||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { definePluginSettings } from "@api/Settings";
|
||||
import { EquicordDevs } from "@utils/constants";
|
||||
import definePlugin, { OptionType } from "@utils/types";
|
||||
import definePlugin from "@utils/types";
|
||||
import { User } from "@vencord/discord-types";
|
||||
import { React } from "@webpack/common";
|
||||
import { Flex, React } from "@webpack/common";
|
||||
|
||||
import { settings } from "./settings";
|
||||
import { UserChatButton, UserDeafenButton, UserMuteButton } from "./utils";
|
||||
|
||||
export const settings = definePluginSettings({
|
||||
showChatButton: {
|
||||
type: OptionType.BOOLEAN,
|
||||
default: true,
|
||||
description: "Whether you want to display the chat button",
|
||||
restartNeeded: true,
|
||||
},
|
||||
showMuteButton: {
|
||||
type: OptionType.BOOLEAN,
|
||||
default: true,
|
||||
description: "Whether you want to display the mute button",
|
||||
restartNeeded: true,
|
||||
},
|
||||
showDeafenButton: {
|
||||
type: OptionType.BOOLEAN,
|
||||
default: true,
|
||||
description: "Whether you want to display the deafen button",
|
||||
restartNeeded: true,
|
||||
},
|
||||
muteSoundboard: {
|
||||
type: OptionType.BOOLEAN,
|
||||
default: true,
|
||||
description: "Toggles their soundboard upon clicking deafen button.",
|
||||
restartNeeded: false,
|
||||
},
|
||||
disableVideo: {
|
||||
type: OptionType.BOOLEAN,
|
||||
default: true,
|
||||
description: "Toggles their video upon clicking deafen button.",
|
||||
restartNeeded: false,
|
||||
},
|
||||
});
|
||||
|
||||
export default definePlugin({
|
||||
name: "VoiceButtons",
|
||||
description: "Quickly DM, mute, or deafen any user right from the voice-call panel.",
|
||||
|
|
@ -55,20 +22,20 @@ export default definePlugin({
|
|||
find: "\"avatarContainerClass\",\"userNameClassName\"",
|
||||
replacement: [
|
||||
{
|
||||
match: /:(\i)\.username.*?flipped\]:\i\}\),children:\[/,
|
||||
replace: "$&$self.renderButtons($1),"
|
||||
match: /flipped\]:\i\}\),children:\[/,
|
||||
replace: "$&$self.renderButtons(arguments[0].user),"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
renderPing(user?: User) {
|
||||
renderButtons(user: User) {
|
||||
if (!user) return null;
|
||||
return (
|
||||
<div style={{ display: "flex", gap: "4px" }}>
|
||||
<Flex direction={Flex.Direction.HORIZONTAL} className="voice-user-buttons">
|
||||
{settings.store.showChatButton && <UserChatButton user={user} />}
|
||||
{settings.store.showMuteButton && <UserMuteButton user={user} />}
|
||||
{settings.store.showDeafenButton && <UserDeafenButton user={user} />}
|
||||
</div>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
|
|
|||
63
src/equicordplugins/voiceButtons/settings.ts
Normal file
63
src/equicordplugins/voiceButtons/settings.ts
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2025 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { definePluginSettings } from "@api/Settings";
|
||||
import { OptionType } from "@utils/types";
|
||||
|
||||
export const settings = definePluginSettings({
|
||||
showChatButton: {
|
||||
type: OptionType.BOOLEAN,
|
||||
default: true,
|
||||
description: "Whether you want to display the chat button",
|
||||
restartNeeded: true,
|
||||
},
|
||||
showMuteButton: {
|
||||
type: OptionType.BOOLEAN,
|
||||
default: true,
|
||||
description: "Whether you want to display the mute button",
|
||||
restartNeeded: true,
|
||||
},
|
||||
showDeafenButton: {
|
||||
type: OptionType.BOOLEAN,
|
||||
default: true,
|
||||
description: "Whether you want to display the deafen button",
|
||||
restartNeeded: true,
|
||||
},
|
||||
muteSoundboard: {
|
||||
type: OptionType.BOOLEAN,
|
||||
default: true,
|
||||
description: "Toggles their soundboard upon clicking deafen button.",
|
||||
restartNeeded: false,
|
||||
},
|
||||
disableVideo: {
|
||||
type: OptionType.BOOLEAN,
|
||||
default: true,
|
||||
description: "Toggles their video upon clicking deafen button.",
|
||||
restartNeeded: false,
|
||||
},
|
||||
showButtonsSelf: {
|
||||
type: OptionType.SELECT,
|
||||
default: "display",
|
||||
description: "Whether you want to display buttons for your own user. Same functionality as other button(s) except it'll open DMs panel and mute/deafen for yourself upon clicking button(s).",
|
||||
restartNeeded: true,
|
||||
options: [
|
||||
{ label: "Display", value: "display" },
|
||||
{ label: "Hide", value: "hide" },
|
||||
{ label: "Disable", value: "disable" },
|
||||
],
|
||||
},
|
||||
whichNameToShow: {
|
||||
type: OptionType.SELECT,
|
||||
default: "both",
|
||||
description: "Choose whether to show nickname or username in tooltip.",
|
||||
restartNeeded: false,
|
||||
options: [
|
||||
{ label: "Global Name", value: "global" },
|
||||
{ label: "Default Username", value: "username" },
|
||||
{ label: "Both", value: "both" },
|
||||
],
|
||||
}
|
||||
});
|
||||
|
|
@ -1,3 +1,7 @@
|
|||
div[class^='list_'][class*='collapsed_'] .click-to-chat-btn {
|
||||
.voice-user-buttons {
|
||||
flex: none !important;
|
||||
}
|
||||
|
||||
div[class^='list_'][class*='collapsed_'] .voice-user-buttons {
|
||||
display: none;
|
||||
}
|
||||
|
|
@ -6,44 +6,42 @@
|
|||
|
||||
import "./styles.css";
|
||||
|
||||
import { openPrivateChannel } from "@utils/discord";
|
||||
import { User } from "@vencord/discord-types";
|
||||
import { findStoreLazy } from "@webpack";
|
||||
import { Button, MediaEngineStore, Tooltip, UserStore, VoiceActions } from "@webpack/common";
|
||||
import { Icon, User } from "@vencord/discord-types";
|
||||
import { findComponentByCodeLazy, findStoreLazy } from "@webpack";
|
||||
import { Button, ChannelStore, MediaEngineStore, NavigationRouter, Tooltip, UserStore, VoiceActions } from "@webpack/common";
|
||||
import { JSX } from "react";
|
||||
|
||||
import { settings } from ".";
|
||||
import { settings } from "./settings";
|
||||
|
||||
const SoundboardStore = findStoreLazy("SoundboardStore");
|
||||
const DeafenIconSelf = findComponentByCodeLazy("M22.7 2.7a1", "1.4l20-20ZM17") as Icon;
|
||||
const DeafenIconOther = findComponentByCodeLazy("M21.76.83a5.02", "M12.38") as Icon;
|
||||
const ChatIcon = findComponentByCodeLazy(".css,d:\"M12 22a10") as Icon;
|
||||
const MuteIconSelf = findComponentByCodeLazy("d:\"m2.7 22.7 20-20a1", "1.4ZM10.8") as Icon;
|
||||
const MuteIconOther = findComponentByCodeLazy("M21.76.83a5.02", "M12 2c.33 0") as Icon;
|
||||
|
||||
export function UserChatButton({ user }: { user: User; }) {
|
||||
if (user.id === UserStore.getCurrentUser().id) return null;
|
||||
function VoiceUserButton({ user, tooltip, icon, onClick }: { user: User; tooltip: string; icon: JSX.Element; onClick: () => void; }) {
|
||||
const isCurrent = (user.id === UserStore.getCurrentUser().id);
|
||||
if (isCurrent && settings.store.showButtonsSelf === "hide") return null;
|
||||
const disabled = isCurrent && settings.store.showButtonsSelf === "disable";
|
||||
return (
|
||||
<div className="click-to-chat-container">
|
||||
<Tooltip text={`Open chat with ${user.globalName} / @${user.username}`}>
|
||||
<div className="voice-user-button-container">
|
||||
<Tooltip text={tooltip} shouldShow={!disabled}>
|
||||
{({ onMouseEnter, onMouseLeave }) => (
|
||||
<Button
|
||||
size={Button.Sizes.MIN}
|
||||
color={Button.Colors.TRANSPARENT}
|
||||
look={Button.Looks.BLANK}
|
||||
disabled={disabled}
|
||||
onClick={e => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
openPrivateChannel({ recipientIds: [user.id] } as any);
|
||||
onClick();
|
||||
}}
|
||||
disabled={user.id === UserStore.getCurrentUser().id}
|
||||
onMouseEnter={onMouseEnter}
|
||||
onMouseLeave={onMouseLeave}
|
||||
>
|
||||
<svg
|
||||
className="click-to-chat-button"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
width="16"
|
||||
height="16"
|
||||
fill="var(--channels-default)"
|
||||
>
|
||||
<path d="M12 22a10 10 0 1 0-8.45-4.64c.13.19.11.44-.04.61l-2.06 2.37A1 1 0 0 0 2.2 22H12Z"></path>
|
||||
</svg>
|
||||
{icon}
|
||||
</Button>
|
||||
)}
|
||||
</Tooltip>
|
||||
|
|
@ -51,125 +49,83 @@ export function UserChatButton({ user }: { user: User; }) {
|
|||
);
|
||||
}
|
||||
|
||||
function getUserName(user: User) {
|
||||
const username = `@${user.username}`;
|
||||
if (settings.store.whichNameToShow === "global") {
|
||||
return user.globalName ?? username;
|
||||
} else if (settings.store.whichNameToShow === "nickname") {
|
||||
return username;
|
||||
} else if (settings.store.whichNameToShow === "both" && user.globalName) {
|
||||
return `${user.globalName} / ${username}`;
|
||||
}
|
||||
return username;
|
||||
}
|
||||
|
||||
export function UserChatButton({ user }: { user: User; }) {
|
||||
const isCurrent = (user.id === UserStore.getCurrentUser().id);
|
||||
return (
|
||||
<VoiceUserButton
|
||||
user={user}
|
||||
tooltip={isCurrent ? "Navigate to DMs" : `Open DMs with ${getUserName(user)}`}
|
||||
icon={<ChatIcon size="sm" />}
|
||||
onClick={() => {
|
||||
if (isCurrent) {
|
||||
NavigationRouter.transitionTo("/users/@me/");
|
||||
return;
|
||||
}
|
||||
const chanId = ChannelStore.getDMFromUserId(user.id);
|
||||
NavigationRouter.transitionTo(`/channels/@me/${chanId}/`);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function UserMuteButton({ user }: { user: User; }) {
|
||||
if (user.id === UserStore.getCurrentUser().id) return null;
|
||||
const isCurrent = (user.id === UserStore.getCurrentUser().id);
|
||||
const isMuted = (isCurrent && MediaEngineStore.isSelfMute()) || MediaEngineStore.isLocalMute(user.id);
|
||||
const color = isMuted ? "var(--status-danger)" : "var(--channels-default)";
|
||||
return (
|
||||
<div className="click-to-chat-container">
|
||||
<Tooltip text={`${MediaEngineStore.isLocalMute(user.id) ? "Unmute" : "Mute"} ${user.globalName} / @${user.username}`}>
|
||||
{({ onMouseEnter, onMouseLeave }) => (
|
||||
<Button
|
||||
size={Button.Sizes.MIN}
|
||||
color={Button.Colors.TRANSPARENT}
|
||||
look={Button.Looks.BLANK}
|
||||
onClick={e => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
VoiceActions.toggleLocalMute(user.id);
|
||||
}}
|
||||
onMouseEnter={onMouseEnter}
|
||||
onMouseLeave={onMouseLeave}
|
||||
>
|
||||
<svg
|
||||
className="click-to-chat-button"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
width="16"
|
||||
height="16"
|
||||
fill={MediaEngineStore.isLocalMute(user.id) ? "red" : "var(--channels-default)"}
|
||||
>
|
||||
<path d="M10.78 17.22c-.25.25-.13.67.22.72V20H9a1 1 0 1 0 0 2h6a1 1 0 1 0 0-2h-2v-2.06A8 8 0 0 0 20 10a1 1 0 1 0-2 0 6 6 0 0 1-5.8 6 .52.52 0 0 0-.35.15l-1.07 1.07ZM15.4 4.6a.46.46 0 0 0 .1-.56 4 4 0 0 0-4.03-2c-.4.05-.47.57-.21.89A3.22 3.22 0 0 1 9.9 8l-1.16.43a.5.5 0 0 0-.3.3L8.01 9.9 8 9.94V10c0 .33.04.66.12.96.08.34.5.42.74.18L15.4 4.6ZM5.1 14.07c.17.26.53.3.75.08l.75-.75c.16-.16.19-.41.08-.62a5.99 5.99 0 0 1-.24-.51.62.62 0 0 0-.7-.35c-.23.05-.47.08-.7.08-.37 0-.7.34-.58.7.18.47.4.93.65 1.37Z"></path>
|
||||
<path d="M9.2 3.86a1.21 1.21 0 0 1 0 2.28l-1.37.5a2 2 0 0 0-1.18 1.19l-.51 1.38a1.21 1.21 0 0 1-2.28 0l-.5-1.38a2 2 0 0 0-1.19-1.18L.79 6.14a1.21 1.21 0 0 1 0-2.28l1.38-.5a2 2 0 0 0 1.18-1.19L3.86.79a1.21 1.21 0 0 1 2.28 0l.5 1.38a2 2 0 0 0 1.19 1.18l1.38.51Z"></path>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M22.7 1.3a1 1 0 0 1 0 1.4l-20 20a1 1 0 0 1-1.4-1.4l20-20a1 1 0 0 1 1.4 0Z"
|
||||
clipRule="evenodd"
|
||||
></path>
|
||||
</svg>
|
||||
</Button>
|
||||
)}
|
||||
</Tooltip>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function DeafenIcon({ user, ...props }) {
|
||||
const isDeafened =
|
||||
MediaEngineStore.isLocalMute(user.id) &&
|
||||
SoundboardStore.isLocalSoundboardMuted(user.id) &&
|
||||
MediaEngineStore.isLocalVideoDisabled(user.id);
|
||||
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={16}
|
||||
height={16}
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
className={""}
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
fill={isDeafened ? "red" : "var(--channels-default)"}
|
||||
fillRule="evenodd"
|
||||
d="M21.76.83a5.02 5.02 0 0 1 .78 7.7 5 5 0 0 1-7.07 0 5.02 5.02 0 0 1 0-7.07 5 5 0 0 1 6.29-.63Zm-4.88 2.05a3 3 0 0 1 3.41-.59l-4 4a3 3 0 0 1 .59-3.41Zm4.83.83-4 4a3 3 0 0 0 4-4Z"
|
||||
clipRule="evenodd"
|
||||
className={""}
|
||||
/>
|
||||
<path
|
||||
fill={isDeafened ? "red" : "var(--channels-default)"}
|
||||
d="M12.38 1c.38.02.58.45.4.78-.15.3-.3.62-.4.95A.4.4 0 0 1 12 3a9 9 0 0 0-8.95 10h1.87a5 5 0 0 1 4.1 2.13l1.37 1.97a3.1 3.1 0 0 1-.17 3.78 2.85 2.85 0 0 1-3.55.74 11 11 0 0 1 5.71-20.61ZM22.22 11.22c.34-.18.76.02.77.4L23 12a11 11 0 0 1-5.67 9.62c-1.27.71-2.73.23-3.55-.74a3.1 3.1 0 0 1-.17-3.78l1.38-1.97a5 5 0 0 1 4.1-2.13h1.86c.03-.33.05-.66.05-1a.4.4 0 0 1 .27-.38c.33-.1.65-.25.95-.4Z"
|
||||
className={""}
|
||||
/>
|
||||
{isDeafened && (
|
||||
<line
|
||||
x1="22"
|
||||
y1="2"
|
||||
x2="2"
|
||||
y2="22"
|
||||
stroke="var(--status-danger)"
|
||||
strokeWidth="2"
|
||||
/>
|
||||
)}
|
||||
</svg>
|
||||
<VoiceUserButton
|
||||
user={user}
|
||||
tooltip={`${isMuted ? "Unmute" : "Mute"} ${isCurrent ? "yourself" : `${getUserName(user)}`}`}
|
||||
icon={isCurrent ? <MuteIconSelf muted={isMuted} size="sm" color={color} /> : <MuteIconOther muted={isMuted} size="sm" color={color} />}
|
||||
onClick={() => {
|
||||
if (isCurrent) {
|
||||
VoiceActions.toggleSelfMute();
|
||||
return;
|
||||
}
|
||||
VoiceActions.toggleLocalMute(user.id);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function UserDeafenButton({ user }: { user: User; }) {
|
||||
if (user.id === UserStore.getCurrentUser().id) return null;
|
||||
const isDeafened =
|
||||
MediaEngineStore.isLocalMute(user.id) &&
|
||||
SoundboardStore.isLocalSoundboardMuted(user.id) &&
|
||||
MediaEngineStore.isLocalVideoDisabled(user.id);
|
||||
const isCurrent = (user.id === UserStore.getCurrentUser().id);
|
||||
const isDeafened = isCurrent && MediaEngineStore.isSelfDeaf() || MediaEngineStore.isLocalMute(user.id) && SoundboardStore.isLocalSoundboardMuted(user.id) && MediaEngineStore.isLocalVideoDisabled(user.id);
|
||||
const color = isDeafened ? "var(--status-danger)" : "var(--channels-default)";
|
||||
return (
|
||||
<div className="click-to-deafen-container">
|
||||
<Tooltip text={`${isDeafened ? "Undeafen" : "Deafen"} ${user.globalName} / @${user.username}`}>
|
||||
{({ onMouseEnter, onMouseLeave }) => (
|
||||
<Button
|
||||
size={Button.Sizes.MIN}
|
||||
color={Button.Colors.TRANSPARENT}
|
||||
look={Button.Looks.BLANK}
|
||||
onClick={e => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
VoiceActions.toggleLocalMute(user.id);
|
||||
if (settings.store.muteSoundboard) {
|
||||
VoiceActions.toggleLocalSoundboardMute(user.id);
|
||||
}
|
||||
if (settings.store.disableVideo) {
|
||||
VoiceActions.setDisableLocalVideo(
|
||||
user.id,
|
||||
MediaEngineStore.isLocalVideoDisabled(user.id) ? "ENABLED" : "DISABLED",
|
||||
"default"
|
||||
);
|
||||
}
|
||||
}}
|
||||
onMouseEnter={onMouseEnter}
|
||||
onMouseLeave={onMouseLeave}
|
||||
>
|
||||
<DeafenIcon user={user} width={16} height={16} />
|
||||
</Button>
|
||||
)}
|
||||
</Tooltip>
|
||||
</div>
|
||||
<VoiceUserButton
|
||||
user={user}
|
||||
tooltip={`${isDeafened ? "Undeafen" : "Deafen"} ${isCurrent ? "yourself" : `${getUserName(user)}`}`}
|
||||
icon={isCurrent ? <DeafenIconSelf muted={isDeafened} size="sm" color={color} /> : <DeafenIconOther muted={isDeafened} size="sm" color={color} />}
|
||||
onClick={() => {
|
||||
if (isCurrent) {
|
||||
VoiceActions.toggleSelfDeaf();
|
||||
return;
|
||||
}
|
||||
VoiceActions.toggleLocalMute(user.id);
|
||||
if (settings.store.muteSoundboard) {
|
||||
VoiceActions.toggleLocalSoundboardMute(user.id);
|
||||
}
|
||||
if (settings.store.disableVideo) {
|
||||
VoiceActions.setDisableLocalVideo(
|
||||
user.id,
|
||||
MediaEngineStore.isLocalVideoDisabled(user.id) ? "ENABLED" : "DISABLED",
|
||||
"default"
|
||||
);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -65,6 +65,15 @@ export default definePlugin({
|
|||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
find: ".DEVELOPER_SECTION,",
|
||||
replacement: [
|
||||
{
|
||||
match: /\i\.\i\.isDeveloper/,
|
||||
replace: "true"
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
find: "#{intl::USER_SETTINGS_ACTIONS_MENU_LABEL}",
|
||||
replacement: {
|
||||
|
|
|
|||
|
|
@ -56,6 +56,12 @@ const settings = definePluginSettings({
|
|||
default: false,
|
||||
restartNeeded: false
|
||||
},
|
||||
allowAutoModMessages: {
|
||||
description: "Allow messages sent by AutoMod to bypass filtering.",
|
||||
type: OptionType.BOOLEAN,
|
||||
default: true,
|
||||
restartNeeded: false,
|
||||
},
|
||||
hideBlockedUserReplies: {
|
||||
description: "Hide replies to blocked/ignored users.",
|
||||
type: OptionType.BOOLEAN,
|
||||
|
|
@ -121,6 +127,7 @@ export default definePlugin({
|
|||
const blocked = this.isBlocked(message);
|
||||
const replyToBlocked = this.isReplyToBlocked(message);
|
||||
|
||||
if (message.type === 24 && settings.store.allowAutoModMessages) return [true, blocked];
|
||||
if (blocked) return [this.hideBlockedMessage(message.author.id), true];
|
||||
if (replyToBlocked) return [this.hideBlockedMessage(replyToBlocked.author.id), true];
|
||||
|
||||
|
|
@ -140,7 +147,7 @@ export default definePlugin({
|
|||
},
|
||||
|
||||
filterStream(channelStream: [ChannelStreamGroupProps | ChannelStreamMessageProps | ChannelStreamDividerProps]) {
|
||||
const { alsoHideIgnoredUsers, disableNotifications, hideBlockedUserReplies, defaultHideUsers, overrideUsers } = settings.use();
|
||||
const { alsoHideIgnoredUsers, disableNotifications, hideBlockedUserReplies, allowAutoModMessages, defaultHideUsers, overrideUsers } = settings.use();
|
||||
|
||||
const newChannelStream = channelStream.map(item => {
|
||||
if (item.type === "MESSAGE_GROUP_BLOCKED" || (item.type === "MESSAGE_GROUP_IGNORED" && alsoHideIgnoredUsers)) {
|
||||
|
|
@ -158,9 +165,9 @@ export default definePlugin({
|
|||
|
||||
let lastItem = newChannelStream[newChannelStream.length - 1];
|
||||
|
||||
// Remove the NEW Message divider if it is the last item,
|
||||
// implying the messages it was announcing got filtered.
|
||||
while (lastItem && lastItem.type === "DIVIDER" && lastItem.unreadId !== undefined) {
|
||||
// Remove the NEW Message and Date dividers if they are the last
|
||||
// item, implying the messages they were separating got filtered.
|
||||
while (lastItem && lastItem.type === "DIVIDER") {
|
||||
newChannelStream.pop();
|
||||
lastItem = newChannelStream[newChannelStream.length - 1];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,10 +21,10 @@ import "./styles.css";
|
|||
import { definePluginSettings } from "@api/Settings";
|
||||
import { classNameFactory } from "@api/Styles";
|
||||
import ErrorBoundary from "@components/ErrorBoundary";
|
||||
import { Devs } from "@utils/constants";
|
||||
import { Devs, EquicordDevs } from "@utils/constants";
|
||||
import definePlugin, { OptionType } from "@utils/types";
|
||||
import { User } from "@vencord/discord-types";
|
||||
import { DateUtils, RelationshipStore, Text, TooltipContainer } from "@webpack/common";
|
||||
import { DateUtils, Flex, RelationshipStore, Text, TooltipContainer } from "@webpack/common";
|
||||
import { PropsWithChildren } from "react";
|
||||
|
||||
const formatter = new Intl.DateTimeFormat(undefined, {
|
||||
|
|
@ -50,24 +50,34 @@ const settings = definePluginSettings({
|
|||
|
||||
export default definePlugin({
|
||||
name: "SortFriendRequests",
|
||||
authors: [Devs.Megu],
|
||||
authors: [Devs.Megu, EquicordDevs.CallMeGii],
|
||||
description: "Sorts friend requests by date of receipt",
|
||||
settings,
|
||||
|
||||
patches: [{
|
||||
find: "getRelationshipCounts(){",
|
||||
replacement: {
|
||||
match: /\}\)\.sortBy\((.+?)\)\.value\(\)/,
|
||||
replace: "}).sortBy(row => $self.wrapSort(($1), row)).value()"
|
||||
patches: [
|
||||
{
|
||||
find: "getRelationshipCounts(){",
|
||||
replacement: {
|
||||
match: /\}\)\.sortBy\((.+?)\)\.value\(\)/,
|
||||
replace: "}).sortBy(row => $self.wrapSort(($1), row)).value()"
|
||||
}
|
||||
}, {
|
||||
find: "#{intl::FRIEND_REQUEST_CANCEL}",
|
||||
replacement: {
|
||||
predicate: () => settings.store.showDates,
|
||||
match: /(?<=\.listItemContents,children:\[)\(0,.+?(?=,\(0)(?<=user:(\i).+?)/,
|
||||
replace: (children, user) => `$self.WrapperDateComponent({user:${user},children:${children}})`
|
||||
}
|
||||
},
|
||||
{
|
||||
find: "peopleListItemRef",
|
||||
replacement: {
|
||||
predicate: () => settings.store.showDates,
|
||||
match: /(?<=children:.*user:(\i),.*subText:).+?(?=,hovered:\i,showAccountIdentifier)/,
|
||||
replace: "$self.makeSubtext($1, $&)"
|
||||
}
|
||||
}
|
||||
}, {
|
||||
find: "#{intl::FRIEND_REQUEST_CANCEL}",
|
||||
replacement: {
|
||||
predicate: () => settings.store.showDates,
|
||||
match: /(?<=\.listItemContents,children:\[)\(0,.+?(?=,\(0)(?<=user:(\i).+?)/,
|
||||
replace: (children, user) => `$self.WrapperDateComponent({user:${user},children:${children}})`
|
||||
}
|
||||
}],
|
||||
],
|
||||
|
||||
wrapSort(comparator: Function, row: any) {
|
||||
return row.type === 3 || row.type === 4
|
||||
|
|
@ -75,6 +85,44 @@ export default definePlugin({
|
|||
: comparator(row);
|
||||
},
|
||||
|
||||
makeSubtext(user: User, origSubtext: any) {
|
||||
const since = this.getSince(user);
|
||||
if (isNaN(since.getTime())) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Flex
|
||||
flexDirection="column"
|
||||
style={{ gap: "0px", flexWrap: "wrap", lineHeight: "0.9rem" }}
|
||||
>
|
||||
<span>{origSubtext}</span>
|
||||
<span>
|
||||
<div className="" style={{ display: "flex", alignItems: "center" }}>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
role="img"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="8"
|
||||
height="8"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
style={{ marginRight: "4px", display: "inline-block" }}
|
||||
>
|
||||
<path
|
||||
fill="var(--input-placeholder-text)"
|
||||
fillRule="evenodd"
|
||||
d="M12 23a11 11 0 1 0 0-22 11 11 0 0 0 0 22Zm1-18a1 1 0 1 0-2 0v7c0 .27.1.52.3.7l3 3a1 1 0 0 0 1.4-1.4L13 11.58V5Z"
|
||||
clipRule="evenodd"
|
||||
></path>
|
||||
</svg>
|
||||
<span>Added — {since.toDateString()}</span>
|
||||
</div>
|
||||
</span>
|
||||
</Flex>
|
||||
);
|
||||
},
|
||||
|
||||
WrapperDateComponent: ErrorBoundary.wrap(({ user, children }: PropsWithChildren<{ user: User; }>) => {
|
||||
const since = getSince(user);
|
||||
|
||||
|
|
|
|||
|
|
@ -19,15 +19,13 @@
|
|||
import { definePluginSettings } from "@api/Settings";
|
||||
import { Flex } from "@components/Flex";
|
||||
import { DeleteIcon } from "@components/Icons";
|
||||
import { Devs } from "@utils/constants";
|
||||
import { Devs, EquicordDevs } from "@utils/constants";
|
||||
import { Logger } from "@utils/Logger";
|
||||
import definePlugin, { OptionType } from "@utils/types";
|
||||
import { Button, Forms, React, TextInput, useState } from "@webpack/common";
|
||||
import { Message } from "@vencord/discord-types";
|
||||
import { Button, Forms, React, Select, TextInput, UserStore, useState } from "@webpack/common";
|
||||
|
||||
const STRING_RULES_KEY = "TextReplace_rulesString";
|
||||
const REGEX_RULES_KEY = "TextReplace_rulesRegex";
|
||||
|
||||
type Rule = Record<"find" | "replace" | "onlyIfIncludes", string>;
|
||||
type Rule = Record<"find" | "replace" | "onlyIfIncludes" | "scope", string>;
|
||||
|
||||
interface TextReplaceProps {
|
||||
title: string;
|
||||
|
|
@ -37,7 +35,8 @@ interface TextReplaceProps {
|
|||
const makeEmptyRule: () => Rule = () => ({
|
||||
find: "",
|
||||
replace: "",
|
||||
onlyIfIncludes: ""
|
||||
onlyIfIncludes: "",
|
||||
scope: "myMessages"
|
||||
});
|
||||
const makeEmptyRuleArray = () => [makeEmptyRule()];
|
||||
|
||||
|
|
@ -65,12 +64,12 @@ const settings = definePluginSettings({
|
|||
stringRules: {
|
||||
type: OptionType.CUSTOM,
|
||||
default: makeEmptyRuleArray(),
|
||||
description: "",
|
||||
description: "Rules for replacing text using string matching."
|
||||
},
|
||||
regexRules: {
|
||||
type: OptionType.CUSTOM,
|
||||
default: makeEmptyRuleArray(),
|
||||
description: "",
|
||||
description: "Rules for replacing text using regular expressions."
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -138,10 +137,16 @@ function TextReplace({ title, rulesArray }: TextReplaceProps) {
|
|||
}
|
||||
}
|
||||
|
||||
const scopeOptions = [
|
||||
{ label: "Apply to your messages (visible to everyone)", value: "myMessages" },
|
||||
{ label: "Apply to others' messages (only visible to you)", value: "othersMessages" },
|
||||
{ label: "Apply to all messages", value: "allMessages" }
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<Forms.FormTitle tag="h4">{title}</Forms.FormTitle>
|
||||
<Flex flexDirection="column" style={{ gap: "0.5em" }}>
|
||||
<Flex flexDirection="column" style={{ gap: "0.5em", paddingBottom: "1.25em" }}>
|
||||
{
|
||||
rulesArray.map((rule, index) =>
|
||||
<React.Fragment key={`${rule.find}-${index}`}>
|
||||
|
|
@ -163,24 +168,25 @@ function TextReplace({ title, rulesArray }: TextReplaceProps) {
|
|||
onChange={e => onChange(e, index, "onlyIfIncludes")}
|
||||
/>
|
||||
</Flex>
|
||||
</Flex>
|
||||
{(index !== rulesArray.length - 1) && <Flex flexDirection="row" style={{ gap: "0.5em" }}>
|
||||
<div style={{ flex: 0.9 }}>
|
||||
<Select
|
||||
options={scopeOptions}
|
||||
isSelected={e => e === rule.scope}
|
||||
select={e => onChange(e, index, "scope")}
|
||||
serialize={e => e}
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
size={Button.Sizes.MIN}
|
||||
onClick={() => onClickRemove(index)}
|
||||
style={{
|
||||
background: "none",
|
||||
color: "var(--status-danger)",
|
||||
...(index === rulesArray.length - 1
|
||||
? {
|
||||
visibility: "hidden",
|
||||
pointerEvents: "none"
|
||||
}
|
||||
: {}
|
||||
)
|
||||
}}
|
||||
style={{ flex: 0.10, backgroundColor: "var(--input-background)", border: "1px solid var(--input-border)", color: "var(--status-danger)" }}
|
||||
>
|
||||
<DeleteIcon />
|
||||
<DeleteIcon style={{ verticalAlign: "middle" }} />
|
||||
</Button>
|
||||
</Flex>
|
||||
</Flex>}
|
||||
{(index !== rulesArray.length - 1) && <Forms.FormDivider style={{ width: "unset", margin: "0.5em 0" }}></Forms.FormDivider>}
|
||||
{isRegexRules && renderFindError(rule.find)}
|
||||
</React.Fragment>
|
||||
)
|
||||
|
|
@ -196,12 +202,12 @@ function TextReplaceTesting() {
|
|||
<>
|
||||
<Forms.FormTitle tag="h4">Test Rules</Forms.FormTitle>
|
||||
<TextInput placeholder="Type a message" onChange={setValue} />
|
||||
<TextInput placeholder="Message with rules applied" editable={false} value={applyRules(value)} />
|
||||
<TextInput placeholder="Message with rules applied" editable={false} value={applyRules(value, "allMessages")} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function applyRules(content: string): string {
|
||||
function applyRules(content: string, scope: "myMessages" | "othersMessages" | "allMessages"): string {
|
||||
if (content.length === 0) {
|
||||
return content;
|
||||
}
|
||||
|
|
@ -209,6 +215,7 @@ function applyRules(content: string): string {
|
|||
for (const rule of settings.store.stringRules) {
|
||||
if (!rule.find) continue;
|
||||
if (rule.onlyIfIncludes && !content.includes(rule.onlyIfIncludes)) continue;
|
||||
if (rule.scope !== "allMessages" && rule.scope !== scope && scope !== "allMessages") continue;
|
||||
|
||||
content = ` ${content} `.replaceAll(rule.find, rule.replace.replaceAll("\\n", "\n")).replace(/^\s|\s$/g, "");
|
||||
}
|
||||
|
|
@ -216,6 +223,7 @@ function applyRules(content: string): string {
|
|||
for (const rule of settings.store.regexRules) {
|
||||
if (!rule.find) continue;
|
||||
if (rule.onlyIfIncludes && !content.includes(rule.onlyIfIncludes)) continue;
|
||||
if (rule.scope !== "allMessages" && rule.scope !== scope && scope !== "allMessages") continue;
|
||||
|
||||
try {
|
||||
const regex = stringToRegex(rule.find);
|
||||
|
|
@ -229,18 +237,49 @@ function applyRules(content: string): string {
|
|||
return content;
|
||||
}
|
||||
|
||||
const TEXT_REPLACE_RULES_CHANNEL_ID = "1102784112584040479";
|
||||
function modifyIncomingMessage(message: Message) {
|
||||
const currentUser = UserStore.getCurrentUser();
|
||||
const messageAuthor = message.author;
|
||||
|
||||
if (!message.content || !currentUser?.id || !messageAuthor?.id || messageAuthor.id === currentUser.id) {
|
||||
return message.content;
|
||||
}
|
||||
|
||||
return applyRules(message.content, "othersMessages");
|
||||
}
|
||||
|
||||
const TEXT_REPLACE_RULES_EXEMPT_CHANNEL_IDS = [
|
||||
"1102784112584040479", // Vencord's Text Replace Rules Channel
|
||||
"1419347113745059961", // Equicord's Requests Channel
|
||||
];
|
||||
|
||||
export default definePlugin({
|
||||
name: "TextReplace",
|
||||
description: "Replace text in your messages. You can find pre-made rules in the #textreplace-rules channel in Vencord's Server",
|
||||
authors: [Devs.AutumnVN, Devs.TheKodeToad],
|
||||
description: "Replace text in your or others' messages. You can find pre-made rules in the #textreplace-rules channel in Vencord's Server.",
|
||||
authors: [Devs.AutumnVN, Devs.TheKodeToad, EquicordDevs.Etorix],
|
||||
|
||||
settings,
|
||||
modifyIncomingMessage,
|
||||
|
||||
patches: [
|
||||
{
|
||||
find: "!1,hideSimpleEmbedContent",
|
||||
replacement: {
|
||||
match: /(let{toAST:.{0,125}?)\(null!=\i\?\i:\i\).content/,
|
||||
replace: "const textReplaceContent=$self.modifyIncomingMessage(arguments[2]?.contentMessage??arguments[1]);$1textReplaceContent"
|
||||
}
|
||||
},
|
||||
],
|
||||
|
||||
start() {
|
||||
const { stringRules, regexRules } = settings.store;
|
||||
stringRules.forEach(rule => { if (!rule.scope) rule.scope = "myMessages"; });
|
||||
regexRules.forEach(rule => { if (!rule.scope) rule.scope = "myMessages"; });
|
||||
},
|
||||
|
||||
onBeforeMessageSend(channelId, msg) {
|
||||
// Channel used for sharing rules, applying rules here would be messy
|
||||
if (channelId === TEXT_REPLACE_RULES_CHANNEL_ID) return;
|
||||
msg.content = applyRules(msg.content);
|
||||
// Replacing text in channels used for sharing/requesting rules may be messy.
|
||||
if (TEXT_REPLACE_RULES_EXEMPT_CHANNEL_IDS.includes(channelId)) return;
|
||||
msg.content = applyRules(msg.content, "myMessages");
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -68,7 +68,7 @@ function TypingIndicator({ channelId, guildId }: { channelId: string; guildId: s
|
|||
const myId = UserStore.getCurrentUser()?.id;
|
||||
|
||||
const typingUsersArray = Object.keys(typingUsers).filter(id =>
|
||||
id !== myId && !(RelationshipStore.isBlocked(id) && !settings.store.includeBlockedUsers)
|
||||
id !== myId && !(RelationshipStore.isBlocked(id) && !settings.store.includeBlockedUsers) && !(RelationshipStore.isIgnored(id) && !settings.store.includeIgnoredUsers)
|
||||
);
|
||||
const [a, b, c] = typingUsersArray;
|
||||
let tooltipText: string;
|
||||
|
|
@ -145,6 +145,11 @@ const settings = definePluginSettings({
|
|||
description: "Whether to show the typing indicator for muted channels.",
|
||||
default: false
|
||||
},
|
||||
includeIgnoredUsers: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Whether to show the typing indicator for ignored users.",
|
||||
default: false
|
||||
},
|
||||
includeBlockedUsers: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Whether to show the typing indicator for blocked users.",
|
||||
|
|
@ -168,17 +173,16 @@ export default definePlugin({
|
|||
settings,
|
||||
|
||||
patches: [
|
||||
// Normal channel
|
||||
{
|
||||
// Normal channel.
|
||||
find: "UNREAD_IMPORTANT:",
|
||||
replacement: {
|
||||
match: /\.Children\.count.+?:null(?<=,channel:(\i).+?)/,
|
||||
replace: "$&,$self.TypingIndicator($1.id,$1.getGuildId())"
|
||||
}
|
||||
},
|
||||
// Theads
|
||||
{
|
||||
// This is the thread "spine" that shows in the left
|
||||
// Thread "spine" that shows in the left.
|
||||
find: "M0 15H2c0 1.6569",
|
||||
replacement: {
|
||||
match: /mentionsCount:\i.+?null(?<=channel:(\i).+?)/,
|
||||
|
|
|
|||
|
|
@ -1152,7 +1152,15 @@ export const EquicordDevs = Object.freeze({
|
|||
nicola02nb: {
|
||||
name: "nicola02nb",
|
||||
id: 257900031351193600n
|
||||
}
|
||||
},
|
||||
qouesm: {
|
||||
name: "qouesm",
|
||||
id: 130388483494641664n,
|
||||
},
|
||||
CallMeGii: {
|
||||
name: "gii",
|
||||
id: 156481332652802048n,
|
||||
},
|
||||
} satisfies Record<string, Dev>);
|
||||
|
||||
// iife so #__PURE__ works correctly
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue