Skip to content

Instantly share code, notes, and snippets.

@0xdevalias
Last active November 10, 2024 15:01
Show Gist options
  • Save 0xdevalias/3d2f5a861335cc1277b21a29d1285cfe to your computer and use it in GitHub Desktop.
Save 0xdevalias/3d2f5a861335cc1277b21a29d1285cfe to your computer and use it in GitHub Desktop.
Custom themes/CSS styling hacks/overrides for Beeper (universal chat app aggregator, built on top of matrix)

Beeper Custom Theme Styles

Table of Contents

devalias' Beeper CSS Hacks

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:

See Also

My Other Related Deepdive Gist's and Projects

Bonus Section

Accessing the Matrix client from within Beeper / Electron's DevTools

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"
})

Enabling 'internal' mode to see extra labs features/etc

(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)

Accessing the React internals for the Beeper 'Rooms' component using Beeper / Electron's DevTools

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, and webapp.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 via npx 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_modules

We 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=1337

Which 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:

image

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:

image

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?

image

Implement plugin manager implementation (like BetterDiscord / GooseMod / etc have)

ponders 🤔

Anyone feel like figuring out/hacking out the plugin implementation from BetterDiscord and making it work as a Beeper customisation?

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 store

do note that most of the UI leans on pre-existing react modules (that no longer exist)

@emma:conduit.rory.gay: betterdiscord's injectors is litterally just const 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

Some of my notes in gists might be useful too:

Most likely these:

Various Beeper Inbox Selectors (Favourite, Pinned, Not Pinned, Unread, Etc)

// 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')

Counting chats that match the above

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;
}

Beeper - Password Reset (JWT)

See the following repo for my notes (formatted as a ChatGPT prompt) on implementing a JWT-based password reset flow using Beeper's new 'email login' flow; as well as Proof of Concept (PoC) code implementing a CLI tool for using this flow.

/* Will this work with https://developer.mozilla.org/en-US/docs/Web/CSS/@import ? I don't think so.. but maybe I was using it wrong..? */
/***************************************************************/
/* De-emphasise distracting chats - *Beeper* Community/Hackers */
/***************************************************************/
.bp_LeftPanel:not(:has(svg[data-src="img/beeper/back16.024b7d1.svg"])) div[data-type="bp_RoomTile"]:has(.nv_BridgedIcon_beeper, svg[data-src="img/beeper/color-beeper16.1c8391b.svg"]):has(div[title*="Beeper"i]):has(div[title*="Community"i], div[title*="Hackers"i]) {
opacity: 0.5;
}
/*******************************************************/
/* De-emphasise distracting chats - UC Outdoors Club */
/*******************************************************/
.bp_LeftPanel:not(:has(svg[data-src="img/beeper/back16.024b7d1.svg"])) div[data-type="bp_RoomTile"]:has(.nv_BridgedIcon_facebook, svg[data-src="img/beeper/color-facebook16.a8cac84.svg"]):has(div[title*="UC Outdoors Club"]) {
opacity: 0.5;
}
/**********************************************/
/* Hide Left Panel/Sidebar While Chat Is Open */
/**********************************************/
/*.mx_MatrixChat:has(> .bp_MainPanel > .mx_RoomView) > .bp_LeftPanel {
display: none;
}
.mx_MatrixChat:has(> .bp_MainPanel > .mx_RoomView) > div:has(.spaceBar) {
display: none;
}*/
/*************************/
/* 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;
}
/******************************************************************/
/* Ensure 'report bug' dialog doesn't take over the entire screen */
/******************************************************************/
#mx_Dialog_Container div[aria-describedby="report_bug"] {
/* Don't take up the entire screen */
width: fit-content;
height: fit-content;
padding: 20px;
border-radius: 20px;
/* Center the dialog */
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
/**********************/
/* Main Settings Menu */
/**********************/
#mx_ContextualMenu_Container .mx_AccessibleButton[aria-label="Chat Networks"] {
display: none;
}
#mx_ContextualMenu_Container .mx_AccessibleButton[aria-label="Mark All As Read"] {
display: none;
}
#mx_ContextualMenu_Container .mx_AccessibleButton[aria-label="Check for Update"] {
display: none;
}
#mx_ContextualMenu_Container .mx_AccessibleButton[aria-label="Download Mobile App"] {
display: none;
}
/*******************/
/* Settings Dialog */
/*******************/
/* Settings Dialog - Tab - Chat Networks */
.mx_SettingsDialog_content > .mx_TabbedView > .mx_TabbedView_tabLabels > .mx_TabbedView_tabLabel:has(> span.mx_UserSettingsDialog_chatSettingsIcon) > .mx_TabbedView_tabLabel_text::after {
content: "🔗";
margin-left: 16px;
}
/* Settings Dialog - Tab - Manage Subscription */
.mx_SettingsDialog_content > .mx_TabbedView > .mx_TabbedView_tabLabels > .mx_TabbedView_tabLabel:has(> span.mx_UserSettingsDialog_manageSubscriptionIcon) > .mx_TabbedView_tabLabel_text::after {
content: "🔗";
margin-left: 16px;
}
/* Settings Dialog - Appearance - Custom CSS TextArea */
.mx_AppearanceUserSettingsTab .mx_Field.mx_Field_textarea {
width: 100% !important;
height: 500px !important;
}
.mx_AppearanceUserSettingsTab .mx_Field.mx_Field_textarea textarea {
font-family: Menlo, monospace;
font-size: 13px !important;
}
/**********************/
/* Inbox - Favourites */
/**********************/
/* Inbox - Favourites */
.bp_LeftPanel .rooms > .favourites {
/* Custom Variables */
--devalias-fav-section-max-height: 30vh;
--devalias-fav-grid-row-gap: 6px;
--devalias-fav-grid-col-gap: 6px;
--devalias-fav-width: 100%;
--devalias-fav-height: fit-content;
--devalias-fav-max-width: 64px;
--devalias-fav-avatar-width: 30px;
--devalias-fav-avatar-height: 30px;
--devalias-fav-avatar-font-size: 17px;
}
.bp_LeftPanel .rooms > .favourites > .favourites__icons > .favourites__tiles {
max-height: var(--devalias-fav-section-max-height, none) !important;
overflow: auto;
padding-left: unset;
padding: 0;
/* Favourites Grid Spacing */
grid-row-gap: unset;
grid-gap: unset;
gap: unset;
row-gap: var(--devalias-fav-grid-row-gap, 12px);
}
.bp_LeftPanel .rooms > .favourites > .favourites__icons > .favourites__tiles > .favourites__row {
height: fit-content;
/* Favourites Grid Spacing */
grid-gap: unset;
gap: unset;
column-gap: var(--devalias-fav-grid-col-gap, 12px);
}
.bp_LeftPanel .rooms > .favourites > .favourites__icons > .favourites__tiles > .favourites__row > div {
height: var(--devalias-fav-height, 60px) !important;
}
.bp_LeftPanel .rooms .favourites .bp_RoomTile.small {
width: var(--devalias-fav-width, 100%) !important;
height: var(--devalias-fav-height, 48px) !important;
max-width: var(--devalias-fav-max-width, 100%) !important;
}
.bp_LeftPanel .rooms .favourites .bp_RoomTile.small .mx_BaseAvatar_image {
width: var(--devalias-fav-avatar-width, 44px) !important;
height: var(--devalias-fav-avatar-height, 44px) !important;
}
.bp_LeftPanel .rooms .favourites .bp_RoomTile.small .mx_BaseAvatar_initial {
font-size: var(--devalias-fav-avatar-font-size, 19.8px) !important;
width: var(--devalias-fav-avatar-width, 44px) !important;
line-height: var(--devalias-fav-avatar-width, 44px) !important;
}
.bp_LeftPanel .rooms .favourites .bp_RoomTile.small .outline {
max-width: var(--devalias-fav-max-width, 110px) !important;
}
.bp_LeftPanel .rooms .favourites .bp_RoomTile.small .outline > span {
max-width: var(--devalias-fav-max-width, 110px) !important;
}
/************************/
/* Beeper Space Sidebar */
/************************/
/* Beeper Space Sidebar - Collapsed - Hide Floating 'Open Archive' Button */
.mx_MatrixChat > div > div > .mx_AccessibleButton:has(> .bp_icon > div > svg[data-src="img/beeper/archive16.2003809.svg"]) {
display: none;
}
/* Beeper Space Sidebar - Show Floating 'Archive All Read Messages' Button */
.mx_MatrixChat > div > div:has(.mx_AccessibleButton > .bp_icon > div > svg[data-src="img/beeper/new-sweep16.978771b.svg"]) {
width: max-content !important;
/*position: absolute;
top: calc(38vh);
left: 15vw;*/
}
.mx_MatrixChat > div > div > div:has(.mx_AccessibleButton > .bp_icon > div > svg[data-src="img/beeper/new-sweep16.978771b.svg"]) {
display: block !important;
}
.mx_MatrixChat > div > div > div > .mx_AccessibleButton:has(> .bp_icon > div > svg[data-src="img/beeper/new-sweep16.978771b.svg"]) {
opacity: unset;
}
/* Beeper Space Sidebar - Hide Original Icons */
#beeperSpaceBar svg:not(svg[data-src="img/beeper/square-inbox16.ea471fd.svg"]):not(svg[data-src="img/beeper/square-lowpriority16.6779879.svg"]):not(svg[data-src="img/beeper/square-archive16.b1ef8a0.svg"]):not(svg[data-src="img/beeper/square-bookmarks16.6853926.svg"]):not(svg[data-src="img/beeper/add-network16.1eb5cb1.svg"]) {
display: none
}
/* Beeper Space Sidebar - Use Lineart Icons */
#beeperSpaceBar > .mx_AccessibleButton:has(.bp_icon svg) > div > div > div {
width: 100%;
height: 100%;
background-repeat: no-repeat;
background-position: center;
background-size: contain;
}
/* Beeper Space Sidebar - Use Lineart Icons - Facebook */
#beeperSpaceBar > .mx_AccessibleButton:has(.bp_icon svg[data-src="img/beeper/square-facebook16.9d9e23d.svg"]) > div > div > div {
background-image: url("nova://nova-web/webapp/img/social/lineart-color/dark/messenger.1544eb2.svg");
}
/* Beeper Space Sidebar - Use Lineart Icons - Instagram */
#beeperSpaceBar > .mx_AccessibleButton:has(.bp_icon svg[data-src="img/beeper/square-instagram16.1e8ed4e.svg"]) > div > div > div {
background-image: url("nova://nova-web/webapp/img/social/lineart-color/dark/instagram.e9184e9.svg");
}
/* Beeper Space Sidebar - Use Lineart Icons - iMessage */
#beeperSpaceBar > .mx_AccessibleButton:has(.bp_icon svg[data-src="img/beeper/square-imessage16.11b6604.svg"]) > div > div > div {
background-image: url("nova://nova-web/webapp/img/social/lineart-color/dark/imessage.aedae37.svg");
}
/* Beeper Space Sidebar - Use Lineart Icons - Twitter */
#beeperSpaceBar > .mx_AccessibleButton:has(.bp_icon svg[data-src="img/beeper/square-twitter16.ddd9bad.svg"]) > div > div > div {
background-image: url("nova://nova-web/webapp/img/social/lineart-color/dark/twitter.972096c.svg");
}
/* Beeper Space Sidebar - Use Lineart Icons - Telegram */
#beeperSpaceBar > .mx_AccessibleButton:has(.bp_icon svg[data-src="img/beeper/square-telegram16.d011ded.svg"]) > div > div > div {
background-image: url("nova://nova-web/webapp/img/social/lineart-color/dark/telegram.77fa320.svg");
}
/* Beeper Space Sidebar - Use Lineart Icons - Signal */
#beeperSpaceBar > .mx_AccessibleButton:has(.bp_icon svg[data-src="img/beeper/square-signal16.85ba0c4.svg"]) > div > div > div {
background-image: url("nova://nova-web/webapp/img/social/lineart-color/dark/signal.315d199.svg");
}
/* Beeper Space Sidebar - Use Lineart Icons - LinkedIn */
#beeperSpaceBar > .mx_AccessibleButton:has(.bp_icon svg[data-src="img/beeper/square-linkedin16.f764edc.svg"]) > div > div > div {
background-image: url("nova://nova-web/webapp/img/social/lineart-color/dark/linkedin.2297fef.svg");
}
/* Beeper Space Sidebar - Use Lineart Icons - Discord */
#beeperSpaceBar > .mx_AccessibleButton:has(.bp_icon svg[data-src="img/beeper/square-discord16.e91cca3.svg"]) > div > div > div {
background-image: url("nova://nova-web/webapp/img/social/lineart-color/dark/discord.6daf490.svg");
}
/* Beeper Space Sidebar - Use Lineart Icons - Beeper */
#beeperSpaceBar > .mx_AccessibleButton:has(.bp_icon svg[data-src="img/beeper/square-beeper16.749ed9b.svg"]) > div > div > div {
background-image: url("nova://nova-web/webapp/img/social/lineart-color/dark/beeper.c685f80.svg");
}
/* Beeper Space Sidebar - Use Lineart Icons - WhatsApp */
#beeperSpaceBar > .mx_AccessibleButton:has(.bp_icon svg[data-src="img/beeper/square-whatsapp16.411f722.svg"]) > div > div > div {
background-image: url("nova://nova-web/webapp/img/social/lineart-color/dark/whatsapp.2210499.svg");
}
/*****************/
/* Uncategorized */
/*****************/
/* Ensure full image preview is visible, rather than a zoomed part of it */
.mx_EventTile_image .mx_MImageBody img.mx_MImageBody_thumbnail {
object-fit: contain !important;
}
@aronaoi
Copy link

aronaoi commented Dec 27, 2023

Is there any way to make Beeper Cloud act like a mobile app? I want to be able to shrink the window size to like 350 pixels, and it will display the chat list when i am not on an open chat, and clicking on a chat will bring me to the chat, and i can go back to the chat menu with a back button (basically, act the same way as Beeper Cloud mobile)

@0xdevalias
Copy link
Author

I want to be able to shrink the window size to like 350 pixels, and it will display the chat list when i am not on an open chat, and clicking on a chat will bring me to the chat, and i can go back to the chat menu with a back button (basically, act the same way as Beeper Cloud mobile)

@aronaoi Does this get you close to what you want?

@aronaoi
Copy link

aronaoi commented Dec 28, 2023

I want to be able to shrink the window size to like 350 pixels, and it will display the chat list when i am not on an open chat, and clicking on a chat will bring me to the chat, and i can go back to the chat menu with a back button (basically, act the same way as Beeper Cloud mobile)

@aronaoi Does this get you close to what you want?

Sorta. My issue is that I cannot resize the window size narrower. I want it to only display the Chat List when no chat is selected, and when clicking on a chat it will bring me to the chat messages like how Beeper Cloud on iPhone / Android works. Let’s say the window size minimum pixel is locked at 600 px but i want to be able to go even smaller like 300 px.

With the addition of a back button (like the mobile ver) if possible <3

@0xdevalias
Copy link
Author

My issue is that I cannot resize the window size narrower.

@aronaoi Did you also see the comment after that about changing the min-width on the left side panel? While it won't solve your issue completely, it might help a little?


I want it to only display the Chat List when no chat is selected, and when clicking on a chat it will bring me to the chat messages like how Beeper Cloud on iPhone / Android works.

@aronaoi The link I shared earlier (Ref) will hide the 'chat list' when a chat is selected, but it won't hide the 'main chat area' when none is selected; so some more work would need to be done on it if that was your desired outcome (though not even sure if it's possible, see below)


Let’s say the window size minimum pixel is locked at 600 px but i want to be able to go even smaller like 300 px.

@aronaoi From a quick ChatGPT/Google, it sounds like for an Electron app (which Beeper is), the minimum width of the window is set in the BrowserWindowConstructorOptions that are passed to the BrowserWindow when it is set up:

Unfortunately this lives on the 'Electron' side of things, and isn't something we are able to manipulate through CSS on the 'app' side of things. To do that, you would need to modify the relevant *.asar files, or perhaps inject some custom JS into Beeper at startup to be able to modify this; both of which are more involved than the typical CSS only changes here.

If you wanted to look deeper into doing this, you might find some useful notes/snippets on some of my gists, eg.:

By starting Beeper in Electron debug mode, and connecting to it from chrome://inspect, we can see the main BrowserWindow config Beeper is using; and how it has minWidth set to 660:

file:///Applications/Beeper.app/Contents/Resources/app.asar/lib/electron-main.js:

// app.asar/lib/electron-main.js; Lines 1282-1315
// ..snip..
const preloadScript = path_1.default.normalize(`${__dirname}/preload.js`);
    mainWindow = global.mainWindow = new electron_1.BrowserWindow({
        // https://www.electronjs.org/docs/faq#the-font-looks-blurry-what-is-this-and-what-can-i-do
        ...(isMac ? {} : { backgroundColor: "#fff" }),
        icon: iconPath,
        show: false,
        autoHideMenuBar: store.get("autoHideMenuBar", true),
        x: mainWindowState.x,
        y: mainWindowState.y,
        minWidth: 660,
        minHeight: 400,
        width: mainWindowState.width,
        height: mainWindowState.height,
        frame: argv["default-frame"] ?? false,
        titleBarStyle: isMac ? "hidden" : null,
        trafficLightPosition: { x: 16, y: 20 },
        webPreferences: {
            //devTools: false,
            //zoomFactor: 0.9,
            preload: preloadScript,
            nodeIntegration: true,
            //sandbox: true, // We enable sandboxing from app.enableSandbox() above
            // We don't use this: it's useful for the preload script to
            // share a context with the main page so we can give select
            // objects to the main page. The sandbox option isolates the
            // main page from the background script.
            spellcheck: true,
            contextIsolation: true,
            webgl: true,
            backgroundThrottling: false,
        },
        vibrancy: "fullscreen-ui",
        visualEffectState: "followWindow",
    });
// ..snip..

With the addition of a back button (like the mobile ver) if possible

@aronaoi Unfortunately I don't think that one will be possible with CSS alone (or at least, doing so is beyond my current skills/knowledge with it), so if you wanted to do that, it would also likely need to also be a JS hack/injection like I mentioned above.

@0xdevalias
Copy link
Author

0xdevalias commented Dec 29, 2023

RE: https://gist.github.com/0xdevalias/3d2f5a861335cc1277b21a29d1285cfe?permalink_comment_id=4808978#gistcomment-4808978

Would love to know how you ended up achieving this!

@jackmawer Which particular part are you referring to by 'this'?

Do you mean this part, from @jorgeparamos (Ref):

I understood half and managed to implement another half of that, and attained that translucid (i.e. sort of transparent, but blurred) look — which I find still benefits the app a lot.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment