From e213c562a5e31efe6df9b5b39ccfcf1cfb968044 Mon Sep 17 00:00:00 2001
From: kmmiio99o <164557248+kmmiio99o@users.noreply.github.com>
Date: Sat, 27 Sep 2025 15:36:59 +0200
Subject: [PATCH] Fix timestamp
---
.../BetterSpotifyIntegration/manifest.json | 6 +
.../src/components/ListenAlongButton.tsx | 39 ++++
.../BetterSpotifyIntegration/src/index.tsx | 33 ++++
.../src/utils/useListeningParty.ts | 15 ++
plugins/HideGiftButton/manifest.json | 7 +
plugins/HideGiftButton/src/index.ts | 19 ++
plugins/HoldGIFSend/manifest.json | 7 +
plugins/HoldGIFSend/src/index.ts | 13 ++
plugins/MoreConfirm/manifest.json | 7 +
plugins/MoreConfirm/src/index.ts | 88 +++++++++
plugins/NoAutoReplyMention/manifest.json | 7 +
plugins/NoAutoReplyMention/src/Settings.tsx | 110 +++++++++++
plugins/NoAutoReplyMention/src/index.ts | 26 +++
plugins/NoIdle/manifest.json | 7 +
plugins/NoIdle/src/index.ts | 8 +
plugins/PyonInstaller/manifest.json | 4 +
plugins/PyonInstaller/src/index.ts | 28 +++
plugins/RichPresence/manifest.json | 7 +
plugins/RichPresence/src/ConfigEditor.tsx | 174 ++++++++++++++++++
plugins/RichPresence/src/Settings.tsx | 40 ++++
plugins/RichPresence/src/index.ts | 121 ++++++++++++
plugins/RichPresence/src/utils.ts | 25 +++
22 files changed, 791 insertions(+)
create mode 100644 plugins/BetterSpotifyIntegration/manifest.json
create mode 100644 plugins/BetterSpotifyIntegration/src/components/ListenAlongButton.tsx
create mode 100644 plugins/BetterSpotifyIntegration/src/index.tsx
create mode 100644 plugins/BetterSpotifyIntegration/src/utils/useListeningParty.ts
create mode 100644 plugins/HideGiftButton/manifest.json
create mode 100644 plugins/HideGiftButton/src/index.ts
create mode 100644 plugins/HoldGIFSend/manifest.json
create mode 100644 plugins/HoldGIFSend/src/index.ts
create mode 100644 plugins/MoreConfirm/manifest.json
create mode 100644 plugins/MoreConfirm/src/index.ts
create mode 100644 plugins/NoAutoReplyMention/manifest.json
create mode 100644 plugins/NoAutoReplyMention/src/Settings.tsx
create mode 100644 plugins/NoAutoReplyMention/src/index.ts
create mode 100644 plugins/NoIdle/manifest.json
create mode 100644 plugins/NoIdle/src/index.ts
create mode 100644 plugins/PyonInstaller/manifest.json
create mode 100644 plugins/PyonInstaller/src/index.ts
create mode 100644 plugins/RichPresence/manifest.json
create mode 100644 plugins/RichPresence/src/ConfigEditor.tsx
create mode 100644 plugins/RichPresence/src/Settings.tsx
create mode 100644 plugins/RichPresence/src/index.ts
create mode 100644 plugins/RichPresence/src/utils.ts
diff --git a/plugins/BetterSpotifyIntegration/manifest.json b/plugins/BetterSpotifyIntegration/manifest.json
new file mode 100644
index 0000000..011e41c
--- /dev/null
+++ b/plugins/BetterSpotifyIntegration/manifest.json
@@ -0,0 +1,6 @@
+{
+ "name": "BetterSpotifyIntegration",
+ "description": "Shrimply better integration with Spotify",
+ "vendetta": { },
+ "main": "src/index.tsx"
+}
diff --git a/plugins/BetterSpotifyIntegration/src/components/ListenAlongButton.tsx b/plugins/BetterSpotifyIntegration/src/components/ListenAlongButton.tsx
new file mode 100644
index 0000000..f96b515
--- /dev/null
+++ b/plugins/BetterSpotifyIntegration/src/components/ListenAlongButton.tsx
@@ -0,0 +1,39 @@
+import { findByProps, findByStoreName } from "@vendetta/metro";
+import { getAssetIDByName } from "@vendetta/ui/assets";
+import { Image, View } from "react-native";
+
+const FluxUtils = findByProps("useStateFromStores");
+const SpotifyStore = findByStoreName("SpotifyStore");
+
+const Button = findByProps("Sizes", "Looks") as any;
+const SpotifySyncUtil = findByProps("play", "sync");
+
+type ListenAlongButtonProps = Partial<{
+ activity: any;
+ user: any;
+ isCurrentUser: boolean;
+ activityButtonColor: string;
+ [key: string]: any;
+}>
+
+export default function ListenAlongButton(props: ListenAlongButtonProps) {
+ const currentPartyId = FluxUtils.useStateFromStores([SpotifyStore], () => SpotifyStore.getActivity()?.party?.id);
+ const alreadySync = props.isCurrentUser || props.activity?.party?.id === currentPartyId;
+
+ return
+ ;
+}
diff --git a/plugins/BetterSpotifyIntegration/src/index.tsx b/plugins/BetterSpotifyIntegration/src/index.tsx
new file mode 100644
index 0000000..8f96ff4
--- /dev/null
+++ b/plugins/BetterSpotifyIntegration/src/index.tsx
@@ -0,0 +1,33 @@
+import { findByName } from "@vendetta/metro";
+import { i18n } from "@vendetta/metro/common";
+import { before } from "@vendetta/patcher";
+import { findInReactTree } from "@vendetta/utils";
+
+import ListenAlongButton from "./components/ListenAlongButton";
+
+let sectionPatch: () => void;
+
+export default {
+ onLoad() {
+ const getListeningHeader = () => i18n.Messages.USER_ACTIVITY_HEADER_LISTENING?.intlMessage?.format({ name: "Spotify" });
+
+ sectionPatch = before("default", findByName("UserProfileSection", false), ([props]) => {
+ if (props.title !== getListeningHeader()) return;
+
+ const spotifyElement = findInReactTree(props.children, e => e?.style?.padding === 0);
+ const actionsIndex = spotifyElement?.children?.findIndex?.(e => e?.type?.name === "Actions");
+
+ if (actionsIndex && actionsIndex !== -1) {
+ const actions = spotifyElement.children[actionsIndex];
+
+ spotifyElement.children[actionsIndex] = <>
+ {actions}
+ {}
+ >;
+ }
+ });
+ },
+ onUnload() {
+ sectionPatch?.();
+ }
+};
diff --git a/plugins/BetterSpotifyIntegration/src/utils/useListeningParty.ts b/plugins/BetterSpotifyIntegration/src/utils/useListeningParty.ts
new file mode 100644
index 0000000..72b4ed4
--- /dev/null
+++ b/plugins/BetterSpotifyIntegration/src/utils/useListeningParty.ts
@@ -0,0 +1,15 @@
+import { findByProps, findByStoreName } from "@vendetta/metro";
+
+const FluxUtils = findByProps("useStateFromStores");
+const SpotifyStore = findByStoreName("SpotifyStore");
+const GamePartyStore = findByStoreName("GamePartyStore");
+const UserStore = findByStoreName("UserStore");
+
+export default function useListeningParty() {
+ const partySet = FluxUtils.useStateFromStores([GamePartyStore, SpotifyStore], () => {
+ const party = GamePartyStore.getParty(SpotifyStore.getActivity()?.party?.id);
+ return party?.size > 1 ? party : null;
+ });
+
+ return partySet ? [...partySet].map(id => UserStore.getUser(id)) : [];
+}
diff --git a/plugins/HideGiftButton/manifest.json b/plugins/HideGiftButton/manifest.json
new file mode 100644
index 0000000..f41fa9f
--- /dev/null
+++ b/plugins/HideGiftButton/manifest.json
@@ -0,0 +1,7 @@
+{
+ "name": "HideGiftButton",
+ "description": "Hides the gift button in the chat input.",
+ "vendetta": {
+ "icon": "ic_gift_24px"
+ }
+}
diff --git a/plugins/HideGiftButton/src/index.ts b/plugins/HideGiftButton/src/index.ts
new file mode 100644
index 0000000..0b653cb
--- /dev/null
+++ b/plugins/HideGiftButton/src/index.ts
@@ -0,0 +1,19 @@
+import { findByName } from "@vendetta/metro";
+import { after } from "@vendetta/patcher";
+import { getAssetIDByName } from "@vendetta/ui/assets";
+import { findInReactTree } from "@vendetta/utils";
+
+const ChatInput = findByName("ChatInput");
+
+let unpatch: () => boolean;
+
+export default {
+ onLoad() {
+ const blockList = ["ic_thread_normal_24px", "ic_gift"].map(n => getAssetIDByName(n));
+ unpatch = after("render", ChatInput.prototype, (_, ret) => {
+ const input = findInReactTree(ret, t => "forceAnimateButtons" in t.props && t.props.actions);
+ input.props.actions = input.props.actions.filter(a => !blockList.includes(a.source));
+ });
+ },
+ onUnload: unpatch
+};
diff --git a/plugins/HoldGIFSend/manifest.json b/plugins/HoldGIFSend/manifest.json
new file mode 100644
index 0000000..19c34b4
--- /dev/null
+++ b/plugins/HoldGIFSend/manifest.json
@@ -0,0 +1,7 @@
+{
+ "name": "HoldGIFSend",
+ "description": "Hold GIF links to chat input before sending them.",
+ "vendetta": {
+ "icon": "gif"
+ }
+}
diff --git a/plugins/HoldGIFSend/src/index.ts b/plugins/HoldGIFSend/src/index.ts
new file mode 100644
index 0000000..9858c92
--- /dev/null
+++ b/plugins/HoldGIFSend/src/index.ts
@@ -0,0 +1,13 @@
+import { findByProps } from "@vendetta/metro";
+import { instead } from "@vendetta/patcher";
+
+const messageInputUtil = findByProps("selectGIF");
+
+export const onUnload = instead("selectGIF", messageInputUtil, (args) => {
+ // args[1] never exists on older version, so we use it as an indicator
+ // prev: selectGIF([GIF]) now: selectGIF(currentChannelId, [GIF]);
+ if (args[1]) args[1] = args[1].url;
+ else args[0] = args[0].url;
+
+ messageInputUtil.insertText(...args);
+});
diff --git a/plugins/MoreConfirm/manifest.json b/plugins/MoreConfirm/manifest.json
new file mode 100644
index 0000000..4950167
--- /dev/null
+++ b/plugins/MoreConfirm/manifest.json
@@ -0,0 +1,7 @@
+{
+ "name": "MoreConfirm",
+ "description": "Prompts confirmations before making irreversible actions.",
+ "vendetta": {
+ "icon": "check"
+ }
+}
diff --git a/plugins/MoreConfirm/src/index.ts b/plugins/MoreConfirm/src/index.ts
new file mode 100644
index 0000000..44b3097
--- /dev/null
+++ b/plugins/MoreConfirm/src/index.ts
@@ -0,0 +1,88 @@
+/*!
+ * https://github.com/amsyarasyiq/aliucordrn-plugins/blob/6c0be39ca673c6ebcf48c0f397246b76d578aa26/MoarConfirm/index.tsx
+ *
+ * Copyright (c) 2022 Amsyar Rasyiq
+ *
+ * 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 { logger, patcher } from "@vendetta";
+import { findByProps, findByStoreName } from "@vendetta/metro";
+
+const dialog = findByProps("show", "confirm", "close");
+const relationshipManager = findByProps("addRelationship");
+const callManager = findByProps("handleStartCall");
+const actionSheetManager = findByProps("hideActionSheet");
+const UserStore = findByStoreName("UserStore");
+
+export default new class MoreConfirm {
+ patches = [] as (() => void)[];
+
+ onLoad() {
+ this.patches.push(patcher.instead("handleStartCall", callManager, (args, orig) => {
+ const [{ rawRecipients: [{ username, discriminator }, multiple] }, isVideo] = args;
+ const action = isVideo ? "video call" : "call";
+
+ // if `multiple` is defined, it's probably a group call
+ dialog.show({
+ title: multiple ? `Start a group ${action}?` : `Start a ${action} with ${username}#${discriminator}?`,
+ body: multiple ? "Are you sure you want to start the group call?" : `Are you sure you want to ${action} ${username}#${discriminator}?`,
+ confirmText: "Yes",
+ cancelText: "Cancel",
+ confirmColor: "brand",
+ onConfirm: () => {
+ try {
+ orig(...args);
+ } catch (e) {
+ logger.error("Failed to start call", e);
+ }
+ },
+ });
+ }));
+
+ this.patches.push(patcher.instead("addRelationship", relationshipManager, (args, orig) => {
+ if (typeof args[0] !== "object" || !args[0].userId) return orig.apply(this, args);
+ const { username, discriminator } = UserStore.getUser(args[0].userId);
+
+ // This is hacky, but it *works*
+ const hideASInterval = setInterval(() => actionSheetManager.hideActionSheet(), 100);
+ setTimeout(() => clearInterval(hideASInterval), 3000);
+
+ const block = args[0].type === 2;
+ return new Promise(r => {
+ dialog.show({
+ title: `${block ? "Block" : "Friend"} ${username}#${discriminator}?`,
+ body: `Are you sure you want to ${block ? "block" : "friend"} ${username}#${discriminator}?`,
+ confirmText: "Yes",
+ cancelText: "Cancel",
+ confirmColor: "brand",
+ onConfirm: () => {
+ try {
+ r(orig.apply(this, args));
+ } catch (e) {
+ logger.error("Failed to add relationship", e);
+ } finally {
+ clearInterval(hideASInterval);
+ }
+ },
+ onCancel: () => void clearInterval(hideASInterval)
+ });
+ });
+ }));
+ }
+
+ onUnload() {
+ for (const unpatch of this.patches) unpatch();
+ }
+};
diff --git a/plugins/NoAutoReplyMention/manifest.json b/plugins/NoAutoReplyMention/manifest.json
new file mode 100644
index 0000000..5ecb191
--- /dev/null
+++ b/plugins/NoAutoReplyMention/manifest.json
@@ -0,0 +1,7 @@
+{
+ "name": "NoAutoReplyMention",
+ "description": "Turns off auto mention when replying to someone.",
+ "vendetta": {
+ "icon": "ic_notif_off"
+ }
+}
diff --git a/plugins/NoAutoReplyMention/src/Settings.tsx b/plugins/NoAutoReplyMention/src/Settings.tsx
new file mode 100644
index 0000000..4cb752d
--- /dev/null
+++ b/plugins/NoAutoReplyMention/src/Settings.tsx
@@ -0,0 +1,110 @@
+import { findByStoreName } from "@vendetta/metro";
+import { NavigationNative, React, stylesheet } from "@vendetta/metro/common";
+import { useProxy } from "@vendetta/storage";
+import { getAssetIDByName } from "@vendetta/ui/assets";
+import { Forms } from "@vendetta/ui/components";
+import { useEffect, useState } from "react";
+import { FlatList, Image, ScrollView, TouchableOpacity } from "react-native";
+import { SettingsSchema, settings } from ".";
+
+const UserStore = findByStoreName("UserStore");
+
+const { FormInput, FormDivider, FormText, FormIcon, FormRow, FormSection, FormSwitchRow } = Forms;
+
+const styles = stylesheet.createThemedStyleSheet({
+ avatar: {
+ height: 36,
+ width: 36,
+ borderRadius: 18,
+ },
+ button: {
+ marginRight: 12,
+ }
+});
+
+function AddButton({ callback }: { callback: () => void }) {
+ return
+ ADD
+ ;
+}
+
+function AddRow({ onFinish }: { onFinish: () => void }) {
+ const [value, setValue] = useState("");
+
+ const onPressCallback = () => {
+ if (!isNaN(parseInt(value)) && UserStore.getUser(value) && !settings.exempted.includes(value)) {
+ settings.exempted = [...settings.exempted, value];
+ setValue("");
+ }
+ onFinish();
+ };
+
+ return (
+ }
+ label={ setValue(value.replace(/[^0-9]/g, "").trim())}
+ returnKeyType="done"
+ />}
+ trailing={(
+
+
+
+ )}
+ />
+ );
+}
+
+export default function Settings() {
+ useProxy(settings) as SettingsSchema;
+
+ const [shouldShowAdd, setShouldShowAdd] = useState(false);
+ const navigation = NavigationNative.useNavigation();
+
+ useEffect(() => {
+ navigation.setOptions({
+ headerRight: () => setShouldShowAdd(true)} />
+ });
+ }, []);
+
+ return (
+
+ }
+ value={settings.isBlacklistMode}
+ onValueChange={(value: boolean) => settings.isBlacklistMode = value}
+ />
+
+ {settings.exempted?.length > 0 && item}
+ renderItem={({ item }) => {
+ const user = UserStore.getUser(item);
+ if (!user) throw new Error("User not found/is not cached");
+
+ return (}
+ trailing={ {
+ settings.exempted = settings.exempted.filter((id) => id !== item);
+ }}>
+
+ }
+ />);
+ }}
+ ItemSeparatorComponent={FormDivider}
+ />}
+ {shouldShowAdd && <>
+
+ setShouldShowAdd(false)} />
+ >}
+
+
+ );
+}
diff --git a/plugins/NoAutoReplyMention/src/index.ts b/plugins/NoAutoReplyMention/src/index.ts
new file mode 100644
index 0000000..d34a9c4
--- /dev/null
+++ b/plugins/NoAutoReplyMention/src/index.ts
@@ -0,0 +1,26 @@
+// export const onUnload = globalThis.vendetta.patcher.before("createPendingReply", globalThis.vendetta.metro.findByProps("createPendingReply"), ([arg]) => arg.shouldMention &&= false);
+
+import { patcher } from "@vendetta";
+import { findByProps } from "@vendetta/metro";
+import { storage } from "@vendetta/plugin";
+import Settings from "./Settings";
+
+export interface SettingsSchema {
+ isBlacklistMode: boolean;
+ exempted: string[];
+}
+
+storage.isBlacklistMode ??= false;
+storage.exempted ??= [];
+
+export const settings = storage as SettingsSchema;
+
+const ReplyManager = findByProps("createPendingReply");
+
+export default {
+ settings: Settings,
+ onUnload: patcher.before("createPendingReply", ReplyManager, ([arg]) => {
+ const isListed = storage.exempted.includes(arg.message?.author?.id);
+ arg.shouldMention &&= storage.isBlacklistMode ? !isListed : isListed;
+ })
+};
diff --git a/plugins/NoIdle/manifest.json b/plugins/NoIdle/manifest.json
new file mode 100644
index 0000000..df16ac2
--- /dev/null
+++ b/plugins/NoIdle/manifest.json
@@ -0,0 +1,7 @@
+{
+ "name": "NoIdle",
+ "description": "Prevent status from idling when Discord is in the background.",
+ "vendetta": {
+ "icon": "StatusIdle"
+ }
+}
diff --git a/plugins/NoIdle/src/index.ts b/plugins/NoIdle/src/index.ts
new file mode 100644
index 0000000..3609dc1
--- /dev/null
+++ b/plugins/NoIdle/src/index.ts
@@ -0,0 +1,8 @@
+import { patcher } from "@vendetta";
+import { FluxDispatcher } from "@vendetta/metro/common";
+
+export const onUnload = patcher.before("dispatch", FluxDispatcher, ([{ type }]) => {
+ if (type !== "IDLE") return;
+
+ return [{ type: "IDLE", idle: false }];
+});
diff --git a/plugins/PyonInstaller/manifest.json b/plugins/PyonInstaller/manifest.json
new file mode 100644
index 0000000..f03fb79
--- /dev/null
+++ b/plugins/PyonInstaller/manifest.json
@@ -0,0 +1,4 @@
+{
+ "name": "PyoncordInstaller",
+ "description": "installs pyoncord.exe (virus)"
+}
diff --git a/plugins/PyonInstaller/src/index.ts b/plugins/PyonInstaller/src/index.ts
new file mode 100644
index 0000000..1126d53
--- /dev/null
+++ b/plugins/PyonInstaller/src/index.ts
@@ -0,0 +1,28 @@
+import { plugin, plugins } from "@vendetta";
+import { config } from "@vendetta/loader";
+import { showConfirmationAlert } from "@vendetta/ui/alerts";
+
+export default {
+ onLoad() {
+ plugins.removePlugin(plugin.id);
+
+ setTimeout(() => showConfirmationAlert({
+ title: "Install Pyoncord?",
+ content: "Loader's settings will be overriden to load Pyoncord instead. You may not be able to revert to Vendetta whenever Pyoncord breaks!",
+ confirmText: "Continue and Restart",
+ cancelText: "Nevermind",
+ onConfirm: this.install
+ }), 300);
+ },
+
+ async install() {
+ if (config.customLoadUrl) {
+ config.customLoadUrl.enabled = true;
+ config.customLoadUrl.url = "https://raw.githubusercontent.com/pyoncord/pyoncord/builds/pyoncord.js";
+
+ setTimeout(globalThis.nativeModuleProxy.BundleUpdaterManager.reload, 100);
+ } else {
+ alert("not a proper vendetta loader!");
+ }
+ }
+};
diff --git a/plugins/RichPresence/manifest.json b/plugins/RichPresence/manifest.json
new file mode 100644
index 0000000..3a8b2b2
--- /dev/null
+++ b/plugins/RichPresence/manifest.json
@@ -0,0 +1,7 @@
+{
+ "name": "RichPresence",
+ "description": "Sets Discord Rich Presence for your profile.",
+ "vendetta": {
+ "icon": "games"
+ }
+}
diff --git a/plugins/RichPresence/src/ConfigEditor.tsx b/plugins/RichPresence/src/ConfigEditor.tsx
new file mode 100644
index 0000000..8345f55
--- /dev/null
+++ b/plugins/RichPresence/src/ConfigEditor.tsx
@@ -0,0 +1,174 @@
+
+import { findByProps, findByStoreName } from "@vendetta/metro";
+import { React, stylesheet } from "@vendetta/metro/common";
+import { storage } from "@vendetta/plugin";
+import { useProxy } from "@vendetta/storage";
+import { semanticColors } from "@vendetta/ui";
+import { Button, Forms } from "@vendetta/ui/components";
+
+import { ScrollView } from "react-native";
+
+const { FormSection, FormInput, FormRow, FormSwitch, FormText } = Forms;
+
+const UserStore = findByStoreName("UserStore");
+const profiles = findByProps("showUserProfile");
+
+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 (
+
+
+ );
+}
diff --git a/plugins/RichPresence/src/Settings.tsx b/plugins/RichPresence/src/Settings.tsx
new file mode 100644
index 0000000..a151633
--- /dev/null
+++ b/plugins/RichPresence/src/Settings.tsx
@@ -0,0 +1,40 @@
+import RPInstance from ".";
+
+import { NavigationNative } from "@vendetta/metro/common";
+import plugin from "@vendetta/plugin";
+import { Forms } from "@vendetta/ui/components";
+import ConfigEditor from "./ConfigEditor";
+
+import { useEffect } from "react";
+import { TouchableOpacity } from "react-native";
+
+const { FormText } = Forms;
+const storage = plugin.storage as typeof plugin.storage & {
+ selected: string;
+ selections: Record;
+};
+
+function UpdateButton() {
+ async function onPressCallback() {
+ RPInstance.onUnload();
+ RPInstance.onLoad();
+ }
+
+ return
+ UPDATE
+ ;
+}
+
+export default function Settings() {
+ const navigation = NavigationNative.useNavigation();
+
+ useEffect(() => {
+ navigation.setOptions({
+ headerRight: UpdateButton
+ });
+ }, []);
+
+ return (
+
+ );
+}
diff --git a/plugins/RichPresence/src/index.ts b/plugins/RichPresence/src/index.ts
new file mode 100644
index 0000000..1f8cef6
--- /dev/null
+++ b/plugins/RichPresence/src/index.ts
@@ -0,0 +1,121 @@
+import { logger, plugin } from "@vendetta";
+import { findByProps } from "@vendetta/metro";
+import { FluxDispatcher } from "@vendetta/metro/common";
+import Settings from "./Settings";
+import { cloneAndFilter } from "./utils";
+
+enum ActivityTypes {
+ PLAYING = 0,
+ STREAMING = 1,
+ LISTENING = 2,
+ WATCHING = 3,
+ // CUSTOM = 4,
+ COMPETING = 5
+}
+
+const assetManager = findByProps("getAssetIds");
+
+const pluginStartSince = Date.now();
+
+const storage = plugin.storage as typeof plugin.storage & {
+ selected: string;
+ selections: Record;
+};
+
+if (typeof storage.selected?.length !== "number") {
+ Object.assign(storage, {
+ selected: "default",
+ selections: {
+ default: createDefaultSelection()
+ }
+ });
+}
+
+function createDefaultSelection(): Activity {
+ return {
+ name: "Discord",
+ application_id: "1054951789318909972",
+ flags: 0,
+ type: ActivityTypes.PLAYING,
+ timestamps: {
+ _enabled: false,
+ start: pluginStartSince
+ },
+ assets: {},
+ // @ts-ignore
+ buttons: [{}, {}]
+ };
+}
+
+async function sendRequest(activity: Activity | null): Promise<{ [K in keyof Activity]: any } & Record> {
+ if (typeof activity !== "object") throw new Error("Invalid activity");
+
+ if (activity) {
+ const timestampEnabled = activity?.timestamps?._enabled;
+ activity = cloneAndFilter(activity);
+
+ if (timestampEnabled) {
+ activity.timestamps.start ||= pluginStartSince;
+ } else {
+ delete activity.timestamps;
+ }
+
+ if (activity.assets) {
+ const args = [activity.application_id, [activity.assets.large_image, activity.assets.small_image]];
+
+ let assetIds = assetManager.getAssetIds(...args);
+ if (!assetIds.length) assetIds = await assetManager.fetchAssetIds(...args);
+
+ activity.assets.large_image = assetIds[0];
+ activity.assets.small_image = assetIds[1];
+ }
+
+ if (activity.buttons?.length) {
+ activity.buttons = activity.buttons.filter(x => x.label && x.url);
+ activity.buttons.length
+ ? Object.assign(activity, {
+ metadata: { button_urls: activity.buttons.map(x => x.url) },
+ buttons: activity.buttons.map(x => x.label)
+ })
+ : delete activity.buttons;
+ } else {
+ delete activity.buttons;
+ }
+ }
+
+ FluxDispatcher.dispatch({
+ type: "LOCAL_ACTIVITY_UPDATE",
+ activity: activity,
+ pid: 1608,
+ socketId: "RichPresence@Vendetta"
+ });
+
+ return activity;
+}
+
+export default new class RichPresence {
+ onLoad() {
+ logger.log("Sending RPC request");
+
+ const currentActivity = storage.selections[storage.selected];
+ if (!currentActivity) {
+ throw new Error("Selected activity does not exist");
+ }
+
+ sendRequest(currentActivity)
+ .then(x => {
+ logger.log("RPC request sent");
+ logger.log(x);
+ }).catch(e => {
+ logger.error("An error occured while sending RPC request :(");
+ logger.error(e?.stack ?? e);
+ });
+ }
+
+ onUnload() {
+ logger.log("Stopping RPC request");
+ sendRequest(null);
+ }
+
+ settings = Settings;
+};
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));
+}