mirror of
https://github.com/Equicord/Equicord.git
synced 2025-01-18 13:23:28 -05:00
Update D3SOX Plugins
Remove NotifyUserChanges as it enables a stalking like behavior
This commit is contained in:
parent
3035edf256
commit
b6cb11578c
15 changed files with 868 additions and 852 deletions
10
README.md
10
README.md
|
@ -21,7 +21,7 @@ An enhanced version of [Vencord](https://github.com/Vendicated/Vencord) by [Vend
|
|||
- Request for plugins from Discord.
|
||||
|
||||
<details>
|
||||
<summary>Extra included plugins (122 additional plugins)</summary>
|
||||
<summary>Extra included plugins (124 additional plugins)</summary>
|
||||
|
||||
- AllCallTimers by MaxHerbold and D3SOX
|
||||
- AltKrispSwitch by newwares
|
||||
|
@ -86,6 +86,7 @@ An enhanced version of [Vencord](https://github.com/Vendicated/Vencord) by [Vend
|
|||
- KeywordNotify by camila314 (maintained by thororen)
|
||||
- LoginWithQR by nexpid
|
||||
- MediaDownloader by Colorman
|
||||
- MediaPlaybackSpeed by D3SOX
|
||||
- Meow by Samwich
|
||||
- MessageColors by Hen
|
||||
- MessageLinkTooltip by Kyuuhachi
|
||||
|
@ -102,7 +103,6 @@ An enhanced version of [Vencord](https://github.com/Vendicated/Vencord) by [Vend
|
|||
- NoNitroUpsell by thororen
|
||||
- NoRoleHeaders by Samwich
|
||||
- NotificationTitle by Kyuuhachi
|
||||
- NotifyUserChanges by D3SOX
|
||||
- OnePingPerDM by ProffDea
|
||||
- PlatformSpoofer by Drag
|
||||
- PurgeMessages by bhop and nyx
|
||||
|
@ -116,9 +116,11 @@ An enhanced version of [Vencord](https://github.com/Vendicated/Vencord) by [Vend
|
|||
- ScreenRecorder by AutumnVN
|
||||
- SearchFix by Jaxx
|
||||
- SekaiStickers by MaiKokain
|
||||
- ServerProfilesToolbox by D3SOX
|
||||
- ServerSearch by camila314
|
||||
- Shakespearean by vmohammad
|
||||
- Shakespearean by vmohammad (Dev build only)
|
||||
- ShowBadgesInChat by Inbestigator and KrystalSkull
|
||||
- SilentTypingEnhanced by Ven, Rini, D3SOX
|
||||
- Slap by Korbo
|
||||
- SoundBoardLogger by Moxxie, fres, echo (maintained by thororen)
|
||||
- SteamStatusSync by niko
|
||||
|
@ -139,7 +141,7 @@ An enhanced version of [Vencord](https://github.com/Vendicated/Vencord) by [Vend
|
|||
- VencordRPC by AutumnVN
|
||||
- VideoSpeed by Samwich
|
||||
- ViewRaw2 by Kyuuhachi
|
||||
- VoiceChatUtilities by Dams and D3SOX
|
||||
- VoiceChatUtilities by D3SOX
|
||||
- WebpackTarball by Kyuuhachi
|
||||
- WhosWatching by fres
|
||||
- WigglyText by nexpid
|
||||
|
|
|
@ -1,73 +0,0 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2024 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import ErrorBoundary from "@components/ErrorBoundary";
|
||||
import { findComponentByCodeLazy } from "@webpack";
|
||||
import { moment, React, useMemo } from "@webpack/common";
|
||||
import { User } from "discord-types/general";
|
||||
|
||||
import { Activity, Application } from "../types";
|
||||
import {
|
||||
formatElapsedTime,
|
||||
getActivityImage,
|
||||
getApplicationIcons,
|
||||
getValidStartTimeStamp,
|
||||
getValidTimestamps
|
||||
} from "../utils";
|
||||
|
||||
const TimeBar = findComponentByCodeLazy<{
|
||||
start: number;
|
||||
end: number;
|
||||
themed: boolean;
|
||||
className: string;
|
||||
}>("isSingleLine");
|
||||
|
||||
interface ActivityTooltipProps {
|
||||
activity: Activity;
|
||||
application?: Application;
|
||||
user: User;
|
||||
cl: ReturnType<typeof import("@api/Styles").classNameFactory>;
|
||||
}
|
||||
|
||||
export default function ActivityTooltip({ activity, application, user, cl }: Readonly<ActivityTooltipProps>) {
|
||||
const image = useMemo(() => {
|
||||
const activityImage = getActivityImage(activity, application);
|
||||
if (activityImage) {
|
||||
return activityImage;
|
||||
}
|
||||
const icon = getApplicationIcons([activity], true)[0];
|
||||
return icon?.image.src;
|
||||
}, [activity]);
|
||||
const timestamps = useMemo(() => getValidTimestamps(activity), [activity]);
|
||||
const startTime = useMemo(() => getValidStartTimeStamp(activity), [activity]);
|
||||
|
||||
const hasDetails = activity.details ?? activity.state;
|
||||
return (
|
||||
<ErrorBoundary>
|
||||
<div className={cl("activity")}>
|
||||
{image && <img className={cl("activity-image")} src={image} alt="Activity logo" />}
|
||||
<div className={cl("activity-title")}>{activity.name}</div>
|
||||
{hasDetails && <div className={cl("activity-divider")} />}
|
||||
<div className={cl("activity-details")}>
|
||||
<div>{activity.details}</div>
|
||||
<div>{activity.state}</div>
|
||||
{!timestamps && startTime &&
|
||||
<div className={cl("activity-time-bar")}>
|
||||
{formatElapsedTime(moment(startTime / 1000), moment())}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
{timestamps && (
|
||||
<TimeBar start={timestamps.start}
|
||||
end={timestamps.end}
|
||||
themed={false}
|
||||
className={cl("activity-time-bar")}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
}
|
|
@ -1,51 +1,234 @@
|
|||
/*
|
||||
* Vencord, a modification for Discord's desktop app
|
||||
* Copyright (c) 2023 Vendicated and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2024 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import "./styles.css";
|
||||
|
||||
import { migratePluginSettings } from "@api/Settings";
|
||||
import { definePluginSettings, migratePluginSettings } from "@api/Settings";
|
||||
import { classNameFactory } from "@api/Styles";
|
||||
import ErrorBoundary from "@components/ErrorBoundary";
|
||||
import { Devs } from "@utils/constants";
|
||||
import definePlugin from "@utils/types";
|
||||
import { findComponentByCodeLazy } from "@webpack";
|
||||
import { PresenceStore, React, Tooltip, useStateFromStores } from "@webpack/common";
|
||||
import definePlugin, { OptionType } from "@utils/types";
|
||||
import { findByPropsLazy, findComponentByCodeLazy, findStoreLazy } from "@webpack";
|
||||
import { PresenceStore, React, Tooltip, useEffect, useMemo, useState, useStateFromStores } from "@webpack/common";
|
||||
import { User } from "discord-types/general";
|
||||
|
||||
import ActivityTooltip from "./components/ActivityTooltip";
|
||||
import { Caret } from "./components/Caret";
|
||||
import { SpotifyIcon } from "./components/SpotifyIcon";
|
||||
import { TwitchIcon } from "./components/TwitchIcon";
|
||||
import settings from "./settings";
|
||||
import { Activity, ActivityListIcon, ActivityViewProps, ApplicationIcon, IconCSSProperties } from "./types";
|
||||
import {
|
||||
getApplicationIcons
|
||||
} from "./utils";
|
||||
import { Activity, ActivityListIcon, Application, ApplicationIcon, IconCSSProperties } from "./types";
|
||||
|
||||
const settings = definePluginSettings({
|
||||
memberList: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Show activity icons in the member list",
|
||||
default: true,
|
||||
restartNeeded: true,
|
||||
},
|
||||
iconSize: {
|
||||
type: OptionType.SLIDER,
|
||||
description: "Size of the activity icons",
|
||||
markers: [10, 15, 20],
|
||||
default: 15,
|
||||
stickToMarkers: false,
|
||||
},
|
||||
specialFirst: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Show special activities first (Currently Spotify and Twitch)",
|
||||
default: true,
|
||||
restartNeeded: false,
|
||||
},
|
||||
renderGifs: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Allow rendering GIFs",
|
||||
default: true,
|
||||
restartNeeded: false,
|
||||
},
|
||||
showAppDescriptions: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Show application descriptions in the activity tooltip",
|
||||
default: true,
|
||||
restartNeeded: false,
|
||||
},
|
||||
divider: {
|
||||
type: OptionType.COMPONENT,
|
||||
description: "",
|
||||
component: () => (
|
||||
<div style={{
|
||||
width: "100%",
|
||||
height: 1,
|
||||
borderTop: "thin solid var(--background-modifier-accent)",
|
||||
paddingTop: 5,
|
||||
paddingBottom: 5
|
||||
}} />
|
||||
),
|
||||
},
|
||||
userPopout: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Show all activities in the profile popout/sidebar",
|
||||
default: true,
|
||||
restartNeeded: true,
|
||||
},
|
||||
allActivitiesStyle: {
|
||||
type: OptionType.SELECT,
|
||||
description: "Style for showing all activities",
|
||||
options: [
|
||||
{
|
||||
default: true,
|
||||
label: "Carousel",
|
||||
value: "carousel",
|
||||
},
|
||||
{
|
||||
label: "List",
|
||||
value: "list",
|
||||
},
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
const cl = classNameFactory("vc-bactivities-");
|
||||
|
||||
const ActivityView = findComponentByCodeLazy<ActivityViewProps>(",onOpenGameProfileModal:");
|
||||
const ApplicationStore: {
|
||||
getApplication: (id: string) => Application | null;
|
||||
} = findStoreLazy("ApplicationStore");
|
||||
|
||||
const { fetchApplication }: {
|
||||
fetchApplication: (id: string) => Promise<Application | null>;
|
||||
} = findByPropsLazy("fetchApplication");
|
||||
|
||||
const ActivityView = findComponentByCodeLazy<{
|
||||
activity: Activity | null;
|
||||
user: User;
|
||||
application?: Application;
|
||||
type?: string;
|
||||
}>(",onOpenGameProfileModal:");
|
||||
|
||||
// if discord one day decides to change their icon this needs to be updated
|
||||
const DefaultActivityIcon = findComponentByCodeLazy("M6,7 L2,7 L2,6 L6,6 L6,7 Z M8,5 L2,5 L2,4 L8,4 L8,5 Z M8,3 L2,3 L2,2 L8,2 L8,3 Z M8.88888889,0 L1.11111111,0 C0.494444444,0 0,0.494444444 0,1.11111111 L0,8.88888889 C0,9.50253861 0.497461389,10 1.11111111,10 L8.88888889,10 C9.50253861,10 10,9.50253861 10,8.88888889 L10,1.11111111 C10,0.494444444 9.5,0 8.88888889,0 Z");
|
||||
|
||||
const fetchedApplications = new Map<string, Application | null>();
|
||||
|
||||
const xboxUrl = "https://discord.com/assets/9a15d086141be29d9fcd.png"; // TODO: replace with "renderXboxImage"?
|
||||
|
||||
const ActivityTooltip = ({ activity, application, user }: Readonly<{ activity: Activity, application?: Application, user: User; }>) => {
|
||||
return (
|
||||
<ErrorBoundary>
|
||||
<div className={cl("activity-tooltip")}>
|
||||
<ActivityView
|
||||
activity={activity}
|
||||
user={user}
|
||||
application={application}
|
||||
type="BiteSizePopout"
|
||||
/>
|
||||
</div>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
};
|
||||
|
||||
function getActivityApplication({ application_id }: Activity) {
|
||||
if (!application_id) return undefined;
|
||||
let application = ApplicationStore.getApplication(application_id);
|
||||
if (!application && fetchedApplications.has(application_id)) {
|
||||
application = fetchedApplications.get(application_id) ?? null;
|
||||
}
|
||||
return application ?? undefined;
|
||||
}
|
||||
|
||||
function getApplicationIcons(activities: Activity[], preferSmall = false) {
|
||||
const applicationIcons: ApplicationIcon[] = [];
|
||||
const applications = activities.filter(activity => activity.application_id || activity.platform);
|
||||
|
||||
for (const activity of applications) {
|
||||
const { assets, application_id, platform } = activity;
|
||||
if (!application_id && !platform) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (assets) {
|
||||
const addImage = (image: string, alt: string) => {
|
||||
if (image.startsWith("mp:")) {
|
||||
const discordMediaLink = `https://media.discordapp.net/${image.replace(/mp:/, "")}`;
|
||||
if (settings.store.renderGifs || !discordMediaLink.endsWith(".gif")) {
|
||||
applicationIcons.push({
|
||||
image: { src: discordMediaLink, alt },
|
||||
activity
|
||||
});
|
||||
}
|
||||
} else {
|
||||
const src = `https://cdn.discordapp.com/app-assets/${application_id}/${image}.png`;
|
||||
applicationIcons.push({
|
||||
image: { src, alt },
|
||||
activity
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const smallImage = assets.small_image;
|
||||
const smallText = assets.small_text ?? "Small Text";
|
||||
const largeImage = assets.large_image;
|
||||
const largeText = assets.large_text ?? "Large Text";
|
||||
if (preferSmall) {
|
||||
if (smallImage) {
|
||||
addImage(smallImage, smallText);
|
||||
} else if (largeImage) {
|
||||
addImage(largeImage, largeText);
|
||||
}
|
||||
} else {
|
||||
if (largeImage) {
|
||||
addImage(largeImage, largeText);
|
||||
} else if (smallImage) {
|
||||
addImage(smallImage, smallText);
|
||||
}
|
||||
}
|
||||
} else if (application_id) {
|
||||
let application = ApplicationStore.getApplication(application_id);
|
||||
if (!application) {
|
||||
if (fetchedApplications.has(application_id)) {
|
||||
application = fetchedApplications.get(application_id) as Application | null;
|
||||
} else {
|
||||
fetchedApplications.set(application_id, null);
|
||||
fetchApplication(application_id).then(app => {
|
||||
fetchedApplications.set(application_id, app);
|
||||
}).catch(console.error);
|
||||
}
|
||||
}
|
||||
|
||||
if (application) {
|
||||
if (application.icon) {
|
||||
const src = `https://cdn.discordapp.com/app-icons/${application.id}/${application.icon}.png`;
|
||||
applicationIcons.push({
|
||||
image: { src, alt: application.name },
|
||||
activity,
|
||||
application
|
||||
});
|
||||
} else if (platform === "xbox") {
|
||||
applicationIcons.push({
|
||||
image: { src: xboxUrl, alt: "Xbox" },
|
||||
activity,
|
||||
application
|
||||
});
|
||||
}
|
||||
} else if (platform === "xbox") {
|
||||
applicationIcons.push({
|
||||
image: { src: xboxUrl, alt: "Xbox" },
|
||||
activity
|
||||
});
|
||||
}
|
||||
} else if (platform === "xbox") {
|
||||
applicationIcons.push({
|
||||
image: { src: xboxUrl, alt: "Xbox" },
|
||||
activity
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return applicationIcons;
|
||||
}
|
||||
|
||||
migratePluginSettings("BetterActivities", "MemberListActivities");
|
||||
|
||||
export default definePlugin({
|
||||
name: "BetterActivities",
|
||||
description: "Shows activity icons in the member list and allows showing all activities",
|
||||
|
@ -68,12 +251,7 @@ export default definePlugin({
|
|||
for (const appIcon of uniqueIcons) {
|
||||
icons.push({
|
||||
iconElement: <img {...appIcon.image} />,
|
||||
tooltip: <ActivityTooltip
|
||||
activity={appIcon.activity}
|
||||
application={appIcon.application}
|
||||
user={user}
|
||||
cl={cl}
|
||||
/>
|
||||
tooltip: <ActivityTooltip activity={appIcon.activity} application={appIcon.application} user={user} />
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -84,7 +262,7 @@ export default definePlugin({
|
|||
const activity = activities[activityIndex];
|
||||
const iconObject: ActivityListIcon = {
|
||||
iconElement: <IconComponent />,
|
||||
tooltip: <ActivityTooltip activity={activity} user={user} cl={cl} />
|
||||
tooltip: <ActivityTooltip activity={activity} user={user} />
|
||||
};
|
||||
|
||||
if (settings.store.specialFirst) {
|
||||
|
@ -131,8 +309,8 @@ export default definePlugin({
|
|||
return null;
|
||||
},
|
||||
|
||||
showAllActivitiesComponent({ activity, user, guild, channelId, onClose }: ActivityViewProps) {
|
||||
const [currentActivity, setCurrentActivity] = React.useState<Activity | null>(
|
||||
showAllActivitiesComponent({ activity, user, ...props }: Readonly<{ activity: Activity; user: User; application: Application; type: string; }>) {
|
||||
const [currentActivity, setCurrentActivity] = useState<Activity | null>(
|
||||
activity?.type !== 4 ? activity! : null
|
||||
);
|
||||
|
||||
|
@ -140,7 +318,7 @@ export default definePlugin({
|
|||
[PresenceStore], () => PresenceStore.getActivities(user.id).filter((activity: Activity) => activity.type !== 4)
|
||||
) ?? [];
|
||||
|
||||
React.useEffect(() => {
|
||||
useEffect(() => {
|
||||
if (!activities.length) {
|
||||
setCurrentActivity(null);
|
||||
return;
|
||||
|
@ -148,75 +326,92 @@ export default definePlugin({
|
|||
|
||||
if (!currentActivity || !activities.includes(currentActivity))
|
||||
setCurrentActivity(activities[0]);
|
||||
|
||||
}, [activities]);
|
||||
|
||||
// we use these for other activities, it would be better to somehow get the corresponding activity props
|
||||
const generalProps = useMemo(() => Object.keys(props).reduce((acc, key) => {
|
||||
// exclude activity specific props to prevent copying them to all activities (e.g. buttons)
|
||||
if (key !== "renderActions" && key !== "application") acc[key] = props[key];
|
||||
return acc;
|
||||
}, {}), [props]);
|
||||
|
||||
if (!activities.length) return null;
|
||||
|
||||
if (settings.store.allActivitiesStyle === "carousel") {
|
||||
return (
|
||||
<div style={{ display: "flex", flexDirection: "column" }}>
|
||||
<ActivityView
|
||||
activity={currentActivity}
|
||||
user={user}
|
||||
guild={guild}
|
||||
channelId={channelId}
|
||||
onClose={onClose} />
|
||||
<div
|
||||
className={cl("controls")}
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
>
|
||||
<Tooltip text="Left" tooltipClassName={cl("controls-tooltip")}>{({
|
||||
onMouseEnter,
|
||||
onMouseLeave
|
||||
}) => {
|
||||
return <span
|
||||
onMouseEnter={onMouseEnter}
|
||||
onMouseLeave={onMouseLeave}
|
||||
onClick={() => {
|
||||
const index = activities.indexOf(currentActivity!);
|
||||
if (index - 1 >= 0)
|
||||
setCurrentActivity(activities[index - 1]);
|
||||
}}
|
||||
>
|
||||
<Caret
|
||||
disabled={activities.indexOf(currentActivity!) < 1}
|
||||
direction="left" />
|
||||
</span>;
|
||||
}}</Tooltip>
|
||||
{currentActivity?.id === activity.id ? (
|
||||
<ActivityView
|
||||
activity={currentActivity}
|
||||
user={user}
|
||||
{...props}
|
||||
/>
|
||||
) : (
|
||||
<ActivityView
|
||||
activity={currentActivity}
|
||||
user={user}
|
||||
// fetch optional application
|
||||
application={getActivityApplication(currentActivity!)}
|
||||
{...generalProps}
|
||||
/>
|
||||
)}
|
||||
{activities.length > 1 &&
|
||||
<div
|
||||
className={cl("controls")}
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
>
|
||||
<Tooltip text="Left" tooltipClassName={cl("controls-tooltip")}>{({
|
||||
onMouseEnter,
|
||||
onMouseLeave
|
||||
}) => {
|
||||
return <span
|
||||
onMouseEnter={onMouseEnter}
|
||||
onMouseLeave={onMouseLeave}
|
||||
onClick={() => {
|
||||
const index = activities.indexOf(currentActivity!);
|
||||
if (index - 1 >= 0)
|
||||
setCurrentActivity(activities[index - 1]);
|
||||
}}
|
||||
>
|
||||
<Caret
|
||||
disabled={activities.indexOf(currentActivity!) < 1}
|
||||
direction="left" />
|
||||
</span>;
|
||||
}}</Tooltip>
|
||||
|
||||
<div className="carousell">
|
||||
{activities.map((activity, index) => (
|
||||
<div
|
||||
key={"dot--" + index}
|
||||
onClick={() => setCurrentActivity(activity)}
|
||||
className={`dot ${currentActivity === activity ? "selected" : ""}`} />
|
||||
))}
|
||||
<div className="carousel">
|
||||
{activities.map((activity, index) => (
|
||||
<div
|
||||
key={"dot--" + index}
|
||||
onClick={() => setCurrentActivity(activity)}
|
||||
className={`dot ${currentActivity === activity ? "selected" : ""}`} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
<Tooltip text="Right" tooltipClassName={cl("controls-tooltip")}>{({
|
||||
onMouseEnter,
|
||||
onMouseLeave
|
||||
}) => {
|
||||
return <span
|
||||
onMouseEnter={onMouseEnter}
|
||||
onMouseLeave={onMouseLeave}
|
||||
onClick={() => {
|
||||
const index = activities.indexOf(currentActivity!);
|
||||
if (index + 1 < activities.length)
|
||||
setCurrentActivity(activities[index + 1]);
|
||||
}}
|
||||
>
|
||||
<Caret
|
||||
disabled={activities.indexOf(currentActivity!) >= activities.length - 1}
|
||||
direction="right" />
|
||||
</span>;
|
||||
}}</Tooltip>
|
||||
</div>
|
||||
|
||||
<Tooltip text="Right" tooltipClassName={cl("controls-tooltip")}>{({
|
||||
onMouseEnter,
|
||||
onMouseLeave
|
||||
}) => {
|
||||
return <span
|
||||
onMouseEnter={onMouseEnter}
|
||||
onMouseLeave={onMouseLeave}
|
||||
onClick={() => {
|
||||
const index = activities.indexOf(currentActivity!);
|
||||
if (index + 1 < activities.length)
|
||||
setCurrentActivity(activities[index + 1]);
|
||||
}}
|
||||
>
|
||||
<Caret
|
||||
disabled={activities.indexOf(currentActivity!) >= activities.length - 1}
|
||||
direction="right" />
|
||||
</span>;
|
||||
}}</Tooltip>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
|
@ -228,16 +423,22 @@ export default definePlugin({
|
|||
gap: "5px",
|
||||
}}
|
||||
>
|
||||
{activities.map((activity, index) => (
|
||||
<ActivityView
|
||||
key={index}
|
||||
activity={activity}
|
||||
user={user}
|
||||
guild={guild}
|
||||
channelId={channelId}
|
||||
onClose={onClose}
|
||||
/>
|
||||
))}
|
||||
{activities.map((activity, index) =>
|
||||
index === 0 ? (
|
||||
<ActivityView
|
||||
key={index}
|
||||
activity={activity}
|
||||
user={user}
|
||||
{...props}
|
||||
/>) : (
|
||||
<ActivityView
|
||||
key={index}
|
||||
activity={activity}
|
||||
user={user}
|
||||
application={getActivityApplication(activity)}
|
||||
{...generalProps}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -254,22 +455,13 @@ export default definePlugin({
|
|||
predicate: () => settings.store.memberList,
|
||||
},
|
||||
{
|
||||
// Show all activities in the profile panel
|
||||
find: "{layout:\"DM_PANEL\",",
|
||||
// Show all activities in the user popout/sidebar
|
||||
find: '"UserActivityContainer"',
|
||||
replacement: {
|
||||
match: /(?<=\(0,\i\.jsx\)\()\i\.\i(?=,{activity:.+?,user:\i,channelId:\i.id,)/,
|
||||
replace: "$self.showAllActivitiesComponent"
|
||||
},
|
||||
predicate: () => settings.store.profileSidebar,
|
||||
},
|
||||
{
|
||||
// Show all activities in the user popout
|
||||
find: "customStatusSection,",
|
||||
replacement: {
|
||||
match: /(?<=\(0,\i\.jsx\)\()\i\.\i(?=,{activity:\i,user:\i,guild:\i,channelId:\i,onClose:\i,)/,
|
||||
replace: "$self.showAllActivitiesComponent"
|
||||
match: /(?<=\(0,\i\.jsx\)\()(\i\.\i)(?=,{...(\i),activity:\i,user:\i,application:\i)/,
|
||||
replace: "$2.type==='BiteSizePopout'?$self.showAllActivitiesComponent:$1"
|
||||
},
|
||||
predicate: () => settings.store.userPopout
|
||||
}
|
||||
},
|
||||
],
|
||||
});
|
||||
|
|
|
@ -1,77 +0,0 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2024 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { definePluginSettings } from "@api/Settings";
|
||||
import { OptionType } from "@utils/types";
|
||||
import { React } from "@webpack/common";
|
||||
|
||||
const settings = definePluginSettings({
|
||||
memberList: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Show activity icons in the member list",
|
||||
default: true,
|
||||
restartNeeded: true,
|
||||
},
|
||||
iconSize: {
|
||||
type: OptionType.SLIDER,
|
||||
description: "Size of the activity icons",
|
||||
markers: [10, 15, 20],
|
||||
default: 15,
|
||||
stickToMarkers: false,
|
||||
},
|
||||
specialFirst: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Show special activities first (Currently Spotify and Twitch)",
|
||||
default: true,
|
||||
},
|
||||
renderGifs: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Allow rendering GIFs",
|
||||
default: true,
|
||||
},
|
||||
divider: {
|
||||
type: OptionType.COMPONENT,
|
||||
description: "",
|
||||
component: () => (
|
||||
<div style={{
|
||||
width: "100%",
|
||||
height: 1,
|
||||
borderTop: "thin solid var(--background-modifier-accent)",
|
||||
paddingTop: 5,
|
||||
paddingBottom: 5
|
||||
}} />
|
||||
),
|
||||
},
|
||||
profileSidebar: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Show all activities in the profile sidebar",
|
||||
default: true,
|
||||
restartNeeded: true,
|
||||
},
|
||||
userPopout: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Show all activities in the user popout",
|
||||
default: true,
|
||||
restartNeeded: true,
|
||||
},
|
||||
allActivitiesStyle: {
|
||||
type: OptionType.SELECT,
|
||||
description: "Style for showing all activities",
|
||||
options: [
|
||||
{
|
||||
default: true,
|
||||
label: "Carousel",
|
||||
value: "carousel",
|
||||
},
|
||||
{
|
||||
label: "List",
|
||||
value: "list",
|
||||
},
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
export default settings;
|
|
@ -19,44 +19,8 @@
|
|||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.vc-bactivities-activity {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.vc-bactivities-activity-title {
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.vc-bactivities-activity-image {
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
border-radius: 50%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.vc-bactivities-activity-divider {
|
||||
width: 100%;
|
||||
border-top: 1px dotted rgb(255 255 255 / 20%);
|
||||
margin-top: 3px;
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
|
||||
.vc-bactivities-activity-details {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
color: var(--text-muted);
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.vc-bactivities-activity-time-bar {
|
||||
width: 100%;
|
||||
margin-top: 3px;
|
||||
margin-bottom: 3px;
|
||||
.vc-bactivities-activity-tooltip {
|
||||
padding: 1px;
|
||||
}
|
||||
|
||||
.vc-bactivities-caret-left,
|
||||
|
@ -101,12 +65,12 @@
|
|||
background: var(--background-modifier-accent);
|
||||
}
|
||||
|
||||
.vc-bactivities-controls .carousell {
|
||||
.vc-bactivities-controls .carousel {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.vc-bactivities-controls .carousell .dot {
|
||||
.vc-bactivities-controls .carousel .dot {
|
||||
margin: 0 4px;
|
||||
width: 10px;
|
||||
cursor: pointer;
|
||||
|
@ -117,11 +81,11 @@
|
|||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.vc-bactivities-controls .carousell .dot:hover:not(.selected) {
|
||||
.vc-bactivities-controls .carousel .dot:hover:not(.selected) {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.vc-bactivities-controls .carousell .dot.selected {
|
||||
.vc-bactivities-controls .carousel .dot.selected {
|
||||
opacity: 1;
|
||||
background: var(--dot-color, var(--brand-500));
|
||||
}
|
||||
|
|
|
@ -4,7 +4,6 @@
|
|||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { Guild, User } from "discord-types/general";
|
||||
import { CSSProperties, ImgHTMLAttributes } from "react";
|
||||
|
||||
export interface Timestamp {
|
||||
|
@ -81,11 +80,3 @@ export interface ActivityListIcon {
|
|||
export interface IconCSSProperties extends CSSProperties {
|
||||
"--icon-size": string;
|
||||
}
|
||||
|
||||
export interface ActivityViewProps {
|
||||
activity: Activity | null;
|
||||
user: User;
|
||||
guild: Guild;
|
||||
channelId: string;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
|
|
@ -1,158 +0,0 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2024 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { findByPropsLazy, findStoreLazy } from "@webpack";
|
||||
import { moment } from "@webpack/common";
|
||||
|
||||
import settings from "./settings";
|
||||
import { Activity, Application, ApplicationIcon, Timestamp } from "./types";
|
||||
|
||||
const ApplicationStore: {
|
||||
getApplication: (id: string) => Application | null;
|
||||
} = findStoreLazy("ApplicationStore");
|
||||
|
||||
const { fetchApplication }: {
|
||||
fetchApplication: (id: string) => Promise<Application | null>;
|
||||
} = findByPropsLazy("fetchApplication");
|
||||
|
||||
export function getActivityImage(activity: Activity, application?: Application): string | undefined {
|
||||
if (activity.type === 2 && activity.name === "Spotify") {
|
||||
// get either from large or small image
|
||||
const image = activity.assets?.large_image ?? activity.assets?.small_image;
|
||||
// image needs to replace 'spotify:'
|
||||
if (image?.startsWith("spotify:")) {
|
||||
// spotify cover art is always https://i.scdn.co/image/ID
|
||||
return image.replace("spotify:", "https://i.scdn.co/image/");
|
||||
}
|
||||
}
|
||||
if (activity.type === 1 && activity.name === "Twitch") {
|
||||
const image = activity.assets?.large_image;
|
||||
// image needs to replace 'twitch:'
|
||||
if (image?.startsWith("twitch:")) {
|
||||
// twitch images are always https://static-cdn.jtvnw.net/previews-ttv/live_user_USERNAME-RESOLTUON.jpg
|
||||
return `${image.replace("twitch:", "https://static-cdn.jtvnw.net/previews-ttv/live_user_")}-108x60.jpg`;
|
||||
}
|
||||
}
|
||||
// TODO: we could support other assets here
|
||||
}
|
||||
|
||||
const fetchedApplications = new Map<string, Application | null>();
|
||||
|
||||
// TODO: replace with "renderXboxImage"?
|
||||
const xboxUrl = "https://discord.com/assets/9a15d086141be29d9fcd.png";
|
||||
|
||||
export function getApplicationIcons(activities: Activity[], preferSmall = false) {
|
||||
const applicationIcons: ApplicationIcon[] = [];
|
||||
const applications = activities.filter(activity => activity.application_id || activity.platform);
|
||||
|
||||
for (const activity of applications) {
|
||||
const { assets, application_id, platform } = activity;
|
||||
if (!application_id && !platform) {
|
||||
continue;
|
||||
}
|
||||
if (assets) {
|
||||
|
||||
const addImage = (image: string, alt: string) => {
|
||||
if (image.startsWith("mp:")) {
|
||||
const discordMediaLink = `https://media.discordapp.net/${image.replace(/mp:/, "")}`;
|
||||
if (settings.store.renderGifs || !discordMediaLink.endsWith(".gif")) {
|
||||
applicationIcons.push({
|
||||
image: { src: discordMediaLink, alt },
|
||||
activity
|
||||
});
|
||||
}
|
||||
} else {
|
||||
const src = `https://cdn.discordapp.com/app-assets/${application_id}/${image}.png`;
|
||||
applicationIcons.push({
|
||||
image: { src, alt },
|
||||
activity
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const smallImage = assets.small_image;
|
||||
const smallText = assets.small_text ?? "Small Text";
|
||||
const largeImage = assets.large_image;
|
||||
const largeText = assets.large_text ?? "Large Text";
|
||||
if (preferSmall) {
|
||||
if (smallImage) {
|
||||
addImage(smallImage, smallText);
|
||||
} else if (largeImage) {
|
||||
addImage(largeImage, largeText);
|
||||
}
|
||||
} else {
|
||||
if (largeImage) {
|
||||
addImage(largeImage, largeText);
|
||||
} else if (smallImage) {
|
||||
addImage(smallImage, smallText);
|
||||
}
|
||||
}
|
||||
} else if (application_id) {
|
||||
let application = ApplicationStore.getApplication(application_id);
|
||||
if (!application) {
|
||||
if (fetchedApplications.has(application_id)) {
|
||||
application = fetchedApplications.get(application_id) as Application | null;
|
||||
} else {
|
||||
fetchedApplications.set(application_id, null);
|
||||
fetchApplication(application_id).then(app => {
|
||||
fetchedApplications.set(application_id, app);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (application) {
|
||||
if (application.icon) {
|
||||
const src = `https://cdn.discordapp.com/app-icons/${application.id}/${application.icon}.png`;
|
||||
applicationIcons.push({
|
||||
image: { src, alt: application.name },
|
||||
activity,
|
||||
application
|
||||
});
|
||||
} else if (platform === "xbox") {
|
||||
applicationIcons.push({
|
||||
image: { src: xboxUrl, alt: "Xbox" },
|
||||
activity,
|
||||
application
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (platform === "xbox") {
|
||||
applicationIcons.push({
|
||||
image: { src: xboxUrl, alt: "Xbox" },
|
||||
activity
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return applicationIcons;
|
||||
}
|
||||
|
||||
export function getValidTimestamps(activity: Activity): Required<Timestamp> | null {
|
||||
if (activity.timestamps?.start !== undefined && activity.timestamps?.end !== undefined) {
|
||||
return activity.timestamps as Required<Timestamp>;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function getValidStartTimeStamp(activity: Activity): number | null {
|
||||
if (activity.timestamps?.start !== undefined) {
|
||||
return activity.timestamps.start;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const customFormat = (momentObj: moment.Moment): string => {
|
||||
const hours = momentObj.hours();
|
||||
const formattedTime = momentObj.format("mm:ss");
|
||||
return hours > 0 ? `${momentObj.format("HH:")}${formattedTime}` : formattedTime;
|
||||
};
|
||||
|
||||
export function formatElapsedTime(startTime: moment.Moment, endTime: moment.Moment): string {
|
||||
const duration = moment.duration(endTime.diff(startTime));
|
||||
return `${customFormat(moment.utc(duration.asMilliseconds()))} elapsed`;
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2024 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
function SpeedIcon() {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
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>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export default SpeedIcon;
|
151
src/equicordplugins/mediaPlaybackSpeed/index.tsx
Normal file
151
src/equicordplugins/mediaPlaybackSpeed/index.tsx
Normal file
|
@ -0,0 +1,151 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2024 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import "./styles.css";
|
||||
|
||||
import { definePluginSettings } from "@api/Settings";
|
||||
import { classNameFactory } from "@api/Styles";
|
||||
import ErrorBoundary from "@components/ErrorBoundary";
|
||||
import { makeRange } from "@components/PluginSettings/components";
|
||||
import { Devs } from "@utils/constants";
|
||||
import definePlugin, { OptionType } from "@utils/types";
|
||||
import { ContextMenuApi, FluxDispatcher, Heading, Menu, React, Tooltip, useEffect } from "@webpack/common";
|
||||
import { RefObject } from "react";
|
||||
|
||||
import SpeedIcon from "./components/SpeedIcon";
|
||||
|
||||
const cl = classNameFactory("vc-media-playback-speed-");
|
||||
|
||||
const min = 0.25;
|
||||
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,
|
||||
description: "Voice messages",
|
||||
markers: speeds,
|
||||
},
|
||||
defaultVideoSpeed: {
|
||||
type: OptionType.SLIDER,
|
||||
default: 1,
|
||||
description: "Videos",
|
||||
markers: speeds,
|
||||
},
|
||||
defaultAudioSpeed: {
|
||||
type: OptionType.SLIDER,
|
||||
default: 1,
|
||||
description: "Audios",
|
||||
markers: speeds,
|
||||
},
|
||||
});
|
||||
|
||||
type MediaRef = RefObject<HTMLMediaElement> | undefined;
|
||||
|
||||
export default definePlugin({
|
||||
name: "MediaPlaybackSpeed",
|
||||
description: "Allows changing the (default) playback speed of media embeds",
|
||||
authors: [Devs.D3SOX],
|
||||
|
||||
settings,
|
||||
|
||||
PlaybackSpeedComponent({ mediaRef }: { mediaRef: MediaRef; }) {
|
||||
const changeSpeed = (speed: number) => {
|
||||
const media = mediaRef?.current;
|
||||
if (media) {
|
||||
media.playbackRate = speed;
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!mediaRef?.current) return;
|
||||
const media = mediaRef.current;
|
||||
if (media.tagName === "AUDIO") {
|
||||
const isVoiceMessage = media.className.includes("audioElement_");
|
||||
changeSpeed(isVoiceMessage ? settings.store.defaultVoiceMessageSpeed : settings.store.defaultAudioSpeed);
|
||||
} else if (media.tagName === "VIDEO") {
|
||||
changeSpeed(settings.store.defaultVideoSpeed);
|
||||
}
|
||||
}, [mediaRef]);
|
||||
|
||||
return (
|
||||
<Tooltip text="Playback speed">
|
||||
{tooltipProps => (
|
||||
<button
|
||||
{...tooltipProps}
|
||||
className={cl("icon")}
|
||||
onClick={e => {
|
||||
ContextMenuApi.openContextMenu(e, () =>
|
||||
<Menu.Menu
|
||||
navId="vc-playback-speed"
|
||||
onClose={() => FluxDispatcher.dispatch({ type: "CONTEXT_MENU_CLOSE" })}
|
||||
aria-label="Playback speed control"
|
||||
>
|
||||
<Menu.MenuGroup
|
||||
label="Playback speed"
|
||||
>
|
||||
{speeds.map(speed => (
|
||||
<Menu.MenuItem
|
||||
key={speed}
|
||||
id={"speed-" + speed}
|
||||
label={`${speed}x`}
|
||||
action={() => changeSpeed(speed)}
|
||||
/>
|
||||
))}
|
||||
</Menu.MenuGroup>
|
||||
</Menu.Menu>
|
||||
);
|
||||
}}>
|
||||
<SpeedIcon />
|
||||
</button>
|
||||
)}
|
||||
</Tooltip>
|
||||
);
|
||||
},
|
||||
|
||||
renderComponent(mediaRef: MediaRef) {
|
||||
return <ErrorBoundary noop>
|
||||
<this.PlaybackSpeedComponent mediaRef={mediaRef} />
|
||||
</ErrorBoundary>;
|
||||
},
|
||||
|
||||
patches: [
|
||||
// voice message embeds
|
||||
{
|
||||
find: "\"--:--\"",
|
||||
replacement: {
|
||||
match: /onVolumeShow:\i,onVolumeHide:\i\}\)(?<=useCallback\(\(\)=>\{let \i=(\i).current;.+?)/,
|
||||
replace: "$&,$self.renderComponent($1)"
|
||||
}
|
||||
},
|
||||
// audio & video embeds
|
||||
{
|
||||
// need to pass media ref via props to make it easily accessible from inside controls
|
||||
find: "renderControls(){",
|
||||
replacement: {
|
||||
match: /onToggleMuted:this.toggleMuted,/,
|
||||
replace: "$&mediaRef:this.mediaRef,"
|
||||
}
|
||||
},
|
||||
{
|
||||
find: "AUDIO:\"AUDIO\"",
|
||||
replacement: {
|
||||
match: /onVolumeHide:\i,iconClassName:\i.controlIcon,iconColor:"currentColor",sliderWrapperClassName:\i.volumeSliderWrapper\}\)\}\),/,
|
||||
replace: "$&$self.renderComponent(this.props.mediaRef),"
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
|
@ -1,11 +0,0 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2024 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import type { SVGProps } from "react";
|
||||
|
||||
export function NotificationsOffIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" {...props}><path fill="currentColor" d="M20 18.69L7.84 6.14L5.27 3.49L4 4.76l2.8 2.8v.01c-.52.99-.8 2.16-.8 3.42v5l-2 2v1h13.73l2 2L21 19.72zM12 22c1.11 0 2-.89 2-2h-4c0 1.11.89 2 2 2m6-7.32V11c0-3.08-1.64-5.64-4.5-6.32V4c0-.83-.67-1.5-1.5-1.5s-1.5.67-1.5 1.5v.68c-.15.03-.29.08-.42.12c-.1.03-.2.07-.3.11h-.01c-.01 0-.01 0-.02.01c-.23.09-.46.2-.68.31c0 0-.01 0-.01.01z"></path></svg>);
|
||||
}
|
|
@ -1,11 +0,0 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2024 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import type { SVGProps } from "react";
|
||||
|
||||
export function NotificationsOnIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" {...props}><path fill="currentColor" d="M10 20h4c0 1.1-.9 2-2 2s-2-.9-2-2m4-11c0 2.61 1.67 4.83 4 5.66V17h2v2H4v-2h2v-7c0-2.79 1.91-5.14 4.5-5.8v-.7c0-.83.67-1.5 1.5-1.5s1.5.67 1.5 1.5v.7c.71.18 1.36.49 1.95.9A5.902 5.902 0 0 0 14 9m10-1h-3V5h-2v3h-3v2h3v3h2v-3h3z"></path></svg>);
|
||||
}
|
|
@ -1,344 +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 { showNotification } from "@api/Notifications";
|
||||
import { definePluginSettings, Settings } from "@api/Settings";
|
||||
import { Devs } from "@utils/constants";
|
||||
import definePlugin, { OptionType } from "@utils/types";
|
||||
import { findByPropsLazy, findStoreLazy } from "@webpack";
|
||||
import { Menu, PresenceStore, React, SelectedChannelStore, Tooltip, UserStore } from "@webpack/common";
|
||||
import type { Channel, User } from "discord-types/general";
|
||||
import { CSSProperties } from "react";
|
||||
|
||||
import { NotificationsOffIcon } from "./components/NotificationsOffIcon";
|
||||
import { NotificationsOnIcon } from "./components/NotificationsOnIcon";
|
||||
|
||||
interface PresenceUpdate {
|
||||
user: {
|
||||
id: string;
|
||||
username?: string;
|
||||
global_name?: string;
|
||||
};
|
||||
clientStatus: {
|
||||
desktop?: string;
|
||||
web?: string;
|
||||
mobile?: string;
|
||||
console?: string;
|
||||
};
|
||||
guildId?: string;
|
||||
status: string;
|
||||
broadcast?: any; // what's this?
|
||||
activities: Array<{
|
||||
session_id: string;
|
||||
created_at: number;
|
||||
id: string;
|
||||
name: string;
|
||||
details?: string;
|
||||
type: number;
|
||||
}>;
|
||||
}
|
||||
|
||||
interface VoiceState {
|
||||
userId: string;
|
||||
channelId?: string;
|
||||
oldChannelId?: string;
|
||||
deaf: boolean;
|
||||
mute: boolean;
|
||||
selfDeaf: boolean;
|
||||
selfMute: boolean;
|
||||
selfStream: boolean;
|
||||
selfVideo: boolean;
|
||||
sessionId: string;
|
||||
suppress: boolean;
|
||||
requestToSpeakTimestamp: string | null;
|
||||
}
|
||||
|
||||
function shouldBeNative() {
|
||||
if (typeof Notification === "undefined") return false;
|
||||
|
||||
const { useNative } = Settings.notifications;
|
||||
if (useNative === "always") return true;
|
||||
if (useNative === "not-focused") return !document.hasFocus();
|
||||
return false;
|
||||
}
|
||||
|
||||
const SessionsStore = findStoreLazy("SessionsStore");
|
||||
|
||||
const StatusUtils = findByPropsLazy("useStatusFillColor", "StatusTypes");
|
||||
|
||||
function Icon(path: string, opts?: { viewBox?: string; width?: number; height?: number; }) {
|
||||
return ({ color, tooltip, small }: { color: string; tooltip: string; small: boolean; }) => (
|
||||
<Tooltip text={tooltip} >
|
||||
{(tooltipProps: any) => (
|
||||
<svg
|
||||
{...tooltipProps}
|
||||
height={(opts?.height ?? 20) - (small ? 3 : 0)}
|
||||
width={(opts?.width ?? 20) - (small ? 3 : 0)}
|
||||
viewBox={opts?.viewBox ?? "0 0 24 24"}
|
||||
fill={color}
|
||||
>
|
||||
<path d={path} />
|
||||
</svg>
|
||||
)}
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
const Icons = {
|
||||
desktop: Icon("M4 2.5c-1.103 0-2 .897-2 2v11c0 1.104.897 2 2 2h7v2H7v2h10v-2h-4v-2h7c1.103 0 2-.896 2-2v-11c0-1.103-.897-2-2-2H4Zm16 2v9H4v-9h16Z"),
|
||||
web: Icon("M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2Zm-1 17.93c-3.95-.49-7-3.85-7-7.93 0-.62.08-1.21.21-1.79L9 15v1c0 1.1.9 2 2 2v1.93Zm6.9-2.54c-.26-.81-1-1.39-1.9-1.39h-1v-3c0-.55-.45-1-1-1H8v-2h2c.55 0 1-.45 1-1V7h2c1.1 0 2-.9 2-2v-.41c2.93 1.19 5 4.06 5 7.41 0 2.08-.8 3.97-2.1 5.39Z"),
|
||||
mobile: Icon("M 187 0 L 813 0 C 916.277 0 1000 83.723 1000 187 L 1000 1313 C 1000 1416.277 916.277 1500 813 1500 L 187 1500 C 83.723 1500 0 1416.277 0 1313 L 0 187 C 0 83.723 83.723 0 187 0 Z M 125 1000 L 875 1000 L 875 250 L 125 250 Z M 500 1125 C 430.964 1125 375 1180.964 375 1250 C 375 1319.036 430.964 1375 500 1375 C 569.036 1375 625 1319.036 625 1250 C 625 1180.964 569.036 1125 500 1125 Z", { viewBox: "0 0 1000 1500", height: 17, width: 17 }),
|
||||
console: Icon("M14.8 2.7 9 3.1V47h3.3c1.7 0 6.2.3 10 .7l6.7.6V2l-4.2.2c-2.4.1-6.9.3-10 .5zm1.8 6.4c1 1.7-1.3 3.6-2.7 2.2C12.7 10.1 13.5 8 15 8c.5 0 1.2.5 1.6 1.1zM16 33c0 6-.4 10-1 10s-1-4-1-10 .4-10 1-10 1 4 1 10zm15-8v23.3l3.8-.7c2-.3 4.7-.6 6-.6H43V3h-2.2c-1.3 0-4-.3-6-.6L31 1.7V25z", { viewBox: "0 0 50 50" }),
|
||||
};
|
||||
type Platform = keyof typeof Icons;
|
||||
|
||||
const PlatformIcon = ({ platform, status, small }: { platform: Platform, status: string; small: boolean; }) => {
|
||||
const tooltip = platform[0].toUpperCase() + platform.slice(1);
|
||||
const Icon = Icons[platform] ?? Icons.desktop;
|
||||
|
||||
return <Icon color={StatusUtils.useStatusFillColor(status)} tooltip={tooltip} small={small} />;
|
||||
};
|
||||
|
||||
interface PlatformIndicatorProps {
|
||||
user: User;
|
||||
wantMargin?: boolean;
|
||||
wantTopMargin?: boolean;
|
||||
small?: boolean;
|
||||
style?: CSSProperties;
|
||||
}
|
||||
|
||||
const PlatformIndicator = ({ user, wantMargin = true, wantTopMargin = false, small = false, style = {} }: PlatformIndicatorProps) => {
|
||||
if (!user || user.bot) return null;
|
||||
|
||||
if (user.id === UserStore.getCurrentUser().id) {
|
||||
const sessions = SessionsStore.getSessions();
|
||||
if (typeof sessions !== "object") return null;
|
||||
const sortedSessions = Object.values(sessions).sort(({ status: a }: any, { status: b }: any) => {
|
||||
if (a === b) return 0;
|
||||
if (a === "online") return 1;
|
||||
if (b === "online") return -1;
|
||||
if (a === "idle") return 1;
|
||||
if (b === "idle") return -1;
|
||||
return 0;
|
||||
});
|
||||
|
||||
const ownStatus = Object.values(sortedSessions).reduce((acc: any, curr: any) => {
|
||||
if (curr.clientInfo.client !== "unknown")
|
||||
acc[curr.clientInfo.client] = curr.status;
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
const { clientStatuses } = PresenceStore.getState();
|
||||
clientStatuses[UserStore.getCurrentUser().id] = ownStatus;
|
||||
}
|
||||
|
||||
const status = PresenceStore.getState()?.clientStatuses?.[user.id] as Record<Platform, string>;
|
||||
if (!status) return null;
|
||||
|
||||
const icons = Object.entries(status).map(([platform, status]) => (
|
||||
<PlatformIcon
|
||||
key={platform}
|
||||
platform={platform as Platform}
|
||||
status={status}
|
||||
small={small}
|
||||
/>
|
||||
));
|
||||
|
||||
if (!icons.length) return null;
|
||||
|
||||
return (
|
||||
<span
|
||||
className="vc-platform-indicator"
|
||||
style={{
|
||||
display: "inline-flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
marginLeft: wantMargin ? 4 : 0,
|
||||
verticalAlign: "top",
|
||||
position: "relative",
|
||||
top: wantTopMargin ? 2 : 0,
|
||||
padding: !wantMargin ? 1 : 0,
|
||||
gap: 2,
|
||||
...style
|
||||
}}
|
||||
|
||||
>
|
||||
{icons}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
export const settings = definePluginSettings({
|
||||
notifyStatus: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Notify on status changes",
|
||||
restartNeeded: false,
|
||||
default: true,
|
||||
},
|
||||
notifyVoice: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Notify on voice channel changes",
|
||||
restartNeeded: false,
|
||||
default: false,
|
||||
},
|
||||
persistNotifications: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Persist notifications",
|
||||
restartNeeded: false,
|
||||
default: false,
|
||||
},
|
||||
userIds: {
|
||||
type: OptionType.STRING,
|
||||
description: "User IDs (comma separated)",
|
||||
restartNeeded: false,
|
||||
default: "",
|
||||
}
|
||||
});
|
||||
|
||||
function getUserIdList() {
|
||||
try {
|
||||
return settings.store.userIds.split(",").filter(Boolean);
|
||||
} catch (e) {
|
||||
settings.store.userIds = "";
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// show rich body with user avatar
|
||||
const getRichBody = (user: User, text: string | React.ReactNode) => <div
|
||||
style={{ display: "flex", flexWrap: "wrap", alignItems: "center", gap: "10px" }}>
|
||||
<div style={{ position: "relative" }}>
|
||||
<img src={user.getAvatarURL(void 0, 80, true)}
|
||||
style={{ width: "80px", height: "80px", borderRadius: "15%" }} alt={`${user.username}'s avatar`} />
|
||||
<PlatformIndicator user={user} style={{ position: "absolute", top: "-8px", right: "-10px" }} />
|
||||
</div>
|
||||
<span>{text}</span>
|
||||
</div>;
|
||||
|
||||
function triggerVoiceNotification(userId: string, userChannelId: string | null) {
|
||||
const user = UserStore.getUser(userId);
|
||||
const myChanId = SelectedChannelStore.getVoiceChannelId();
|
||||
|
||||
const name = user.username;
|
||||
|
||||
const title = shouldBeNative() ? `User ${name} changed voice status` : "User voice status change";
|
||||
if (userChannelId) {
|
||||
if (userChannelId !== myChanId) {
|
||||
showNotification({
|
||||
title,
|
||||
body: "joined a new voice channel",
|
||||
noPersist: !settings.store.persistNotifications,
|
||||
richBody: getRichBody(user, `${name} joined a new voice channel`),
|
||||
});
|
||||
}
|
||||
} else {
|
||||
showNotification({
|
||||
title,
|
||||
body: "left their voice channel",
|
||||
noPersist: !settings.store.persistNotifications,
|
||||
richBody: getRichBody(user, `${name} left their voice channel`),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function toggleUserNotify(userId: string) {
|
||||
const userIds = getUserIdList();
|
||||
if (userIds.includes(userId)) {
|
||||
userIds.splice(userIds.indexOf(userId), 1);
|
||||
} else {
|
||||
userIds.push(userId);
|
||||
}
|
||||
settings.store.userIds = userIds.join(",");
|
||||
}
|
||||
|
||||
interface UserContextProps {
|
||||
channel?: Channel;
|
||||
guildId?: string;
|
||||
user: User;
|
||||
}
|
||||
|
||||
const UserContext: NavContextMenuPatchCallback = (children, { user }: UserContextProps) => {
|
||||
if (!user || user.id === UserStore.getCurrentUser().id) return;
|
||||
const isNotifyOn = getUserIdList().includes(user.id);
|
||||
const label = isNotifyOn ? "Don't notify on changes" : "Notify on changes";
|
||||
const icon = isNotifyOn ? NotificationsOffIcon : NotificationsOnIcon;
|
||||
|
||||
children.splice(-1, 0, (
|
||||
<Menu.MenuGroup>
|
||||
<Menu.MenuItem
|
||||
id="toggle-notify-user"
|
||||
label={label}
|
||||
action={() => toggleUserNotify(user.id)}
|
||||
icon={icon}
|
||||
/>
|
||||
</Menu.MenuGroup>
|
||||
));
|
||||
};
|
||||
|
||||
const lastStatuses = new Map<string, string>();
|
||||
|
||||
export default definePlugin({
|
||||
name: "NotifyUserChanges",
|
||||
description: "Adds a notify option in the user context menu to get notified when a user changes voice channels or online status",
|
||||
authors: [Devs.D3SOX],
|
||||
|
||||
settings,
|
||||
|
||||
contextMenus: {
|
||||
"user-context": UserContext
|
||||
},
|
||||
|
||||
flux: {
|
||||
VOICE_STATE_UPDATES({ voiceStates }: { voiceStates: VoiceState[]; }) {
|
||||
if (!settings.store.notifyVoice || !settings.store.userIds) {
|
||||
return;
|
||||
}
|
||||
for (const { userId, channelId, oldChannelId } of voiceStates) {
|
||||
if (channelId !== oldChannelId) {
|
||||
const isFollowed = getUserIdList().includes(userId);
|
||||
if (!isFollowed) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (channelId) {
|
||||
// move or join new channel
|
||||
triggerVoiceNotification(userId, channelId);
|
||||
} else if (oldChannelId) {
|
||||
// leave
|
||||
triggerVoiceNotification(userId, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
PRESENCE_UPDATES({ updates }: { updates: PresenceUpdate[]; }) {
|
||||
if (!settings.store.notifyStatus || !settings.store.userIds) {
|
||||
return;
|
||||
}
|
||||
for (const { user: { id: userId, username }, status } of updates) {
|
||||
const isFollowed = getUserIdList().includes(userId);
|
||||
if (!isFollowed) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// this is also triggered for multiple guilds and when only the activities change, so we have to check if the status actually changed
|
||||
if (lastStatuses.has(userId) && lastStatuses.get(userId) !== status) {
|
||||
const user = UserStore.getUser(userId);
|
||||
const name = username ?? user.username;
|
||||
|
||||
showNotification({
|
||||
title: shouldBeNative() ? `User ${name} changed status` : "User status change",
|
||||
body: `is now ${status}`,
|
||||
noPersist: !settings.store.persistNotifications,
|
||||
richBody: getRichBody(user, `${name}'s status is now ${status}`),
|
||||
});
|
||||
}
|
||||
lastStatuses.set(userId, status);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
});
|
190
src/equicordplugins/serverProfilesToolbox/index.tsx
Normal file
190
src/equicordplugins/serverProfilesToolbox/index.tsx
Normal file
|
@ -0,0 +1,190 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2024 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { Devs } from "@utils/constants";
|
||||
import definePlugin from "@utils/types";
|
||||
import { findByPropsLazy, findComponentByCodeLazy } from "@webpack";
|
||||
import {
|
||||
Button,
|
||||
Clipboard,
|
||||
GuildMemberStore,
|
||||
Text,
|
||||
Toasts,
|
||||
UserProfileStore,
|
||||
UserStore
|
||||
} from "@webpack/common";
|
||||
import { GuildMember } from "discord-types/general";
|
||||
|
||||
const SummaryItem = findComponentByCodeLazy("borderType", "showBorder", "hideDivider");
|
||||
|
||||
interface SavedProfile {
|
||||
nick: string | null;
|
||||
pronouns: string | null;
|
||||
bio: string | null;
|
||||
themeColors: number[] | undefined;
|
||||
banner: string | undefined;
|
||||
avatar: string | undefined;
|
||||
profileEffectId: string | undefined;
|
||||
avatarDecoration: string | undefined;
|
||||
}
|
||||
|
||||
const savedProfile: SavedProfile = {
|
||||
nick: null,
|
||||
pronouns: null,
|
||||
bio: null,
|
||||
themeColors: undefined,
|
||||
banner: undefined,
|
||||
avatar: undefined,
|
||||
profileEffectId: undefined,
|
||||
avatarDecoration: undefined,
|
||||
};
|
||||
|
||||
const {
|
||||
setPendingAvatar,
|
||||
setPendingBanner,
|
||||
setPendingBio,
|
||||
setPendingNickname,
|
||||
setPendingPronouns,
|
||||
setPendingThemeColors,
|
||||
setPendingProfileEffectId,
|
||||
setPendingAvatarDecoration,
|
||||
}: {
|
||||
setPendingAvatar: (a: string | undefined) => void;
|
||||
setPendingBanner: (a: string | undefined) => void;
|
||||
setPendingBio: (a: string | null) => void;
|
||||
setPendingNickname: (a: string | null) => void;
|
||||
setPendingPronouns: (a: string | null) => void;
|
||||
setPendingThemeColors: (a: number[] | undefined) => void;
|
||||
setPendingProfileEffectId: (a: string | undefined) => void;
|
||||
setPendingAvatarDecoration: (a: string | undefined) => void;
|
||||
} = findByPropsLazy("setPendingNickname", "setPendingPronouns");
|
||||
|
||||
export default definePlugin({
|
||||
name: "ServerProfilesToolbox",
|
||||
authors: [Devs.D3SOX],
|
||||
description: "Adds a copy/paste/reset button to the server profiles editor",
|
||||
|
||||
patchServerProfiles({ guildId }: { guildId: string; }) {
|
||||
const currentUser = UserStore.getCurrentUser();
|
||||
const premiumType = currentUser.premiumType ?? 0;
|
||||
|
||||
const copy = () => {
|
||||
const profile = UserProfileStore.getGuildMemberProfile(currentUser.id, guildId);
|
||||
const nick = GuildMemberStore.getNick(guildId, currentUser.id);
|
||||
const selfMember = GuildMemberStore.getMember(guildId, currentUser.id) as GuildMember & { avatarDecoration: string | undefined; };
|
||||
savedProfile.nick = nick ?? "";
|
||||
savedProfile.pronouns = profile.pronouns;
|
||||
savedProfile.bio = profile.bio;
|
||||
savedProfile.themeColors = profile.themeColors;
|
||||
savedProfile.banner = profile.banner;
|
||||
savedProfile.avatar = selfMember.avatar;
|
||||
savedProfile.profileEffectId = profile.profileEffectId;
|
||||
savedProfile.avatarDecoration = selfMember.avatarDecoration;
|
||||
};
|
||||
|
||||
const paste = () => {
|
||||
setPendingNickname(savedProfile.nick);
|
||||
setPendingPronouns(savedProfile.pronouns);
|
||||
if (premiumType === 2) {
|
||||
setPendingBio(savedProfile.bio);
|
||||
setPendingThemeColors(savedProfile.themeColors);
|
||||
setPendingBanner(savedProfile.banner);
|
||||
setPendingAvatar(savedProfile.avatar);
|
||||
setPendingProfileEffectId(savedProfile.profileEffectId);
|
||||
setPendingAvatarDecoration(savedProfile.avatarDecoration);
|
||||
}
|
||||
};
|
||||
|
||||
const reset = () => {
|
||||
setPendingNickname(null);
|
||||
setPendingPronouns("");
|
||||
if (premiumType === 2) {
|
||||
setPendingBio(null);
|
||||
setPendingThemeColors([]);
|
||||
setPendingBanner(undefined);
|
||||
setPendingAvatar(undefined);
|
||||
setPendingProfileEffectId(undefined);
|
||||
setPendingAvatarDecoration(undefined);
|
||||
}
|
||||
};
|
||||
|
||||
const copyToClipboard = () => {
|
||||
copy();
|
||||
Clipboard.copy(JSON.stringify(savedProfile));
|
||||
};
|
||||
|
||||
const pasteFromClipboard = async () => {
|
||||
try {
|
||||
const clip = await navigator.clipboard.readText();
|
||||
if (!clip) {
|
||||
Toasts.show({
|
||||
message: "Clipboard is empty",
|
||||
type: Toasts.Type.FAILURE,
|
||||
id: Toasts.genId(),
|
||||
});
|
||||
return;
|
||||
}
|
||||
const clipboardProfile: SavedProfile = JSON.parse(clip);
|
||||
|
||||
if (!("nick" in clipboardProfile)) {
|
||||
Toasts.show({
|
||||
message: "Data is not in correct format",
|
||||
type: Toasts.Type.FAILURE,
|
||||
id: Toasts.genId(),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
Object.assign(savedProfile, JSON.parse(clip));
|
||||
paste();
|
||||
} catch (e) {
|
||||
Toasts.show({
|
||||
message: `Failed to read clipboard data: ${e}`,
|
||||
type: Toasts.Type.FAILURE,
|
||||
id: Toasts.genId(),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return <SummaryItem title="Server Profiles Toolbox" hideDivider={false} forcedDivider>
|
||||
<div style={{ display: "flex", alignItems: "center", flexDirection: "column", gap: "5px" }}>
|
||||
<Text variant="text-md/normal">
|
||||
Use the following buttons to mange the currently selected server
|
||||
</Text>
|
||||
<div style={{ display: "flex", gap: "5px" }}>
|
||||
<Button onClick={copy}>
|
||||
Copy profile
|
||||
</Button>
|
||||
<Button onClick={paste}>
|
||||
Paste profile
|
||||
</Button>
|
||||
<Button onClick={reset}>
|
||||
Reset profile
|
||||
</Button>
|
||||
</div>
|
||||
<div style={{ display: "flex", gap: "5px" }}>
|
||||
<Button onClick={copyToClipboard}>
|
||||
Copy to clipboard
|
||||
</Button>
|
||||
<Button onClick={pasteFromClipboard}>
|
||||
Paste from clipboard
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</SummaryItem>;
|
||||
},
|
||||
|
||||
patches: [
|
||||
{
|
||||
find: ".PROFILE_CUSTOMIZATION_GUILD_SELECT_TITLE",
|
||||
replacement: {
|
||||
match: /return\(0(.{10,350})\}\)\}\)\}/,
|
||||
replace: "return [(0$1})}),$self.patchServerProfiles(e)]}"
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
});
|
177
src/equicordplugins/silentTypingEnhanced/index.tsx
Normal file
177
src/equicordplugins/silentTypingEnhanced/index.tsx
Normal file
|
@ -0,0 +1,177 @@
|
|||
/*
|
||||
* Vencord, a modification for Discord's desktop app
|
||||
* Copyright (c) 2022 Vendicated and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { addChatBarButton, ChatBarButton, removeChatBarButton } from "@api/ChatButtons";
|
||||
import { ApplicationCommandInputType, ApplicationCommandOptionType, findOption, sendBotMessage } from "@api/Commands";
|
||||
import { definePluginSettings } from "@api/Settings";
|
||||
import { Devs } from "@utils/constants";
|
||||
import definePlugin, { OptionType } from "@utils/types";
|
||||
import { ChannelStore, FluxDispatcher, React } from "@webpack/common";
|
||||
|
||||
const settings = definePluginSettings({
|
||||
showIcon: {
|
||||
type: OptionType.BOOLEAN,
|
||||
default: true,
|
||||
description: "Show an icon for toggling the plugin",
|
||||
restartNeeded: true,
|
||||
},
|
||||
isEnabled: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Toggle functionality",
|
||||
default: true,
|
||||
},
|
||||
specificChats: {
|
||||
type: OptionType.BOOLEAN,
|
||||
default: false,
|
||||
description: "Disable silent typing for specific chats instead (use icon to toggle)",
|
||||
restartNeeded: false,
|
||||
},
|
||||
disabledFor: {
|
||||
type: OptionType.STRING,
|
||||
description: "Disable functionality for these chats (comma separated list of guild or user IDs)",
|
||||
default: "",
|
||||
},
|
||||
});
|
||||
|
||||
const SilentTypingToggle: ChatBarButton = ({ isMainChat, channel }) => {
|
||||
const { isEnabled, showIcon, specificChats, disabledFor } = settings.use(["isEnabled", "showIcon", "specificChats", "disabledFor"]);
|
||||
const id = channel.guild_id ?? channel.id;
|
||||
|
||||
const toggleGlobal = () => {
|
||||
settings.store.isEnabled = !settings.store.isEnabled;
|
||||
};
|
||||
const toggle = () => {
|
||||
if (specificChats) {
|
||||
if (!settings.store.isEnabled) {
|
||||
toggleGlobal();
|
||||
} else {
|
||||
const disabledChannels = getDisabledChannelsList(disabledFor);
|
||||
if (disabledChannels.includes(id)) {
|
||||
disabledChannels.splice(disabledChannels.indexOf(id), 1);
|
||||
} else {
|
||||
disabledChannels.push(id);
|
||||
}
|
||||
settings.store.disabledFor = disabledChannels.join(", ");
|
||||
}
|
||||
} else {
|
||||
toggleGlobal();
|
||||
}
|
||||
};
|
||||
const shouldEnable = isEnabled && (!specificChats || !getDisabledChannelsList(disabledFor).includes(id));
|
||||
|
||||
let tooltip = shouldEnable ? "Disable Silent Typing" : "Enable Silent Typing";
|
||||
if (specificChats) {
|
||||
if (!isEnabled) {
|
||||
tooltip = "Re-enable Silent Typing globally";
|
||||
} else {
|
||||
const chatType = channel.guild_id ? "guild" : "user";
|
||||
tooltip = shouldEnable ? `Disable Silent Typing for current ${chatType} (right-click to toggle globally)`
|
||||
: `Enable Silent Typing for current ${chatType} (right-click to toggle globally)`;
|
||||
}
|
||||
}
|
||||
|
||||
if (!isMainChat || !showIcon) return null;
|
||||
|
||||
return (
|
||||
<ChatBarButton
|
||||
tooltip={tooltip}
|
||||
onClick={toggle}
|
||||
onContextMenu={toggleGlobal}
|
||||
>
|
||||
<svg width="24" height="24" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512">
|
||||
<path fill="currentColor"
|
||||
d="M528 448H48c-26.51 0-48-21.49-48-48V112c0-26.51 21.49-48 48-48h480c26.51 0 48 21.49 48 48v288c0 26.51-21.49 48-48 48zM128 180v-40c0-6.627-5.373-12-12-12H76c-6.627 0-12 5.373-12 12v40c0 6.627 5.373 12 12 12h40c6.627 0 12-5.373 12-12zm96 0v-40c0-6.627-5.373-12-12-12h-40c-6.627 0-12 5.373-12 12v40c0 6.627 5.373 12 12 12h40c6.627 0 12-5.373 12-12zm96 0v-40c0-6.627-5.373-12-12-12h-40c-6.627 0-12 5.373-12 12v40c0 6.627 5.373 12 12 12h40c6.627 0 12-5.373 12-12zm96 0v-40c0-6.627-5.373-12-12-12h-40c-6.627 0-12 5.373-12 12v40c0 6.627 5.373 12 12 12h40c6.627 0 12-5.373 12-12zm96 0v-40c0-6.627-5.373-12-12-12h-40c-6.627 0-12 5.373-12 12v40c0 6.627 5.373 12 12 12h40c6.627 0 12-5.373 12-12zm-336 96v-40c0-6.627-5.373-12-12-12h-40c-6.627 0-12 5.373-12 12v40c0 6.627 5.373 12 12 12h40c6.627 0 12-5.373 12-12zm96 0v-40c0-6.627-5.373-12-12-12h-40c-6.627 0-12 5.373-12 12v40c0 6.627 5.373 12 12 12h40c6.627 0 12-5.373 12-12zm96 0v-40c0-6.627-5.373-12-12-12h-40c-6.627 0-12 5.373-12 12v40c0 6.627 5.373 12 12 12h40c6.627 0 12-5.373 12-12zm96 0v-40c0-6.627-5.373-12-12-12h-40c-6.627 0-12 5.373-12 12v40c0 6.627 5.373 12 12 12h40c6.627 0 12-5.373 12-12zm-336 96v-40c0-6.627-5.373-12-12-12H76c-6.627 0-12 5.373-12 12v40c0 6.627 5.373 12 12 12h40c6.627 0 12-5.373 12-12zm288 0v-40c0-6.627-5.373-12-12-12H172c-6.627 0-12 5.373-12 12v40c0 6.627 5.373 12 12 12h232c6.627 0 12-5.373 12-12zm96 0v-40c0-6.627-5.373-12-12-12h-40c-6.627 0-12 5.373-12 12v40c0 6.627 5.373 12 12 12h40c6.627 0 12-5.373 12-12z" />
|
||||
{shouldEnable &&
|
||||
<path d="M13 432L590 48" stroke="var(--red-500)" strokeWidth="72" strokeLinecap="round" />}
|
||||
{specificChats && !settings.store.isEnabled &&
|
||||
<path
|
||||
transform="matrix(0.27724514,0,0,0.27724514,34.252062,-35.543268)"
|
||||
d="M 1827.701,303.065 698.835,1431.801 92.299,825.266 0,917.564 698.835,1616.4 1919.869,395.234 Z"
|
||||
stroke="var(--green-500)"
|
||||
strokeWidth="150" strokeLinecap="round"
|
||||
fillRule="evenodd" />
|
||||
}
|
||||
</svg>
|
||||
</ChatBarButton>
|
||||
);
|
||||
};
|
||||
|
||||
function getDisabledChannelsList(list = settings.store.disabledFor) {
|
||||
try {
|
||||
return list.split(",").map(x => x.trim()).filter(Boolean);
|
||||
} catch (e) {
|
||||
settings.store.disabledFor = "";
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function isEnabled(channelId: string) {
|
||||
if (!settings.store.isEnabled) return false;
|
||||
if (settings.store.specificChats) {
|
||||
// need to resolve guild id for guild channels
|
||||
const guildId = ChannelStore.getChannel(channelId)?.guild_id;
|
||||
return !getDisabledChannelsList().includes(guildId ?? channelId);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
export default definePlugin({
|
||||
name: "SilentTypingEnhanced",
|
||||
authors: [Devs.Ven, Devs.Rini, Devs.D3SOX],
|
||||
description: "Hide that you are typing",
|
||||
dependencies: ["CommandsAPI", "ChatInputButtonAPI"],
|
||||
settings,
|
||||
|
||||
patches: [
|
||||
{
|
||||
find: '.dispatch({type:"TYPING_START_LOCAL"',
|
||||
replacement: {
|
||||
match: /startTyping\(\i\){.+?},stop/,
|
||||
replace: "startTyping:$self.startTyping,stop"
|
||||
}
|
||||
},
|
||||
],
|
||||
|
||||
commands: [{
|
||||
name: "silenttype",
|
||||
description: "Toggle whether you're hiding that you're typing or not.",
|
||||
inputType: ApplicationCommandInputType.BUILT_IN,
|
||||
options: [
|
||||
{
|
||||
name: "value",
|
||||
description: "whether to hide or not that you're typing (default is toggle)",
|
||||
required: false,
|
||||
type: ApplicationCommandOptionType.BOOLEAN,
|
||||
},
|
||||
],
|
||||
execute: async (args, ctx) => {
|
||||
settings.store.isEnabled = !!findOption(args, "value", !settings.store.isEnabled);
|
||||
sendBotMessage(ctx.channel.id, {
|
||||
content: settings.store.isEnabled ? "Silent typing enabled!" : "Silent typing disabled!",
|
||||
});
|
||||
},
|
||||
}],
|
||||
|
||||
async startTyping(channelId: string) {
|
||||
if (isEnabled(channelId)) return;
|
||||
FluxDispatcher.dispatch({ type: "TYPING_START_LOCAL", channelId });
|
||||
},
|
||||
|
||||
start: () => addChatBarButton("SilentTyping", SilentTypingToggle),
|
||||
stop: () => removeChatBarButton("SilentTyping"),
|
||||
});
|
|
@ -7,7 +7,7 @@
|
|||
import { NavContextMenuPatchCallback } from "@api/ContextMenu";
|
||||
import { definePluginSettings } from "@api/Settings";
|
||||
import { makeRange } from "@components/PluginSettings/components";
|
||||
import { Devs, EquicordDevs } from "@utils/constants";
|
||||
import { Devs } from "@utils/constants";
|
||||
import definePlugin, { OptionType } from "@utils/types";
|
||||
import { findStoreLazy } from "@webpack";
|
||||
import { GuildChannelStore, Menu, React, RestAPI, UserStore } from "@webpack/common";
|
||||
|
@ -159,8 +159,10 @@ const settings = definePluginSettings({
|
|||
export default definePlugin({
|
||||
name: "VoiceChatUtilities",
|
||||
description: "This plugin allows you to perform multiple actions on an entire channel (move, mute, disconnect, etc.) (originally by dutake)",
|
||||
authors: [EquicordDevs.Dams, Devs.D3SOX],
|
||||
authors: [Devs.D3SOX],
|
||||
|
||||
settings,
|
||||
|
||||
contextMenus: {
|
||||
"channel-context": VoiceChannelContext
|
||||
},
|
||||
|
|
Loading…
Reference in a new issue