- devalias' Beeper CSS Hacks
- See Also
- Bonus Section
- Accessing the React internals for the Beeper 'Rooms' component using Beeper / Electron's DevTools
- Some notes on how to achieve custom Beeper / Electron JS hacks/customisations (eg. more customizability than CSS hacks alone)
- Various Beeper Inbox Selectors (Favourite, Pinned, Not Pinned, Unread, Etc)
See devalias-beeper-css-hacks.css
for the customisations I am using myself.
You'll also find a number of hacks/techniques on my theme issue:
- Improving UI/UX for users with lots of favourites
DES-10976
- Improving UI/UX with the Custom CSS TextArea
DES-10977
- Add an 'external link' icon next to the 'Chat Networks' 'tab' in the main 'Settings' dialog
DES-10656
- Reduce the 'in your face' bright/colourful/'glare' from the network icons on the new 'View Space Bar in side panel' lab
DES-10915
- Add the 🔗 emoji after the 'Chat Networks' item in the main settings menu + remove extra noise from settings menu
Community Request
DES-10656
- Remove the 'Archive' button from the new 'beeper spaces sidebar'
Community Request
- Hide the floating 'Archived' and 'Sweep' buttons in the new 'beeper space sidebar'
+Community Contribution
DES-10914
- Change the network icons in the new 'beeper space sidebar' to use the old lineart icons
DES-10915
- With the 'new' 'Beeper Space Sidebar', hide the floating 'open archive' button, and always show the floating 'Archive All Read Messages' button
DES-11233
- Reduce the size of the 'new' 'Beeper Space Sidebar' + it's 'chat network' buttons
Community Request
- Hide Left Panel/Sidebar While Chat Is Open
Community Request
- Change the
min-width
of the Left 'Inbox' Side PanelCommunity Contribution
- Ensure full image preview is visible, rather than a zoomed part of it
- Change the colours of existing
svg
images embedded in the DOMCommunity Request
- Beeper Spacebar Icon Modifications
Community Contribution
- Hide the Pin/Unpin and/or 'Archive' buttons that appear on hover of each chat room in inbox
Community Request
- Hide the help question mark above the left hand sidebar
Community Request
- Hiding the white border to the right of the left hand sidebar
Community Contribution
- Kinda hacky beeper "compact room list" css theme
Community Contribution
- Hide the timestamp on messages
Community Request
- Hide certain chats, chats by network, or entire networks from the unified inbox (while still showing in their specific network view)
Community Request
- Ensure 'Report a Problem' dialog doesn't take over the entire screen
- etc
- https://www.beeper.com/
- https://github.com/beeper/themes
-
Community Themes
- https://github.com/beeper/themes#how-to-use-themes
-
Click Themes -> select the theme you would like to try
-
Select the text content and copy. This code is called CSS.
-
Open Beeper Desktop -> Settings -> Appearance
-
Paste what you copied earlier into the box and hit Apply
-
- beeper/themes#6
-
A few custom theme hacks for improving Inbox Favourites (max-height, scrollable, reduce avatar size, etc)
-
- beeper/themes#7
- https://github.com/beeper/themes/pull/7/files
- https://github.com/beeper/themes/tree/main/themes/utility
-
Reusable css that could be added onto other themes for additional functionality
-
- Announcement Tweet: https://twitter.com/_devalias/status/1651766148046921728
- Reddit Thread: https://www.reddit.com/r/beeper/comments/13cezdc/beeper_ui_ux_hacksimprovements_via_custom_themes/
- https://github.com/0xdevalias/chatgpt-source-watch : Analyzing the evolution of ChatGPT's codebase through time with curated archives and scripts.
- Deobfuscating / Unminifying Obfuscated Web App Code (0xdevalias' gist)
- Reverse Engineering Webpack Apps (0xdevalias' gist)
- Reverse Engineered Webpack Tailwind-Styled-Component (0xdevalias' gist)
- React Server Components, Next.js v13+, and Webpack: Notes on Streaming Wire Format (
__next_f
, etc) (0xdevalias' gist)) - Fingerprinting Minified JavaScript Libraries / AST Fingerprinting / Source Code Similarity / Etc (0xdevalias' gist)
- Bypassing Cloudflare, Akamai, etc (0xdevalias' gist)
- Debugging Electron Apps (and related memory issues) (0xdevalias gist)
- Reverse Engineering Golang (0xdevalias' gist)
- Reverse Engineering on macOS (0xdevalias' gist)
mxMatrixClientPeg.matrixClient
eg. to send a message:
const roomId = "!KlacjKWnARbprTLuRM:nova.chat";
mxMatrixClientPeg.matrixClient.sendMessage(roomId, {
msgtype: "m.text",
body: "This is a test message sent using mxMatrixClientPeg.matrixClient.sendMessage"
})
(The following tip comes via @cameronaaron
from the Beeper Community)
Open DevTools console, and enter the following:
bpWhoamiMonitor.whoami.userInfo.channel = "INTERNAL"
Then go to your Beeper Settings -> Labs -> and look at all the new features you can flip (note that you can also probably flip all of these directly via /devtools
-> Settings Explorer as well)
const el = $('#matrixchat > .mx_MatrixChat_wrapper > .mx_MatrixChat > .bp_LeftPanel > .bp_LeftPanel_contentWrapper > .bp_LeftPanel_content > .rooms')
const elProps = Object.getOwnPropertyNames(el);
const elReactFiberKey = elProps.filter(k => k.includes('__reactFiber'))
const elReactPropsKey = elProps.filter(k => k.includes('__reactProps'))
const elReactInternals = {
reactFiber: el[elReactFiberKey],
reactProps: el[elReactPropsKey],
}
//console.log(elReactInternals)
const UnreadList = elReactInternals.reactProps.children[3].props.children[0]
const ReadList = elReactInternals.reactProps.children[3].props.children[1]
console.log('Inbox Chats', UnreadList.props.unreads)
// (303) [Room, Room, ...]
console.log('Archived Chats', ReadList.props.rooms)
// (1633) [Room, Room, ...]
Some notes on how to achieve custom Beeper / Electron JS hacks/customisations (eg. more customizability than CSS hacks alone)
Warning: This section is way more advanced than CSS hacks, and comes with much higher risks if you run arbitrary untrusted code, as using malicious JS code might steal your beeper auth tokens, any of your private messages, etc, etc. You've been warned.
These notes originally come from my Debugging Electron Apps (and related memory issues) (0xdevalias gist) gist (Ref):
On macOS you can get to the Beeper
*.asar
files by:⇒ cd /Applications/Beeper.app/Contents/Resources ⇒ ls af.lproj/ cs.lproj/ fa.lproj/ icon.icns ml.lproj/ ru.lproj/ todesktop-runtime-config.json am.lproj/ da.lproj/ fi.lproj/ icons/ mr.lproj/ sk.lproj/ tr.lproj/ app-update.yml de.lproj/ fil.lproj/ id.lproj/ ms.lproj/ sl.lproj/ uk.lproj/ app.asar el.lproj/ fr.lproj/ it.lproj/ nb.lproj/ sr.lproj/ ur.lproj/ app.asar.unpacked/ en.lproj/ gu.lproj/ ja.lproj/ nl.lproj/ sv.lproj/ vi.lproj/ ar.lproj/ en_GB.lproj/ he.lproj/ kn.lproj/ pl.lproj/ sw.lproj/ webapp.asar bg.lproj/ es.lproj/ hi.lproj/ ko.lproj/ pt_BR.lproj/ ta.lproj/ zh_CN.lproj/ bn.lproj/ es_419.lproj/ hr.lproj/ lt.lproj/ pt_PT.lproj/ te.lproj/ zh_TW.lproj/ ca.lproj/ et.lproj/ hu.lproj/ lv.lproj/ ro.lproj/ th.lproj/
Where the most relevant files/folders there are:
app.asar
app.asar.unpacked/
webapp.asar
From memory, I believe
app.asar
is more related to the core electron/element type features, andwebapp.asar
was more related to the more custom Beeper features; but I didn't look super deeply into that side of things.We can then use the node
asar
package vianpx
to inspect the contents of the*.asar
files:⇒ npx asar list --is-pack app.asar | grep -v node_modules ⇒ npx asar list --is-pack webapp.asar | grep -v node_modulesWe can then run Beeper passing the node remote debugging
--inspect-brk
command to set a breakpoint at the entrypoint of the code:⇒ open /Applications/Beeper.app --args --inspect-brk=1337Which we can then connect to by opening a Chrome browser, navigating to
chrome://inspect/#devices
, and under 'Remote Target' looking for something like the following:electron/js2c/browser_init file:///
Then clicking on 'inspect', which will open the Chrome DevTools 'Sources' tab and show the entrypoint line where the debugger has stopped execution, in this case, in the
file:///Applications/Beeper.app/Contents/Resources/app.asar/lib/electron-main.js
file:We can then skim around the code in this file to understand what it does, and what other options are available.
For example, here are some command line arguments documentation; of which
--devtools
sounds interesting:if (argv["help"]) { console.log("Options:"); console.log(" --profile-dir {path}: Path to where to store the profile."); console.log(" --profile {name}: Name of alternate profile to use, allows for running multiple accounts."); console.log(" --devtools: Install and use react-devtools and react-perf."); console.log(" --no-update: Disable automatic updating."); console.log(" --default-frame: Use OS-default window decorations."); console.log(" --hidden: Start the application hidden in the system tray."); console.log(" --help: Displays this help message."); console.log("And more such as --proxy, see:" + "https://electronjs.org/docs/api/command-line-switches"); electron_1.app.exit(); }We can also see some path loading aspects of where the app looks for
webapp.asar
:// Find the webapp resources and set up things that require them async function setupGlobals() { // find the webapp asar. asarPath = await tryPaths("webapp", __dirname, [ // If run from the source checkout, this will be in the directory above '../webapp.asar', // but if run from a packaged application, electron-main.js will be in // a different asar file so it will be two levels above '../../webapp.asar', // also try without the 'asar' suffix to allow symlinking in a directory '../webapp', // from a packaged application '../../webapp', // Workaround for developing beeper on windows, where symlinks are poorly supported. "../../nova-web/webapp", ]); console.log("Web App Path is", asarPath); iconsPath = await tryPaths("icons", __dirname, [ '../res/icons', '../../icons' ]); console.log("iconsPath path is", iconsPath); // eslint-disable-next-line @typescript-eslint/no-var-requires vectorConfig = require(asarPath + 'config.json'); console.log("Loading vector config for brand", vectorConfig.brand); try { // Load local config and use it to override values from the one baked with the build // eslint-disable-next-line @typescript-eslint/no-var-requires const localConfig = require(path_1.default.join(electron_1.app.getPath('userData'), 'config.json'));There are also some hidden/undocumented CLI arguments,
localdev
/localapi
:const localdev = Array.isArray(argv._) && argv._.includes("localdev"); const localapi = Array.isArray(argv._) && argv._.includes("localapi");We find the code that processes the
--devtools
arg here:if (argv['devtools']) { try { const { default: installExt, REACT_DEVELOPER_TOOLS, REACT_PERF, } = require("electron-devtools-installer"); await installExt([REACT_DEVELOPER_TOOLS, REACT_PERF], { loadExtensionOptions: { allowFileAccess: true } }); } catch (e) { console.log(e); } }A little further down we see how
localdev
/localapi
are handled:if (localdev) { // Open dev tools at startup if in dev mode mainWindow.webContents.openDevTools(); electron_1.app.on("certificate-error", (event, webContents, url, error, certificate, callback) => { // On certificate error we disable default behaviour (stop loading the page) // and we then say "it is all fine - true" to the callback event.preventDefault(); callback(true); }); } if (localapi) { vectorConfig.novaApiUrl = `https://localhost:4001`; } mainWindow.loadURL(localdev ? "http://localhost:8080" : 'nova://nova-web/webapp/');Then beyond that, you're sort of getting deeper into the internals of Electron apps and how Beeper / Element is built on top of Electron; so really depends what you're wanting to achieve at that point.
While at the initial 'entrypoint' debugger breakpoint (from
--inspect-brk
), we could also choose to manually load/inject some custom code of our own. For example:With a code file like:
// /Users/devalias/Desktop/beeperInjectionHax.js console.log("Hello World, is this custom JS in Beeper?");We could run the following in the Chrome Devtools console while at the initial app loading debug breakpoint:
require('/Users/devalias/Desktop/beeperInjectionHax.js')Which would show an output message such as:
beeperInjectionHax.js:1 Hello World, is this custom JS in Beeper?
ponders 🤔
Anyone feel like figuring out/hacking out the plugin implementation from BetterDiscord and making it work as a Beeper customisation?
- https://betterdiscord.app/
-
BetterDiscord comes with a builtin plugin loader and plugin API. Plugins can increase the functionality and user experience of the app through JavaScript. Write your own or download plugins made by the community.
-
- https://betterdiscord.app/plugins
- https://github.com/BetterDiscord/BetterDiscord
- https://github.com/search?q=repo%3ABetterDiscord%2FBetterDiscord%20plugin&type=code
- https://github.com/BetterDiscord/BetterDiscord/blob/main/renderer/src/modules/pluginmanager.js#L28
- https://github.com/BetterDiscord/BetterDiscord/tree/main/injector/src
- https://github.com/BetterDiscord/BetterDiscord/tree/main/injector/src/modules
- https://github.com/BetterDiscord/BetterDiscord/blob/main/injector/src/modules/csp.js
- Strips content-security-policy headers from requests (in a rather 'blunt' way)
- https://github.com/BetterDiscord/BetterDiscord/blob/main/injector/src/modules/reactdevtools.js
- Finds/loads a local copy of ReactDevTools
- https://github.com/BetterDiscord/BetterDiscord/blob/main/injector/src/modules/ipc.js
- https://github.com/BetterDiscord/BetterDiscord/blob/main/injector/src/modules/csp.js
- https://github.com/BetterDiscord/BetterDiscord/tree/main/injector/src/modules
Another user from the 'Unofficial Beeper Hackers' community suggested the following as a better example for injection/etc concepts:
@emma:conduit.rory.gay
: GooseMod may be more interesting to look at (though dead), came with an in-app plugin/theme storedo note that most of the UI leans on pre-existing react modules (that no longer exist)
- https://github.com/GooseMod/GooseMod
- https://github.com/GooseMod/GooseMod/blob/master/src/moduleManager.js
- https://github.com/GooseMod/GooseMod/blob/master/src/index.js#L170
- https://github.com/GooseMod/GooseMod/blob/master/src/util/discord/webpackModules.js
- This module in particular looks interesting; it seems to be a bunch of helpers for injecting into webpack modules at runtime
@emma:conduit.rory.gay
: betterdiscord's injectors is litterally justconst BetterDiscord = require("./modules/betterdiscord").default;
oh, also intercepting all web requests to unset content-security-policy header
devalias: The other method I was pondering at one point was basically hijacking just enough to load a chrome browser extension, then implement the rest of it via those API's; but didn't explore too deeply into how limited they are within electron
- https://www.electronjs.org/docs/latest/api/extensions
-
Electron supports a subset of the Chrome Extensions API, primarily to support DevTools extensions and Chromium-internal extensions, but it also happens to support some other extension capabilities.
- https://www.electronjs.org/docs/latest/api/extensions#supported-extensions-apis
- A bunch, including the below
- https://www.electronjs.org/docs/latest/api/extensions#chromemanagement
- https://developer.chrome.com/docs/extensions/reference/api/management
-
The
chrome.management
API provides ways to manage the list of extensions/apps that are installed and running.
-
- https://developer.chrome.com/docs/extensions/reference/api/management
- https://www.electronjs.org/docs/latest/api/extensions#chromescripting
-
All features of this API are supported.
- https://developer.chrome.com/docs/extensions/reference/api/scripting
-
Use the
chrome.scripting
API to execute script in different contexts.
-
-
- https://www.electronjs.org/docs/latest/api/extensions#supported-extensions-apis
-
Electron only supports loading unpacked extensions (i.e., .crx files do not work). Extensions are installed per-session. To load an extension, call
ses.loadExtension
-
Some of my notes in gists might be useful too:
Most likely these:
- https://gist.github.com/0xdevalias/428e56a146e3c09ec129ee58584583ba#debugging-electron-apps-and-related-memory-issues
- https://gist.github.com/0xdevalias/3d2f5a861335cc1277b21a29d1285cfe#some-notes-on-how-to-achieve-custom-beeper--electron-js-hackscustomisations-eg-more-customizability-than-css-hacks-alone
-
Me: you could inject/hijack/patch webpack/etc modules directly at runtime: https://gist.github.com/0xdevalias/8c621c5d09d780b1d321bfdb86d67cdd#reverse-engineering-webpack-apps
// Inbox - Favourites List
$$('.rooms > .favourites [data-type="bp_RoomTile"]')
// Inbox - Favourites List - Unread
$$('.rooms > .favourites .isUnread[data-type="bp_RoomTile"]')
// Inbox - Favourites List - Muted
$$('.rooms > .favourites .isMuted[data-type="bp_RoomTile"]')
// Inbox Chats
$$('.rooms > .rooms_scroll-container [data-type="bp_RoomTile"]')
// Inbox Chats - Favourite
$$('.rooms > .rooms_scroll-container [data-type="bp_RoomTile"]:has([data-src="img/beeper/heart-filled16.b7ad82d.svg"])')
// Inbox Chats - Pinned
$$('.rooms > .rooms_scroll-container [data-type="bp_RoomTile"]:has([data-src="img/beeper/pin-filled16.b7cb2af.svg"])')
// Inbox Chats - Not Pinned
$$('.rooms > .rooms_scroll-container [data-type="bp_RoomTile"]:not(:has([data-src="img/beeper/pin-filled16.b7cb2af.svg"]))')
// Inbox - Unread - Favourite-Avatar
$$('.rooms > .rooms_scroll-container [data-type="bp_RoomTile"] > div > .favourite-avatar+div > div > div > span:not(.bp_icon)+span > .bp_icon')
// Inbox - Unread - Avatar
$$('.rooms > .rooms_scroll-container [data-type="bp_RoomTile"] > div > .avatar+div > div > div > span:not(.bp_icon)+span > .bp_icon')
// Inbox - Unread - Combined
$$('.rooms > .rooms_scroll-container [data-type="bp_RoomTile"] > div > .favourite-avatar+div > div > div > span:not(.bp_icon)+span > .bp_icon, .rooms > .rooms_scroll-container [data-type="bp_RoomTile"] > div > .avatar+div > div > div > span:not(.bp_icon)+span > .bp_icon')
This is a bit of a hacky WIP / PoC, but it seems to do the trick:
/*************************/
/* Counting Chat Types */
/************************/
/* Initialize counters */
.rooms {
counter-reset: unread favourite pinned not-pinned;
}
/* Increment favorites */
.rooms > .rooms_scroll-container [data-type="bp_RoomTile"] [data-src="img/beeper/heart-filled16.b7ad82d.svg"] {
counter-increment: favourite;
}
/* Increment pinned */
.rooms > .rooms_scroll-container [data-type="bp_RoomTile"] [data-src="img/beeper/pin-filled16.b7cb2af.svg"] {
counter-increment: pinned;
}
/* Increment not pinned */
.rooms > .rooms_scroll-container [data-type="bp_RoomTile"]:not(:has([data-src="img/beeper/pin-filled16.b7cb2af.svg"])) {
counter-increment: not-pinned;
}
/* Increment unread */
.rooms > .rooms_scroll-container [data-type="bp_RoomTile"] > div > .favourite-avatar+div > div > div > span:not(.bp_icon)+span > .bp_icon,
.rooms > .rooms_scroll-container [data-type="bp_RoomTile"] > div > .avatar+div > div > div > span:not(.bp_icon)+span > .bp_icon {
counter-increment: unread;
}
/* Show Counters */
/*.rooms::after {
content: "Counters: Unread (" counter(unread) "), Favourite (" counter(favourite) "), Pinned (" counter(pinned) "), Not-Pinned (" counter(not-pinned) ")"
}*/
.rooms::after {
content: "Unread (" counter(unread) "), Not-Pinned (" counter(not-pinned) ")";
font-size: 10px;
position: absolute;
top: calc(31vh - 2px);
left: calc(8vw - 4px);
background-color: dimgrey;
}
/* Set position to relative for the hovered element */
.rooms > .rooms_scroll-container [data-type="bp_RoomTile"] {
position: relative;
}
/* Show the not-pinned counter on hover */
.rooms > .rooms_scroll-container [data-type="bp_RoomTile"]:hover::before {
content: "Not-Pinned above: " counter(not-pinned);
font-size: 10px;
white-space: nowrap;
position: absolute;
bottom: 5px;
left: 50%;
transform: translateX(-50%);
z-index: 1000;
border-radius: 3px;
padding: 2px;
background-color: dimgrey;
}
@jorgeparamos Glad it's helpful :)
@jorgeparamos This took a while to figure out, as it wasn't super clear where the background colours for the messages were even coming from at first..
The DOM structure of a message in a chat looks like this, with the particularly interesting bit for the colours of the message body seeming to be the
<div class="mx_EventTile_line mx_EventTile_line--with-ts">
:Looking at the CSS styles applied here, I can see some rules in
theme-dark.css
that seem to impact these messages + set a background on them; and then there are some other rules that also seem to match for the replies part of things:From a quick 'playing around', it seems like toggling off the backgrounds in all of those rules removes them from the chat:
If we then write some rules like this, we can set all of the bits of the backgrounds to the colours we choose (there seems to be the 'main bubble', the 'bubble tail', and then the 'reply section':
Looking a little closer at the DOM structure and the CSS classes applied to it, the following look interesting/potentially useful:
If we were wanting to target/set a colour for a specific username, it seems we could write a CSS selector that matches against the username in the
title
attr of the profile picture with something like this (not that that is necessarily helpful to the usecase here, but figured i'd note it as I explored things):My first thought was that maybe we could do something tricky using CSS variables, but the easiest way for that to work would have been if all of the 'grouped messages' of a user had the same parent element we could target, but it seems that's not the case:
My next thought is that perhaps we could do something tricky using CSS counters somehow, though not exactly sure what that would be/how it would work exactly (if it's even possible); and even if we do manage to make it work, I'm not sure that it would easily allow us to always keep the same colour mapped to the same user:
The high level of my thought with CSS counters is that maybe we could have it set a colour value at the start of a group of messages, use that throughout those messages, and then have it reset at the end of that group (or at the start of the next one); but I'm not even sure if that is actually possible.
This is the type of thing that would be almost trivial to achieve with JavaScript/similar; but through CSS alone might be super challenging if not impossible unfortunately.
@jorgeparamos This isn't something I'm deeply familiar with, but I found the following resources for how to achieve this with an electron app (which is what Beeper is built on):
Based on that, the short answer is that it relies on being able to change JavaScript in some of the core internals of Beeper/Electron, so I believe that it's basically not possible to do with just CSS changes alone.
If we start Beeper with remote debugging enabled (eg.
/Applications/Beeper.app/Contents/MacOS/Beeper --inspect-brk=1337
), then connect to it from Chrome DevTools (chrome://inspect
), then we can search the source forBrowserWindow
, which we find inelectron-main.js
. There are 2 main instances of this:Looking at the options passed to that 2nd
BrowserWindow
, we can see that it setsframe
asframe: argv['default-frame'] ?? false
, implying that it might be possible to influence this via command line arguments; but there doesn't seem to be atransparent
option defined at all.Even if I hack the source to enable
transparent
in the debugger, and then manually crawl through the various DOM nodes with background colours and set them to transparent as well, it still seems to give me a blurred/'frosted' looking transparent, rather than purely transparent (which weirdly seems to become opaque again when I don't have the Beeper window selected):I'll also note that there are these settings in the standard Beeper settings, that give some transparency, but also not the full transparency you seem to be after:
Though looking at the code, they might only be shown to Mac users for some reason (even though apparently electron for windows can handle transparency as well):
Looking in
theme-dark.css
via Beeper's DevTools, there seem to be a lot of CSS variables that could be overridden to tweak things. I haven't looked too deeply at them, but the 'Transparency - Chat View' settings toggle seems to interact with the--main-panel-transparent-bg
CSS variable; so that might potentially be an area worth looking into more:There may be some other CSS properties interacting with this 'frosted'/'blurred' transparency, such as
background-blend-mode
orblur
or otherwise.. or it might just be a limitation in how electron does things in general. Hard to say for sure without diving deeper into things: