diff --git a/package.json b/package.json index a81d176..085ff83 100644 --- a/package.json +++ b/package.json @@ -5,22 +5,22 @@ "lint": "eslint plugins" }, "devDependencies": { - "@rollup/plugin-commonjs": "^24.0.1", - "@rollup/plugin-node-resolve": "^15.0.1", - "@swc/core": "^1.3.42", - "@types/react": "^18.0.28", - "@types/react-native": "^0.70.12", - "@typescript-eslint/eslint-plugin": "^5.56.0", - "@typescript-eslint/parser": "^5.56.0", - "esbuild": "^0.17.12", - "eslint": "^8.36.0", + "@rollup/plugin-commonjs": "^24.1.0", + "@rollup/plugin-node-resolve": "^15.0.2", + "@swc/core": "^1.3.51", + "@types/react": "^18.0.37", + "@types/react-native": "^0.70.13", + "@typescript-eslint/eslint-plugin": "^5.59.0", + "@typescript-eslint/parser": "^5.59.0", + "esbuild": "^0.17.17", + "eslint": "^8.38.0", "eslint-plugin-react": "^7.32.2", - "rollup": "^3.20.1", + "rollup": "^3.20.6", "rollup-plugin-esbuild": "^5.0.0", "typescript": "^4.9.5", - "vendetta-types": "^2.4.0" + "vendetta-types": "^2.4.18" }, "dependencies": { - "@swc/helpers": "^0.4.14" + "@swc/helpers": "^0.5.0" } } diff --git a/plugins/RichPresence/src/ConfigEditor.tsx b/plugins/RichPresence/src/ConfigEditor.tsx new file mode 100644 index 0000000..1fac69d --- /dev/null +++ b/plugins/RichPresence/src/ConfigEditor.tsx @@ -0,0 +1,159 @@ +import { React, stylesheet } from "@vendetta/metro/common"; +import { storage } from "@vendetta/plugin"; +import { useProxy } from "@vendetta/storage"; +import { semanticColors } from "@vendetta/ui"; +import { Forms } from "@vendetta/ui/components"; + +import { ScrollView } from "react-native"; + +const { FormSection, FormInput, FormRow, FormSwitch, FormText } = Forms; + +const styles = stylesheet.createThemedStyleSheet({ + subText: { + fontSize: 14, + marginLeft: 16, + marginRight: 16, + color: semanticColors.TEXT_MUTED + }, + textLink: { + color: semanticColors.TEXT_LINK, + } +}); + +export default function ConfigEditor({ selection }: { selection: string }) { + const settings = useProxy(storage.selections[selection]) as Activity; + + return ( + + + settings.name = v} + /> + settings.application_id = v} + keyboardType="numeric" + /> + settings.type = v} + keyboardType="numeric" + /> + settings.details = v} + /> + settings.state = v} + /> + + + settings.assets.large_image = v} + /> + settings.assets.large_text = v} + /> + settings.assets.small_image = v} + /> + settings.assets.small_text = v} + /> + + + {"Image assets key can be either a Discord app asset's name or a URL to an image."} + + + settings.timestamps._enabled = v} + />} + /> + settings.timestamps.start = v} + keyboardType="numeric" + /> + settings.timestamps.end = v} + keyboardType="numeric" + /> + settings.timestamps.start = String(Date.now())} + trailing={FormRow.Arrow} + /> + + Leaving start timestamp blank will use the time the Discord started. + + + + settings.buttons[0].label = v} + /> + settings.buttons[0].url = v} + /> + settings.buttons[1].label = v} + /> + settings.buttons[1].url = v} + /> + + + ); +} diff --git a/plugins/RichPresence/src/Settings.tsx b/plugins/RichPresence/src/Settings.tsx index 8e703c1..c4cbc6d 100644 --- a/plugins/RichPresence/src/Settings.tsx +++ b/plugins/RichPresence/src/Settings.tsx @@ -1,159 +1,11 @@ -import { React, stylesheet } from "@vendetta/metro/common"; -import { storage } from "@vendetta/plugin"; -import { useProxy } from "@vendetta/storage"; -import { semanticColors } from "@vendetta/ui"; -import { Forms } from "@vendetta/ui/components"; +import plugin from "@vendetta/plugin"; +import ConfigEditor from "./ConfigEditor"; -import { ScrollView } from "react-native"; +const storage = plugin.storage as typeof plugin.storage & { + selected: string; + selections: Record; +}; -const { FormSection, FormInput, FormRow, FormSwitch, FormText } = Forms; - -const styles = stylesheet.createThemedStyleSheet({ - subText: { - fontSize: 14, - marginLeft: 16, - marginRight: 16, - color: semanticColors.TEXT_MUTED - }, - textLink: { - color: semanticColors.TEXT_LINK, - } -}); - -export default function Settings() { - const settings = useProxy(storage) as { [key: string]: any }; - - return ( - - - settings.app_name = v} - /> - settings.application_id = v} - keyboardType="numeric" - /> - settings.type = v} - keyboardType="numeric" - /> - settings.details = v} - /> - settings.state = v} - /> - - - settings.large_image = v} - /> - settings.large_image_text = v} - /> - settings.small_image = v} - /> - settings.small_image_text = v} - /> - - {"Image assets key can be either a Discord app asset's name or a URL to an image."} - - - - settings.enable_timestamps = v} - />} - /> - settings.start_timestamp = v} - keyboardType="numeric" - /> - settings.end_timestamp = v} - keyboardType="numeric" - /> - settings.start_timestamp = String(Date.now())} - trailing={FormRow.Arrow} - /> - - Leaving start timestamp blank will use the time the Discord started. - - - - settings.button1_text = v} - /> - settings.button1_URL = v} - /> - settings.button2_text = v} - /> - settings.button2_URL = v} - /> - - - ); -} +export default () => ( + +) diff --git a/plugins/RichPresence/src/index.ts b/plugins/RichPresence/src/index.ts index c33783b..9fe6eb2 100644 --- a/plugins/RichPresence/src/index.ts +++ b/plugins/RichPresence/src/index.ts @@ -1,10 +1,8 @@ -import { logger } from "@vendetta"; +import { logger, plugin } from "@vendetta"; import { findByProps } from "@vendetta/metro"; import { FluxDispatcher } from "@vendetta/metro/common"; -import { storage } from "@vendetta/plugin"; import Settings from "./Settings"; - -const AssetManager = findByProps("getAssetIds"); +import { cloneAndFilter } from "./utils"; enum ActivityTypes { PLAYING = 0, @@ -15,59 +13,52 @@ enum ActivityTypes { COMPETING = 5 } -// :nyaboom: -function constructActivity() { - const activity: Activity = { - name: storage.app_name || "Discord", - application_id: storage.application_id || "1054951789318909972", +const AssetManager = findByProps("getAssetIds"); +const pluginStartSince = Date.now(); + +const storage = plugin.storage as typeof plugin.storage & { + selected: string; + selections: Record; +}; + +if (!storage.selected) { + Object.assign(storage, { + selected: "default", + selections: { + default: createDefaultSelection() + } + }); +} + +function createDefaultSelection(): Activity { + return { + name: "Discord", + application_id: "1054951789318909972", flags: 0, - type: ActivityTypes.PLAYING, // PLAYING - state: storage.state, - details: storage.details + type: ActivityTypes.PLAYING, + timestamps: { + _enabled: false, + start: pluginStartSince + }, + assets: {}, + // @ts-ignore + buttons: [{}, {}] }; - - // Construct timestamps - if (storage.enable_timestamps) { - const timestamps = {} as any; - - timestamps.start = Number(storage.start_timestamp) || Date.now(); - if (!isNaN(storage.end_timestamp)) timestamps.end = Number(storage.end_timestamp); - - activity.timestamps = timestamps; - } - - // Construct assets - const assets = {} as ActivityAssets; - - if (storage.large_image) { - assets.large_image = storage.large_image; - if (storage.large_image_text) assets.large_text = storage.large_image_text; - } - - if (storage.small_image) { - assets.small_image = storage.small_image; - if (storage.small_image_text) assets.small_text = storage.small_image_text; - } - - if (Object.keys(assets).length) activity.assets = assets; - - // Construct buttons - if (storage.button1_text && storage.button1_URL) { - activity.buttons = [ - { label: storage.button1_text, url: storage.button1_URL } - ]; - } - - if (storage.button2_text && storage.button2_URL) { - (activity.buttons ??= []).push({ label: storage.button2_text, url: storage.button2_URL }); - } - - return activity; } async function sendRequest(activity: Activity | null): Promise { if (typeof activity !== "object") throw new Error("Invalid activity"); + const timestampEnabled = activity?.timestamps?._enabled; + activity = cloneAndFilter(activity); + + if (timestampEnabled) { + delete activity.timestamps._enabled; + activity.timestamps.start ||= pluginStartSince; + } else { + delete activity.timestamps; + } + if (activity?.assets) { const [largeImage, smallImage] = await AssetManager.getAssetIds(activity.application_id, [activity.assets.large_image, activity.assets.small_image]); activity.assets.large_image = largeImage; @@ -76,20 +67,12 @@ async function sendRequest(activity: Activity | null): Promise { if (activity?.buttons?.length) { activity.buttons = activity.buttons.filter(x => x.label && x.url); - Object.assign(activity, { + activity.buttons.length && Object.assign(activity, { metadata: { button_urls: activity.buttons.map(x => x.url) }, buttons: activity.buttons.map(x => x.label) }); } - // validate activity - for (const k in activity) { - if (activity[k] === undefined || activity[k] === null) delete activity[k]; - if (typeof activity[k].length === "number" && activity[k].length === 0) delete activity[k]; - if (typeof activity[k] === "object" && Object.keys(activity[k]).length === 0) delete activity[k]; - if (typeof activity[k] === "object" && activity[k].length === 0) delete activity[k]; - } - FluxDispatcher.dispatch({ type: "LOCAL_ACTIVITY_UPDATE", activity: activity @@ -101,7 +84,9 @@ async function sendRequest(activity: Activity | null): Promise { export default new class RichPresence { onLoad() { logger.log("Sending RPC request"); - sendRequest(constructActivity()).catch().then(x => { + + const currentActivity = storage.selections[storage.selected]; + sendRequest(currentActivity).catch().then(x => { logger.log("RPC request sent"); console.log(x); }); diff --git a/plugins/RichPresence/src/utils.ts b/plugins/RichPresence/src/utils.ts new file mode 100644 index 0000000..97e1c9a --- /dev/null +++ b/plugins/RichPresence/src/utils.ts @@ -0,0 +1,25 @@ +function isValid(val: any) { + if (val && typeof val.length === "number") return val.length > 0; + return val || val === 0 || val === false; +} + +// Deep clone and filter empty objects/values or keys starting with "_" +export function cloneAndFilter(obj: T): T { + const filter = (k: PropertyKey, v: any) => { + if (v === obj) return v; + if (typeof k === "string" && k.startsWith("_")) return undefined; + + switch (typeof v) { + case "object": { + if (Array.isArray(v) && v.length === 0) return undefined; + + const filteredEntries = Object.entries(v).filter((e) => isValid(e[1])); + return filteredEntries.length > 0 ? Object.fromEntries(filteredEntries) : undefined; + } + default: + return isValid(v) ? v : undefined; + } + }; + + return JSON.parse(JSON.stringify(obj, filter)); +} diff --git a/plugins/defs.d.ts b/plugins/defs.d.ts index 1587215..dc372e7 100644 --- a/plugins/defs.d.ts +++ b/plugins/defs.d.ts @@ -6,11 +6,12 @@ type Activity = { details?: string; state?: string; timestamps?: { - start: number; - end?: number; + _enabled: boolean; + start: number | string; + end?: number | string; }; - assets?: ActivityAssets; - buttons?: ActivityButton[]; + assets: ActivityAssets; + buttons: ActivityButton[]; }; type ActivityButton = {