Plugin

MutualGroupDMs

Shows mutual group dms in profiles

Friends Appearance
index.tsx
Download

Source

src/plugins/mutualGroupDMs/index.tsx
1/*
2 * FiveCord — a Discord client mod
3 * Copyright (c) 2025 FiveCord
4 * SPDX-License-Identifier: GPL-3.0-or-later
5 */
6
7import "./style.css";
8
9import { BaseText } from "@components/BaseText";
10import ErrorBoundary from "@components/ErrorBoundary";
11import { Devs } from "@utils/constants";
12import { isNonNullish } from "@utils/guards";
13import { Logger } from "@utils/Logger";
14import definePlugin from "@utils/types";
15import { Channel, User } from "@vencord/discord-types";
16import { findByPropsLazy, findCssClassesLazy } from "@webpack";
17import { Avatar, ChannelStore, Clickable, IconUtils, RelationshipStore, ScrollerThin, Text, useMemo, UserStore } from "@webpack/common";
18import { ComponentType, JSX } from "react";
19
20const SelectedChannelActionCreators = findByPropsLazy("selectPrivateChannel");
21const UserUtils = findByPropsLazy("getGlobalName");
22
23const ProfileListClasses = findCssClassesLazy("empty", "textContainer", "connectionIcon");
24const TabBarClasses = findCssClassesLazy("tabPanelScroller", "tabBarPanel");
25const MutualsListClasses = findCssClassesLazy("row", "icon", "name", "details");
26
27let ExpandableList: ComponentType<any> = () => null;
28
29function getGroupDMName(channel: Channel) {
30 return channel.name ||
31 channel.recipients
32 .map(UserStore.getUser)
33 .filter(isNonNullish)
34 .map(c => RelationshipStore.getNickname(c.id) || UserUtils.getName(c))
35 .join(", ");
36}
37
38const getMutualGroupDms = (userId: string) =>
39 ChannelStore.getSortedPrivateChannels()
40 .filter(c => c.isGroupDM() && c.recipients.includes(userId));
41
42const isBotOrSelf = (user: User) => user.bot || user.id === UserStore.getCurrentUser().id;
43
44function getMutualGDMCountText(user: User) {
45 const count = getMutualGroupDms(user.id).length;
46 return `${count === 0 ? "No" : count} Mutual Group${count !== 1 ? "s" : ""}`;
47}
48
49function renderClickableGDMs(mutualDms: Channel[], onClose: () => void) {
50 return mutualDms.map(c => (
51 <Clickable
52 key={c.id}
53 className={MutualsListClasses.row}
54 onClick={() => {
55 onClose();
56 SelectedChannelActionCreators.selectPrivateChannel(c.id);
57 }}
58 >
59 <Avatar
60 src={IconUtils.getChannelIconURL({ id: c.id, icon: c.icon, size: 32 })}
61 size="SIZE_40"
62 className={MutualsListClasses.icon}
63 >
64 </Avatar>
65 <div className={MutualsListClasses.details}>
66 <div className={MutualsListClasses.name}>{getGroupDMName(c)}</div>
67 <Text variant="text-xs/medium">{c.recipients.length + 1} Members</Text>
68 </div>
69 </Clickable>
70 ));
71}
72
73const IS_PATCHED = Symbol("MutualGroupDMs.Patched");
74
75export default definePlugin({
76 name: "MutualGroupDMs",
77 description: "Shows mutual group dms in profiles",
78 tags: ["Friends", "Appearance"],
79 authors: [Devs.amia],
80
81 patches: [
82 // User Profile Modal
83 {
84 find: ".BOT_DATA_ACCESS?(",
85 replacement: [
86 {
87 match: /\i\.useEffect.{0,100}(\i)\[0\]\.section/,
88 replace: "$self.pushSection($1,arguments[0].user);$&"
89 },
90 {
91 match: /\(0,\i\.jsx\)\(\i,\{items:\i,section:(\i)/,
92 replace: "$1===&#039;MUTUAL_GDMS&#039;?$self.renderMutualGDMs(arguments[0]):$&"
93 },
94 // Discord adds spacing between each item which pushes our tab off screen.
95 // set the gap to zero to ensure ours stays on screen
96 {
97 match: /className:\i\.\i(?=,type:"top")/,
98 replace: &#039;$& + " vc-mutual-gdms-modal-tab-bar"&#039;
99 }
100 ]
101 },
102 // User Profile Modal v2
103 {
104 find: ".WIDGETS?",
105 replacement: [
106 {
107 match: /items:(\i),.+?(?=return\(0,\i\.jsxs?\)\("div)/,
108 replace: "$&$self.pushSection($1,arguments[0].user);"
109 },
110 {
111 match: /children:(?=.{0,100}?component:.+?section:(\i))/,
112 replace: "$&$1===&#039;MUTUAL_GDMS&#039;?$self.renderMutualGDMs(arguments[0]):"
113 },
114 // Make the gap between each item smaller so our tab can fit.
115 {
116 match: /type:"top",/,
117 replace: &#039;$&className:"vc-mutual-gdms-modal-v2-tab-bar",&#039;
118 },
119 ]
120 },
121 {
122 find: &#039;section:"MUTUAL_FRIENDS"&#039;,
123 replacement: [
124 {
125 match: /\i\|\|\i(?=\?\(0,\i\.jsxs?\)\(\i\.\i\.Overlay,)/,
126 replace: "$&||$self.getMutualGroupDms(arguments[0].user.id).length>0"
127 },
128 {
129 match: /\.openUserProfileModal.+?\)}\)}\)(?<=,(\i)&&(\i)&&(\(0,\i\.jsxs?\)\(\i\.\i,{className:(\i)\.\i}\)).{0,50}?"MUTUAL_FRIENDS".+?)/,
130 replace: (m, hasMutualGuilds, hasMutualFriends, Divider, classes) => "" +
131 `${m},$self.renderDMPageList({user:arguments[0].user,hasDivider:${hasMutualGuilds}||${hasMutualFriends},Divider:${Divider},listStyle:${classes}.list})`
132 },
133 {
134 match: /(?=function (\i)\(\i\){let{section:\i,header:\i[^}]+?onExpand:)/,
135 replace: "$self.ExpandableList=$1;"
136 }
137 ]
138 }
139 ],
140
141 set ExpandableList(value: any) {
142 ExpandableList = value;
143 },
144
145 getMutualGroupDms(userId: string) {
146 try {
147 return getMutualGroupDms(userId);
148 } catch (e) {
149 new Logger("MutualGroupDMs").error("Failed to get mutual group dms:", e);
150 }
151
152 return [];
153 },
154
155 pushSection(sections: any[], user: User) {
156 try {
157 if (isBotOrSelf(user) || sections[IS_PATCHED]) return;
158
159 sections[IS_PATCHED] = true;
160 sections.push({
161 text: getMutualGDMCountText(user),
162 section: "MUTUAL_GDMS",
163 });
164 } catch (e) {
165 new Logger("MutualGroupDMs").error("Failed to push mutual group dms section:", e);
166 }
167 },
168
169 renderMutualGDMs: ErrorBoundary.wrap(({ user, onClose }: { user: User, onClose: () => void; }) => {
170 const mutualGDms = useMemo(() => getMutualGroupDms(user.id), [user.id]);
171 const entries = renderClickableGDMs(mutualGDms, onClose);
172
173 return (
174 <ScrollerThin
175 className={TabBarClasses.tabPanelScroller}
176 fade={true}
177 onClose={onClose}
178 >
179 {entries.length > 0
180 ? entries
181 : (
182 <div className={ProfileListClasses.empty}>
183 <div className={ProfileListClasses.textContainer}>
184 <BaseText tag="h3" size="md" weight="medium" style={{ color: "var(--text-strong)" }}>You don&#039;t have any group chats in common</BaseText>
185 </div>
186 </div>
187 )
188 }
189 </ScrollerThin>
190 );
191 }),
192
193 renderDMPageList: ErrorBoundary.wrap(({ user, hasDivider, Divider, listStyle }: { user: User, hasDivider: boolean, Divider: JSX.Element, listStyle: string; }) => {
194 const mutualGDms = getMutualGroupDms(user.id);
195 if (mutualGDms.length === 0) return null;
196
197 return (
198 <>
199 {hasDivider && Divider}
200 <ExpandableList
201 listClassName={listStyle}
202 header={"Mutual Groups"}
203 isLoading={false}
204 items={renderClickableGDMs(mutualGDms, () => { })}
205 />
206 </>
207 );
208 }, { noop: true })
209});
210