Making Slack Night Mode
Update Jan 22, 2016: Slack has fixed this vulnerability, in under 24 hours! This means the solution in this article about using Slack Night Mode on Slack's desktop applications is no longer viable.
As with other companies, Slack is a valuable communication tool for us at Hund. Using integrations helps centralize notifications from services like GitLab, Sentry, DeployBot, Datadog, etc. It's no longer necessary to have tabs open for each of these services while developing, as any relevant information is sent to Slack. But, there is still one issue that many people face with Slack: its lack of a night mode/dark theme.
Before going further, I should mention that I'm a huge fan of f.lux and Redshift due to the benefits they offer. These applications are a life-changer for developers who find themselves groggy upon waking up after working late. Still, bright-white pages like Slack can be annoying to have open on a second monitor when chatting or developing.
It would be impractical for Slack to focus on adding a night mode. Such a change would require significant refactoring of their enormous stylesheets, all for a minor feature that many people wouldn't care about. Although they've stated it's a "future possibility", as well as having been a highly requested feature since 2014, I doubt we'll see something like this for some time.
Making my own solution
Since 2014, I've maintained a Stylish style named Slack Night Mode. I've refactored everything I wrote, switching from a single CSS file, branching out to what is now over 50 Sass files. Recently, I switched from Slack's primary aubergine color, to a more practical black color which quickly overtook the original in popularity.
One issue I ran into was how large the style had grown. Userstyles.org (a Stylish theme repository) has a large 100KB limit for themes, so I was rather shocked when I met that limit. I still needed to expand the style to support larger Slack features (like their post editor). Jason, the owner of Userstyles kindly lifted the limit so I could continue.
So a solution already exists, for the browser. But what about the popular desktop application?
Supporting the desktop application
For those unaware, Slack uses Electron to package applications. Electron lets you develop native desktop applications using web technologies. Going into this, I knew this was going to be a bit challenging considering how Slack doesn't open source their desktop application. This wouldn't be as simple as changing the source.
A temporary solution
I do have experience with Electron, so being familiar with it helped me know what to look for. As such, I looked for a way to enable the application's developer tools (because Electron uses Chromium). Slack's desktop application will enable them if the SLACK_DEVELOPER_MENU
environment variable is set.
With these developer tools, I could attempt to inject the style into the chat's webview. Now, I can't just include the styles in a stylesheet link tag, as Stylish requires some Mozilla-specific domain constraints, so none of the styles would even load. What I needed to do was strip this constraint from the stylesheet, then inject the CSS. I ended up with this:
$.ajax({
url: "https://cdn.rawgit.com/laCour/slack-night-mode/master/css/black.css",
success: function(css) {
$("<style></style>").appendTo("head").html(css.slice(35).slice(0, -2));
}
});
This is run on the chat's webview, not the containing frame. If this were run on the containing frame, we'd need to send it to the webview as a string to eval
by using executeJavaScript
.
Persisting the change
This is the tricky part. It shouldn't be possible to persist a client-side change that's executed only once. I immediately assumed that it wouldn't be possible to do this, so I looked for other ways in the package's source. Eventually I came across Slack's deep link URI handler, which handles slack:
protocol links. This grabbed my attention as a simple way to (hopefully) inject styles upon each run regardless of the platform.
Suffice it to say Slack parses our deep link and creates a JSON object with the desired information to provide to the chat's webview. This JSON object is stringified, URI encoded, then base64 encoded (in the executeJavaScript
function of the SlackWebViewContext
class). This is then sent to the webview by means of the built-in executeJavaScript
:
// NB: This is a secret handshake with the embedded page
this.wv.executeJavaScript('window.rendererEvalAsync(\"${btoa(encodeURIComponent(JSON.stringify(msg)))}\")');
return ret;
Now, let's take a closer look at window.rendererEvalAsync
... immediately we see something of concern (this is in the context of the chat webview):
data = JSON.parse(decodeURIComponent(atob(blob)));
let result = eval(data.code);
Well hello there, eval
. Nice to meet you. Don't mind if I do:
A solution
Linux
slack "slack://?s=');jQuery.ajax({url:'https://cdn.rawgit.com/laCour/slack-night-mode/master/css/black.css',success:function(e){jQuery('<style></style>').appendTo('head').html(e.slice(35).slice(0,-2))}});//"
I recommend putting this in a user application desktop entry. cp /usr/share/applications/slack ~/.local/share/applications/
will get you started.
Mac
No Electron here. :(
Windows
C:\Users\-\AppData\Local\slack\Update.exe --processStart slack.exe --process-start-args "slack://?s="');jQuery.ajax({url:'https://cdn.rawgit.com/laCour/slack-night-mode/master/css/black.css',success:function(e){$('style').append(e.slice(35).slice(0,-2))}});//"
To persist this, make a .bat
file.
Conclusion
So, I doubt this will be possible for much longer given what is being done here. But regardless, enjoy, and give your eyes a break.
Full-stack developer. Co-founder at Hund.io.