;
152 | isBlocked(user: Snowflake): boolean;
153 | isFriend(user: Snowflake): boolean;
154 | __getLocalVars();
155 | }
156 |
157 | export const RelationshipStore: RelationshipStore = /* @__PURE__ */ Finder.byName("RelationshipStore");
158 |
--------------------------------------------------------------------------------
/packages/dium/src/modules/util.ts:
--------------------------------------------------------------------------------
1 | import {Filters, Finder} from "../api";
2 |
3 | export interface AudioConvert {
4 | amplitudeToPerceptual(amplitude: number): number;
5 | perceptualToAmplitude(perceptual: number): number;
6 | }
7 |
8 | export const AudioConvert: AudioConvert = /* @__PURE__ */ Finder.demangle({
9 | amplitudeToPerceptual: Filters.bySource("Math.log10"),
10 | perceptualToAmplitude: Filters.bySource("Math.pow(10")
11 | });
12 |
--------------------------------------------------------------------------------
/packages/dium/src/react-internals.ts:
--------------------------------------------------------------------------------
1 | import type {Usable} from "react";
2 | import {React, ReactDOM} from "./modules";
3 | import type {Fiber, ReactContext, EventPriority} from "react-reconciler";
4 |
5 | export type {Fiber} from "react-reconciler";
6 |
7 | export interface ForwardRefExoticComponent extends React.ForwardRefExoticComponent
{
8 | render: React.ForwardRefRenderFunction;
9 | }
10 |
11 | type CompareFn = Parameters[1];
12 |
13 | export interface MemoExoticComponent> extends React.MemoExoticComponent {
14 | compare?: CompareFn;
15 | }
16 |
17 | /** A fiber node with React component as state node. */
18 | export interface OwnerFiber extends Fiber {
19 | stateNode: React.Component;
20 | }
21 |
22 | export interface MutableSource {
23 | _source: Source;
24 | _getVersion: (source: Source) => any;
25 | _workInProgressVersionPrimary?: any;
26 | _workInProgressVersionSecondary?: any;
27 | }
28 | export type TransitionStatus = any;
29 |
30 | export interface Dispatcher {
31 | use(usable: Usable): T;
32 | readContext(context: ReactContext, observedBits?: number | boolean): T;
33 | useState: typeof React.useState;
34 | useReducer: typeof React.useReducer;
35 | useContext: typeof React.useContext;
36 | useRef: typeof React.useRef;
37 | useEffect: typeof React.useEffect;
38 | useEffectEvent? any>(callback: F): F;
39 | useInsertionEffect: typeof React.useInsertionEffect;
40 | useLayoutEffect: typeof React.useLayoutEffect;
41 | useCallback: typeof React.useCallback;
42 | useMemo: typeof React.useMemo;
43 | useImperativeHandle: typeof React.useImperativeHandle;
44 | useDebugValue: typeof React.useDebugValue;
45 | useDefferedValue: typeof React.useDeferredValue;
46 | useTransition: typeof React.useTransition;
47 | useSyncExternalStore: typeof React.useSyncExternalStore;
48 | useId: typeof React.useId;
49 | useCacheRefresh?(): (f: () => T, t?: T) => void;
50 | useMemoCache?(size: number): any[];
51 | useHostTransitionStatus?(): TransitionStatus;
52 | useOptimistic?: typeof React.useOptimistic;
53 | useFormState?(
54 | action: (awaited: Awaited, p: P) => S,
55 | initialState: Awaited,
56 | permalink?: string,
57 | ): [Awaited, (p: P) => void, boolean];
58 | useActionState?: typeof React.useActionState;
59 | }
60 |
61 | export interface AsyncDispatcher {
62 | getCacheForType(resourceType: () => T): T;
63 | }
64 |
65 | export interface BatchConfigTransition {
66 | name?: string;
67 | startTime?: number;
68 | _updatedFibers?: Set;
69 | }
70 |
71 | export interface ReactInternals {
72 | H?: Dispatcher; // ReactCurrentDispatcher for Hooks
73 | A?: AsyncDispatcher; // ReactCurrentCache for Cache
74 | T?: BatchConfigTransition; // ReactCurrentBatchConfig for Transitions
75 | S?(transition: BatchConfigTransition, mixed: any): void; // onStartTransitionFinish
76 | }
77 |
78 | export const ReactInternals: ReactInternals = (React as any)?.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE;
79 |
80 | export type CrossOriginEnum = any;
81 | export type PreloadImplOptions = any;
82 | export type PreloadModuleImplOptions = any;
83 | export type PreinitStyleOptions = any;
84 | export type PreinitScriptOptions = any;
85 | export type PreinitModuleScriptOptions = any;
86 |
87 | export interface HostDispatcher {
88 | f(): boolean | void; // flushSyncWork
89 | R(form: HTMLFormElement): void; // requestFormReset
90 | D(href: string): void; // prefetchDNS
91 | C(href: string, crossOrigin?: CrossOriginEnum): void; // preconnect
92 | L(href: string, as: string, options?: PreloadImplOptions): void; // preload
93 | m(href: string, options?: PreloadModuleImplOptions): void; // preloadModule
94 | S(href: string, precedence: string, options?: PreinitStyleOptions): void; // preinitStyle
95 | X(src: string, options?: PreinitScriptOptions): void; // preinitScript
96 | M(src: string, options?: PreinitModuleScriptOptions): void; // preinitModuleScript
97 | }
98 |
99 | export interface ReactDOMInternals {
100 | d: HostDispatcher; // ReactDOMCurrentDispatcher
101 | p: EventPriority; // currrentUpdatePriority
102 | findDOMNode?(componentOrElement: React.Component): null | Element | Text;
103 | }
104 |
105 | export const ReactDOMInternals: ReactDOMInternals = (ReactDOM as any)?.__DOM_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE;
106 |
--------------------------------------------------------------------------------
/packages/dium/src/require.ts:
--------------------------------------------------------------------------------
1 | export type Id = number | string;
2 |
3 | export type Exports = Record;
4 |
5 | export interface Module {
6 | id: Id;
7 | loaded: boolean;
8 | exports: Exports;
9 | }
10 |
11 | export type ModuleFactory = (this: Exports, module: Module, exports: Exports, require: Require) => void;
12 |
13 | export interface Require {
14 | (id: Id): any;
15 |
16 | /** Module factories. */
17 | m: Record;
18 |
19 | /** Module exports cache. */
20 | c: Record;
21 |
22 | /** Module execution interceptor. */
23 | i: any[];
24 |
25 | /** Chunk loaded. */
26 | O(result: any, chunkIds: any, fn: any, priority: any): any;
27 |
28 | /** Get default export. */
29 | n(module: any): any;
30 |
31 | /** Create fake namespace object. */
32 | t(value: any, mode: any): any;
33 |
34 | /** Define property getters. */
35 | d(exports: any, definition: any): any;
36 |
37 | /** Ensure chunk. */
38 | e(chunkId: Id): Promise;
39 |
40 | /** Get chunk filename. */
41 | u(chunkId: Id): any;
42 |
43 | /** Global. */
44 | g(): typeof globalThis;
45 |
46 | /** hasOwnProperty shorthand. */
47 | o(obj: any, prop: any): any;
48 |
49 | /** Load script. */
50 | l(url: any, done: any, key: any, chunkId: Id): any;
51 |
52 | /** Make namespace object. */
53 | r(exports: any): any;
54 |
55 | /** Node module decorator. */
56 | nmd(module: any): any;
57 |
58 | /** publicPath. */
59 | p: string;
60 | }
61 |
--------------------------------------------------------------------------------
/packages/dium/src/settings-container.tsx:
--------------------------------------------------------------------------------
1 | import {React, classNames} from "./modules";
2 | import {Flex, Button, FormSection, FormDivider, margins} from "./components";
3 | import {confirm} from "./utils";
4 |
5 | export interface SettingsContainerProps {
6 | name: string;
7 | children?: React.ReactNode;
8 | onReset?: () => void;
9 | }
10 |
11 | export const SettingsContainer = ({name, children, onReset}: SettingsContainerProps): React.JSX.Element => (
12 |
13 | {children}
14 | {onReset ? (
15 | <>
16 |
17 |
18 |
24 |
25 | >
26 | ) : null}
27 |
28 | );
29 |
--------------------------------------------------------------------------------
/packages/dium/src/settings.ts:
--------------------------------------------------------------------------------
1 | import {React, Flux} from "./modules";
2 | import * as Data from "./api/data";
3 |
4 | export type Listener = (current: T) => void;
5 |
6 | export type Update = Partial | ((current: T) => Partial);
7 |
8 | export type Setter = (update: Update) => void;
9 |
10 | export type SettingsType> = S["defaults"];
11 |
12 | export class SettingsStore> implements Flux.StoreLike {
13 | /** Default settings values. */
14 | defaults: T;
15 |
16 | /** Current settings state. */
17 | current: T;
18 |
19 | /** Settings load callback. */
20 | onLoad?: () => void;
21 |
22 | /** Currently registered listeners. */
23 | listeners: Set> = new Set();
24 |
25 | /**
26 | * Creates a new settings store.
27 | *
28 | * @param defaults Default settings to use initially & revert to on reset.
29 | * @param onLoad Optional callback for when the settings are loaded.
30 | */
31 | constructor(defaults: T, onLoad?: () => void) {
32 | this.defaults = defaults;
33 | this.onLoad = onLoad;
34 | }
35 |
36 | /** Loads settings. */
37 | load(): void {
38 | this.current = {...this.defaults, ...Data.load("settings")};
39 | this.onLoad?.();
40 | this._dispatch(false);
41 | }
42 |
43 | /**
44 | * Dispatches a settings update.
45 | *
46 | * @param save Whether to save the settings.
47 | */
48 | _dispatch(save: boolean): void {
49 | for (const listener of this.listeners) {
50 | listener(this.current);
51 | }
52 | if (save) {
53 | Data.save("settings", this.current);
54 | }
55 | }
56 |
57 | /**
58 | * Updates settings state.
59 | *
60 | * Similar to React's `setState()`.
61 | *
62 | * @param settings Partial settings or callback receiving current settings and returning partial settings.
63 | *
64 | * @example
65 | * ```js
66 | * Settings.update({myKey: "foo"})
67 | * Settings.update((current) => ({settingA: current.settingB}))
68 | * ```
69 | */
70 | update = (settings: Update): void => {
71 | // TODO: copy and use comparators?
72 | Object.assign(this.current, typeof settings === "function" ? settings(this.current) : settings);
73 | this._dispatch(true);
74 | };
75 |
76 | /** Resets all settings to their defaults. */
77 | reset(): void {
78 | this.current = {...this.defaults};
79 | this._dispatch(true);
80 | }
81 |
82 | /** Deletes settings using their keys. */
83 | delete(...keys: string[]): void {
84 | for (const key of keys) {
85 | delete this.current[key];
86 | }
87 | this._dispatch(true);
88 | }
89 |
90 | /**
91 | * Returns the current settings state.
92 | *
93 | * @example
94 | * ```js
95 | * const currentSettings = Settings.useCurrent();
96 | * ```
97 | */
98 | useCurrent(): T {
99 | return Flux.useStateFromStores([this], () => this.current, undefined, () => false);
100 | }
101 |
102 | /**
103 | * Returns the current settings state mapped with a selector.
104 | *
105 | * Similar to Redux' `useSelector()`, but with optional dependencies.
106 | *
107 | * @param selector A function selecting a part of the current settings.
108 | * @param deps Dependencies of the selector.
109 | * @param compare An equality function to compare two results of the selector. Strict equality `===` by default.
110 | *
111 | * @example
112 | * ```js
113 | * const entry = Settings.useSelector((current) => current.entry);
114 | * ```
115 | */
116 | useSelector(selector: (current: T) => R, deps?: React.DependencyList, compare?: Flux.Comparator): R {
117 | return Flux.useStateFromStores([this], () => selector(this.current), deps, compare);
118 | }
119 |
120 | /**
121 | * Returns the current settings state & a setter function.
122 | *
123 | * Similar to React's `useState()`.
124 | *
125 | * @example
126 | * ```js
127 | * const [currentSettings, setSettings] = Settings.useState();
128 | * ```
129 | */
130 | useState(): [T, Setter] {
131 | return Flux.useStateFromStores([this], () => [
132 | this.current,
133 | this.update
134 | ]);
135 | }
136 |
137 | /**
138 | * Returns the current settings state, defaults & a setter function.
139 | *
140 | * Similar to React's `useState()`, but with another entry.
141 | *
142 | * @example
143 | * ```js
144 | * const [currentSettings, defaultSettings, setSettings] = Settings.useStateWithDefaults();
145 | * ```
146 | */
147 | useStateWithDefaults(): [T, T, Setter] {
148 | return Flux.useStateFromStores([this], () => [
149 | this.current,
150 | this.defaults,
151 | this.update
152 | ]);
153 | }
154 |
155 | /**
156 | * Adds a new settings change listener from within a component.
157 | *
158 | * @param listener Listener function to be called when settings state changes.
159 | * @param deps Dependencies of the listener function. Defaults to the listener function itself.
160 | */
161 | useListener(listener: Listener, deps?: React.DependencyList): void {
162 | React.useEffect(() => {
163 | this.addListener(listener);
164 | return () => this.removeListener(listener);
165 | }, deps ?? [listener]);
166 | }
167 |
168 | /** Registers a new settings change listener. */
169 | addListener(listener: Listener): Listener {
170 | this.listeners.add(listener);
171 | return listener;
172 | }
173 |
174 | /** Removes a previously added settings change listener. */
175 | removeListener(listener: Listener): void {
176 | this.listeners.delete(listener);
177 | }
178 |
179 | /** Removes all current settings change listeners. */
180 | removeAllListeners(): void {
181 | this.listeners.clear();
182 | }
183 |
184 | // compatibility with discord's flux interface
185 | addReactChangeListener = this.addListener;
186 | removeReactChangeListener = this.removeListener;
187 | }
188 |
189 | /**
190 | * Creates new settings.
191 | *
192 | * For details see {@link SettingsStore}.
193 | */
194 | export const createSettings = >(defaults: T, onLoad?: () => void): SettingsStore => new SettingsStore(defaults, onLoad);
195 |
--------------------------------------------------------------------------------
/packages/dium/src/utils/general.ts:
--------------------------------------------------------------------------------
1 | import type * as BD from "betterdiscord";
2 |
3 | export const hasOwnProperty = (object: unknown, property: PropertyKey): boolean => Object.prototype.hasOwnProperty.call(object, property);
4 |
5 | export const sleep = (duration: number): Promise => new Promise((resolve) => setTimeout(resolve, duration));
6 |
7 | export const alert = (title: string, content: string | React.ReactNode): void => BdApi.UI.alert(title, content);
8 |
9 | export type ConfirmOptions = BD.ConfirmationModalOptions;
10 |
11 | /** Shows a confirmation modal. */
12 | // TODO: change to promise?
13 | export const confirm = (title: string, content: string | React.ReactNode, options: ConfirmOptions = {}): string => BdApi.UI.showConfirmationModal(title, content, options);
14 |
15 | export const enum ToastType {
16 | Default = "",
17 | Info = "info",
18 | Success = "success",
19 | Warn = "warn",
20 | Warning = "warning",
21 | Danger = "danger",
22 | Error = "error"
23 | }
24 |
25 | export interface ToastOptions extends BD.ToastOptions {
26 | type?: ToastType;
27 | }
28 |
29 | /** Shows a toast notification. */
30 | export const toast = (content: string, options: ToastOptions): void => BdApi.UI.showToast(content, options);
31 |
32 | export type MappedProxy<
33 | T extends Record,
34 | M extends Record
35 | > = {
36 | [K in keyof M | keyof T]: T[M[K] extends never ? K : M[K]];
37 | };
38 |
39 | /** Creates a proxy mapping additional properties to other properties on the original. */
40 | export const mappedProxy = <
41 | T extends Record,
42 | M extends Record
43 | >(target: T, mapping: M): MappedProxy => {
44 | const map = new Map(Object.entries(mapping));
45 | return new Proxy(target, {
46 | get(target, prop) {
47 | return target[map.get(prop as any) ?? prop];
48 | },
49 | set(target, prop, value) {
50 | target[map.get(prop as any) ?? prop] = value;
51 | return true;
52 | },
53 | deleteProperty(target, prop) {
54 | delete target[map.get(prop as any) ?? prop];
55 | map.delete(prop as any);
56 | return true;
57 | },
58 | has(target, prop) {
59 | return map.has(prop as any) || prop in target;
60 | },
61 | ownKeys() {
62 | return [...map.keys(), ...Object.keys(target)];
63 | },
64 | getOwnPropertyDescriptor(target, prop) {
65 | return Object.getOwnPropertyDescriptor(target, map.get(prop as any) ?? prop);
66 | },
67 | defineProperty(target, prop, attributes) {
68 | Object.defineProperty(target, map.get(prop as any) ?? prop, attributes);
69 | return true;
70 | }
71 | }) as any;
72 | };
73 |
--------------------------------------------------------------------------------
/packages/dium/src/utils/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./general";
2 | export * from "./react";
3 |
--------------------------------------------------------------------------------
/packages/dium/tests/mock.ts:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import Reconciler, {Fiber} from "react-reconciler";
3 | import {DefaultEventPriority, LegacyRoot} from "react-reconciler/constants";
4 |
5 | (global as any).TESTING = true;
6 |
7 | (global as any).BdApi = {
8 | React,
9 | Webpack: {
10 | getModule: () => null
11 | },
12 | Plugins: {
13 | get: () => ({})
14 | }
15 | };
16 |
17 | const reconciler = Reconciler({
18 | supportsMutation: false,
19 | supportsPersistence: true,
20 | createInstance() {},
21 | createTextInstance() {},
22 | appendInitialChild() {},
23 | finalizeInitialChildren: () => false,
24 | shouldSetTextContent: () => false,
25 | getRootHostContext: () => null,
26 | getChildHostContext: (parent) => parent,
27 | getPublicInstance: (instance) => instance,
28 | prepareForCommit: () => null,
29 | resetAfterCommit() {},
30 | preparePortalMount() {},
31 | scheduleTimeout: setTimeout,
32 | cancelTimeout: clearTimeout,
33 | noTimeout: -1,
34 | isPrimaryRenderer: true,
35 | cloneInstance() {},
36 | createContainerChildSet() {},
37 | appendChildToContainerChildSet() {},
38 | finalizeContainerChildren() {},
39 | replaceContainerChildren() {},
40 | cloneHiddenInstance() {},
41 | cloneHiddenTextInstance() {},
42 | getInstanceFromNode: () => null,
43 | beforeActiveInstanceBlur() {},
44 | afterActiveInstanceBlur() {},
45 | prepareScopeUpdate() {},
46 | getInstanceFromScope() {},
47 | detachDeletedInstance() {},
48 | supportsHydration: false,
49 | NotPendingTransition: null,
50 | HostTransitionContext: React.createContext(null) as any,
51 | setCurrentUpdatePriority() {},
52 | getCurrentUpdatePriority: () => DefaultEventPriority,
53 | resolveUpdatePriority: () => DefaultEventPriority,
54 | resetFormInstance() {},
55 | requestPostPaintCallback() {},
56 | shouldAttemptEagerTransition: () => true,
57 | trackSchedulerEvent() {},
58 | resolveEventType: () => null,
59 | resolveEventTimeStamp: () => 0,
60 | maySuspendCommit: () => false,
61 | preloadInstance: () => true,
62 | startSuspendingCommit() {},
63 | suspendInstance() {},
64 | waitForCommitToBeReady: () => null
65 | });
66 |
67 | export const createFiber = (element: React.ReactElement): Fiber => {
68 | const root = reconciler.createContainer({}, LegacyRoot, null, false, false, "", console.error, null);
69 | (reconciler as any).updateContainerSync(element, root);
70 | (reconciler as any).flushSyncWork();
71 | return root.current;
72 | };
73 |
--------------------------------------------------------------------------------
/packages/dium/tests/utils/general.ts:
--------------------------------------------------------------------------------
1 | import {describe, it} from "mocha";
2 | import {strict as assert} from "assert";
3 |
4 | import {mappedProxy} from "../../src/utils";
5 |
6 | describe("Utilities", () => {
7 | describe("mappedProxy", () => {
8 | const original = {
9 | a: "foo",
10 | get b() {
11 | return [1, 2, 3];
12 | },
13 | c: (arg: any) => console.log(arg)
14 | };
15 | const mapping = {
16 | name: "a",
17 | data: "b",
18 | log: "c"
19 | } as const;
20 |
21 | it("maps read", () => {
22 | const mapped = mappedProxy(original, mapping);
23 |
24 | assert.equal(mapped.a, original.a);
25 | assert.equal(mapped.name, original.a);
26 | assert.deepEqual(mapped.b, original.b);
27 | assert.deepEqual(mapped.data, original.b);
28 | assert.equal(mapped.c, original.c);
29 | assert.equal(mapped.log, original.c);
30 | });
31 |
32 | it("maps keys", () => {
33 | const mapped = mappedProxy(original, mapping);
34 |
35 | assert("name" in mapped);
36 | assert("a" in mapped);
37 | assert.deepEqual(Object.keys(mapped).sort(), [...Object.keys(original), ...Object.keys(mapping)].sort());
38 | });
39 |
40 | it("maps write", () => {
41 | const cloned = {...original};
42 | const mapped = mappedProxy(cloned, mapping);
43 |
44 | assert.doesNotThrow(() => mapped.name = "bar");
45 | assert.equal(mapped.name, "bar");
46 | assert.equal(mapped.a, "bar");
47 | assert.equal(cloned.a, "bar");
48 | });
49 |
50 | it("maps delete", () => {
51 | const cloned = {...original};
52 | const mapped = mappedProxy(cloned, mapping);
53 | delete mapped.log;
54 |
55 | assert.equal(mapped.log, undefined, "value remained in mapped");
56 | assert(!("log" in mapped), "key remained in mapped");
57 | assert.equal(cloned.c, undefined, "value remained in original");
58 | assert(!("c" in cloned), "key remained in original");
59 | });
60 |
61 | it("maps descriptor get", () => {
62 | const mapped = mappedProxy(original, mapping);
63 |
64 | assert.deepEqual(Object.getOwnPropertyDescriptor(mapped, "name"), Object.getOwnPropertyDescriptor(original, "a"));
65 | assert.deepEqual(Object.getOwnPropertyDescriptor(mapped, "a"), Object.getOwnPropertyDescriptor(original, "a"));
66 | assert.deepEqual(Object.getOwnPropertyDescriptor(mapped, "data"), Object.getOwnPropertyDescriptor(original, "b"));
67 | assert.deepEqual(Object.getOwnPropertyDescriptor(mapped, "log"), Object.getOwnPropertyDescriptor(original, "c"));
68 | });
69 |
70 | it("maps descriptor set", () => {
71 | const cloned = {...original};
72 | const mapped = mappedProxy(cloned, mapping);
73 | Object.defineProperty(mapped, "data", {
74 | get: () => []
75 | });
76 |
77 | assert.deepEqual(mapped.data, []);
78 | assert.deepEqual(cloned.b, []);
79 | });
80 | });
81 | });
82 |
--------------------------------------------------------------------------------
/packages/dium/tests/utils/react.tsx:
--------------------------------------------------------------------------------
1 | import {describe, it} from "mocha";
2 | import {strict as assert} from "assert";
3 | import React from "react";
4 | import {createFiber} from "../mock";
5 |
6 | import {Direction, Predicate, queryFiber, queryTree, queryTreeAll} from "../../src/utils/react";
7 | import type {Fiber} from "../../src/react-internals";
8 |
9 | const TestComponent = ({children}: {children: React.JSX.Element}): React.JSX.Element => children;
10 |
11 | const elements = (
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | );
21 |
22 | describe("React element tree", () => {
23 | const tree = elements;
24 |
25 | describe("queryTree", () => {
26 | it("finds a result", () => {
27 | assert(queryTree(tree, (node) => node.type === "span") instanceof Object);
28 | });
29 |
30 | it("finds the correct node", () => {
31 | assert.equal(queryTree(tree, (node) => node.type === "span"), tree.props.children[1]);
32 | });
33 | });
34 |
35 | describe("queryTreeAll", () => {
36 | it("finds a result", () => {
37 | assert(queryTreeAll(tree, (node) => node.type === "span").length > 0);
38 | });
39 |
40 | it("finds the correct nodes", () => {
41 | assert.deepEqual(
42 | queryTreeAll(tree, (node) => node.type === "span"),
43 | [tree.props.children[1], tree.props.children[0].props.children]
44 | );
45 | });
46 | });
47 | });
48 |
49 | describe("React Fiber", () => {
50 | const root = createFiber(elements);
51 | const parent = root.child;
52 | const child = parent.child.child;
53 |
54 | const deepRoot = {key: "0"} as Fiber;
55 | let deepChild = deepRoot;
56 | for (let i = 1; i < 100; i++) {
57 | const child = {
58 | key: i.toString(),
59 | return: deepChild
60 | } as Fiber;
61 | deepChild.child = child;
62 | deepChild = child;
63 | }
64 |
65 | const createTrackingPredicate = (): [Predicate, Set] => {
66 | const calledOn = new Set();
67 | const predicate = (node: Fiber) => {
68 | calledOn.add(parseInt(node.key));
69 | return false;
70 | };
71 | return [predicate, calledOn];
72 | };
73 |
74 | describe("queryFiber", () => {
75 | it("finds a result upwards", () => {
76 | assert(queryFiber(child, (node) => node.type === "div", Direction.Up) instanceof Object);
77 | });
78 |
79 | it("finds the correct node upwards", () => {
80 | assert.equal(queryFiber(parent, (node) => node.type === "div", Direction.Up), parent);
81 | });
82 |
83 | it("finds a result downwards", () => {
84 | assert(queryFiber(parent, (node) => node.type === "span", Direction.Down) instanceof Object);
85 | });
86 |
87 | it("finds the correct node downwards", () => {
88 | assert.equal(queryFiber(parent, (node) => node.type === "span", Direction.Down), child);
89 | });
90 |
91 | it("stops after max depth upwards", () => {
92 | const depth = 30;
93 | const [predicate, calledOn] = createTrackingPredicate();
94 | queryFiber(deepChild, predicate, Direction.Up);
95 |
96 | const expected = new Set([...Array(depth + 1).keys()].map((i) => 99 - i)); // includes call on node itself
97 | assert.deepEqual(calledOn, expected);
98 | });
99 |
100 | it("stops after max depth downwards", () => {
101 | const depth = 30;
102 | const [predicate, calledOn] = createTrackingPredicate();
103 | queryFiber(deepRoot, predicate, Direction.Down);
104 |
105 | const expected = new Set(Array(depth + 1).keys()); // includes call on node itself
106 | assert.deepEqual(calledOn, expected);
107 | });
108 | });
109 | });
110 |
--------------------------------------------------------------------------------
/packages/dium/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es2020",
4 | "jsx": "react",
5 | "moduleResolution": "node",
6 | "esModuleInterop": true,
7 | "resolveJsonModule": true
8 | },
9 | "ts-node": {
10 | "files": true
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/packages/rollup-plugin-bd-meta/index.ts:
--------------------------------------------------------------------------------
1 | import path from "path";
2 | import type {Plugin} from "rollup";
3 | import {resolvePkg, readMetaFromPkg, writeMeta, Meta} from "bd-meta";
4 |
5 | export interface Options {
6 | meta?: Partial;
7 | authorGithub?: boolean;
8 | }
9 |
10 | /**
11 | * Rollup plugin for BetterDiscord plugin meta generation.
12 | */
13 | export function bdMeta({meta, authorGithub}: Options = {}): Plugin {
14 | const pkgFiles: Record = {};
15 |
16 | return {
17 | name: "bd-meta",
18 | async buildStart({input}) {
19 | const inputFiles = Array.isArray(input) ? input : Object.values(input);
20 | for (const input of inputFiles) {
21 | const pkg = await resolvePkg(path.dirname(input));
22 | pkgFiles[input] = pkg;
23 | this.addWatchFile(pkg);
24 | }
25 | },
26 | async watchChange(id, {event}) {
27 | for (const input of Object.keys(pkgFiles)) {
28 | if (pkgFiles[input] == id) {
29 | if (event === "delete") {
30 | const pkg = await resolvePkg(path.dirname(input));
31 | pkgFiles[input] = pkg;
32 | this.addWatchFile(pkg);
33 | }
34 | break;
35 | }
36 | }
37 | },
38 | renderChunk: {
39 | order: "post",
40 | async handler(code, chunk) {
41 | if (chunk.isEntry) {
42 | const pkg = pkgFiles[chunk.facadeModuleId];
43 | const combinedMeta = {
44 | ...pkg ? await readMetaFromPkg(pkg, {authorGithub}) : {},
45 | ...meta
46 | };
47 | if (Object.keys(combinedMeta).length > 0) {
48 | return {
49 | code: writeMeta(combinedMeta) + code,
50 | map: {mappings: ""}
51 | };
52 | }
53 | }
54 | }
55 | }
56 | };
57 | }
58 |
59 | export default bdMeta;
60 |
--------------------------------------------------------------------------------
/packages/rollup-plugin-bd-meta/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "rollup-plugin-bd-meta",
3 | "version": "0.1.0",
4 | "author": "Zerthox",
5 | "peerDependencies": {
6 | "rollup": "^4.0.0"
7 | },
8 | "devDependencies": {
9 | "rollup": "^4.0.0"
10 | },
11 | "dependencies": {
12 | "bd-meta": "^0.1.0"
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/packages/rollup-plugin-bd-wscript/index.ts:
--------------------------------------------------------------------------------
1 | import path from "path";
2 | import {readFileSync} from "fs";
3 | import type {Plugin} from "rollup";
4 |
5 | const wscript = readFileSync(path.join(__dirname, "wscript.js"), "utf8")
6 | .split("\n")
7 | .filter((line) => {
8 | const trim = line.trim();
9 | return trim.length > 0 && !trim.startsWith("//") && !trim.startsWith("/*");
10 | }).join("\n");
11 |
12 | /**
13 | * Rollup plugin for BetterDiscord WScript warning generation.
14 | */
15 | export function bdWScript(): Plugin {
16 | return {
17 | name: "bd-meta",
18 | banner: `/*@cc_on @if (@_jscript)\n${wscript}\n@else @*/\n`,
19 | footer: "/*@end @*/"
20 | };
21 | }
22 |
23 | export default bdWScript;
24 |
--------------------------------------------------------------------------------
/packages/rollup-plugin-bd-wscript/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "rollup-plugin-bd-wscript",
3 | "version": "0.1.0",
4 | "author": "Zerthox",
5 | "peerDependencies": {
6 | "rollup": "^4.0.0"
7 | },
8 | "devDependencies": {
9 | "rollup": "^4.0.0"
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/packages/rollup-plugin-bd-wscript/wscript.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 | var pluginName = WScript.ScriptName.split(".")[0];
3 | var shell = WScript.CreateObject("WScript.Shell");
4 |
5 | shell.Popup(
6 | "Do NOT run scripts from the internet with the Windows Script Host!\nMove this file to your BetterDiscord plugins folder.",
7 | 0,
8 | pluginName + ": Warning!",
9 | 0x1030
10 | );
11 |
12 | var fso = new ActiveXObject("Scripting.FileSystemObject");
13 | var pluginsPath = shell.expandEnvironmentStrings("%appdata%\\BetterDiscord\\plugins");
14 | if (!fso.FolderExists(pluginsPath)) {
15 | var popup = shell.Popup(
16 | "Unable to find BetterDiscord on your computer.\nOpen the download page of BetterDiscord?",
17 | 0,
18 | pluginName + ": BetterDiscord not found",
19 | 0x34
20 | );
21 | if (popup === 6) {
22 | shell.Exec("explorer \"https://betterdiscord.app\"");
23 | }
24 | } else if (WScript.ScriptFullName === pluginsPath + "\\" + WScript.ScriptName) {
25 | shell.Popup(
26 | "This plugin is already in the correct folder.\nNavigate to the \"Plugins\" settings tab in Discord and enable it there.",
27 | 0,
28 | pluginName,
29 | 0x40
30 | );
31 | } else {
32 | var popup = shell.Popup(
33 | "Open the BetterDiscord plugins folder?",
34 | 0,
35 | pluginName,
36 | 0x34
37 | );
38 | if (popup === 6) {
39 | shell.Exec("explorer " + pluginsPath);
40 | }
41 | }
42 |
43 | WScript.Quit();
44 |
--------------------------------------------------------------------------------
/packages/rollup-plugin-style-modules/index.ts:
--------------------------------------------------------------------------------
1 | import postcss from "postcss";
2 | import postcssModules from "postcss-modules";
3 | import camelCase from "lodash.camelcase";
4 | import type {Plugin} from "rollup";
5 |
6 | export type PostCSSModulesOptions = Parameters[0];
7 |
8 | export interface Options {
9 | modules?: Omit;
10 | cleanup?: boolean;
11 | }
12 |
13 | /**
14 | * Rollup plugin for custom style handling.
15 | *
16 | * Transforms CSS modules and removes empty rules.
17 | * Exports styles as string as named `css` export and mapped classNames as `default` export.
18 | */
19 | export function styleModules({modules, cleanup = true}: Options = {}): Plugin {
20 | const filter = (id: string) => /\.module\.(css|scss|sass)$/.test(id);
21 |
22 | return {
23 | name: "style-modules",
24 | async transform(code, id) {
25 | if (filter(id)) {
26 | // we expect stringified css string to be the default export
27 | const [, cssString] = /\s*export\s+default\s+(.+)$/.exec(code) ?? [];
28 | if (cssString) {
29 | const css = JSON.parse(cssString);
30 |
31 | let mapping: Record;
32 | const result = await postcss()
33 | .use(postcssModules({
34 | ...modules,
35 | getJSON: (_file, json) => mapping = json
36 | }))
37 | .use(cleanup ? {
38 | postcssPlugin: "cleanup",
39 | OnceExit(root) {
40 | for (const child of root.nodes) {
41 | if (child.type === "rule") {
42 | const contents = child.nodes.filter((node) => node.type !== "comment");
43 | if (contents.length === 0) {
44 | child.remove();
45 | }
46 | }
47 | }
48 | }
49 | } : null)
50 | .process(css, {from: id});
51 |
52 | const named = Object.entries(mapping).map(([key, value]) => ` ${camelCase(key)}: ${JSON.stringify(value)}`).join(",\n");
53 | return {
54 | code: `export const css = ${JSON.stringify(result.css)};\nexport default {\n${named}\n}`,
55 | map: {
56 | mappings: ""
57 | }
58 | };
59 | }
60 | }
61 | }
62 | };
63 | }
64 |
65 | export default styleModules;
66 |
--------------------------------------------------------------------------------
/packages/rollup-plugin-style-modules/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "rollup-plugin-style-modules",
3 | "version": "0.1.0",
4 | "author": "Zerthox",
5 | "dependencies": {
6 | "lodash.camelcase": "^4.3.0",
7 | "postcss": "^8.4.21",
8 | "postcss-modules": "^6.0.0"
9 | },
10 | "peerDependencies": {
11 | "rollup": "^4.0.0"
12 | },
13 | "devDependencies": {
14 | "rollup": "^4.0.0"
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/rollup.config.ts:
--------------------------------------------------------------------------------
1 | import {defineConfig} from "rollup";
2 | import json from "@rollup/plugin-json";
3 | import scss from "rollup-plugin-scss";
4 | import typescript from "@rollup/plugin-typescript";
5 | import cleanup from "rollup-plugin-cleanup";
6 |
7 | export default defineConfig({
8 | output: {
9 | format: "cjs",
10 | exports: "default",
11 | generatedCode: {
12 | constBindings: true,
13 | objectShorthand: true
14 | },
15 | freeze: false
16 | },
17 | plugins: [
18 | json({
19 | namedExports: true,
20 | preferConst: true
21 | }),
22 | scss({
23 | output: false
24 | }),
25 | typescript(),
26 | cleanup({
27 | comments: [/[@#]__PURE__/],
28 | maxEmptyLines: 0,
29 | extensions: ["js", "ts", "tsx"],
30 | sourcemap: false
31 | })
32 | ],
33 | treeshake: {
34 | preset: "smallest",
35 | annotations: true,
36 | moduleSideEffects: false,
37 | propertyReadSideEffects: false
38 | }
39 | });
40 |
--------------------------------------------------------------------------------
/scripts/build.ts:
--------------------------------------------------------------------------------
1 | import path from "path";
2 | import {readdirSync} from "fs";
3 | import minimist from "minimist";
4 | import chalk from "chalk";
5 | import * as rollup from "rollup";
6 | import styleModules from "rollup-plugin-style-modules";
7 | import {resolvePkg, readMetaFromPkg} from "bd-meta";
8 | import bdMeta from "rollup-plugin-bd-meta";
9 | import bdWScript from "rollup-plugin-bd-wscript";
10 | import rollupConfig from "../rollup.config";
11 | import {repository} from "../package.json";
12 |
13 | const success = (msg: string) => console.log(chalk.green(msg));
14 | const warn = (msg: string) => console.warn(chalk.yellow(`Warn: ${msg}`));
15 | const error = (msg: string) => console.error(chalk.red(`Error: ${msg}`));
16 |
17 | // find sources
18 | const sourceFolder = path.resolve(__dirname, "../src");
19 | const sourceEntries = readdirSync(sourceFolder, {withFileTypes: true}).filter((entry) => entry.isDirectory());
20 |
21 | // parse args
22 | const args = minimist(process.argv.slice(2), {boolean: ["dev", "watch"]});
23 |
24 | // resolve input paths
25 | let inputPaths: string[] = [];
26 | if (args._.length === 0) {
27 | inputPaths = sourceEntries.map((entry) => path.resolve(sourceFolder, entry.name));
28 | } else {
29 | for (const name of args._) {
30 | const entry = sourceEntries.find((entry) => entry.name.toLowerCase() === name.toLowerCase());
31 | if (entry) {
32 | inputPaths.push(path.resolve(sourceFolder, entry.name));
33 | } else {
34 | warn(`Unknown plugin "${name}"`);
35 | }
36 | }
37 | }
38 |
39 | // check for inputs
40 | if (inputPaths.length === 0) {
41 | error("No plugin inputs");
42 | process.exit(1);
43 | }
44 |
45 | // resolve output directory
46 | const outDir = args.dev ? path.resolve(
47 | process.platform === "win32" ? process.env.APPDATA
48 | : process.platform === "darwin" ? path.resolve(process.env.HOME, "Library/Application Support")
49 | : path.resolve(process.env.HOME, ".config"),
50 | "BetterDiscord/plugins"
51 | ) : path.resolve(__dirname, "../dist/bd");
52 |
53 | const watchers: Record = {};
54 |
55 | // build each input
56 | for (const inputPath of inputPaths) {
57 | const outputPath = path.resolve(outDir, `${path.basename(inputPath)}.plugin.js`);
58 |
59 | if (args.watch) {
60 | // watch for changes
61 | watch(inputPath, outputPath).then(() => console.log(`Watching for changes in "${inputPath}"`));
62 | } else {
63 | // build once
64 | build(inputPath, outputPath);
65 | }
66 | }
67 | if (args.watch) {
68 | // keep process alive
69 | process.stdin.resume();
70 | process.stdin.on("end", () => {
71 | for (const watcher of Object.values(watchers)) {
72 | watcher.close();
73 | }
74 | });
75 | }
76 |
77 | async function build(inputPath: string, outputPath: string): Promise {
78 | const meta = await readMetaFromPkg(await resolvePkg(inputPath));
79 | const config = generateRollupConfig(meta.name, inputPath, outputPath);
80 |
81 | // bundle plugin
82 | const bundle = await rollup.rollup(config);
83 | await bundle.write(config.output);
84 | success(`Built ${meta.name} v${meta.version} to "${outputPath}"`);
85 |
86 | await bundle.close();
87 | }
88 |
89 | async function watch(inputPath: string, outputPath: string): Promise {
90 | const pkgPath = await resolvePkg(inputPath);
91 | const meta = await readMetaFromPkg(pkgPath);
92 | const {plugins, ...config} = generateRollupConfig(meta.name, inputPath, outputPath);
93 |
94 | // start watching
95 | const watcher = rollup.watch({
96 | ...config,
97 | plugins: [
98 | plugins,
99 | {
100 | name: "package-watcher",
101 | buildStart() {
102 | this.addWatchFile(pkgPath);
103 | }
104 | }
105 | ]
106 | });
107 |
108 | // close finished bundles
109 | watcher.on("event", (event) => {
110 | if (event.code === "BUNDLE_END") {
111 | success(`Built ${meta.name} v${meta.version} to "${outputPath}" [${event.duration}ms]`);
112 | event.result.close();
113 | }
114 | });
115 |
116 | // restart on config changes
117 | watcher.on("change", (file) => {
118 | // check for config changes
119 | if (file === pkgPath) {
120 | watchers[inputPath].close();
121 | watch(inputPath, outputPath);
122 | }
123 |
124 | console.log(`=> Changed "${file}"`);
125 | });
126 |
127 | watchers[inputPath] = watcher;
128 | }
129 |
130 | interface RollupConfig extends Omit {
131 | output: rollup.OutputOptions;
132 | }
133 |
134 | function generateRollupConfig(name: string, inputPath: string, outputPath: string): RollupConfig {
135 | const {output, plugins, ...rest} = rollupConfig;
136 |
137 | return {
138 | ...rest,
139 | input: path.resolve(inputPath, "index.tsx"),
140 | plugins: [
141 | plugins,
142 | styleModules({
143 | modules: {
144 | generateScopedName: `[local]-${name}`
145 | },
146 | cleanup: true
147 | }),
148 | bdMeta({
149 | meta: {
150 | website: repository,
151 | source: `${repository}/tree/master/src/${path.basename(inputPath)}`
152 | },
153 | authorGithub: true
154 | }),
155 | bdWScript()
156 | ],
157 | output: {
158 | ...output,
159 | file: outputPath
160 | }
161 | };
162 | }
163 |
--------------------------------------------------------------------------------
/src/BetterFolders/icon.tsx:
--------------------------------------------------------------------------------
1 | import {Finder, Logger, React, Utils} from "dium";
2 | import {Settings, FolderData} from "./settings";
3 | import styles from "./styles.module.scss";
4 |
5 | const folderStyles = Finder.byKeys(["folderIcon", "folderIconWrapper", "folderPreviewWrapper"]);
6 |
7 | export const renderIcon = (data: FolderData): React.JSX.Element => (
8 |
12 | );
13 |
14 | export interface BetterFolderIconProps {
15 | data?: FolderData;
16 | childProps: any;
17 | FolderIcon: React.FunctionComponent;
18 | }
19 |
20 | export const BetterFolderIcon = ({data, childProps, FolderIcon}: BetterFolderIconProps): React.JSX.Element => {
21 | if (FolderIcon) {
22 | const result = FolderIcon(childProps) as React.JSX.Element;
23 | if (data?.icon) {
24 | const replace = renderIcon(data);
25 | const iconWrapper = Utils.queryTree(result, (node) => node?.props?.className === folderStyles.folderIconWrapper);
26 | if (iconWrapper) {
27 | Utils.replaceElement(iconWrapper, replace);
28 | } else {
29 | Logger.error("Failed to find folderIconWrapper element");
30 | }
31 | if (data.always) {
32 | const previewWrapper = Utils.queryTree(result, (node) => node?.props?.className === folderStyles.folderPreviewWrapper);
33 | if (previewWrapper) {
34 | Utils.replaceElement(previewWrapper, replace);
35 | } else {
36 | Logger.error("Failed to find folderPreviewWrapper element");
37 | }
38 | }
39 | }
40 | return result;
41 | } else {
42 | return null;
43 | }
44 | };
45 |
46 | export interface ConnectedBetterFolderIconProps {
47 | folderId: number;
48 | childProps: any;
49 | FolderIcon: React.FunctionComponent;
50 | }
51 |
52 | const compareFolderData = (a?: FolderData, b?: FolderData): boolean => a?.icon === b?.icon && a?.always === b?.always;
53 |
54 | export const ConnectedBetterFolderIcon = ({folderId, ...props}: ConnectedBetterFolderIconProps): React.JSX.Element => {
55 | const data = Settings.useSelector(
56 | (current) => current.folders[folderId],
57 | [folderId],
58 | compareFolderData
59 | );
60 | return ;
61 | };
62 |
--------------------------------------------------------------------------------
/src/BetterFolders/index.tsx:
--------------------------------------------------------------------------------
1 | import {createPlugin, Logger, Filters, Finder, Patcher, Utils, React, Fiber} from "dium";
2 | import {ClientActions, ExpandedGuildFolderStore} from "@dium/modules";
3 | import {FormSwitch} from "@dium/components";
4 | import {Settings} from "./settings";
5 | import {ConnectedBetterFolderIcon} from "./icon";
6 | import {folderModalPatch, FolderSettingsModal} from "./modal";
7 | import {css} from "./styles.module.scss";
8 |
9 | const guildStyles = Finder.byKeys(["guilds", "base"]);
10 |
11 | const getGuildsOwner = () => Utils.findOwner(Utils.getFiber(document.getElementsByClassName(guildStyles.guilds)?.[0]));
12 |
13 | const triggerRerender = async (guildsFiber: Fiber) => {
14 | if (await Utils.forceFullRerender(guildsFiber)) {
15 | Logger.log("Rerendered guilds");
16 | } else {
17 | Logger.warn("Unable to rerender guilds");
18 | }
19 | };
20 |
21 | export default createPlugin({
22 | start() {
23 | let FolderIcon = null;
24 | const guildsOwner = getGuildsOwner();
25 |
26 | // patch folder icon wrapper
27 | // icon is in same module, not exported
28 | const FolderIconWrapper = Finder.findWithKey>(Filters.bySource("folderIconWrapper"));
29 | Patcher.after(...FolderIconWrapper, ({args: [props], result}) => {
30 | const icon = Utils.queryTree(result, (node) => node?.props?.folderNode) as React.ReactElement>;
31 | if (!icon) {
32 | return Logger.error("Unable to find FolderIcon component");
33 | }
34 |
35 | // save icon component
36 | if (!FolderIcon) {
37 | Logger.log("Found FolderIcon component");
38 | FolderIcon = icon.type;
39 | }
40 |
41 | // replace icon with own component
42 | const replace = ;
47 | Utils.replaceElement(icon, replace);
48 | }, {name: "FolderIconWrapper"});
49 | triggerRerender(guildsOwner);
50 |
51 | // patch folder expand
52 | Patcher.after(ClientActions, "toggleGuildFolderExpand", ({original, args: [folderId]}) => {
53 | if (Settings.current.closeOnOpen) {
54 | for (const id of ExpandedGuildFolderStore.getExpandedFolders()) {
55 | if (id !== folderId) {
56 | original(id);
57 | }
58 | }
59 | }
60 | });
61 |
62 | // patch folder settings render
63 | Finder.waitFor(Filters.bySource(".folderName", ".onClose"), {entries: true}).then((FolderSettingsModal: FolderSettingsModal) => {
64 | if (FolderSettingsModal) {
65 | Patcher.after(
66 | FolderSettingsModal.prototype,
67 | "render",
68 | folderModalPatch,
69 | {name: "GuildFolderSettingsModal"}
70 | );
71 | }
72 | });
73 | },
74 | stop() {
75 | triggerRerender(getGuildsOwner());
76 | },
77 | styles: css,
78 | Settings,
79 | SettingsPanel: () => {
80 | const [{closeOnOpen}, setSettings] = Settings.useState();
81 |
82 | return (
83 | {
88 | if (checked) {
89 | // close all folders except one
90 | for (const id of Array.from(ExpandedGuildFolderStore.getExpandedFolders()).slice(1)) {
91 | ClientActions.toggleGuildFolderExpand(id);
92 | }
93 | }
94 | setSettings({closeOnOpen: checked});
95 | }}
96 | >Close on open
97 | );
98 | }
99 | });
100 |
--------------------------------------------------------------------------------
/src/BetterFolders/modal.tsx:
--------------------------------------------------------------------------------
1 | import {React, Logger, PatchDataWithResult, Utils} from "dium";
2 | import {SortedGuildStore, GuildsTreeFolder} from "@dium/modules";
3 | import {RadioGroup, FormItem} from "@dium/components";
4 | import {BetterFolderUploader} from "./uploader";
5 | import {Settings} from "./settings";
6 |
7 | export interface FolderSettingsModalProps {
8 | folderId: number;
9 | folderName: string;
10 | folderColor: number;
11 | onClose: () => void;
12 | transitionState: number;
13 | }
14 |
15 | export interface FolderSettingsModalState {
16 | name: string;
17 | color: number;
18 | }
19 |
20 | export type FolderSettingsModal = typeof React.Component;
21 |
22 | const enum IconType {
23 | Default = "default",
24 | Custom = "custom"
25 | }
26 |
27 | interface PatchedFolderSettingsModalState extends FolderSettingsModalState {
28 | iconType: IconType;
29 | icon?: string;
30 | always?: boolean;
31 | }
32 |
33 | type PatchedModal = React.Component;
34 |
35 | export const folderModalPatch = ({context, result}: PatchDataWithResult): void => {
36 | const {folderId} = context.props;
37 | const {state} = context;
38 |
39 | // find form
40 | const form = Utils.queryTree(result, (node) => node?.type === "form");
41 | if (!form) {
42 | Logger.warn("Unable to find form");
43 | return;
44 | }
45 |
46 | // add custom state
47 | if (!state.iconType) {
48 | const {icon = null, always = false} = Settings.current.folders[folderId] ?? {};
49 | Object.assign(state, {
50 | iconType: icon ? IconType.Custom : IconType.Default,
51 | icon,
52 | always
53 | });
54 | }
55 |
56 | // render icon select
57 | const {children} = form.props;
58 | const {className} = children[0].props;
59 | children.push(
60 |
61 | context.setState({iconType: value})}
68 | />
69 |
70 | );
71 |
72 | if (state.iconType === IconType.Custom) {
73 | // render custom icon options
74 | const tree = SortedGuildStore.getGuildsTree();
75 | children.push(
76 |
77 | context.setState({icon, always})}
82 | />
83 |
84 | );
85 | }
86 |
87 | // override submit onclick
88 | const button = Utils.queryTree(result, (node) => node?.props?.type === "submit");
89 | const original = button.props.onClick;
90 | button.props.onClick = (...args: any[]) => {
91 | original(...args);
92 |
93 | // update folders if necessary
94 | const {folders} = Settings.current;
95 | if (state.iconType === IconType.Custom && state.icon) {
96 | folders[folderId] = {icon: state.icon, always: state.always};
97 | Settings.update({folders});
98 | } else if ((state.iconType === IconType.Default || !state.icon) && folders[folderId]) {
99 | delete folders[folderId];
100 | Settings.update({folders});
101 | }
102 | };
103 | };
104 |
--------------------------------------------------------------------------------
/src/BetterFolders/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "BetterFolders",
3 | "author": "Zerthox",
4 | "version": "3.6.2",
5 | "description": "Adds new functionality to server folders. Custom Folder Icons. Close other folders on open.",
6 | "dependencies": {
7 | "dium": "*"
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/src/BetterFolders/settings.ts:
--------------------------------------------------------------------------------
1 | import {createSettings} from "dium";
2 |
3 | export interface FolderData {
4 | icon: string;
5 | always: boolean;
6 | }
7 |
8 | export const Settings = createSettings({
9 | closeOnOpen: false,
10 | folders: {} as Record
11 | });
12 |
--------------------------------------------------------------------------------
/src/BetterFolders/styles.module.scss:
--------------------------------------------------------------------------------
1 | .customIcon {
2 | box-sizing: border-box;
3 | border-radius: var(--radius-lg);
4 | width: var(--guildbar-folder-size);
5 | height: var(--guildbar-folder-size);
6 | padding: var(--custom-folder-preview-padding);
7 | background-size: contain;
8 | background-position: center;
9 | background-repeat: no-repeat;
10 | }
11 |
--------------------------------------------------------------------------------
/src/BetterFolders/uploader.tsx:
--------------------------------------------------------------------------------
1 | import {React} from "dium";
2 | import {GuildsTreeFolder} from "@dium/modules";
3 | import {Flex, Button, FormSwitch, FormText, ImageInput, margins} from "@dium/components";
4 | import {FolderData} from "./settings";
5 | import {renderIcon} from "./icon";
6 |
7 | export interface BetterFolderUploaderProps extends FolderData {
8 | folderNode: GuildsTreeFolder;
9 | onChange(data: FolderData): void;
10 | }
11 |
12 | export const BetterFolderUploader = ({icon, always, onChange}: BetterFolderUploaderProps): React.JSX.Element => (
13 | <>
14 |
15 |
19 | Preview:
20 | {renderIcon({icon, always: true})}
21 |
22 | onChange({icon, always: checked})}
27 | >Always display icon
28 | >
29 | );
30 |
--------------------------------------------------------------------------------
/src/BetterVolume/experiment.tsx:
--------------------------------------------------------------------------------
1 | // legacy code for disabling audio experiment
2 |
3 | import {Logger, React, Utils, getMeta} from "dium";
4 | import {ExperimentStore, ExperimentTreatment} from "@dium/modules";
5 | import {Text} from "@dium/components";
6 | import {Settings} from "./settings";
7 |
8 | const AUDIO_EXPERIMENT = "2022-09_remote_audio_settings";
9 |
10 | let initialAudioBucket = ExperimentTreatment.NOT_ELIGIBLE;
11 |
12 | export const hasExperiment = (): boolean => initialAudioBucket > ExperimentTreatment.CONTROL;
13 |
14 | const setAudioBucket = (bucket: number): void => {
15 | if (hasExperiment()) {
16 | Logger.log("Changing experiment bucket to", bucket);
17 | const audioExperiment = ExperimentStore.getUserExperimentDescriptor(AUDIO_EXPERIMENT);
18 | audioExperiment.bucket = bucket;
19 | }
20 | };
21 |
22 | // update on settings change
23 | Settings.addListener(({disableExperiment}) => setAudioBucket(disableExperiment ? ExperimentTreatment.CONTROL : initialAudioBucket));
24 |
25 | const onLoadExperiments = (): void => {
26 | // initialize bucket
27 | initialAudioBucket = ExperimentStore.getUserExperimentBucket(AUDIO_EXPERIMENT);
28 | Logger.log("Initial experiment bucket", initialAudioBucket);
29 |
30 | if (hasExperiment()) {
31 | const {disableExperiment} = Settings.current;
32 | Logger.log("Experiment setting:", disableExperiment);
33 | // check if we have to disable
34 | if (disableExperiment) {
35 | // simply setting this should be fine, seems to be only changed on connect etc.
36 | setAudioBucket(0);
37 | } else if (disableExperiment === null) {
38 | // initial value means we set to false and ask the user
39 | Settings.update({disableExperiment: false});
40 | Utils.confirm(getMeta().name, (
41 |
42 | Your client has an experiment interfering with volumes greater than 200% enabled.
43 | Do you wish to disable it now and on future restarts?
44 |
45 | ), {
46 | onConfirm: () => Settings.update({disableExperiment: true})
47 | });
48 | }
49 | }
50 | };
51 |
52 | export const handleExperiment = (): void => {
53 | if (ExperimentStore.hasLoadedExperiments) {
54 | Logger.log("Experiments already loaded");
55 | onLoadExperiments();
56 | } else {
57 | Logger.log("Waiting for experiments load");
58 | const listener = () => {
59 | if (ExperimentStore.hasLoadedExperiments) {
60 | Logger.log("Experiments loaded after wait");
61 | ExperimentStore.removeChangeListener(listener);
62 | onLoadExperiments();
63 | }
64 | };
65 | ExperimentStore.addChangeListener(listener);
66 | }
67 | };
68 |
69 | export const resetExperiment = (): void => {
70 | // reset experiment to initial bucket
71 | if (Settings.current.disableExperiment) {
72 | setAudioBucket(initialAudioBucket);
73 | }
74 | };
75 |
--------------------------------------------------------------------------------
/src/BetterVolume/index.tsx:
--------------------------------------------------------------------------------
1 | import {createPlugin, Finder, Filters, Patcher, React} from "dium";
2 | import {Snowflake, MediaEngineStore, MediaEngineContext, AudioConvert, MediaEngineActions} from "@dium/modules";
3 | import {Settings} from "./settings";
4 | import {css} from "./styles.module.scss";
5 | import {MenuItem} from "@dium/components";
6 | import {NumberInput} from "./input";
7 | import {handleVolumeSync, resetVolumeSync} from "./sync";
8 |
9 | type UseUserVolumeItem = (userId: Snowflake, context: MediaEngineContext) => React.JSX.Element;
10 |
11 | const useUserVolumeItemFilter = Filters.bySource("user-volume");
12 |
13 | export default createPlugin({
14 | start() {
15 | // handle volume override sync
16 | handleVolumeSync();
17 |
18 | // add number input to user volume item
19 | Finder.waitFor(useUserVolumeItemFilter, {resolve: false}).then((result: Record) => {
20 | const useUserVolumeItem = Finder.resolveKey(result, useUserVolumeItemFilter);
21 | Patcher.after(...useUserVolumeItem, ({args: [userId, context], result}) => {
22 | // check for original render
23 | if (result) {
24 | // we can read this directly, the original has a hook to ensure updates
25 | const volume = MediaEngineStore.getLocalVolume(userId, context);
26 |
27 | return (
28 | <>
29 | {result}
30 |