Plugin
MutualGroupDMs
Shows mutual group dms in profiles
1
/*2
* FiveCord — a Discord client mod3
* Copyright (c) 2025 FiveCord4
* SPDX-License-Identifier: GPL-3.0-or-later5
*/6
7
import "./style.css";8
9
import { BaseText } from "@components/BaseText";10
import ErrorBoundary from "@components/ErrorBoundary";11
import { Devs } from "@utils/constants";12
import { isNonNullish } from "@utils/guards";13
import { Logger } from "@utils/Logger";14
import definePlugin from "@utils/types";15
import { Channel, User } from "@vencord/discord-types";16
import { findByPropsLazy, findCssClassesLazy } from "@webpack";17
import { Avatar, ChannelStore, Clickable, IconUtils, RelationshipStore, ScrollerThin, Text, useMemo, UserStore } from "@webpack/common";18
import { ComponentType, JSX } from "react";19
20
const SelectedChannelActionCreators = findByPropsLazy("selectPrivateChannel");21
const UserUtils = findByPropsLazy("getGlobalName");22
23
const ProfileListClasses = findCssClassesLazy("empty", "textContainer", "connectionIcon");24
const TabBarClasses = findCssClassesLazy("tabPanelScroller", "tabBarPanel");25
const MutualsListClasses = findCssClassesLazy("row", "icon", "name", "details");26
27
let ExpandableList: ComponentType<any> = () => null;28
29
function getGroupDMName(channel: Channel) {30
return channel.name ||31
channel.recipients32
.map(UserStore.getUser)33
.filter(isNonNullish)34
.map(c => RelationshipStore.getNickname(c.id) || UserUtils.getName(c))35
.join(", ");36
}37
38
const getMutualGroupDms = (userId: string) =>39
ChannelStore.getSortedPrivateChannels()40
.filter(c => c.isGroupDM() && c.recipients.includes(userId));41
42
const isBotOrSelf = (user: User) => user.bot || user.id === UserStore.getCurrentUser().id;43
44
function getMutualGDMCountText(user: User) {45
const count = getMutualGroupDms(user.id).length;46
return `${count === 0 ? "No" : count} Mutual Group${count !== 1 ? "s" : ""}`;47
}48
49
function renderClickableGDMs(mutualDms: Channel[], onClose: () => void) {50
return mutualDms.map(c => (51
<Clickable52
key={c.id}53
className={MutualsListClasses.row}54
onClick={() => {55
onClose();56
SelectedChannelActionCreators.selectPrivateChannel(c.id);57
}}58
>59
<Avatar60
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
73
const IS_PATCHED = Symbol("MutualGroupDMs.Patched");74
75
export 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 Modal83
{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_GDMS039;?$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 screen96
{97
match: /className:\i\.\i(?=,type:"top")/,98
replace: 039;$& + " vc-mutual-gdms-modal-tab-bar"039;99
}100
]101
},102
// User Profile Modal v2103
{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_GDMS039;?$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
<ScrollerThin175
className={TabBarClasses.tabPanelScroller}176
fade={true}177
onClose={onClose}178
>179
{entries.length > 0180
? entries181
: (182
<div className={ProfileListClasses.empty}>183
<div className={ProfileListClasses.textContainer}>184
<BaseText tag="h3" size="md" weight="medium" style={{ color: "var(--text-strong)" }}>You don039;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
<ExpandableList201
listClassName={listStyle}202
header={"Mutual Groups"}203
isLoading={false}204
items={renderClickableGDMs(mutualGDms, () => { })}205
/>206
</>207
);208
}, { noop: true })209
});210