535 lines
20 KiB
JavaScript
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)
|
|
});
|
|
}
|
|
};
|