Compare commits

...

47 commits

Author SHA1 Message Date
camila
51b623e376
Merge e7dd0638a8 into b875ebf92d 2024-09-19 12:14:22 +00:00
Nuckyz
b875ebf92d
UserVoiceShow: Fix setting name
Some checks are pending
Sync to Codeberg / codeberg (push) Waiting to run
test / test (push) Waiting to run
2024-09-19 08:48:56 -03:00
camila
e7dd0638a8
Merge branch 'dev' into prbranch 2024-06-25 14:57:42 -05:00
camila
899bfc6a23
Merge pull request #2 from x3rt/author
Add x3rt to authors
2024-06-24 13:45:45 -05:00
x3rt
bfbb1f1aad Add x3rt to authors 2024-06-24 10:35:24 -06:00
camila314
a572da18f5 Ignore case toggle 2024-06-24 02:46:52 -05:00
camila314
bac0ac1b6b do good changes 2024-06-24 01:56:14 -05:00
camila
31b18b221d
Merge branch 'dev' into prbranch 2024-06-23 20:20:24 -05:00
camila314
b33e203775 Merge remote-tracking branch 'refs/remotes/origin/main' 2024-06-23 14:33:16 -05:00
camila314
4e7e09f4ff review changes 2024-06-23 14:33:11 -05:00
camila
ae5c1014cb
Merge branch 'dev' into main 2024-06-23 14:13:52 -05:00
camila314
5c843bdbb7 make work again 2024-06-21 14:07:01 -05:00
camila
d3a6f71b1d
Merge branch 'Vendicated:main' into main 2024-06-19 19:05:54 -05:00
camila314
8b32786678 remove useless comment 2024-06-18 00:03:59 -05:00
camila314
f496724422 remove console log 2024-06-17 23:59:29 -05:00
camila314
91f6541d8a Update 2024-06-17 23:42:50 -05:00
camila314
b9b037bdb9 merge 2024-06-17 23:39:49 -05:00
camila
7934318967
Merge branch 'Vendicated:main' into main 2024-04-19 00:54:03 -05:00
camila
37c1172311
Merge branch 'Vendicated:main' into main 2024-04-03 11:24:26 -05:00
camila
aeb3096e77
Merge branch 'Vendicated:main' into main 2024-03-24 19:55:20 -05:00
camila
57d22f9b6e
Merge branch 'Vendicated:main' into main 2024-03-08 15:28:57 -06:00
camila
e157097c7e
Merge branch 'Vendicated:main' into main 2024-01-31 10:13:13 -06:00
camila
db2b08d2fb
Merge branch 'Vendicated:main' into main 2024-01-21 09:31:44 -06:00
camila314
0e9c1ebea8 embed support 2024-01-04 12:54:08 -06:00
camila
79118251c8
Merge branch 'Vendicated:main' into main 2023-12-31 12:32:34 -06:00
camila
1d3579d788
Merge branch 'Vendicated:main' into main 2023-12-15 12:26:21 -06:00
camila314
b5804d548c someone made destructuring with strings not work lol 2023-11-28 02:09:34 -06:00
camila314
c7ed529a6c add ignore bots 2023-11-26 08:32:37 -06:00
camila314
42b09a3d0b bring back destructuring 2023-11-25 17:18:42 -06:00
camila
2870bd7003
Merge branch 'Vendicated:main' into main 2023-11-25 17:16:00 -06:00
camila314
59902a3729 no more mapMangledModule 2023-11-25 17:13:19 -06:00
camila314
5b647c307d fix small crash bug 2023-11-25 11:48:10 -06:00
camila314
5240085285 remove default from find/replace 2023-11-21 06:47:18 -06:00
camila314
5648b16107 brand new inbox feature 2023-11-20 21:21:23 -06:00
camila314
808c664d93 no longer crashes on invalid regex 2023-11-19 18:25:11 -06:00
camila
4b4ad65c6e
Merge branch 'main' into main 2023-11-17 01:13:27 -06:00
camila
3cfe1dd9cc
Update src/plugins/keywordNotify/index.tsx
Co-authored-by: AutumnVN <autumnvnchino@gmail.com>
2023-11-17 00:35:11 -06:00
camila314
311c4bee3c my bad. this time i actually fixed it 2023-11-16 11:14:39 -06:00
camila314
8ab224562a fix newly generated rule causing mass pings 2023-11-15 23:06:49 -06:00
camila314
c0fb89496f KeywordNotify now highlights messages 2023-11-11 22:36:37 -06:00
camila
99036977bb
Update src/plugins/keywordNotify/index.tsx
Co-authored-by: AutumnVN <autumnvnchino@gmail.com>
2023-11-11 20:36:19 -06:00
camila314
473d147854 Merge remote-tracking branch 'refs/remotes/origin/main' 2023-11-11 17:54:28 -06:00
camila314
defa669d0b i have to have a readme 2023-11-11 17:54:21 -06:00
camila
6d4d38c2d5
Update src/plugins/keywordNotify/index.tsx
Co-authored-by: ant0n <antonickadoo@gmail.com>
2023-11-11 17:50:25 -06:00
camila
2792158eb5
Merge branch 'main' into main 2023-11-11 10:15:16 -06:00
camila314
c8df5e044b update keyword notify 2023-11-07 18:22:33 -06:00
camila314
32062fbc05 Keyword Notify 2023-11-05 20:17:58 -06:00
7 changed files with 495 additions and 7 deletions

View file

@ -0,0 +1,3 @@
# KeywordNotify
Allows for custom regex-defined keywords to notify the user exactly how a ping would. Adds a custom inbox for viewing keywords next to the mentions.

View file

@ -0,0 +1,456 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2023 Vendicated, camila314, and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import "./style.css";
import definePlugin, { OptionType } from "@utils/types";
import { Button, ChannelStore, Forms, Select, Switch, SelectedChannelStore, TabBar, TextInput, UserStore, UserUtils, useState } from "@webpack/common";
import { classNameFactory } from "@api/Styles";
import { DataStore } from "@api/index";
import { definePluginSettings } from "@api/Settings";
import { DeleteIcon } from "@components/Icons";
import { Devs } from "@utils/constants";
import { findByCodeLazy, findByPropsLazy } from "@webpack";
import { Flex } from "@components/Flex";
import { Margins } from "@utils/margins";
import { Message, User } from "discord-types/general/index.js";
import { useForceUpdater } from "@utils/react";
type KeywordEntry = { regex: string, listIds: Array<string>, listType: ListType, ignoreCase: boolean };
let keywordEntries: Array<KeywordEntry> = [];
let currentUser: User;
let keywordLog: Array<any> = [];
const MenuHeader = findByCodeLazy(".sv)()?(0,");
const Popout = findByCodeLazy(".loadingMore&&null==");
const recentMentionsPopoutClass = findByPropsLazy("recentMentionsPopout");
const createMessageRecord = findByCodeLazy("THREAD_CREATED?[]:(0,");
const KEYWORD_ENTRIES_KEY = "KeywordNotify_keywordEntries";
const KEYWORD_LOG_KEY = "KeywordNotify_log";
const cl = classNameFactory("vc-keywordnotify-");
async function addKeywordEntry(forceUpdate: () => void) {
keywordEntries.push({ regex: "", listIds: [], listType: ListType.BlackList, ignoreCase: false });
await DataStore.set(KEYWORD_ENTRIES_KEY, keywordEntries);
forceUpdate();
}
async function removeKeywordEntry(idx: number, forceUpdate: () => void) {
keywordEntries.splice(idx, 1);
await DataStore.set(KEYWORD_ENTRIES_KEY, keywordEntries);
forceUpdate();
}
function safeMatchesRegex(str: string, regex: string, flags: string) {
try {
return str.match(new RegExp(regex, flags));
} catch {
return false;
}
}
enum ListType {
BlackList = "BlackList",
Whitelist = "Whitelist"
}
function highlightKeywords(str: string, entries: Array<KeywordEntry>) {
let regexes: Array<RegExp>;
try {
regexes = entries.map(e => new RegExp(e.regex, "g" + (e.ignoreCase ? "i" : "")));
} catch (err) {
return [str];
}
const matches = regexes.map(r => str.match(r)).flat().filter(e => e != null);
if (matches.length == 0) {
return [str];
}
const idx = str.indexOf(matches[0]);
return [
<span>{str.substring(0, idx)}</span>,
<span className="highlight">{matches[0]}</span>,
<span>{str.substring(idx + matches[0].length)}</span>
];
}
function Collapsible({ title, children }) {
const [isOpen, setIsOpen] = useState(false);
return (
<div>
<Button
onClick={() => setIsOpen(!isOpen)}
look={Button.Looks.BLANK}
size={Button.Sizes.ICON}
className={cl("collapsible")}>
<div style={{ display: "flex", alignItems: "center" }}>
<div style={{ marginLeft: "auto", color: "var(--text-muted)", paddingRight: "5px" }}>{isOpen ? "▼" : "▶"}</div>
<Forms.FormTitle tag="h4">{title}</Forms.FormTitle>
</div>
</Button>
{isOpen && children}
</div>
);
}
function ListedIds({ listIds, setListIds }) {
const update = useForceUpdater();
const [values] = useState(listIds);
async function onChange(e: string, index: number) {
values[index] = e;
setListIds(values);
update();
}
const elements = values.map((currentValue: string, index: number) => {
return (
<Flex flexDirection="row" style={{ marginBottom: "5px" }}>
<div style={{ flexGrow: 1 }}>
<TextInput
placeholder="ID"
spellCheck={false}
value={currentValue}
onChange={e => onChange(e, index)}
/>
</div>
<Button
onClick={() => {
values.splice(index, 1);
setListIds(values);
update();
}}
look={Button.Looks.BLANK}
size={Button.Sizes.ICON}
className={cl("delete")}>
<DeleteIcon/>
</Button>
</Flex>
);
});
return (
<>
{elements}
</>
);
}
function ListTypeSelector({ listType, setListType }) {
return (
<Select
options={[
{ label: "Whitelist", value: ListType.Whitelist },
{ label: "Blacklist", value: ListType.BlackList }
]}
placeholder={"Select a list type"}
isSelected={v => v === listType}
closeOnSelect={true}
value={listType}
select={setListType}
serialize={v => v}
/>
);
}
function KeywordEntries() {
const update = useForceUpdater();
const [values] = useState(keywordEntries);
async function setRegex(index: number, value: string) {
keywordEntries[index].regex = value;
await DataStore.set(KEYWORD_ENTRIES_KEY, keywordEntries);
update();
}
async function setListType(index: number, value: ListType) {
keywordEntries[index].listType = value;
await DataStore.set(KEYWORD_ENTRIES_KEY, keywordEntries);
update();
}
async function setListIds(index: number, value: Array<string>) {
keywordEntries[index].listIds = value ?? [];
await DataStore.set(KEYWORD_ENTRIES_KEY, keywordEntries);
update();
}
const elements = keywordEntries.map((entry, i) => {
return (
<>
<Collapsible title={`Keyword Entry ${i + 1}`}>
<Flex flexDirection="row">
<div style={{ flexGrow: 1 }}>
<TextInput
placeholder="example|regex"
spellCheck={false}
value={values[i].regex}
onChange={e => setRegex(i, e)}
/>
</div>
<Button
onClick={() => removeKeywordEntry(i, update)}
look={Button.Looks.BLANK}
size={Button.Sizes.ICON}
className={cl("delete")}>
<DeleteIcon/>
</Button>
</Flex>
<Switch
value={values[i].ignoreCase}
onChange={() => {
values[i].ignoreCase = !values[i].ignoreCase;
update();
}}
style={{ marginTop: "0.5em", marginRight: "40px" }}
>
Ignore Case
</Switch>
<Forms.FormDivider className={[Margins.top8, Margins.bottom8].join(" ") }/>
<Forms.FormTitle tag="h5">Whitelist/Blacklist</Forms.FormTitle>
<Flex flexDirection="row">
<div style={{ flexGrow: 1 }}>
<ListedIds listIds={values[i].listIds} setListIds={e => setListIds(i, e)}/>
</div>
</Flex>
<div className={[Margins.top8, Margins.bottom8].join(" ") }/>
<Flex flexDirection="row">
<Button onClick={() => {
values[i].listIds.push("");
update();
}}>Add ID</Button>
<div style={{ flexGrow: 1 }}>
<ListTypeSelector listType={values[i].listType} setListType={e => setListType(i, e)}/>
</div>
</Flex>
</Collapsible>
</>
);
});
return (
<>
{elements}
<div><Button onClick={() => addKeywordEntry(update)}>Add Keyword Entry</Button></div>
</>
);
}
const settings = definePluginSettings({
ignoreBots: {
type: OptionType.BOOLEAN,
description: "Ignore messages from bots",
default: true
},
keywords: {
type: OptionType.COMPONENT,
component: () => <KeywordEntries/>
}
});
export default definePlugin({
name: "KeywordNotify",
authors: [Devs.camila314, Devs.x3rt],
description: "Sends a notification if a given message matches certain keywords or regexes",
settings,
patches: [
{
find: "Dispatch.dispatch(...) called without an action type",
replacement: {
match: /}_dispatch\((\i),\i\){/,
replace: "$&$1=$self.modify($1);"
}
},
{
find: "Messages.UNREADS_TAB_LABEL}",
replacement: {
match: /\i\?\(0,\i\.jsxs\)\(\i\.TabBar\.Item/,
replace: "$self.keywordTabBar(),$&"
}
},
{
find: "location:\"RecentsPopout\"",
replacement: {
match: /:(\i)===\i\.\i\.MENTIONS\?\(0,.+?setTab:(\i),onJump:(\i),badgeState:\i,closePopout:(\i)/,
replace: ": $1 === 5 ? $self.tryKeywordMenu($2, $3, $4) $&"
}
},
{
find: ".guildFilter:null",
replacement: {
match: /function (\i)\(\i\){let{message:\i,gotoMessage/,
replace: "$self.renderMsg = $1; $&"
}
},
{
find: ".guildFilter:null",
replacement: {
match: /onClick:\(\)=>(\i\.\i\.deleteRecentMention\((\i)\.id\))/,
replace: "onClick: () => $2._keyword ? $self.deleteKeyword($2.id) : $1"
}
}
],
async start() {
keywordEntries = await DataStore.get(KEYWORD_ENTRIES_KEY) ?? [];
currentUser = UserStore.getCurrentUser();
this.onUpdate = () => null;
(await DataStore.get(KEYWORD_LOG_KEY) ?? []).map(e => JSON.parse(e)).forEach(e => {
this.addToLog(e);
});
},
applyKeywordEntries(m: Message) {
let matches = false;
for (let entry of keywordEntries) {
if (entry.regex === "") {
return;
}
let listed = entry.listIds.some(id => id === m.channel_id || id === m.author.id);
if (!listed) {
const channel = ChannelStore.getChannel(m.channel_id);
if (channel != null) {
listed = entry.listIds.some(id => id === channel.guild_id);
}
}
const whitelistMode = entry.listType === ListType.Whitelist;
if (!whitelistMode && listed) {
return;
}
if (whitelistMode && !listed) {
return;
}
if (settings.store.ignoreBots && m.author.bot && (!whitelistMode || !entry.listIds.includes(m.author.id))) {
return;
}
const flags = entry.ignoreCase ? "i" : "";
if (safeMatchesRegex(m.content, entry.regex, flags)) {
matches = true;
}
for (const embed of m.embeds as any) {
if (safeMatchesRegex(embed.description, entry.regex, flags) || safeMatchesRegex(embed.title, entry.regex, flags)) {
matches = true;
} else if (embed.fields != null) {
for (const field of embed.fields as Array<{ name: string, value: string }>) {
if (safeMatchesRegex(field.value, entry.regex, flags) || safeMatchesRegex(field.name, entry.regex, flags)) {
matches = true;
}
}
}
}
}
if (matches) {
m.mentions.push(currentUser);
if (m.author.id !== currentUser.id)
this.addToLog(m);
}
},
addToLog(m: Message) {
if (m == null || keywordLog.some(e => e.id === m.id))
return;
DataStore.get(KEYWORD_LOG_KEY).then(log => {
DataStore.set(KEYWORD_LOG_KEY, [...log, JSON.stringify(m)]);
});
const thing = createMessageRecord(m);
keywordLog.push(thing);
keywordLog.sort((a, b) => b.timestamp - a.timestamp);
if (keywordLog.length > 50)
keywordLog.pop();
this.onUpdate();
},
deleteKeyword(id) {
keywordLog = keywordLog.filter(e => e.id !== id);
this.onUpdate();
},
keywordTabBar() {
return (
<TabBar.Item className="vc-settings-tab-bar-item" id={5}>
Keywords
</TabBar.Item>
);
},
tryKeywordMenu(setTab, onJump, closePopout) {
const header = (
<MenuHeader tab={5} setTab={setTab} closePopout={closePopout} badgeState={{ badgeForYou: false }}/>
);
const channel = ChannelStore.getChannel(SelectedChannelStore.getChannelId());
const [tempLogs, setKeywordLog] = useState(keywordLog);
this.onUpdate = () => {
const newLog = Array.from(keywordLog);
setKeywordLog(newLog);
};
const messageRender = (e, t) => {
e._keyword = true;
e.customRenderedContent = {
content: highlightKeywords(e.content, keywordEntries)
};
const msg = this.renderMsg({
message: e,
gotoMessage: t,
dismissible: true
});
return [msg];
};
return (
<>
<Popout
className={recentMentionsPopoutClass.recentMentionsPopout}
renderHeader={() => header}
renderMessage={messageRender}
channel={channel}
onJump={onJump}
onFetch={() => null}
onCloseMessage={this.deleteKeyword}
loadMore={() => null}
messages={tempLogs}
renderEmptyState={() => null}
/>
</>
);
},
modify(e) {
if (e.type === "MESSAGE_CREATE") {
this.applyKeywordEntries(e.message);
} else if (e.type === "LOAD_MESSAGES_SUCCESS") {
for (let msg = 0; msg < e.messages.length; ++msg) {
this.applyKeywordEntries(e.messages[msg]);
}
}
return e;
}
});

View file

@ -0,0 +1,16 @@
.vc-keywordnotify-delete:hover {
color: var(--status-danger);
}
.vc-keywordnotify-delete {
padding: 0;
color: var(--primary-400);
transition: color 0.2s ease-in-out;
}
.vc-keywordnotify-collapsible {
display: flex;
align-items: center;
padding: 8px;
cursor: pointer;
}

View file

@ -103,7 +103,7 @@ function VoiceChannelTooltip({ channel }: VoiceChannelTooltipProps) {
<UserSummaryItem <UserSummaryItem
users={users} users={users}
renderIcon={false} renderIcon={false}
max={7} max={14}
size={18} size={18}
/> />
</div> </div>
@ -159,6 +159,7 @@ export const VoiceChannelIndicator = ErrorBoundary.wrap(({ userId }: VoiceChanne
<Tooltip <Tooltip
text={<VoiceChannelTooltip channel={channel} />} text={<VoiceChannelTooltip channel={channel} />}
tooltipClassName={cl("tooltip-container")} tooltipClassName={cl("tooltip-container")}
tooltipContentClassName={cl("tooltip-content")}
> >
{props => {props =>
isLocked ? isLocked ?

View file

@ -32,7 +32,7 @@ const settings = definePluginSettings({
default: true, default: true,
restartNeeded: true restartNeeded: true
}, },
showInVoiceMemberList: { showInMemberList: {
type: OptionType.BOOLEAN, type: OptionType.BOOLEAN,
description: "Show a user's Voice Channel indicator in the member and DMs list", description: "Show a user's Voice Channel indicator in the member and DMs list",
default: true, default: true,
@ -82,12 +82,12 @@ export default definePlugin({
match: /\.subtext,children:.+?}\)\]}\)(?=])/, match: /\.subtext,children:.+?}\)\]}\)(?=])/,
replace: "$&,$self.VoiceChannelIndicator({userId:arguments[0]?.user?.id})" replace: "$&,$self.VoiceChannelIndicator({userId:arguments[0]?.user?.id})"
}, },
predicate: () => settings.store.showInVoiceMemberList predicate: () => settings.store.showInMemberList
} }
], ],
start() { start() {
if (settings.store.showInVoiceMemberList) { if (settings.store.showInMemberList) {
addDecorator("UserVoiceShow", ({ user }) => user == null ? null : <VoiceChannelIndicator userId={user.id} />); addDecorator("UserVoiceShow", ({ user }) => user == null ? null : <VoiceChannelIndicator userId={user.id} />);
} }
}, },

View file

@ -15,7 +15,13 @@
} }
.vc-uvs-tooltip-container { .vc-uvs-tooltip-container {
max-width: 200px; max-width: 300px;
}
.vc-uvs-tooltip-content {
display: flex;
flex-direction: column;
gap: 6px;
} }
.vc-uvs-guild-name { .vc-uvs-guild-name {
@ -31,7 +37,5 @@
.vc-uvs-vc-members { .vc-uvs-vc-members {
display: flex; display: flex;
margin: 8px 0;
flex-direction: row;
gap: 6px; gap: 6px;
} }

View file

@ -375,6 +375,14 @@ export const Devs = /* #__PURE__*/ Object.freeze({
name: "ProffDea", name: "ProffDea",
id: 609329952180928513n id: 609329952180928513n
}, },
camila314: {
name: "camila314",
id: 738592270617542716n
},
x3rt: {
name: "x3rt",
id: 131602100332396544n
},
UlyssesZhan: { UlyssesZhan: {
name: "UlyssesZhan", name: "UlyssesZhan",
id: 586808226058862623n id: 586808226058862623n