Skip to content

Commit 01d9522

Browse files
committed
add voiceTextLink
1 parent 54f47f5 commit 01d9522

File tree

4 files changed

+202
-0
lines changed

4 files changed

+202
-0
lines changed

src/voiceTextLink/index.ts

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { ExtensionWebpackModule, Patch } from "@moonlight-mod/types";
2+
3+
export const patches: Patch[] = [
4+
{
5+
find: "Missing channel in Channel.renderSidebar",
6+
replace: [
7+
// point to linked channel
8+
{
9+
match: /render\(\){(?=let{channel:\i,)/,
10+
replacement: (orig: string) => `${orig}
11+
this.props._realChannel = this.props.channel;
12+
this.props.channel = this.props.voiceChannel ? require("voiceTextLink_logic").getChannel(this.props._realChannel) : this.props._realChannel;`
13+
},
14+
{
15+
match: /renderCall\(\){let{channel:(\i)}=this\.props;/,
16+
replacement: (orig, channel) => `${orig}
17+
${channel} = (this.props.voiceChannel && this.props.channelId === this.props.voiceChannel.id) ? this.props.voiceChannel : this.props.channel;`
18+
},
19+
20+
// make renderCall render text channels
21+
{
22+
match: /case (\i\.\i)\.PRIVATE_THREAD:(?=let \i=this\.props\.height-200;)/,
23+
replacement: (orig, ChannelTypes) => `${orig}case ${ChannelTypes}.GUILD_TEXT:`
24+
}
25+
]
26+
},
27+
28+
// sidebar
29+
{
30+
find: '"trackCallTileContextMenuImpression"',
31+
replace: {
32+
match: /{channel:(\i),(guild:\i,maxWidth:\i}\),\i&&\(0,\i\.jsx\)\(\i\.\i,{channel:)\i,/,
33+
replacement: (_, channel, body) =>
34+
`{channel:require("voiceTextLink_logic").getChannel(${channel}),${body}require("voiceTextLink_logic").getChannel(${channel}),`
35+
}
36+
}
37+
];
38+
39+
export const webpackModules: Record<string, ExtensionWebpackModule> = {
40+
logic: {
41+
dependencies: [{ ext: "common", id: "stores" }]
42+
},
43+
context: {
44+
dependencies: [
45+
{ id: "react" },
46+
{ ext: "contextMenu", id: "contextMenu" },
47+
{ ext: "common", id: "stores" },
48+
{ id: "discord/components/common/index" },
49+
{ id: "discord/Constants" }
50+
],
51+
entrypoint: true
52+
}
53+
};

src/voiceTextLink/manifest.json

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
{
2+
"$schema": "https://moonlight-mod.github.io/manifest.schema.json",
3+
"id": "voiceTextLink",
4+
"version": "1.0.0",
5+
"meta": {
6+
"name": "Voice Text Link",
7+
"tagline": "Link voice text chat to a normal text channel",
8+
"description": "Recommended to use with DM Call Layout for best results",
9+
"authors": ["Cynosphere"],
10+
"tags": ["qol", "voice", "chat"],
11+
"source": "https://github.com/Cynosphere/moonlight-extensions",
12+
"donate": "https://ko-fi.com/Cynosphere"
13+
},
14+
"settings": {
15+
"channelMap": {
16+
"type": "dictionary",
17+
"displayName": "Channel Map",
18+
"description": "Voice ID -> Channel ID (You might want to use the context menu on the voice channel for convenience)",
19+
"advice": "none"
20+
}
21+
},
22+
"dependencies": ["contextMenu", "common"],
23+
"apiLevel": 2
24+
}
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
import React from "@moonlight-mod/wp/react";
2+
import spacepack from "@moonlight-mod/wp/spacepack_spacepack";
3+
4+
import { addItem, MenuItem, MenuSeparator } from "@moonlight-mod/wp/contextMenu_contextMenu";
5+
import { ToastType, createToast, showToast } from "@moonlight-mod/wp/discord/components/common/index";
6+
import { ChannelTypes, Permissions } from "@moonlight-mod/wp/discord/Constants";
7+
import { GuildChannelStore, PermissionStore, ChannelListStore } from "@moonlight-mod/wp/common_stores";
8+
9+
const { has: hasFlag } = spacepack.require("discord/utils/BigFlagUtils");
10+
11+
const logger = moonlight.getLogger("Voice Text Link");
12+
13+
async function linkChannel(channelMap: Record<string, string>, voice: any, text: any) {
14+
try {
15+
channelMap[voice.id] = text.id;
16+
await moonlight.setConfigOption("voiceTextLink", "channelMap", channelMap);
17+
showToast(createToast(`Linked "${text.name}" to "${voice.name}"`, ToastType.SUCCESS));
18+
} catch (err) {
19+
logger.error(`Failed to link "${text.name}" (${text.id}) to "${voice.name}" (${voice.id}):`, err);
20+
showToast(createToast("Failed to link text channel", ToastType.FAILURE));
21+
}
22+
}
23+
24+
addItem(
25+
"channel-context",
26+
({ channel }: { channel: any }) => {
27+
const channelMap = moonlight.getConfigOption<Record<string, string>>("voiceTextLink", "channelMap") ?? {};
28+
const linked = channelMap[channel.id] != null;
29+
30+
const muted = ChannelListStore.getGuildWithoutChangingGuildActionRows(channel.guild_id)?.guildChannels
31+
?.mutedChannelIds;
32+
33+
const textChannels =
34+
GuildChannelStore.getChannels(channel.guild_id)
35+
?.SELECTABLE?.map((c: any) => c.channel)
36+
?.filter((c: any) => c.type === ChannelTypes.GUILD_TEXT)
37+
?.filter((c: any) => hasFlag(PermissionStore.getChannelPermissions(c), Permissions.SEND_MESSAGES))
38+
?.filter((c: any) => !muted.has(c.id) && !muted.has(c.parent_id)) ?? [];
39+
const likelyChannels = textChannels.filter((c: any) => c.name.includes("voice") || c.name.includes("vc-"));
40+
const likelyIds = likelyChannels.map((c: any) => c.id);
41+
const otherChannels = textChannels.filter((c: any) => !likelyIds.includes(c.id));
42+
43+
return channel.guild_id == null || channel.type !== ChannelTypes.GUILD_VOICE ? null : (
44+
<MenuItem
45+
id="voice-text-link"
46+
label={linked ? "Unlink Text Channel" : "Link To Text Channel"}
47+
action={
48+
linked
49+
? async () => {
50+
try {
51+
delete channelMap[channel.id];
52+
await moonlight.setConfigOption("voiceTextLink", "channelMap", channelMap);
53+
showToast(createToast(`Unlinked text channel from "${channel.name}"`, ToastType.SUCCESS));
54+
} catch (err) {
55+
logger.error(`Failed to unlink text channel from "${channel.name}" (${channel.id}):`, err);
56+
showToast(createToast("Failed to unlink text channel", ToastType.FAILURE));
57+
}
58+
}
59+
: () => {}
60+
}
61+
>
62+
{linked ? null : likelyChannels.length > 0 ? (
63+
<>
64+
{likelyChannels.map((c: any) => (
65+
<MenuItem
66+
id={c.id}
67+
label={`#${c.name}`}
68+
action={async () => {
69+
await linkChannel(channelMap, channel, c);
70+
}}
71+
/>
72+
))}
73+
<MenuSeparator />
74+
<MenuItem id="others" label="Other Channels">
75+
{otherChannels.map((c: any) => (
76+
<MenuItem
77+
id={c.id}
78+
label={`#${c.name}`}
79+
action={async () => {
80+
await linkChannel(channelMap, channel, c);
81+
}}
82+
/>
83+
))}
84+
</MenuItem>
85+
</>
86+
) : (
87+
textChannels.map((c: any) => (
88+
<MenuItem
89+
id={c.id}
90+
label={`#${c.name}`}
91+
action={async () => {
92+
await linkChannel(channelMap, channel, c);
93+
}}
94+
/>
95+
))
96+
)}
97+
</MenuItem>
98+
);
99+
},
100+
"mute-channel",
101+
true
102+
);
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { ChannelStore } from "@moonlight-mod/wp/common_stores";
2+
3+
export function getChannel(channel: any) {
4+
if (!channel) return channel;
5+
6+
const channelMap = moonlight.getConfigOption<Record<string, string>>("voiceTextLink", "channelMap") ?? {};
7+
const id = channelMap[channel.id];
8+
if (id) {
9+
const linkedChannel = ChannelStore.getChannel(id);
10+
if (linkedChannel) {
11+
return linkedChannel;
12+
} else {
13+
return channel;
14+
}
15+
} else {
16+
return channel;
17+
}
18+
}
19+
20+
export function getChannelId(id: string) {
21+
const channelMap = moonlight.getConfigOption<Record<string, string>>("voiceTextLink", "channelMap") ?? {};
22+
return channelMap[id] ?? id;
23+
}

0 commit comments

Comments
 (0)