Plugin

MutualGroupDMs

Shows mutual group dms in profiles

Friends Appearance
index.tsx
Download

Source

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