Skip to content

Instantly share code, notes, and snippets.

@zurfyx
Last active February 23, 2026 16:22
Show Gist options
  • Select an option

  • Save zurfyx/be68a2b732369d69de3ea3bb5e50389a to your computer and use it in GitHub Desktop.

Select an option

Save zurfyx/be68a2b732369d69de3ea3bb5e50389a to your computer and use it in GitHub Desktop.
Beautify Facebook Messages (Messenger) TamperMonkey script -- Note that Messenger.com won't be available after April 14, 2026
// ==UserScript==
// @name Facebook -> Messenger Mode (flat + safer sizing)
// @namespace https://zurfyx.local/
// @version 3.1.1
// @description Toggle Messenger mode: redirect, hide header, set --header-height=0, force 100vh wrapper chains, remove borders/padding.
// @match https://www.facebook.com/*
// @match https://facebook.com/*
// @run-at document-start
// @grant none
// ==/UserScript==
(() => {
'use strict';
const TARGET_MESSAGES = 'https://www.facebook.com/messages';
const FLAG_KEY = 'tm_fb_messenger_mode';
const STORAGE = 'session'; // 'session' or 'local'
const store = STORAGE === 'local' ? localStorage : sessionStorage;
// Left pane width clamp (you asked for this too; minimal, no min-width hacks)
const LEFT_PANE_WIDTH = 'clamp(320px, 30vw, 420px)';
const STYLE_ID = 'tm-fb-messenger-style';
const BTN_ID = 'tm-fb-messenger-toggle';
const INLINE_BTN_ID = 'tm-fb-messenger-exit-inline';
const enabled = () => store.getItem(FLAG_KEY) === '1';
const setEnabled = (v) => (v ? store.setItem(FLAG_KEY, '1') : store.removeItem(FLAG_KEY));
const restricted = (p) =>
p.startsWith('/login') || p.startsWith('/checkpoint') || p.startsWith('/recover');
function getMount() {
return document.querySelector('div[id^="mount_"]') || document.body;
}
function findThreadList() {
return (
document.querySelector('[role="navigation"][aria-label="Thread list"]') ||
document.querySelector('[aria-label="Thread list"]')
);
}
function findConversationMain() {
return document.querySelector('[role="main"]');
}
function closestCommonAncestor(a, b) {
if (!a || !b) return null;
const seen = new Set();
let x = a;
for (let i = 0; x && i < 40; i++) {
seen.add(x);
x = x.parentElement;
}
let y = b;
for (let i = 0; y && i < 40; i++) {
if (seen.has(y)) return y;
y = y.parentElement;
}
return null;
}
// Find the split-pane container (flex row) that holds both thread list and main
function findSplitContainer(threadList, main) {
const common = closestCommonAncestor(threadList, main);
if (!common) return null;
// Walk upward a few steps to find a flex row container
let el = common;
for (let i = 0; el && i < 10; i++) {
const cs = getComputedStyle(el);
if (cs.display.includes('flex') && (cs.flexDirection === 'row' || cs.flexDirection === 'row-reverse')) {
return el;
}
el = el.parentElement;
}
return common;
}
// Find direct child of split that contains a node (for width pinning)
function getDirectChildUnder(split, node) {
if (!split || !node) return null;
let el = node;
for (let i = 0; el && i < 60; i++) {
if (el.parentElement === split) return el;
el = el.parentElement;
if (el === split) break;
}
return null;
}
// ---------------- CSS ----------------
function injectCss() {
if (document.getElementById(STYLE_ID)) return;
const style = document.createElement('style');
style.id = STYLE_ID;
style.textContent = `
:root { --header-height: 0px !important; }
body { --header-height: 0px !important; }
html, body { height: 100% !important; min-height: 100% !important; }
body, #facebook, div[id^="mount_"] { background: #fff !important; }
[data-pagelet="TopBar"],
header[role="banner"],
div[role="banner"] {
display: none !important;
height: 0 !important;
min-height: 0 !important;
max-height: 0 !important;
overflow: hidden !important;
}
[role="main"] div,
[aria-label="Thread list"] div {
box-shadow: none !important;
}
`;
document.documentElement.appendChild(style);
}
function pinHeaderVar() {
document.documentElement.style.setProperty('--header-height', '0px', 'important');
if (document.body) document.body.style.setProperty('--header-height', '0px', 'important');
}
function hideBannerInline() {
document
.querySelectorAll('[data-pagelet="TopBar"], header[role="banner"], div[role="banner"]')
.forEach((el) => el.style.setProperty('display', 'none', 'important'));
}
// ---------------- Overrides ----------------
// Exactly what you showed (no min-width!)
const OVERRIDES = [
['--header-height', '0px'],
['height', '100vh'],
['min-height', '100vh'],
['max-height', '100%'],
['padding', '0px'],
['margin', '0px'],
['border', 'none'],
['border-radius', '0px'],
['box-shadow', 'none'],
['background', '#fff'],
['outline', 'none'],
];
function applyOverrides(el) {
if (!el || !el.style) return;
for (const [k, v] of OVERRIDES) el.style.setProperty(k, v, 'important');
// Only apply flex growth if this is a flex *column* container.
// This avoids messing with the split-pane widths (flex row).
const cs = getComputedStyle(el);
if (cs.display.includes('flex') && (cs.flexDirection === 'column' || cs.flexDirection === 'column-reverse')) {
el.style.setProperty('flex', '1 1 auto', 'important');
// ✅ IMPORTANT FIX:
// DO NOT clobber min-height:100vh with min-height:0px.
// That was undoing your own override and causing the bottom gap chase.
// (If you ever need "min-height:0" for a specific scroll container, we can target it explicitly later.)
}
}
function applyChainUp(anchor, stopAt) {
const mount = stopAt || getMount();
if (!anchor || !mount) return;
// Ensure mount can stretch but DO NOT set flex/width-related things on it.
mount.style.setProperty('min-height', '100vh', 'important');
mount.style.setProperty('height', '100%', 'important');
mount.style.setProperty('background', '#fff', 'important');
let el = anchor;
for (let i = 0; el && i < 32; i++) {
applyOverrides(el);
if (el === mount) break;
el = el.parentElement;
}
}
// ✅ Apply min-height:100vh to the *specific* descendants you showed:
// "selected wrapper + its children" (2 levels) under each panel.
function apply100vhDown(anchor, levels = 2) {
if (!anchor) return;
let el = anchor;
for (let i = 0; el && i < levels; i++) {
const kids = Array.from(el.children || []);
const next = kids.find(k => /^(DIV|SECTION|MAIN|NAV|ASIDE)$/.test(k.tagName)) || kids[0];
if (!next) break;
next.style.setProperty('min-height', '100vh', 'important');
next.style.setProperty('--header-height', '0px', 'important');
el = next;
}
}
// Fix containers with calc-based max-height (e.g. calc(100vh - 2px - var(--header-height)))
// Only touches elements whose computed max-height is close to but less than viewport height.
function fixRestrictiveMaxHeights() {
const vh = window.innerHeight;
const anchors = [findConversationMain(), findThreadList()];
for (const anchor of anchors) {
if (!anchor) continue;
let el = anchor;
for (let i = 0; i < 8; i++) {
const kids = Array.from(el.children || []);
const next = kids.find(k => /^(DIV|SECTION|MAIN|NAV|ASIDE)$/.test(k.tagName)) || kids[0];
if (!next) break;
const mh = parseFloat(getComputedStyle(next).maxHeight);
if (Number.isFinite(mh) && mh > vh * 0.8 && mh < vh) {
next.style.setProperty('max-height', '100vh', 'important');
next.style.setProperty('min-height', '100vh', 'important');
}
el = next;
}
}
}
// Force a reasonable left pane width (no min-width)
function pinLeftPaneWidth(split, threadList, main) {
if (!split || !threadList) return;
const leftPane = getDirectChildUnder(split, threadList);
if (!leftPane) return;
leftPane.style.setProperty('flex', `0 0 ${LEFT_PANE_WIDTH}`, 'important');
leftPane.style.setProperty('width', LEFT_PANE_WIDTH, 'important');
leftPane.style.setProperty('max-width', LEFT_PANE_WIDTH, 'important');
// Ensure right side can flex without causing weird width swings
const rightPane = getDirectChildUnder(split, main);
if (rightPane) rightPane.style.setProperty('flex', '1 1 auto', 'important');
}
// Kill 1px dividers + wrapper padding, but ONLY on large layout wrappers (not bubbles)
function flattenSplitPaneChrome(split) {
if (!split) return;
// include split + a bounded subtree scan
const nodes = split.querySelectorAll('div, section, nav, main, aside');
const cap = Math.min(nodes.length, 1200);
for (let i = 0; i < cap; i++) {
const el = nodes[i];
const cs = getComputedStyle(el);
// Only touch large wrappers
const big = el.clientHeight > 200 && el.clientWidth > 240;
if (!big) continue;
// Borders (common split divider)
const bw =
(parseFloat(cs.borderLeftWidth) || 0) +
(parseFloat(cs.borderRightWidth) || 0) +
(parseFloat(cs.borderTopWidth) || 0) +
(parseFloat(cs.borderBottomWidth) || 0);
// Padding (wrapper gutters)
const pad =
(parseFloat(cs.paddingLeft) || 0) +
(parseFloat(cs.paddingRight) || 0) +
(parseFloat(cs.paddingTop) || 0) +
(parseFloat(cs.paddingBottom) || 0);
// Shadows
const hasShadow = cs.boxShadow && cs.boxShadow !== 'none';
if (bw > 0) el.style.setProperty('border', 'none', 'important');
if (pad >= 8) el.style.setProperty('padding', '0px', 'important');
if (hasShadow) el.style.setProperty('box-shadow', 'none', 'important');
// Force white background to blend flat
if (cs.backgroundColor !== 'rgba(0, 0, 0, 0)') {
el.style.setProperty('background', '#fff', 'important');
}
}
}
// Hide the tablist tabs above the chat list, keep search and header.
function hideThreadListChrome() {
const nav = findThreadList();
if (!nav) return;
nav.querySelectorAll('[role="tablist"]').forEach(tl => {
// Walk up to hide the largest container that only holds this tablist,
// stopping before we reach one that also contains the search or header
let el = tl;
while (el.parentElement && el.parentElement !== nav) {
const p = el.parentElement;
if (p.querySelector('[aria-label="Search Messenger"], [aria-label="Settings, help and more"], [aria-label="New message"]')) break;
el = p;
}
el.style.setProperty('display', 'none', 'important');
});
}
// FB margin hack becomes pathological at header-height 0
function neutralizeVhMarginHack() {
const root = getMount();
if (!root) return;
const approxMinusVh = -window.innerHeight;
const nodes = root.querySelectorAll('div');
for (let i = 0; i < nodes.length && i < 1600; i++) {
const el = nodes[i];
const cs = getComputedStyle(el);
const mb = parseFloat(cs.marginBottom || '0');
if (!Number.isFinite(mb)) continue;
const looksLikeHack = mb < approxMinusVh * 0.75 && Math.abs(mb - approxMinusVh) < 180;
if (!looksLikeHack) continue;
el.style.setProperty('margin-bottom', '0px', 'important');
applyOverrides(el);
break;
}
}
// ---------------- Main apply ----------------
function applyMessengerMode() {
if (!enabled()) return;
if (!location.pathname.startsWith('/messages')) return;
injectCss();
pinHeaderVar();
hideBannerInline();
const mount = getMount();
const threadList = findThreadList();
const main = findConversationMain();
const split = findSplitContainer(threadList, main);
// Replicate your override style chains for both panels
applyChainUp(threadList, mount);
applyChainUp(main, mount);
// ✅ Apply 100vh to the specific descendants you showed (selected + its children)
// Do it for both panes.
apply100vhDown(threadList);
apply100vhDown(main);
// Left pane width clamp
pinLeftPaneWidth(split, threadList, main);
// Flatten the split pane chrome (borders/padding/shadows)
flattenSplitPaneChrome(split);
// Fix the calc(-100vh + var(--header-height)) issue at header-height 0
neutralizeVhMarginHack();
// Fix calc-based max-height gaps
fixRestrictiveMaxHeights();
// Hide search/tabs chrome above chat list
hideThreadListChrome();
// Place exit button inline in menu bar
ensureInlineExitButton();
}
// ---------------- SPA scheduling ----------------
let raf = false;
function schedule() {
if (raf) return;
raf = true;
requestAnimationFrame(() => {
raf = false;
applyMessengerMode();
});
}
let observer = null;
function ensureObserver() {
if (observer) return;
observer = new MutationObserver(schedule);
observer.observe(document.documentElement, { subtree: true, childList: true });
}
function applyIfNeeded() {
if (!enabled()) return;
if (restricted(location.pathname)) return;
if (!location.pathname.startsWith('/messages')) {
location.assign(TARGET_MESSAGES);
return;
}
applyMessengerMode();
ensureObserver();
}
// ---------------- Inline exit button (inside menu bar) ----------------
function ensureInlineExitButton() {
if (!enabled()) return;
if (!location.pathname.startsWith('/messages')) return;
if (document.getElementById(INLINE_BTN_ID)) return;
const settingsBtn = document.querySelector('[aria-label="Settings, help and more"]');
if (!settingsBtn) return;
// Walk up to the div.x1diwwjn wrapper that contains the settings button
let wrapper = settingsBtn;
for (let i = 0; i < 5; i++) {
if (!wrapper.parentElement) break;
wrapper = wrapper.parentElement;
if (wrapper.tagName === 'DIV' && wrapper.parentElement && wrapper.parentElement.children.length > 1) break;
}
const container = wrapper.parentElement;
if (!container) return;
const btn = document.createElement('div');
btn.id = INLINE_BTN_ID;
btn.role = 'button';
btn.tabIndex = 0;
btn.title = 'Exit Messenger mode';
// Use an "X" SVG icon to match the other circular icon buttons
btn.innerHTML = `<svg viewBox="0 0 20 20" width="20" height="20" fill="currentColor"><path d="M6.28 5.22a.75.75 0 0 0-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 1 0 1.06 1.06L10 11.06l3.72 3.72a.75.75 0 1 0 1.06-1.06L11.06 10l3.72-3.72a.75.75 0 0 0-1.06-1.06L10 8.94 6.28 5.22z"/></svg>`;
// Copy dimensions and shape from the settings button
const settingsCs = getComputedStyle(settingsBtn);
btn.style.cssText = `
display: flex;
align-items: center;
justify-content: center;
width: ${settingsCs.width};
height: ${settingsCs.height};
border-radius: 50%;
cursor: pointer;
background: transparent;
border: none;
color: ${settingsCs.color};
user-select: none;
`;
btn.addEventListener('mouseenter', () => { btn.style.background = 'rgba(0,0,0,0.05)'; });
btn.addEventListener('mouseleave', () => { btn.style.background = 'transparent'; });
btn.addEventListener('mousedown', () => { btn.style.background = 'rgba(0,0,0,0.1)'; });
btn.addEventListener('mouseup', () => { btn.style.background = 'rgba(0,0,0,0.05)'; });
btn.addEventListener('click', () => {
setEnabled(false);
location.assign('https://www.facebook.com/');
});
// Wrap in a div matching the sibling wrappers
const btnWrapper = document.createElement('div');
if (wrapper.className) btnWrapper.className = wrapper.className;
btnWrapper.appendChild(btn);
container.insertBefore(btnWrapper, container.firstChild);
// Hide floating button when inline is visible
const floatingBtn = document.getElementById(BTN_ID);
if (floatingBtn) floatingBtn.style.display = 'none';
}
// ---------------- Toggle button ----------------
function ensureButton() {
if (document.getElementById(BTN_ID)) return;
const btn = document.createElement('button');
btn.id = BTN_ID;
btn.type = 'button';
const refresh = () => {
btn.textContent = enabled() ? 'Exit Messenger mode' : 'Messenger mode';
// Hide floating button when inline exit is present, or on restricted pages
const inlinePresent = !!document.getElementById(INLINE_BTN_ID);
if (restricted(location.pathname)) {
btn.style.display = 'none';
} else if (inlinePresent) {
btn.style.display = 'none';
} else {
btn.style.display = 'block';
}
};
btn.style.cssText = `
position: fixed;
right: 12px;
bottom: 12px;
z-index: 2147483647;
padding: 10px 12px;
border-radius: 999px;
border: 1px solid rgba(0,0,0,.12);
background: rgba(255,255,255,.92);
color: #111;
font: 13px/1 -apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica,Arial,sans-serif;
cursor: pointer;
box-shadow: 0 6px 18px rgba(0,0,0,.12);
backdrop-filter: blur(8px);
`;
btn.addEventListener('click', () => {
if (!enabled()) {
setEnabled(true);
refresh();
applyIfNeeded();
} else {
setEnabled(false);
refresh();
location.assign('https://www.facebook.com/');
}
});
document.documentElement.appendChild(btn);
refresh();
hookSpa(refresh);
}
function hookSpa(onChange) {
const _push = history.pushState;
const _replace = history.replaceState;
if (history.__tm_fb_hooked) return;
history.__tm_fb_hooked = true;
history.pushState = function (...args) {
const ret = _push.apply(this, args);
queueMicrotask(() => { onChange(); applyIfNeeded(); });
return ret;
};
history.replaceState = function (...args) {
const ret = _replace.apply(this, args);
queueMicrotask(() => { onChange(); applyIfNeeded(); });
return ret;
};
window.addEventListener('popstate', () => { onChange(); applyIfNeeded(); });
}
// ---------------- Boot ----------------
applyIfNeeded();
const startUI = () => ensureButton();
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', startUI, { once: true });
} else {
startUI();
}
window.addEventListener('load', applyIfNeeded);
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment