- 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;
}
Amazing work @0xdevalias, thank you!
Two suggestions/requests (that may have been covered elsewher, but I couldn't find them) to achieve the effect in the mock-up:
Any help is appreciated: from sharing a full solution to just pointing me to which CSS elements are relevant. Thanks!