2022-11-21 16:44:30 +02:00
|
|
|
/*
|
|
|
|
|
* 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/>.
|
|
|
|
|
*/
|
|
|
|
|
|
2024-06-26 18:09:21 +02:00
|
|
|
import "./style.css";
|
|
|
|
|
|
2025-01-23 02:48:44 +01:00
|
|
|
import { addProfileBadge, BadgePosition, BadgeUserArgs, ProfileBadge, removeProfileBadge } from "@api/Badges";
|
|
|
|
|
import { addMemberListDecorator, removeMemberListDecorator } from "@api/MemberListDecorators";
|
|
|
|
|
import { addMessageDecoration, removeMessageDecoration } from "@api/MessageDecorations";
|
2023-05-06 01:36:00 +02:00
|
|
|
import { Settings } from "@api/Settings";
|
2022-11-28 13:37:55 +01:00
|
|
|
import { Devs } from "@utils/constants";
|
|
|
|
|
import definePlugin, { OptionType } from "@utils/types";
|
2025-12-20 01:30:58 +07:00
|
|
|
import { DiscordPlatform, OnlineStatus, User } from "@vencord/discord-types";
|
2025-01-29 01:04:36 -03:00
|
|
|
import { filters, findStoreLazy, mapMangledModuleLazy } from "@webpack";
|
2025-09-23 23:58:38 +02:00
|
|
|
import { AuthenticationStore, PresenceStore, Tooltip, UserStore, useStateFromStores } from "@webpack/common";
|
2025-12-20 01:30:58 +07:00
|
|
|
import desktopIcon from "file://icons/desktopIcon.svg?minify";
|
|
|
|
|
import embeddedIcon from "file://icons/embeddedIcon.svg?minify";
|
|
|
|
|
import mobileIcon from "file://icons/mobileIcon.svg?minify";
|
|
|
|
|
import webIcon from "file://icons/webIcon.svg?minify";
|
|
|
|
|
import type { JSX } from "react";
|
2022-11-21 16:44:30 +02:00
|
|
|
|
2024-06-26 18:09:21 +02:00
|
|
|
export interface Session {
|
|
|
|
|
sessionId: string;
|
|
|
|
|
status: string;
|
|
|
|
|
active: boolean;
|
|
|
|
|
clientInfo: {
|
|
|
|
|
version: number;
|
|
|
|
|
os: string;
|
|
|
|
|
client: string;
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const SessionsStore = findStoreLazy("SessionsStore") as {
|
|
|
|
|
getSessions(): Record<string, Session>;
|
|
|
|
|
};
|
2025-12-20 01:30:58 +07:00
|
|
|
const { useStatusFillColor } = mapMangledModuleLazy(".concat(.5625*", {
|
|
|
|
|
useStatusFillColor: filters.byCode(".hex")
|
|
|
|
|
});
|
2022-12-23 04:16:17 +01:00
|
|
|
|
2025-12-20 01:30:58 +07:00
|
|
|
function Icon(svg: string, size = 20) {
|
2023-10-31 23:56:13 +01:00
|
|
|
return ({ color, tooltip, small }: { color: string; tooltip: string; small: boolean; }) => (
|
|
|
|
|
<Tooltip text={tooltip} >
|
2025-12-20 01:30:58 +07:00
|
|
|
{tooltipProps => (
|
|
|
|
|
<img
|
2023-10-31 23:56:13 +01:00
|
|
|
{...tooltipProps}
|
2025-12-20 01:44:49 +01:00
|
|
|
src={"data:image/svg+xml;utf8," + encodeURIComponent(svg.replace("#123456", color))}
|
2025-12-20 01:30:58 +07:00
|
|
|
height={size - (small ? 3 : 0)}
|
|
|
|
|
width={size - (small ? 3 : 0)}
|
|
|
|
|
/>
|
2023-10-31 23:56:13 +01:00
|
|
|
)}
|
|
|
|
|
</Tooltip>
|
|
|
|
|
);
|
2022-11-21 16:44:30 +02:00
|
|
|
}
|
|
|
|
|
|
2025-12-20 01:30:58 +07:00
|
|
|
type IconData = {
|
|
|
|
|
svg: string,
|
|
|
|
|
size: number,
|
|
|
|
|
component: (props: { color: string, tooltip: string, small: boolean; }) => JSX.Element;
|
|
|
|
|
};
|
2022-11-21 16:44:30 +02:00
|
|
|
|
2025-12-20 01:30:58 +07:00
|
|
|
const Icons: Record<DiscordPlatform, IconData> = {
|
|
|
|
|
desktop: iconData(desktopIcon),
|
|
|
|
|
web: iconData(webIcon),
|
|
|
|
|
mobile: iconData(mobileIcon),
|
|
|
|
|
embedded: iconData(embeddedIcon),
|
|
|
|
|
};
|
2022-11-21 16:44:30 +02:00
|
|
|
|
2025-12-20 01:30:58 +07:00
|
|
|
function iconData(svg: string, size: number = 20): IconData {
|
|
|
|
|
return {
|
|
|
|
|
svg,
|
|
|
|
|
size,
|
|
|
|
|
component: Icon(svg, size),
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function getPlatformTooltip(platform: DiscordPlatform): string {
|
|
|
|
|
return platform === "embedded"
|
2024-06-06 04:39:18 +03:00
|
|
|
? "Console"
|
|
|
|
|
: platform[0].toUpperCase() + platform.slice(1);
|
2025-12-20 01:30:58 +07:00
|
|
|
}
|
2024-06-06 04:39:18 +03:00
|
|
|
|
2025-12-20 01:30:58 +07:00
|
|
|
const PlatformIcon = ({ platform, status, small }: { platform: DiscordPlatform, status: OnlineStatus; small: boolean; }) => {
|
|
|
|
|
const tooltip = getPlatformTooltip(platform as DiscordPlatform);
|
|
|
|
|
|
|
|
|
|
const Icon = (Icons[platform] ?? Icons.desktop).component;
|
2022-11-21 16:44:30 +02:00
|
|
|
|
2025-01-29 01:04:36 -03:00
|
|
|
return <Icon color={useStatusFillColor(status)} tooltip={tooltip} small={small} />;
|
2022-11-21 16:44:30 +02:00
|
|
|
};
|
|
|
|
|
|
2024-06-26 18:09:21 +02:00
|
|
|
function ensureOwnStatus(user: User) {
|
2025-09-23 23:58:38 +02:00
|
|
|
if (user.id === AuthenticationStore.getId()) {
|
2023-03-18 00:58:49 -03:00
|
|
|
const sessions = SessionsStore.getSessions();
|
2022-12-23 04:16:17 +01:00
|
|
|
if (typeof sessions !== "object") return null;
|
2024-06-26 18:09:21 +02:00
|
|
|
const sortedSessions = Object.values(sessions).sort(({ status: a }, { status: b }) => {
|
2022-12-23 04:16:17 +01:00
|
|
|
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;
|
|
|
|
|
});
|
|
|
|
|
|
2024-06-26 18:09:21 +02:00
|
|
|
const ownStatus = Object.values(sortedSessions).reduce((acc, curr) => {
|
2022-12-23 04:16:17 +01:00
|
|
|
if (curr.clientInfo.client !== "unknown")
|
|
|
|
|
acc[curr.clientInfo.client] = curr.status;
|
|
|
|
|
return acc;
|
|
|
|
|
}, {});
|
|
|
|
|
|
|
|
|
|
const { clientStatuses } = PresenceStore.getState();
|
2025-09-23 23:58:38 +02:00
|
|
|
clientStatuses[AuthenticationStore.getId()] = ownStatus;
|
2022-12-23 04:16:17 +01:00
|
|
|
}
|
2024-06-26 18:09:21 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function getBadges({ userId }: BadgeUserArgs): ProfileBadge[] {
|
2025-12-20 01:30:58 +07:00
|
|
|
const colorMap = {
|
|
|
|
|
online: useStatusFillColor("online"),
|
|
|
|
|
idle: useStatusFillColor("idle"),
|
|
|
|
|
dnd: useStatusFillColor("dnd"),
|
|
|
|
|
offline: useStatusFillColor("offline"),
|
|
|
|
|
streaming: useStatusFillColor("streaming"),
|
|
|
|
|
};
|
|
|
|
|
|
2024-06-26 18:09:21 +02:00
|
|
|
const user = UserStore.getUser(userId);
|
|
|
|
|
|
|
|
|
|
if (!user || user.bot) return [];
|
|
|
|
|
|
|
|
|
|
ensureOwnStatus(user);
|
|
|
|
|
|
2025-11-15 13:39:36 +01:00
|
|
|
const status = PresenceStore.getClientStatus(user.id);
|
2024-06-26 18:09:21 +02:00
|
|
|
if (!status) return [];
|
|
|
|
|
|
2025-12-20 01:30:58 +07:00
|
|
|
return Object.entries(status).map(([platform, status]) => {
|
|
|
|
|
const tooltip = getPlatformTooltip(platform as DiscordPlatform);
|
|
|
|
|
|
|
|
|
|
const icon = Icons[platform as DiscordPlatform] ?? Icons.desktop;
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
description: tooltip,
|
2025-12-20 01:44:49 +01:00
|
|
|
iconSrc: "data:image/svg+xml;utf8," + encodeURIComponent(icon.svg.replace("#123456", colorMap[status] ?? colorMap.offline)),
|
2025-12-20 01:30:58 +07:00
|
|
|
props: {
|
|
|
|
|
style: { width: icon.size, height: icon.size },
|
|
|
|
|
},
|
|
|
|
|
key: `vc-platform-indicator-${platform}`,
|
|
|
|
|
} satisfies ProfileBadge;
|
|
|
|
|
});
|
2024-06-26 18:09:21 +02:00
|
|
|
}
|
|
|
|
|
|
2025-02-02 17:59:22 -03:00
|
|
|
const PlatformIndicator = ({ user, small = false }: { user: User; small?: boolean; }) => {
|
2024-06-26 18:09:21 +02:00
|
|
|
ensureOwnStatus(user);
|
2022-12-23 04:16:17 +01:00
|
|
|
|
2025-11-15 13:39:36 +01:00
|
|
|
const status = useStateFromStores([PresenceStore], () => PresenceStore.getClientStatus(user.id));
|
2022-11-21 16:44:30 +02:00
|
|
|
if (!status) return null;
|
|
|
|
|
|
|
|
|
|
const icons = Object.entries(status).map(([platform, status]) => (
|
|
|
|
|
<PlatformIcon
|
|
|
|
|
key={platform}
|
2025-11-15 13:39:36 +01:00
|
|
|
platform={platform as DiscordPlatform}
|
2022-11-21 16:44:30 +02:00
|
|
|
status={status}
|
2023-10-21 19:41:56 +03:00
|
|
|
small={small}
|
2022-11-21 16:44:30 +02:00
|
|
|
/>
|
|
|
|
|
));
|
|
|
|
|
|
2022-11-21 20:25:21 +02:00
|
|
|
if (!icons.length) return null;
|
|
|
|
|
|
2022-12-23 04:16:17 +01:00
|
|
|
return (
|
2023-05-12 03:54:57 +02:00
|
|
|
<span
|
|
|
|
|
className="vc-platform-indicator"
|
2025-02-02 17:59:22 -03:00
|
|
|
style={{ gap: "2px" }}
|
2023-05-12 03:54:57 +02:00
|
|
|
>
|
2022-11-21 16:44:30 +02:00
|
|
|
{icons}
|
2023-04-30 01:56:03 +02:00
|
|
|
</span>
|
2022-12-23 04:16:17 +01:00
|
|
|
);
|
2022-12-21 21:16:32 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const badge: ProfileBadge = {
|
2024-06-26 18:09:21 +02:00
|
|
|
getBadges,
|
2022-12-21 21:16:32 +02:00
|
|
|
position: BadgePosition.START,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const indicatorLocations = {
|
|
|
|
|
list: {
|
|
|
|
|
description: "In the member list",
|
2025-09-23 23:58:38 +02:00
|
|
|
onEnable: () => addMemberListDecorator("platform-indicator", ({ user }) =>
|
|
|
|
|
user && !user.bot ? <PlatformIndicator user={user} small={true} /> : null
|
2022-12-21 21:16:32 +02:00
|
|
|
),
|
2025-01-23 02:48:44 +01:00
|
|
|
onDisable: () => removeMemberListDecorator("platform-indicator")
|
2022-12-21 21:16:32 +02:00
|
|
|
},
|
|
|
|
|
badges: {
|
|
|
|
|
description: "In user profiles, as badges",
|
2025-01-23 02:48:44 +01:00
|
|
|
onEnable: () => addProfileBadge(badge),
|
|
|
|
|
onDisable: () => removeProfileBadge(badge)
|
2022-12-21 21:16:32 +02:00
|
|
|
},
|
|
|
|
|
messages: {
|
|
|
|
|
description: "Inside messages",
|
2025-09-23 23:58:38 +02:00
|
|
|
onEnable: () => addMessageDecoration("platform-indicator", props => {
|
|
|
|
|
const user = props.message?.author;
|
|
|
|
|
return user && !user.bot ? <PlatformIndicator user={props.message?.author} /> : null;
|
|
|
|
|
}),
|
2025-01-23 02:48:44 +01:00
|
|
|
onDisable: () => removeMessageDecoration("platform-indicator")
|
2022-12-21 21:16:32 +02:00
|
|
|
}
|
2022-11-21 16:44:30 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export default definePlugin({
|
|
|
|
|
name: "PlatformIndicators",
|
|
|
|
|
description: "Adds platform indicators (Desktop, Mobile, Web...) to users",
|
2023-05-13 22:56:31 +02:00
|
|
|
authors: [Devs.kemo, Devs.TheSun, Devs.Nuckyz, Devs.Ven],
|
2022-12-21 21:16:32 +02:00
|
|
|
dependencies: ["MessageDecorationsAPI", "MemberListDecoratorsAPI"],
|
|
|
|
|
|
|
|
|
|
start() {
|
|
|
|
|
const settings = Settings.plugins.PlatformIndicators;
|
|
|
|
|
Object.entries(indicatorLocations).forEach(([key, value]) => {
|
|
|
|
|
if (settings[key]) value.onEnable();
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
stop() {
|
|
|
|
|
Object.entries(indicatorLocations).forEach(([_, value]) => {
|
|
|
|
|
value.onDisable();
|
|
|
|
|
});
|
|
|
|
|
},
|
2022-11-21 16:44:30 +02:00
|
|
|
|
2023-03-18 00:58:49 -03:00
|
|
|
patches: [
|
|
|
|
|
{
|
|
|
|
|
find: ".Masks.STATUS_ONLINE_MOBILE",
|
|
|
|
|
predicate: () => Settings.plugins.PlatformIndicators.colorMobileIndicator,
|
|
|
|
|
replacement: [
|
|
|
|
|
{
|
|
|
|
|
// Return the STATUS_ONLINE_MOBILE mask if the user is on mobile, no matter the status
|
2023-10-26 01:29:52 +02:00
|
|
|
match: /\.STATUS_TYPING;switch(?=.+?(if\(\i\)return \i\.\i\.Masks\.STATUS_ONLINE_MOBILE))/,
|
|
|
|
|
replace: ".STATUS_TYPING;$1;switch"
|
2023-03-18 00:58:49 -03:00
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
// Return the STATUS_ONLINE_MOBILE mask if the user is on mobile, no matter the status
|
2023-10-26 01:29:52 +02:00
|
|
|
match: /switch\(\i\)\{case \i\.\i\.ONLINE:(if\(\i\)return\{[^}]+\})/,
|
|
|
|
|
replace: "$1;$&"
|
2023-03-18 00:58:49 -03:00
|
|
|
}
|
|
|
|
|
]
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
find: ".AVATAR_STATUS_MOBILE_16;",
|
|
|
|
|
predicate: () => Settings.plugins.PlatformIndicators.colorMobileIndicator,
|
|
|
|
|
replacement: [
|
|
|
|
|
{
|
|
|
|
|
// Return the AVATAR_STATUS_MOBILE size mask if the user is on mobile, no matter the status
|
|
|
|
|
match: /\i===\i\.\i\.ONLINE&&(?=.{0,70}\.AVATAR_STATUS_MOBILE_16;)/,
|
|
|
|
|
replace: ""
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
// Fix sizes for mobile indicators which aren't online
|
|
|
|
|
match: /(?<=\(\i\.status,)(\i)(?=,(\i),\i\))/,
|
|
|
|
|
replace: (_, userStatus, isMobile) => `${isMobile}?"online":${userStatus}`
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
// Make isMobile true no matter the status
|
|
|
|
|
match: /(?<=\i&&!\i)&&\i===\i\.\i\.ONLINE/,
|
|
|
|
|
replace: ""
|
|
|
|
|
}
|
|
|
|
|
]
|
|
|
|
|
},
|
|
|
|
|
{
|
2023-10-26 00:03:51 +02:00
|
|
|
find: "}isMobileOnline(",
|
2023-03-18 00:58:49 -03:00
|
|
|
predicate: () => Settings.plugins.PlatformIndicators.colorMobileIndicator,
|
|
|
|
|
replacement: {
|
|
|
|
|
// Make isMobileOnline return true no matter what is the user status
|
|
|
|
|
match: /(?<=\i\[\i\.\i\.MOBILE\])===\i\.\i\.ONLINE/,
|
|
|
|
|
replace: "!= null"
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
],
|
|
|
|
|
|
2022-11-21 16:44:30 +02:00
|
|
|
options: {
|
2022-12-21 21:16:32 +02:00
|
|
|
...Object.fromEntries(
|
|
|
|
|
Object.entries(indicatorLocations).map(([key, value]) => {
|
|
|
|
|
return [key, {
|
|
|
|
|
type: OptionType.BOOLEAN,
|
|
|
|
|
description: `Show indicators ${value.description.toLowerCase()}`,
|
|
|
|
|
// onChange doesn't give any way to know which setting was changed, so restart required
|
|
|
|
|
restartNeeded: true,
|
2022-12-23 04:16:17 +01:00
|
|
|
default: true
|
2022-12-21 21:16:32 +02:00
|
|
|
}];
|
|
|
|
|
})
|
2023-03-18 00:58:49 -03:00
|
|
|
),
|
|
|
|
|
colorMobileIndicator: {
|
|
|
|
|
type: OptionType.BOOLEAN,
|
|
|
|
|
description: "Whether to make the mobile indicator match the color of the user status.",
|
|
|
|
|
default: true,
|
|
|
|
|
restartNeeded: true
|
|
|
|
|
}
|
2022-11-21 16:44:30 +02:00
|
|
|
}
|
|
|
|
|
});
|