Plugin

OpenInApp

Open links in their respective apps instead of your browser

Utility
index.ts
Download

Source

src/plugins/openInApp/index.ts
1import { definePluginSettings } from "@api/Settings";
2import { Devs } from "@utils/constants";
3import definePlugin, { OptionType, PluginNative, SettingsDefinition } from "@utils/types";
4import { showToast, Toasts } from "@webpack/common";
5import type { MouseEvent } from "react";
6
7interface URLReplacementRule {
8 match: RegExp;
9 replace: (...matches: string[]) => string;
10 displayName?: string;
11 description: string;
12 shortlinkMatch?: RegExp;
13 accountViewReplace?: (userId: string) => string;
14}
15
16// Do not forget to add protocols to the ALLOWED_PROTOCOLS constant
17const UrlReplacementRules: Record<string, URLReplacementRule> = {
18 spotify: {
19 match: /^https:\/\/open\.spotify\.com\/(?:intl-[a-z]{2}\/)?(track|album|artist|playlist|user|episode|prerelease)\/(.+)(?:\?.+?)?$/,
20 replace: (_, type, id) => `spotify:class="ts-cmt">//${type}/${id}`,
21 description: "Open Spotify links in the Spotify app",
22 shortlinkMatch: /^https:\/\/spotify\.link\/.+$/,
23 accountViewReplace: userId => `spotify:user:${userId}`,
24 },
25 steam: {
26 match: /^https:\/\/(steamcommunity\.com|(?:help|store)\.steampowered\.com)\/.+$/,
27 replace: match => `steam:class="ts-cmt">//openurl/${match}`,
28 description: "Open Steam links in the Steam app",
29 shortlinkMatch: /^https:\/\/s.team\/.+$/,
30 accountViewReplace: userId => `steam:class="ts-cmt">//openurl/https://steamcommunity.com/profiles/${userId}`,
31 },
32 epic: {
33 match: /^https:\/\/store\.epicgames\.com\/(.+)$/,
34 replace: (_, id) => `com.epicgames.launcher:class="ts-cmt">//store/${id}`,
35 description: "Open Epic Games links in the Epic Games Launcher",
36 },
37 tidal: {
38 match: /^https:\/\/(?:listen\.)?tidal\.com\/(?:browse\/)?(track|album|artist|playlist|user|video|mix)\/([a-f0-9-]+).*/,
39 replace: (_, type, id) => `tidal:class="ts-cmt">//${type}/${id}`,
40 description: "Open Tidal links in the Tidal app",
41 },
42 itunes: {
43 match: /^https:\/\/(?:geo\.)?music\.apple\.com\/([a-z]{2}\/)?(album|artist|playlist|song|curator)\/([^/?#]+)\/?([^/?#]+)?(?:\?.*)?(?:#.*)?$/,
44 replace: (_, lang, type, name, id) => id ? `itunes:class="ts-cmt">//music.apple.com/us/${type}/${name}/${id}` : `itunes://music.apple.com/us/${type}/${name}`,
45 displayName: "iTunes",
46 description: "Open Apple Music links in the iTunes app"
47 },
48};
49
50const pluginSettings = definePluginSettings(
51 Object.entries(UrlReplacementRules).reduce((acc, [key, rule]) => {
52 acc[key] = {
53 type: OptionType.BOOLEAN,
54 displayName: rule.displayName,
55 description: rule.description,
56 default: true,
57 };
58 return acc;
59 }, {} as SettingsDefinition)
60);
61
62
63const Native = VencordNative.pluginHelpers.OpenInApp as PluginNative<typeof import("./native")>;
64
65export default definePlugin({
66 name: "OpenInApp",
67 description: "Open links in their respective apps instead of your browser",
68 tags: ["Utility"],
69 authors: [Devs.Ven, Devs.surgedevs],
70 settings: pluginSettings,
71
72 patches: [
73 {
74 find: "trackAnnouncementMessageLinkClicked({",
75 replacement: {
76 match: /function (\i\(\i,\i\)\{)(?=.{0,150}trusted:)/,
77 replace: "async function $1 if(await $self.handleLink(...arguments)) return;"
78 }
79 },
80 {
81 find: "no artist ids in metadata",
82 predicate: () => !IS_DISCORD_DESKTOP && pluginSettings.store.spotify,
83 replacement: [
84 {
85 match: /\i\.\i\.isProtocolRegistered\(\)/g,
86 replace: "true"
87 },
88 {
89 match: /\(0,\i\.isDesktop\)\(\)/,
90 replace: "true"
91 }
92 ]
93 },
94
95 // User Profile Modal & User Profile Modal v2
96 ...[".__invalid_connectedAccountOpenIconContainer", ".BLUESKY||"].map(find => ({
97 find,
98 replacement: {
99 match: /(?<=onClick:(\i)=>\{)(?=.{0,100}\.CONNECTED_ACCOUNT_VIEWED)(?<==(\i)\.metadata.+?)/,
100 replace: "if($self.handleAccountView($1,$2.type,$2.id)) return;"
101 }
102 }))
103 ],
104
105 async handleLink(data: { href: string; }, event?: MouseEvent) {
106 if (!data) return false;
107
108 let url = data.href;
109 if (!url) return false;
110
111 for (const [key, rule] of Object.entries(UrlReplacementRules)) {
112 if (!pluginSettings.store[key]) continue;
113
114 if (rule.shortlinkMatch?.test(url)) {
115 event?.preventDefault();
116 url = await Native.resolveRedirect(url);
117 }
118
119 if (rule.match.test(url)) {
120 showToast("Opened link in native app", Toasts.Type.SUCCESS);
121
122 const newUrl = url.replace(rule.match, rule.replace);
123 VencordNative.native.openExternal(newUrl);
124
125 event?.preventDefault();
126 return true;
127 }
128 }
129
130 // in case short url didn't end up being something we can handle
131 if (event?.defaultPrevented) {
132 window.open(url, "_blank");
133 return true;
134 }
135
136 return false;
137 },
138
139 handleAccountView(e: MouseEvent, platformType: string, userId: string) {
140 const rule = UrlReplacementRules[platformType];
141 if (rule?.accountViewReplace && pluginSettings.store[platformType]) {
142 VencordNative.native.openExternal(rule.accountViewReplace(userId));
143 e.preventDefault();
144 return true;
145 }
146 }
147});
148