Plugin

BetterFolders

Shows server folders on dedicated sidebar and adds folder related improvements

Organisation Servers Appearance
index.tsx
Download

Source

src/plugins/betterFolders/index.tsx
1import "./style.css";
2
3import { definePluginSettings } from "@api/Settings";
4import { Devs } from "@utils/constants";
5import { getIntlMessage } from "@utils/discord";
6import { Logger } from "@utils/Logger";
7import definePlugin, { OptionType } from "@utils/types";
8import { findByPropsLazy, findStoreLazy } from "@webpack";
9import { FluxDispatcher } from "@webpack/common";
10import { ReactNode } from "react";
11
12import FolderSideBar from "./FolderSideBar";
13
14enum FolderIconDisplay {
15 Never,
16 Always,
17 MoreThanOneFolderExpanded
18}
19
20export const ExpandedGuildFolderStore = findStoreLazy("ExpandedGuildFolderStore");
21export const SortedGuildStore = findStoreLazy("SortedGuildStore");
22const FolderUtils = findByPropsLazy("move", "toggleGuildFolderExpand");
23
24let lastGuildId = null as string | null;
25let dispatchingFoldersClose = false;
26
27function getGuildFolder(id: string) {
28 return SortedGuildStore.getGuildFolders().find(folder => folder.guildIds.includes(id));
29}
30
31function closeFolders() {
32 for (const id of ExpandedGuildFolderStore.getExpandedFolders())
33 FolderUtils.toggleGuildFolderExpand(id);
34}
35
36// Nuckyz: Unsure if this should be a general utility or not
37function filterTreeWithTargetNode(children: any, predicate: (node: any) => boolean) {
38 if (children == null) {
39 return false;
40 }
41
42 if (!Array.isArray(children)) {
43 if (predicate(children)) {
44 return true;
45 }
46
47 return filterTreeWithTargetNode(children.props?.children, predicate);
48 }
49
50 let childIsTargetChild = false;
51 for (let i = 0; i < children.length; i++) {
52 const shouldKeep = filterTreeWithTargetNode(children[i], predicate);
53 if (shouldKeep) {
54 childIsTargetChild = true;
55 continue;
56 }
57
58 children.splice(i--, 1);
59 }
60
61 return childIsTargetChild;
62}
63
64export const settings = definePluginSettings({
65 sidebar: {
66 type: OptionType.BOOLEAN,
67 description: "Display servers from folder on dedicated sidebar",
68 restartNeeded: true,
69 default: true
70 },
71 sidebarAnim: {
72 type: OptionType.BOOLEAN,
73 description: "Animate opening the folder sidebar",
74 default: true
75 },
76 closeAllFolders: {
77 type: OptionType.BOOLEAN,
78 description: "Close all folders when selecting a server not in a folder",
79 default: false
80 },
81 closeAllHomeButton: {
82 type: OptionType.BOOLEAN,
83 description: "Close all folders when clicking on the home button",
84 restartNeeded: true,
85 default: false
86 },
87 closeOthers: {
88 type: OptionType.BOOLEAN,
89 description: "Close other folders when opening a folder",
90 default: false
91 },
92 forceOpen: {
93 type: OptionType.BOOLEAN,
94 description: "Force a folder to open when switching to a server of that folder",
95 default: false
96 },
97 keepIcons: {
98 type: OptionType.BOOLEAN,
99 description: "Keep showing guild icons in the primary guild bar folder when it&#039;s open in the BetterFolders sidebar",
100 restartNeeded: true,
101 default: false
102 },
103 showFolderIcon: {
104 type: OptionType.SELECT,
105 description: "Show the folder icon above the folder guilds in the BetterFolders sidebar",
106 options: [
107 { label: "Never", value: FolderIconDisplay.Never },
108 { label: "Always", value: FolderIconDisplay.Always, default: true },
109 { label: "When more than one folder is expanded", value: FolderIconDisplay.MoreThanOneFolderExpanded }
110 ],
111 restartNeeded: true
112 }
113});
114
115const IS_BETTER_FOLDERS_VAR = "typeof isBetterFolders!==&#039;undefined&#039;?isBetterFolders:arguments[0]?.isBetterFolders";
116const BETTER_FOLDERS_EXPANDED_IDS_VAR = "typeof betterFoldersExpandedIds!==&#039;undefined&#039;?betterFoldersExpandedIds:arguments[0]?.betterFoldersExpandedIds";
117const GRID_STYLE_NAME = "vc-betterFolders-sidebar-grid";
118
119export default definePlugin({
120 name: "BetterFolders",
121 description: "Shows server folders on dedicated sidebar and adds folder related improvements",
122 authors: [Devs.juby, Devs.AutumnVN, Devs.Nuckyz],
123 tags: ["Organisation", "Servers", "Appearance"],
124 settings,
125
126 patches: [
127 {
128 find: &#039;("guildsnav")&#039;,
129 predicate: () => settings.store.sidebar,
130 replacement: [
131 // Create the isBetterFolders and betterFoldersExpandedIds variables in the GuildsBar component
132 // Needed because we access this from a non-arrow closure so we can't use arguments[0]
133 {
134 match: /let{disableAppDownload:\i=\i\.isPlatformEmbedded,isOverlay:.+?(?=}=\i)/,
135 replace: "$&,isBetterFolders,betterFoldersExpandedIds"
136 },
137 // Export the isBetterFolders and betterFoldersExpandedIds variable to the Guild List component
138 {
139 match: /,{guildDiscoveryButton:\i,/g,
140 replace: "$&isBetterFolders:arguments[0]?.isBetterFolders,betterFoldersExpandedIds:arguments[0]?.betterFoldersExpandedIds,"
141 },
142 // Wrap the guild node (guild or folder) component in a div with display: none if it's not an expanded folder or a guild in an expanded folder
143 {
144 match: /switch\((\i)\.type\){.+?default:return null}/,
145 replace: `return $self.wrapGuildNodeComponent($1,()=>{$&},${IS_BETTER_FOLDERS_VAR},${BETTER_FOLDERS_EXPANDED_IDS_VAR});`
146 },
147 // Export the isBetterFolders variable to the folder component
148 {
149 match: /switch\(\i\.type\){case \i\.\i\.FOLDER:.+?folderNode:\i,/,
150 replace: `$&isBetterFolders:${IS_BETTER_FOLDERS_VAR},`
151 },
152 // Make the callback for returning the guild node component depend on isBetterFolders and betterFoldersExpandedIds
153 {
154 match: /switch\(\i\.type\).+?,\i,\i\.setNodeRef/,
155 replace: "$&,arguments[0]?.isBetterFolders,arguments[0]?.betterFoldersExpandedIds"
156 },
157 // If we are rendering the Better Folders sidebar, we filter out everything but the guilds and folders from the Guild List children
158 {
159 match: /lastTargetNode:\i\[\i\.length-1\].+?}\)(?::null)?\](?=}\))/,
160 replace: "$&.filter($self.makeGuildsBarGuildListFilter(!!arguments[0]?.isBetterFolders))"
161 },
162 // If we are rendering the Better Folders sidebar, we filter out everything but the Guild List from the Sidebar children
163 {
164 match: /reverse:!0,.{0,150}?barClassName:.+?\}\)\]/,
165 replace: "$&.filter($self.makeGuildsBarSidebarFilter(!!arguments[0]?.isBetterFolders))"
166 }
167 ]
168 },
169 {
170 // This is the parent folder component
171 find: ".toggleGuildFolderExpand(",
172 predicate: () => settings.store.sidebar && settings.store.showFolderIcon !== FolderIconDisplay.Always,
173 replacement: [
174 {
175 // Modify the expanded state to instead return the list of expanded folders
176 match: /(\],\(\)=>)(\i\.\i)\.isFolderExpanded\(\i\)\)/,
177 replace: (_, rest, ExpandedGuildFolderStore) => `${rest}${ExpandedGuildFolderStore}.getExpandedFolders())`,
178 },
179 {
180 // Modify the expanded prop to use the boolean if the above patch fails, or check if the folder is expanded from the list if it succeeds
181 // Also export the list of expanded folders to the child folder component if the patch above succeeds, else export undefined
182 match: /(?<=\.\.\.\i,folderNode:(\i),expanded:)\i(?=,)/,
183 replace: (isExpandedOrExpandedIds, folderNote) => ""
184 + `typeof ${isExpandedOrExpandedIds}==="boolean"?${isExpandedOrExpandedIds}:${isExpandedOrExpandedIds}.has(${folderNote}.id),`
185 + `betterFoldersExpandedIds:${isExpandedOrExpandedIds} instanceof Set?${isExpandedOrExpandedIds}:void 0`
186 }
187 ]
188 },
189 {
190 find: ".FOLDER_ITEM_ANIMATION_DURATION),",
191 predicate: () => settings.store.sidebar,
192 replacement: [
193 // We use arguments[0] to access the isBetterFolders variable in this nested folder component (the parent exports all the props so we don't have to patch it)
194
195 // If we are rendering the normal GuildsBar sidebar, we make Discord think the folder is always collapsed to show better icons (the mini guild icons) and avoid transitions
196 {
197 predicate: () => settings.store.keepIcons,
198 match: /(?<=let ?(?:\i,)*?{folderNode:\i,setNodeRef:\i,.+?expanded:(\i),.+?;)(?=let)/,
199 replace: (_, isExpanded) => `${isExpanded}=!!arguments[0]?.isBetterFolders&&${isExpanded};`
200 },
201 // Disable expanding and collapsing folders transition in the normal GuildsBar sidebar
202 {
203 predicate: () => !settings.store.keepIcons,
204 match: /(?=,\{from:\{height)/,
205 replace: "&&$self.shouldShowTransition(arguments[0])"
206 },
207 // If we are rendering the normal GuildsBar sidebar, we avoid rendering guilds from folders that are expanded
208 {
209 predicate: () => !settings.store.keepIcons,
210 match: /"--custom-folder-color".+?(?=\i\(\(\i,\i,\i\)=>{let{key:.{0,70}"ul")(?<=selected:\i,expanded:(\i),.+?)/,
211 replace: (m, isExpanded) => `${m}$self.shouldRenderContents(arguments[0],${isExpanded})?null:`
212 },
213 // Decide if we should render the expanded folder background if we are rendering the Better Folders sidebar
214 {
215 predicate: () => settings.store.showFolderIcon !== FolderIconDisplay.Always,
216 match: /"--custom-folder-color".{0,110}?children:\[/,
217 replace: "$&$self.shouldShowFolderIconAndBackground(!!arguments[0]?.isBetterFolders,arguments[0]?.betterFoldersExpandedIds)&&"
218 },
219 // Decide if we should render the expanded folder icon if we are rendering the Better Folders sidebar
220 {
221 predicate: () => settings.store.showFolderIcon !== FolderIconDisplay.Always,
222 match: /"--custom-folder-color".+?className:\i\.\i}\),(?=\i,)/,
223 replace: "$&!$self.shouldShowFolderIconAndBackground(!!arguments[0]?.isBetterFolders,arguments[0]?.betterFoldersExpandedIds)?null:"
224 }
225 ]
226 },
227 {
228 find: "APPLICATION_LIBRARY,render:",
229 predicate: () => settings.store.sidebar,
230 group: true,
231 replacement: [
232 {
233 // Render the Better Folders sidebar
234 // Discord has two different places where they render the sidebar.
235 // One is for visual refresh, one is not,
236 // and each has a bunch of conditions &&ed in front of it.
237 // Add the betterFolders sidebar to both, keeping the conditions Discord uses.
238 match: /(?<=[[,])((?:!?\i&&)+)\(.{0,50}({className:\i\.\i,themeOverride:\i})\)/g,
239 replace: (m, conditions, props) => `${m},${conditions}$self.FolderSideBar(${props})`
240 },
241 {
242 // Add grid styles to fix aligment with other visual refresh elements
243 match: /(?<=className:)\i\.\i(?=,"data-fullscreen")/,
244 replace: `"${GRID_STYLE_NAME} "+$&`
245 }
246 ]
247 },
248 {
249 find: "#{intl::DISCODO_DISABLED}",
250 predicate: () => settings.store.closeAllHomeButton,
251 replacement: {
252 // Close all folders when clicking the home button
253 match: /(?<=onClick:\(\)=>{)(?=.{0,300}"discodo")/,
254 replace: "$self.closeFolders();"
255 }
256 }
257 ],
258
259 flux: {
260 CHANNEL_SELECT(data) {
261 if (!settings.store.closeAllFolders && !settings.store.forceOpen)
262 return;
263
264 if (lastGuildId !== data.guildId) {
265 lastGuildId = data.guildId;
266 const guildFolder = getGuildFolder(data.guildId);
267
268 if (guildFolder?.folderId) {
269 if (settings.store.forceOpen && !ExpandedGuildFolderStore.isFolderExpanded(guildFolder.folderId)) {
270 FolderUtils.toggleGuildFolderExpand(guildFolder.folderId);
271 }
272 } else if (settings.store.closeAllFolders) {
273 closeFolders();
274 }
275 }
276 },
277
278 TOGGLE_GUILD_FOLDER_EXPAND(data) {
279 if (settings.store.closeOthers && !dispatchingFoldersClose) {
280 dispatchingFoldersClose = true;
281
282 FluxDispatcher.wait(() => {
283 const expandedFolders = ExpandedGuildFolderStore.getExpandedFolders();
284
285 if (expandedFolders.size > 1) {
286 for (const id of expandedFolders) if (id !== data.folderId)
287 FolderUtils.toggleGuildFolderExpand(id);
288 }
289
290 dispatchingFoldersClose = false;
291 });
292 }
293 },
294
295 LOGOUT() {
296 closeFolders();
297 }
298 },
299
300 FolderSideBar,
301 closeFolders,
302
303
304 wrapGuildNodeComponent(node: any, originalComponent: () => ReactNode, isBetterFolders: boolean, expandedFolderIds?: Set<any>) {
305 if (
306 !isBetterFolders ||
307 node.type === "folder" && expandedFolderIds?.has(node.id) ||
308 node.type === "guild" && expandedFolderIds?.has(node.parentId)
309 ) {
310 return originalComponent();
311 }
312
313 return (
314 <div style={{ display: "none" }}>
315 {originalComponent()}
316 </div>
317 );
318 },
319
320 makeGuildsBarGuildListFilter(isBetterFolders: boolean) {
321 return (child: any) => {
322 if (!isBetterFolders) {
323 return true;
324 }
325
326 try {
327 // can cause hang if intl message is not found
328 const serversIntlMsg = getIntlMessage("SERVERS");
329 if (!serversIntlMsg) {
330 new Logger("BetterFolders").error("Failed to get SERVERS intl message");
331 return true;
332 }
333 return child?.props?.["aria-label"] === serversIntlMsg;
334 } catch (e) {
335 console.error(e);
336 return true;
337 }
338 };
339 },
340
341 makeGuildsBarSidebarFilter(isBetterFolders: boolean) {
342 return (child: any) => {
343 if (!isBetterFolders) {
344 return true;
345 }
346
347 try {
348 return filterTreeWithTargetNode(child, child => child?.props?.renderTreeNode != null);
349 } catch (e) {
350 console.error(e);
351 return true;
352 }
353 };
354 },
355
356 shouldShowFolderIconAndBackground(isBetterFolders: boolean, expandedFolderIds?: Set<any>) {
357 if (!isBetterFolders) {
358 return true;
359 }
360
361 switch (settings.store.showFolderIcon) {
362 case FolderIconDisplay.Never:
363 return false;
364 case FolderIconDisplay.Always:
365 return true;
366 case FolderIconDisplay.MoreThanOneFolderExpanded:
367 return (expandedFolderIds?.size ?? 0) > 1;
368 default:
369 return true;
370 }
371 },
372
373 shouldShowTransition(props: any) {
374 // Pending guilds
375 if (props?.folderNode?.id === 1) return true;
376
377 return !!props?.isBetterFolders;
378 },
379
380 shouldRenderContents(props: any, isExpanded: boolean) {
381 // Pending guilds
382 if (props?.folderNode?.id === 1) return false;
383
384 return !props?.isBetterFolders && isExpanded;
385 }
386});
387