aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/plugins/pronoundb/PronounComponent.tsx27
-rw-r--r--src/plugins/pronoundb/index.ts23
-rw-r--r--src/plugins/pronoundb/types.ts29
-rw-r--r--src/plugins/pronoundb/utils.ts59
-rw-r--r--src/utils/misc.tsx7
5 files changed, 142 insertions, 3 deletions
diff --git a/src/plugins/pronoundb/PronounComponent.tsx b/src/plugins/pronoundb/PronounComponent.tsx
new file mode 100644
index 0000000..35cd44b
--- /dev/null
+++ b/src/plugins/pronoundb/PronounComponent.tsx
@@ -0,0 +1,27 @@
+import { fetchPronouns } from "./utils";
+import { classes, lazyWebpack, useAwaiter } from "../../utils/misc";
+import { PronounMapping } from "./types";
+import { filters } from "../../webpack";
+import { Message } from "discord-types/general";
+
+const styles: Record<string, string> = lazyWebpack(filters.byProps(["timestampInline"]));
+
+export default function PronounComponent({ message }: { message: Message; }) {
+ // Don't bother fetching bot or system users
+ if (message.author.bot && message.author.system) return null;
+
+ const [result, , isPending] = useAwaiter(
+ () => fetchPronouns(message.author.id),
+ null,
+ e => console.error("Fetching pronouns failed: ", e)
+ );
+
+ // If the promise completed, the result was not "unspecified", and there is a mapping for the code, then return a span with the pronouns
+ if (!isPending && result && result !== "unspecified" && PronounMapping[result]) return (
+ <span
+ className={classes(styles.timestampInline, styles.timestamp)}
+ >• {PronounMapping[result]}</span>
+ );
+ // Otherwise, return null so nothing else is rendered
+ else return null;
+}
diff --git a/src/plugins/pronoundb/index.ts b/src/plugins/pronoundb/index.ts
new file mode 100644
index 0000000..bc31b48
--- /dev/null
+++ b/src/plugins/pronoundb/index.ts
@@ -0,0 +1,23 @@
+import definePlugin from "../../utils/types";
+import PronounComponent from "./PronounComponent";
+import { fetchPronouns } from "./utils";
+
+export default definePlugin({
+ name: "PronounDB",
+ authors: [{
+ name: "Tyman",
+ id: 487443883127472129n
+ }],
+ description: "Adds pronouns to user messages using pronoundb",
+ patches: [
+ {
+ find: "showCommunicationDisabledStyles",
+ replacement: {
+ match: /(?<=return\s+\w{1,3}\.createElement\(.+!\w{1,3}&&)(\w{1,3}.createElement\(.+?\{.+?\}\))/,
+ replace: "[$1, Vencord.Plugins.plugins.PronounDB.PronounComponent(e)]"
+ }
+ }
+ ],
+ // Re-export the component on the plugin object so it is easily accessible in patches
+ PronounComponent
+});
diff --git a/src/plugins/pronoundb/types.ts b/src/plugins/pronoundb/types.ts
new file mode 100644
index 0000000..98a0bfc
--- /dev/null
+++ b/src/plugins/pronoundb/types.ts
@@ -0,0 +1,29 @@
+export interface PronounsResponse {
+ [id: string]: PronounCode;
+}
+
+export type PronounCode = keyof typeof PronounMapping;
+
+export const PronounMapping = {
+ unspecified: "Unspecified",
+ hh: "He/Him",
+ hi: "He/It",
+ hs: "He/She",
+ ht: "He/They",
+ ih: "It/Him",
+ ii: "It/Its",
+ is: "It/She",
+ it: "It/They",
+ shh: "She/He",
+ sh: "She/Her",
+ si: "She/It",
+ st: "She/They",
+ th: "They/He",
+ ti: "They/It",
+ ts: "They/She",
+ tt: "They/Them",
+ any: "Any pronouns",
+ other: "Other pronouns",
+ ask: "Ask me my pronouns",
+ avoid: "Avoid pronouns, use my name"
+} as const;
diff --git a/src/plugins/pronoundb/utils.ts b/src/plugins/pronoundb/utils.ts
new file mode 100644
index 0000000..9d3c076
--- /dev/null
+++ b/src/plugins/pronoundb/utils.ts
@@ -0,0 +1,59 @@
+import { debounce } from "../../utils";
+import { PronounCode, PronounsResponse } from "./types";
+
+// A map of cached pronouns so the same request isn't sent twice
+const cache: Record<string, PronounCode> = {};
+// A map of ids and callbacks that should be triggered on fetch
+const requestQueue: Record<string, ((pronouns: PronounCode) => void)[]> = {};
+
+// Executes all queued requests and calls their callbacks
+const bulkFetch = debounce(async () => {
+ const ids = Object.keys(requestQueue);
+ const pronouns = await bulkFetchPronouns(ids);
+ for (const id of ids) {
+ // Call all callbacks for the id
+ requestQueue[id].forEach(c => c(pronouns[id]));
+ delete requestQueue[id];
+ }
+});
+
+// Fetches the pronouns for one id, returning a promise that resolves if it was cached, or once the request is completed
+export function fetchPronouns(id: string): Promise<PronounCode> {
+ return new Promise(res => {
+ // If cached, return the cached pronouns
+ if (id in cache) res(cache[id]);
+ // If there is already a request added, then just add this callback to it
+ else if (id in requestQueue) requestQueue[id].push(res);
+ // If not already added, then add it and call the debounced function to make sure the request gets executed
+ else {
+ requestQueue[id] = [res];
+ bulkFetch();
+ }
+ });
+}
+
+async function bulkFetchPronouns(ids: string[]): Promise<PronounsResponse> {
+ const params = new URLSearchParams();
+ params.append("platform", "discord");
+ params.append("ids", ids.join(","));
+
+ try {
+ const req = await fetch("https://pronoundb.org/api/v1/lookup-bulk?" + params, {
+ method: "GET",
+ headers: {
+ "Accept": "application/json"
+ }
+ });
+ return await req.json()
+ .then((res: PronounsResponse) => {
+ Object.assign(cache, res);
+ return res;
+ });
+ } catch (e) {
+ // If the request errors, treat it as if no pronouns were found for all ids, and log it
+ console.error("PronounDB fetching failed: ", e);
+ const dummyPronouns = Object.fromEntries(ids.map(id => [id, "unspecified"] as const));
+ Object.assign(cache, dummyPronouns);
+ return dummyPronouns;
+ }
+}
diff --git a/src/utils/misc.tsx b/src/utils/misc.tsx
index f6ea36c..66ea202 100644
--- a/src/utils/misc.tsx
+++ b/src/utils/misc.tsx
@@ -30,10 +30,11 @@ export function lazyWebpack<T = any>(filter: FilterFn): T {
*/
export function useAwaiter<T>(factory: () => Promise<T>): [T | null, any, boolean];
export function useAwaiter<T>(factory: () => Promise<T>, fallbackValue: T): [T, any, boolean];
-export function useAwaiter<T>(factory: () => Promise<T>, fallbackValue: T | null = null): [T | null, any, boolean] {
+export function useAwaiter<T>(factory: () => Promise<T>, fallbackValue: null, onError: (e: unknown) => unknown): [T, any, boolean];
+export function useAwaiter<T>(factory: () => Promise<T>, fallbackValue: T | null = null, onError?: (e: unknown) => unknown): [T | null, any, boolean] {
const [state, setState] = React.useState({
value: fallbackValue,
- error: null as any,
+ error: null,
pending: true
});
@@ -41,7 +42,7 @@ export function useAwaiter<T>(factory: () => Promise<T>, fallbackValue: T | null
let isAlive = true;
factory()
.then(value => isAlive && setState({ value, error: null, pending: false }))
- .catch(error => isAlive && setState({ value: null, error, pending: false }));
+ .catch(error => isAlive && (setState({ value: null, error, pending: false }), onError?.(error)));
return () => void (isAlive = false);
}, []);