BD-js/plugins/InvisibleTyping.plugin.js

535 lines
20 KiB
JavaScript

/**
* @name InvisibleTyping
* @version 1.3.3
* @description Makes your typing invisible to other people.
* @author Strencher
* @invite gvA2ree
* @changelog [fixed] Fixed cleanup of observer.
* @changelogDate 2022-10-22T22:00:00.000Z
* @changelogImage https://cdn.discordapp.com/attachments/939319506428391495/1032360180303790163/Untitled-1.jpg
*/
var meta;
const {React, DOM, Patcher, UI, Data, Utils, ReactUtils, ContextMenu, Webpack: _Webpack} = new BdApi("InvisibleTyping");
const Webpack = {
..._Webpack,
getByProps(...props) {return this.getModule(this.Filters.byProps(...props));},
getStore(name) {return this.getModule(m => m?._dispatchToken && m?.getName() === name);},
getBulk(...queries) {return _Webpack.getBulk(...queries.map(q => typeof q === "function" ? {filter: q} : q));}
};
Utilities: {
var removeItem = function (array, item) {
while (array.includes(item)) {
array.splice(array.indexOf(item), 1);
}
return array;
};
var onceAdded = (selector, callback, signal) => {
let directMatch;
if (directMatch = document.querySelector(selector)) {
callback(directMatch);
return () => null;
}
const cancel = () => observer.disconnect();
const observer = new MutationObserver(changes => {
for (const change of changes) {
if (!change.addedNodes.length) continue;
for (const node of change.addedNodes) {
const match = (node.matches(selector) && node) || node.querySelector(selector);
if (!match) continue;
cancel();
signal.removeEventListener("abort", cancel);
callback(match);
}
}
});
observer.observe(document.body, {childList: true, subtree: true});
signal.addEventListener("abort", cancel);
};
var Fluxify = (component, stores, getter) => {
Object.assign(component.prototype, {
componentDidMount() {
this._handleStoreChange = this._handleStoreChange.bind(this);
this._handleStoreChange();
for (const store of stores) store.addChangeListener(this._handleStoreChange);
},
_handleStoreChange() {
if (this._gone) return;
this.setState(getter(this.props));
},
componentWillUnmount() {
this._gone = true;
for (const store of stores) store.removeChangeListener(this._handleStoreChange);
}
});
};
var Settings = {
_listeners: new Set,
settings: Data.load("settings") ?? {},
getSetting(id, defValue) {return this.settings[id] ?? defValue;},
updateSetting(id, value) {return this.settings[id] = value, this.saveSettings();},
saveSettings() {Data.save("settings", this.settings), this.emitChange();},
emitChange() {this._listeners.forEach(callback => callback());},
addChangeListener(listener) {this._listeners.add(listener);},
removeChangeListener(listener) {this._listeners.delete(listener);}
};
};
Components: {
var InvisibleTypingButton = class InvisibleTypingButton extends React.Component {
state = {enabled: false};
static DMChannels = new Set([1, 3]);
static canViewChannel(channel) {
if (!channel) return false;
if (this.DMChannels.has(channel.type)) return true;
try {
return this.defaultProps.PermissionUtils.can({
context: channel,
user: this.defaultProps.UserStore.getCurrentUser(),
permission: /*SEND_MESSAGES*/ 2048n
});
} catch (error) {
console.error("Failed to request permissions:", error);
return true;
}
}
static shouldShow(children, props) {
if (!Array.isArray(children)) return false;
if (props.type?.analyticsName === "profile_bio_input") return false;
if (children.some(child => child && child.type === InvisibleTypingButton)) return false;
if (!this.canViewChannel(props.channel)) return false;
return true;
}
static getState(channelId) {
const isGlobal = Settings.getSetting("autoEnable", true);
const isExcluded = Settings.getSetting("exclude", []).includes(channelId);
if (isExcluded) return isGlobal;
return !isGlobal;
}
handleClick = () => {
const {channel, isEmpty, TypingModule} = this.props;
const excludeList = Settings.getSetting("exclude", []).concat();
if (excludeList.includes(channel.id)) {
removeItem(excludeList, channel.id);
TypingModule.stopTyping(channel.id);
} else {
excludeList.push(channel.id);
if (!isEmpty) TypingModule.startTyping(channel.id);
}
Settings.updateSetting("exclude", excludeList);
}
renderContextMenu() {
const globalState = Settings.getSetting("autoEnable", false);
return ContextMenu.buildMenu([
{
id: "globally-disable-or-enable-typing",
label: !globalState ? "Disable Globally" : "Enable Globally",
onClick: () => {Settings.updateSetting("autoEnable", !globalState);}
},
{
id: "reset-config",
color: "colorDanger",
disabled: !Settings.getSetting("exclude", []).length,
label: "Reset Config",
onClick() {
Settings.updateSetting("exclude", []);
UI.showToast("Successfully reset config for all channels.", {type: "success"});
}
}
]);
}
renderButton = props => {
return React.createElement("button", {
...props,
ref: e => e && (e.unmount = () => {
this.render = () => null;
this.forceUpdate();
}),
onClick: this.handleClick,
onContextMenu: e => ContextMenu.open(e, this.renderContextMenu()),
className: Utils.className("invisible-typing-button", {enabled: this.state.enabled, disabled: !this.state.disabled}),
children: React.createElement("svg", {
width: "25",
height: "25",
viewBox: "0 0 576 512"
}, React.createElement("path", {
fill: "currentColor",
d: "M528 448H48c-26.51 0-48-21.49-48-48V112c0-26.51 21.49-48 48-48h480c26.51 0 48 21.49 48 48v288c0 26.51-21.49 48-48 48zM128 180v-40c0-6.627-5.373-12-12-12H76c-6.627 0-12 5.373-12 12v40c0 6.627 5.373 12 12 12h40c6.627 0 12-5.373 12-12zm96 0v-40c0-6.627-5.373-12-12-12h-40c-6.627 0-12 5.373-12 12v40c0 6.627 5.373 12 12 12h40c6.627 0 12-5.373 12-12zm96 0v-40c0-6.627-5.373-12-12-12h-40c-6.627 0-12 5.373-12 12v40c0 6.627 5.373 12 12 12h40c6.627 0 12-5.373 12-12zm96 0v-40c0-6.627-5.373-12-12-12h-40c-6.627 0-12 5.373-12 12v40c0 6.627 5.373 12 12 12h40c6.627 0 12-5.373 12-12zm96 0v-40c0-6.627-5.373-12-12-12h-40c-6.627 0-12 5.373-12 12v40c0 6.627 5.373 12 12 12h40c6.627 0 12-5.373 12-12zm-336 96v-40c0-6.627-5.373-12-12-12h-40c-6.627 0-12 5.373-12 12v40c0 6.627 5.373 12 12 12h40c6.627 0 12-5.373 12-12zm96 0v-40c0-6.627-5.373-12-12-12h-40c-6.627 0-12 5.373-12 12v40c0 6.627 5.373 12 12 12h40c6.627 0 12-5.373 12-12zm96 0v-40c0-6.627-5.373-12-12-12h-40c-6.627 0-12 5.373-12 12v40c0 6.627 5.373 12 12 12h40c6.627 0 12-5.373 12-12zm96 0v-40c0-6.627-5.373-12-12-12h-40c-6.627 0-12 5.373-12 12v40c0 6.627 5.373 12 12 12h40c6.627 0 12-5.373 12-12zm-336 96v-40c0-6.627-5.373-12-12-12H76c-6.627 0-12 5.373-12 12v40c0 6.627 5.373 12 12 12h40c6.627 0 12-5.373 12-12zm288 0v-40c0-6.627-5.373-12-12-12H172c-6.627 0-12 5.373-12 12v40c0 6.627 5.373 12 12 12h232c6.627 0 12-5.373 12-12zm96 0v-40c0-6.627-5.373-12-12-12h-40c-6.627 0-12 5.373-12 12v40c0 6.627 5.373 12 12 12h40c6.627 0 12-5.373 12-12z"
}), !this.state.enabled ? React.createElement("rect", {
className: "disabled-stroke-through",
x: "10",
y: "10",
width: "600pt",
height: "70px",
fill: "#f04747"
}) : null)
});
}
render() {
const {Tooltip} = this.props;
return React.createElement(Tooltip, {
text: this.state.enabled ? "Disable Typing" : "Enable Typing",
children: this.renderButton
});
}
}
Fluxify(InvisibleTypingButton, [Settings], (props) => ({enabled: InvisibleTypingButton.getState(props.channel.id)}));
Settings: {
var SimpleSwitch = ({state = false, name = "", note = "", onChange}) => {
const [currState, toggle] = React.useReducer(n => !n, state);
const handleChange = () => (onChange(!currState), toggle());
return React.createElement("div", {className: "it-switch-wrapper"},
React.createElement("div", {className: "it-switch-header"},
React.createElement("h5", {className: "it-switch-name"}, name),
React.createElement("div", {
className: Utils.className("it-switch-item", currState && "it-switch-checked"),
onClick: handleChange,
}, React.createElement("div", {className: "it-switch-dot"}))
),
React.createElement("span", {className: "it-switch-note"}, note)
);
}
}
}
module.exports = class InvisibleTyping {
constructor(metaObject) {meta = this.meta = metaObject;}
cleanup = new Set([
() => Patcher.unpatchAll(),
() => DOM.removeStyle(),
() => new Set(document.getElementsByClassName("invisible-typing-button")).forEach(el => el.unmount?.())
]);
start() {
DOM.addStyle(`
.it-title-wrap {
font-size: 18px;
}
.it-title-wrap span {
font-size: 12px;
color: var(--text-muted);
font-family: var(--font-primary);
}
.invisible-typing-button svg {
color: var(--interactive-normal);
overflow: visible;
}
.invisible-typing-button .disabled-stroke-through {
position: absolute;
transform: translateX(-15px) translateY(530px) rotate(-45deg);
}
.invisible-typing-button {
margin-top: 3px;
background: transparent;
}
.invisible-typing-button:hover:not(.disabled) svg {
color: var(--interactive-hover);
}
.it-switch-wrapper {
color: #fff;
margin-bottom: 10px;
}
.it-switch-header {
margin-bottom: 15px;
display: flex;
justify-content: space-between;
align-items: center;
}
.it-switch-name {
font-size: 18px;
font-weight: 500;
}
.it-switch-note {
font-size: 14px;
color: var(--text-muted);
}
.it-switch-item.it-switch-checked {
background: var(--brand-experiment);
}
.it-switch-item {
width: 40px;
height: 24px;
background: rgb(114, 118, 125);
border-radius: 100px;
cursor: pointer;
}
.it-switch-dot {
width: 18px;
height: 18px;
background: #fff;
border-radius: 100px;
top: 3px;
left: 3px;
position: relative;
transition: transform .3s ease-in-out;
}
.it-switch-checked .it-switch-dot {
transform: translateX(16px);
}
.it-switch-wrapper {
color: #fff;
margin-bottom: 10px;
}
.it-switch-header {
margin-bottom: 15px;
display: flex;
justify-content: space-between;
align-items: center;
}
.it-switch-name {
font-size: 18px;
font-weight: 500;
}
.it-switch-note {
font-size: 14px;
color: var(--text-muted);
}
.it-switch-item.it-switch-checked {
background: var(--brand-experiment);
}
.it-switch-item {
width: 40px;
height: 24px;
background: rgb(114, 118, 125);
border-radius: 100px;
cursor: pointer;
}
.it-switch-dot {
width: 18px;
height: 18px;
background: #fff;
border-radius: 100px;
top: 3px;
left: 3px;
position: relative;
transition: transform .3s ease-in-out;
}
.it-switch-checked .it-switch-dot {
transform: translateX(16px);
}
.it-changelog-item {
color: #fff;
}
.it-changelog-header {
text-transform: uppercase;
font-weight: 700;
display: flex;
align-items: center;
margin-bottom: 10px;
}
.item-changelog-added .it-changelog-header {
color: #45BA6A;
}
.item-changelog-fixed .it-changelog-header {
color: #EC4245;
}
.item-changelog-improved .it-changelog-header {
color: #5865F2;
}
.it-changelog-header::after {
content: "";
flex-grow: 1;
height: 1px;
background: currentColor;
margin-left: 7px;
}
.it-changelog-item span {
display: list-item;
margin-left: 5px;
list-style: inside;
}
.it-changelog-item span::marker {
color: var(--background-accent);
}
.it-changelog-banner {
width: 405px;
border-radius: 8px;
margin-bottom: 20px;
}
`);
InvisibleTypingButton.defaultProps ??= {};
[
InvisibleTypingButton.defaultProps.PermissionUtils,
InvisibleTypingButton.defaultProps.UserStore,
InvisibleTypingButton.defaultProps.Tooltip
] = Webpack.getBulk(
{searchExports: true, filter: Webpack.Filters.byProps("can", "areChannelsLocked")},
m => m?._dispatchToken && m.getName() === "UserStore",
{searchExports: true, filter: Webpack.Filters.byPrototypeFields("renderTooltip")}
);
this.patchTextAreaButtons().catch(() => {});
this.patchStartTyping();
this.maybeShowChangelog();
}
maybeShowChangelog() {
if (this.meta.version === Settings.getSetting("latestUsedVersion")) return;
const items = Array.from(meta.changelog.matchAll(/\[(\w+)\]\s?([^\n]+)/g), ([, type, content]) => {
let className = "it-changelog-item";
switch (type) {
case "fixed":
case "improved":
case "added": {
className += " item-changelog-" + type;
break;
};
}
return React.createElement("div", {
className,
children: [
React.createElement("h4", {className: "it-changelog-header"}, type),
React.createElement("span", null, content)
]
});
});
"changelogImage" in meta && items.unshift(
React.createElement("img", {
className: "it-changelog-banner",
src: meta.changelogImage
})
);
Settings.updateSetting("latestUsedVersion", meta.version);
const formatter = new Intl.DateTimeFormat(document.documentElement.lang, {month: "long", day: "numeric", year: "numeric"});
UI.alert(React.createElement("div", {
className: "it-title-wrap",
children: [
React.createElement("h1", null, "What's New - InvisibleTyping"),
React.createElement("span", null, formatter.format(new Date(meta.changelogDate)))
]
}), items);
}
async patchTextAreaButtons() {
const buttonsClassName = Webpack.getByProps("profileBioInput", "buttons")?.buttons
if (!buttonsClassName) return UI.showToast(`[${this.meta.name}] Could not add button to textarea.`, {type: "error"});
const controller = new AbortController();
const instance = await new Promise((resolve, reject) => {
onceAdded("." + buttonsClassName, e => {
const vnode = ReactUtils.getInternalInstance(e);
if (!vnode) return;
for (let curr = vnode, max = 100; curr !== null && max--; curr = curr.return) {
const tree = curr?.pendingProps?.children;
let buttons;
if (Array.isArray(tree) && (buttons = tree.find(s => s?.props?.type && s.props.channel && s.type?.$$typeof))) {
resolve(buttons.type);
break;
}
}
}, controller.signal);
const abort = controller.abort.bind(controller);
controller.signal.addEventListener("abort", () => {
this.cleanup.delete(abort);
reject();
});
this.cleanup.add(abort);
});
Patcher.after(instance, "type", (_, [props], res) => {
if (!InvisibleTypingButton.shouldShow(res?.props?.children, props)) return;
res.props.children.unshift(React.createElement(InvisibleTypingButton, props));
});
}
patchStartTyping() {
const TypingModule = InvisibleTypingButton.defaultProps.TypingModule = Webpack.getByProps("startTyping");
Patcher.instead(TypingModule, "startTyping", (_, [channelId], originalMethod) => {
if (InvisibleTypingButton.getState(channelId)) originalMethod(channelId);
});
}
stop() {
this.cleanup.forEach(clean => clean());
}
getSettingsPanel() {
return React.createElement(SimpleSwitch, {
get state() {return Settings.getSetting("autoEnable", false);},
name: "Globally Toggle",
note: "Enable/Disable your typing information globally and use excluded channels as white-/blacklist.",
onChange: value => Settings.updateSetting("autoEnable", value)
});
}
};