DEV Community

F53
F53

Posted on

Slack Mod: Cleanup

Intro

After improving the launcher for my Slack Mod, it's almost ready for release! Before that, I would like to add a couple of tweaks and adjustments to make the whole mod feel a bit more polished.

Editor Animation:

Slack's preferences window is pretty small, which works perfectly for the menus it has by default. Problem is this can feel a little cramped when writing code:

At the end of my first SlackMod blog, I wrote a little bit of CSS into the editor to both demonstrate its use and solve this problem:

/* Increase width and height of Preferences to allow more room for code */
body > div.c-sk-modal_portal > div > div {
    max-width: 100%!important;
    max-height: 100%!important;
    height:100%;
    width:100%;
}
Enter fullscreen mode Exit fullscreen mode

Problem is, the other preferences tabs weren't designed to use this space, so it feels very odd.

I solved this by hardcoding it so when you click on the Custom CSS tab, the editor expands:

customTab.addEventListener("click", ()=>{
    ...
    // hardcoded styling for CSS Tab:
    // increase width and height of preferences modal for more code room
    ["max-width","width","max-height","height"].forEach(
        style => document.querySelector(`div[aria-label="Preferences"]`).style[style] = "100%")
}
Enter fullscreen mode Exit fullscreen mode

This can feel a bit jarring because the Preferences screen instantly "snaps" to taking up the whole screen.

Given that, I added a bit of easing to the hardcoded styling:

// hardcoded styling for CSS Tab:
...
// smoothly expand preferences modal
    document.querySelector(`div[aria-label="Preferences"]`).style["transition"] = "500ms ease all"
Enter fullscreen mode Exit fullscreen mode

That feels really nice, but then our Ace editor doesn't use all the space we are giving it!

I thought this would be a pretty simple fix, so I went for the same idea as the other hardcoded stuff:

customTab.addEventListener("click", ()=>{
    ...
    const editor = ace.edit('slackMod-editor');
    ...
    // hardcoded styling for CSS Tab:
    ...
    editor.style["height"] = "100%";
}
Enter fullscreen mode Exit fullscreen mode

But that doesn't work, as Ace has some internal CSS determining its size that takes a very high priority. This is set once when the editor is initialized to fit the div it's put in.

According to Ace's docs we can call editor.resize() to make the editor expand to fit its parent div.

In theory, since we're currently expanding the div containing the editor for 500 milliseconds, we could just wait 500 ms, then call editor.resize():

setTimeout(()=>{editor.resize()}, 500);
Enter fullscreen mode Exit fullscreen mode

But in practice that looks a bit odd as the editor "snaps" to fit the space after the animation is done:

I am not too proud of my solution for this, but hey, it works.

// make editor resize editor every 5ms for 500ms
// smoothly expanding it with the preferences modal
for (let i = 5; i<=500; i+=5) {
    setTimeout(()=>{editor.resize()}, i);
}
Enter fullscreen mode Exit fullscreen mode

Why 5 milliseconds? It is fast enough to look smooth at even 144fps, while also being evenly divisible into 500.

Hey, thats pretty good!

Error on switching to other tabs:

I mentioned this briefly in my first blog on SlackMod: when you click the Custom CSS tab, then select a different category, the Preferences modal crashes.

I spent ages trying to fix this before and eventually gave up.

Giving the problem some more thought, I decided to start by preventing the error that happens when you click a tab.

I figured I could just do something like this

customTab.addEventListener("click", ()=>{
    ...
    // make it so when you click a different tab, it doesn't crash you
    ([...settingsTabList.children]).slice(0,-1).map((tab)=>{
        tab.addEventListener("click", (event)=>{
            // prevent the crash that normally happens
            event.stopPropagation()
        })
    })
}
Enter fullscreen mode Exit fullscreen mode

But, no, for some reason event.stopPropagation() doesn't do what its supposed to. After a lot of thought of other ways to solve the issue, I remembered how I removed event listeners in a lab before to solve this exact problem!

By replacing an element with a clone of itself, you effectively remove all of that element's event listeners:

element.parentElement.replaceChild(element.cloneNode(true), element)
Enter fullscreen mode Exit fullscreen mode

Given this, removing all of the click event listeners from the tabs is easy!

First, to iterate through all our tabs, we have to make an iterable array of them. Spreading our tabList's children, then re-wrapping it as an array works for this:

([...settingsTabList.children])
Enter fullscreen mode Exit fullscreen mode

Then, we can do our "remove event listener" trick on each of these tabs:

customTab.addEventListener("click", ()=>{
    ...
    ([...settingsTabList.children]).map((tab)=>{
        // remove old click event listeners
        tab.parentElement.replaceChild(tab.cloneNode(true), tab)
    })
})
Enter fullscreen mode Exit fullscreen mode

Now that clicking the settings tabs doesn't immediately crash the Preferences screen, we can use our own method to select the tab.

The only way I thought of to "switch" tabs was pretty brute force:

Why don't we just close the Preferences, reopen it, then open the tab the user clicked?

Looking into how to simulate the clicks I needed for this, I came across the docs for EventTarget.dispatchEvent()

As a quick test, I tried to close the preferences modal using this command in Slack's dev tools.

Given that success, I made a helper function for clicking elements and tried using it to pull up the correct screen.

const clickNodeBySelector = (selector) =>
    document.querySelector(selector).dispatchEvent(new Event("click", {bubbles:true}))
...
// replace tab click events with our own click event for switching tabs
([...settingsTabList.children]).map((tab)=>{
    // remove old click event listeners
    tab.parentElement.replaceChild(tab.cloneNode(true), tab)
    // add our own click event
    const tabID = tab.id
    document.getElementById(tabID).addEventListener("click", (event)=>{
        if (event.isTrusted) {
            // close the preferences screen
            clickNodeBySelector(`[aria-label="Close"]`)
            // re-open the preferences window
            clickNodeBySelector(".p-ia__nav__user__button")
            clickNodeBySelector("div.ReactModalPortal div:nth-child(7) div")
            // go to the tab that was clicked
            clickNodeBySelector("#"+tabID)
            // add back the custom css tab
            addSettingsTab()
        }
    })
})
Enter fullscreen mode Exit fullscreen mode

This almost worked, but it wasn't switching to a tab after entering the preferences menu.

This is because it's clicking the tab on the Preferences screen that we are closing.

I fixed this by adding a slight delay to it:

setTimeout(()=>{clickNodeBySelector("#"+tabID)},delay)
Enter fullscreen mode Exit fullscreen mode

What I found annoying is that sometimes this would work with a delay of just 5, and other times I need a delay of atleast 50, which starts to get noticeable.

I actually already fixed a similar problem to this with the addSettingsTab() function. I check if the elements I need exist, and if they dont, I try again in a bit.

function addSettingsTab() {
    if (document.querySelector(".p-prefs_dialog__menu") !== null) {
        ...
    } else {
        setTimeout(()=>{addSettingsTab()}, 1)
    }
}
Enter fullscreen mode Exit fullscreen mode

This effectively makes it run once every millisecond until the element it hooks into exists.

Given that we need this exact same solution again, I broke it out into an abstract function:

function tryTillTrue(expression, callback) {
    setTimeout(()=>{
        if (expression()) { callback() }
        else { tryTillTrue(expression,callback)}
    }, 1)
}
Enter fullscreen mode Exit fullscreen mode

Heres how it's implemented in addSettingsTab():

function addSettingsTab() {
    tryTillTrue(()=>document.querySelector(".p-prefs_dialog__menu") !== null, ()=>{
        ...
    })
}
Enter fullscreen mode Exit fullscreen mode

We still need a little wait before trying to click into the tab because we still have the problem of clicking the one on the window we are closing:

// go to the tab that was clicked
tryTillTrue(()=>document.querySelector("#"+tabID) !== null,
    ()=>clickNodeBySelector("#"+tabID))
Enter fullscreen mode Exit fullscreen mode

With that, it works!

But, it does look pretty jarring as the background flashes and the window instantly shrinks. To fix this, I added a bit of styling to make the window ease in:

// go to the tab that was clicked
tryTillTrue(()=>document.querySelector("#"+tabID) !== null,
    ()=>clickNodeBySelector("#"+tabID), 0.025)
// add back the custom css tab
addSettingsTab()

// make it smoothly shrink back to normal window size
setTimeout(()=>{
    ["max-width","width","max-height","height"].forEach(
        style => document.querySelector(`div[aria-label="Preferences"]`).style[style] = "100%")

    setTimeout(()=>{
        document.querySelector(`div[aria-label="Preferences"]`).style["transition"] = "500ms ease all"
        document.querySelector(`div[aria-label="Preferences"]`).style["height"]="700px"
        document.querySelector(`div[aria-label="Preferences"]`).style["width"]="800px"
    },0.025)
},0.025)
Enter fullscreen mode Exit fullscreen mode

Yeah... that code looks absolutely disgusting, but it works:

Ok yea it renders 2 wrong frames, sue me.

Devtools Shortcuts

If you are writing Custom CSS, the devtools are kinda required.

By default Slack allows you to use the command /slackdevtools to open them, but that is really slow in comparison to using a keyboard shortcut like f12 or ctrl+shift+i.

I am going to skip past my research for how to open devtools, because it came up fruitless and eventually I decided to brute force it with fake user input.

To begin, I tried selecting the chat window and adding some text to it:

document.querySelector(".ql-editor").innerText = "/slackdevtools"
Enter fullscreen mode Exit fullscreen mode

That worked, so then I simulated a click on the send button:

clickNodeBySelector(`[aria-label="Send now"]`)
Enter fullscreen mode Exit fullscreen mode

That also worked!

Once I put them together, they didnt work, so I added a bit of delay to clicking the button:

document.querySelector(".ql-editor").innerText = "/slackdevtools"
setTimeout(()=>{clickNodeBySelector(`[aria-label="Send now"]`)}, 1)
Enter fullscreen mode Exit fullscreen mode

And that worked perfectly!

This does end up clearing the chatbox if you already have something in it, so I added 2 extra lines to save and restore the contents of it:

// save contents of chat editor
let oldText = document.querySelector(".ql-editor").innerText
// type and send slackdevtools command
document.querySelector(".ql-editor").innerText = "/slackdevtools"
setTimeout(()=>{clickNodeBySelector(`[aria-label="Send now"]`)}, 1)
// restore old contents of chat editor
setTimeout(()=>{document.querySelector(".ql-editor").innerText = oldText}, 100)
Enter fullscreen mode Exit fullscreen mode

This didnt end up consistent until I had the delay set to 100ms on the timeout for putting text back into the box.

To hook this up to key combos, I added an event listener, initially using "keypress":

document.addEventListener("keypress", (event) => {
    console.log(event)
Enter fullscreen mode Exit fullscreen mode

In testing, I found that for some reason this didnt log presses of the function keys, which we need for triggering on f12.

Given that, I switched to "keyup":

document.addEventListener("keyup", (event) => {
    console.log(event)
Enter fullscreen mode Exit fullscreen mode

Here are the highlights of the events this logs when we press the keycombos we want to use.

f12:

KeyboardEvent {
    code: "F12"
}
Enter fullscreen mode Exit fullscreen mode

ctrl+shift+i:

KeyboardEvent {
    code: "KeyI",
    ctrlKey: true,
    shiftKey: true
}
Enter fullscreen mode Exit fullscreen mode

Given this, I wrote out the code for binding the devtools hotkeys.

I started by destructuring the event out into the variables we care about:

document.addEventListener("keyup", ({ code, ctrlKey, shiftKey }) => {
Enter fullscreen mode Exit fullscreen mode

Then a simple check if the key combo is one we care about:

if (code === "F12" || (ctrlKey && shiftKey && code === "KeyI")) {
Enter fullscreen mode Exit fullscreen mode

Inside I put the code we ended up with for sending the command and restoring chat contents, leading to a final result that looks like this:

document.addEventListener("keyup", ({ code, ctrlKey, shiftKey }) => {
    if (code === "F12" || (ctrlKey && shiftKey && code === "KeyI")) {
        // save contents of chat editor
        let oldText = document.querySelector(".ql-editor").innerText
        // type and send slackdevtools command
        document.querySelector(".ql-editor").innerText = "/slackdevtools"
        setTimeout(()=>{clickNodeBySelector(`[aria-label="Send now"]`)}, 1)
        // restore old contents of chat editor
        setTimeout(()=>{document.querySelector(".ql-editor").innerText = oldText}, 100)
    }
});
Enter fullscreen mode Exit fullscreen mode

Top comments (1)

Collapse
 
f53 profile image
F53

If anyone reading is using macos and is familiar with making launch scripts for programs (think .bat files on windows) please hit me up on discord at CodeF53#0241

My next blog is an install guide for the mod, and I want to have a section for how to create launch scripts for each os.