mirror of
https://github.com/Equicord/Equicord.git
synced 2025-06-25 22:37:02 -04:00
forked!!
This commit is contained in:
parent
538b87062a
commit
ea7451bcdc
326 changed files with 24876 additions and 2280 deletions
|
@ -0,0 +1,11 @@
|
|||
/*
|
||||
* 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>);
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
/*
|
||||
* 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>);
|
||||
}
|
344
src/equicordplugins/notifyUserChanges/index.tsx
Normal file
344
src/equicordplugins/notifyUserChanges/index.tsx
Normal file
|
@ -0,0 +1,344 @@
|
|||
/*
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
});
|
Loading…
Add table
Add a link
Reference in a new issue