diff --git a/src/plugins/implicitRelationships/README.md b/src/plugins/implicitRelationships/README.md
new file mode 100644
index 000000000..a76e298f7
--- /dev/null
+++ b/src/plugins/implicitRelationships/README.md
@@ -0,0 +1,7 @@
+# ImplicitRelationships
+
+Shows your implicit relationships in the Friends tab.
+
+Implicit relationships on Discord are people with whom you've frecently interacted and share a mutual server; even though Discord thinks you should be friends with them, you haven't added them as friends.
+
+![](https://camo.githubusercontent.com/6927161ee0c933f7ef6d61f243cca3e6ea4c8db9d1becd8cbf73c45e1bd0d127/68747470733a2f2f692e646f6c66692e65732f7055447859464662674d2e706e673f6b65793d736e3950343936416c32444c7072)
diff --git a/src/plugins/implicitRelationships/index.ts b/src/plugins/implicitRelationships/index.ts
new file mode 100644
index 000000000..9ae9fb512
--- /dev/null
+++ b/src/plugins/implicitRelationships/index.ts
@@ -0,0 +1,182 @@
+/*
+ * Vencord, a modification for Discord's desktop app
+ * Copyright (c) 2023 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 .
+*/
+
+import { definePluginSettings } from "@api/Settings";
+import { Devs } from "@utils/constants";
+import definePlugin, { OptionType } from "@utils/types";
+import { findByProps, findStoreLazy } from "@webpack";
+import { ChannelStore, FluxDispatcher, GuildStore, RelationshipStore, SnowflakeUtils, UserStore } from "@webpack/common";
+import { Settings } from "Vencord";
+
+const UserAffinitiesStore = findStoreLazy("UserAffinitiesStore");
+
+interface UserAffinity {
+ user_id: string;
+ affinity: number;
+}
+
+export default definePlugin({
+ name: "ImplicitRelationships",
+ description: "Shows your implicit relationships in the Friends tab.",
+ authors: [Devs.Dolfies],
+ patches: [
+ // Counts header
+ {
+ find: ".FRIENDS_ALL_HEADER",
+ replacement: {
+ match: /toString\(\)\}\);case (\i\.\i)\.BLOCKED/,
+ replace: 'toString()});case $1.IMPLICIT:return "Implicit — "+arguments[1];case $1.BLOCKED'
+ },
+ },
+ // No friends page
+ {
+ find: "FriendsEmptyState: Invalid empty state",
+ replacement: {
+ match: /case (\i\.\i)\.ONLINE:(?=return (\i)\.SECTION_ONLINE)/,
+ replace: "case $1.ONLINE:case $1.IMPLICIT:"
+ },
+ },
+ // Sections header
+ {
+ find: ".FRIENDS_SECTION_ONLINE",
+ replacement: {
+ match: /(\(0,\i\.jsx\)\(\i\.TabBar\.Item,\{id:\i\.\i)\.BLOCKED,className:([^\s]+?)\.item,children:\i\.\i\.Messages\.BLOCKED\}\)/,
+ replace: "$1.IMPLICIT,className:$2.item,children:\"Implicit\"}),$&"
+ },
+ },
+ // Sections content
+ {
+ find: '"FriendsStore"',
+ replacement: {
+ match: /(?<=case (\i\.\i)\.BLOCKED:return (\i)\.type===\i\.\i\.BLOCKED)/,
+ replace: ";case $1.IMPLICIT:return $2.type===5"
+ },
+ },
+ // Piggyback relationship fetch
+ {
+ find: ".fetchRelationships()",
+ replacement: {
+ match: /(\i\.\i)\.fetchRelationships\(\)/,
+ // This relationship fetch is actually completely useless, but whatevs
+ replace: "$1.fetchRelationships(),$self.fetchImplicitRelationships()"
+ },
+ },
+ // Modify sort -- thanks megu for the patch (from sortFriendRequests)
+ {
+ find: "getRelationshipCounts(){",
+ replacement: {
+ predicate: () => Settings.plugins.ImplicitRelationships.sortByAffinity,
+ match: /\.sortBy\(\i=>\i\.comparator\)/,
+ replace: "$&.sortBy((row) => $self.sortList(row))"
+ }
+ },
+
+ // Add support for the nonce parameter to Discord's shitcode
+ {
+ find: ".REQUEST_GUILD_MEMBERS",
+ replacement: {
+ match: /\.send\(8,{/,
+ replace: "$&nonce:arguments[1].nonce,"
+ }
+ },
+ {
+ find: "GUILD_MEMBERS_REQUEST:",
+ replacement: {
+ match: /presences:!!(\i)\.presences/,
+ replace: "$&,nonce:$1.nonce"
+ },
+ },
+ {
+ find: ".not_found",
+ replacement: {
+ match: /notFound:(\i)\.not_found/,
+ replace: "$&,nonce:$1.nonce"
+ },
+ }
+ ],
+ settings: definePluginSettings(
+ {
+ sortByAffinity: {
+ type: OptionType.BOOLEAN,
+ default: true,
+ description: "Whether to sort implicit relationships by their affinity to you.",
+ restartNeeded: true
+ },
+ }
+ ),
+
+ sortList(row: any) {
+ return row.type === 5
+ ? -UserAffinitiesStore.getUserAffinity(row.user.id)?.affinity ?? 0
+ : row.comparator;
+ },
+
+ async fetchImplicitRelationships() {
+ // Implicit relationships are defined as users that you:
+ // 1. Have an affinity for
+ // 2. Do not have a relationship with // TODO: Check how this works with pending/blocked relationships
+ // 3. Have a mutual guild with
+ const userAffinities: Set = UserAffinitiesStore.getUserAffinitiesUserIds();
+ const nonFriendAffinities = Array.from(userAffinities).filter(
+ id => !RelationshipStore.getRelationshipType(id)
+ );
+
+ // I would love to just check user cache here (falling back to the gateway of course)
+ // However, users in user cache may just be there because they share a DM or group DM with you
+ // So there's no guarantee that a user being in user cache means they have a mutual with you
+ // To get around this, we request users we have DMs with, and ignore them below if we don't get them back
+ const dmUserIds = new Set(
+ Object.values(ChannelStore.getSortedPrivateChannels()).flatMap(c => c.recipients)
+ );
+ const toRequest = nonFriendAffinities.filter(id => !UserStore.getUser(id) || dmUserIds.has(id));
+ const allGuildIds = Object.keys(GuildStore.getGuilds());
+ const sentNonce = SnowflakeUtils.fromTimestamp(Date.now());
+ let count = allGuildIds.length * Math.ceil(toRequest.length / 100);
+
+ // OP 8 Request Guild Members allows 100 user IDs at a time
+ const ignore = new Set(toRequest);
+ const relationships = RelationshipStore.getRelationships();
+ const callback = ({ nonce, members }) => {
+ if (nonce !== sentNonce) return;
+ members.forEach(member => {
+ ignore.delete(member.user.id);
+ });
+
+ nonFriendAffinities.map(id => UserStore.getUser(id)).filter(user => user && !ignore.has(user.id)).forEach(user => relationships[user.id] = 5);
+ RelationshipStore.emitChange();
+ if (--count === 0) {
+ FluxDispatcher.unsubscribe("GUILD_MEMBERS_CHUNK", callback);
+ }
+ };
+
+ FluxDispatcher.subscribe("GUILD_MEMBERS_CHUNK", callback);
+ for (let i = 0; i < toRequest.length; i += 100) {
+ FluxDispatcher.dispatch({
+ type: "GUILD_MEMBERS_REQUEST",
+ guildIds: allGuildIds,
+ userIds: toRequest.slice(i, i + 100),
+ nonce: sentNonce,
+ });
+ }
+ },
+
+ start() {
+ const { FriendsSections } = findByProps("FriendsSections");
+ FriendsSections.IMPLICIT = "IMPLICIT";
+ }
+});
diff --git a/src/plugins/sortFriendRequests/index.tsx b/src/plugins/sortFriendRequests/index.tsx
index c40a18140..32579a803 100644
--- a/src/plugins/sortFriendRequests/index.tsx
+++ b/src/plugins/sortFriendRequests/index.tsx
@@ -42,7 +42,7 @@ export default definePlugin({
find: "getRelationshipCounts(){",
replacement: {
match: /\.sortBy\(\i=>\i\.comparator\)/,
- replace: ".sortBy((row) => $self.sortList(row))"
+ replace: "$&.sortBy((row) => $self.sortList(row))"
}
}, {
find: ".Messages.FRIEND_REQUEST_CANCEL",