From 3f12d9c9bded7cd5fa4a80bfc27cc3f1ed00fd60 Mon Sep 17 00:00:00 2001 From: Anatolii Date: Sun, 24 May 2026 21:33:25 -0700 Subject: [PATCH 01/16] Hide in-feed Who to follow suggestions --- .../src/modules/features/dynamic.js | 14 ++++++++++++-- .../src/modules/options/timeline.js | 19 +++++++++++++++++++ 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/content-scripts/src/modules/features/dynamic.js b/content-scripts/src/modules/features/dynamic.js index 2e66bed7..d61d9c0f 100644 --- a/content-scripts/src/modules/features/dynamic.js +++ b/content-scripts/src/modules/features/dynamic.js @@ -15,6 +15,7 @@ import { KeyHideViewCount, KeyListsButton, KeyRemoveTimelineTabs, + KeyRemoveTopicsToFollow, KeyTopicsButton, KeyTrendsHomeTimeline, KeyTypefullyGrowTab, @@ -24,7 +25,7 @@ import { } from "../../../../storage-keys"; import changeHideViewCounts from "../options/hideViewCount"; import { addAnalyticsButton, addCommunitiesButton, addListsButton, addTopicsButton, addXPremiumButton, hideGrokDrawer, changeNavigationButtonsLabels } from "../options/navigation"; -import { changeFollowingTimeline, changeRecentMedia, changeTimelineTabs, changeTrendsHomeTimeline, enableGrokDrawerOnGrokButtonClick } from "../options/timeline"; +import { changeFollowingTimeline, changeRecentMedia, changeTimelineTabs, changeTopicsToFollow, changeTrendsHomeTimeline, enableGrokDrawerOnGrokButtonClick } from "../options/timeline"; import { changeWriterMode } from "../options/writerMode"; import { addTypefullyComposerPlug, addTypefullyReplyPlug, saveCurrentReplyToLink, addTypefullySecurityAndAccountAccessPlug, addTypefullySchedulePlug } from "../typefullyPlugs"; import hideRightSidebar from "../utilities/hideRightSidebar"; @@ -70,6 +71,7 @@ export const dynamicFeatures = { changeWriterMode(data[KeyWriterMode]); } else { changeTimelineTabs(data[KeyRemoveTimelineTabs], data[KeyWriterMode]); + changeTopicsToFollow(data[KeyRemoveTopicsToFollow]); changeTrendsHomeTimeline(data[KeyTrendsHomeTimeline], data[KeyWriterMode]); changeFollowingTimeline(data[KeyFollowingTimeline]); } @@ -77,7 +79,15 @@ export const dynamicFeatures = { }; export const runDynamicFeatures = throttle(async () => { - const data = await getStorage([KeyWriterMode, KeyFollowingTimeline, KeyTrendsHomeTimeline, KeyRemoveTimelineTabs, KeyHideGrokDrawer, KeyNavigationButtonsLabels]); + const data = await getStorage([ + KeyWriterMode, + KeyFollowingTimeline, + KeyTrendsHomeTimeline, + KeyRemoveTimelineTabs, + KeyRemoveTopicsToFollow, + KeyHideGrokDrawer, + KeyNavigationButtonsLabels, + ]); if (data) { dynamicFeatures.general(); diff --git a/content-scripts/src/modules/options/timeline.js b/content-scripts/src/modules/options/timeline.js index 1aef790d..ce67a979 100644 --- a/content-scripts/src/modules/options/timeline.js +++ b/content-scripts/src/modules/options/timeline.js @@ -162,6 +162,7 @@ export const changeTopicsToFollow = (removeTopicsToFollow) => { switch (removeTopicsToFollow) { case "off": removeStyles("removeTopicsToFollow"); + document.querySelectorAll(".mt-whoToFollow").forEach((section) => section.classList.remove("mt-whoToFollow")); break; case "on": @@ -173,15 +174,33 @@ export const changeTopicsToFollow = (removeTopicsToFollow) => { ${selectors.mainColumn} a[href*="/i/topics/picker/home"] { display: none; } + .mt-whoToFollow { + display: none; + } [aria-label="Lists timeline"] section[aria-labelledby^="accessible-list-"] > div[aria-label$="Carousel"] { display: flex; } ` ); + hideWhoToFollowSuggestions(); break; } }; +function hideWhoToFollowSuggestions() { + const suggestionSections = document.querySelectorAll(`${selectors.mainColumn} section[aria-labelledby^="accessible-list-"]`); + + suggestionSections.forEach((section) => { + if (section.closest('[aria-label="Lists timeline"]')) return; + + const headingText = section.querySelector('h2, [role="heading"]')?.textContent?.trim().toLowerCase(); + + if (headingText === "who to follow") { + section.classList.add("mt-whoToFollow"); + } + }); +} + export const changeTimelineTabs = (removeTimelineTabs, writerMode) => { if (writerMode === "on" || window.location.pathname.includes("compose/tweet") || !window.location.pathname.includes("/home") || !window.location.pathname === "/") { removeStyles("removeTimelineTabs"); From d740a5e0f2fff3e699734cc87e847b32fd76fe71 Mon Sep 17 00:00:00 2001 From: Anatolii Date: Sun, 24 May 2026 22:10:05 -0700 Subject: [PATCH 02/16] Add Zen Writer Mode sidebar toggle --- .../src/modules/features/dynamic.js | 7 ++--- .../src/modules/features/static.js | 2 ++ .../src/modules/options/navigation.js | 26 +++++++++++++++++++ .../src/modules/options/writerMode.js | 10 ++++++- content-scripts/src/modules/svgAssets.js | 3 +++ css/main.css | 17 ++++++++++++ 6 files changed, 61 insertions(+), 4 deletions(-) diff --git a/content-scripts/src/modules/features/dynamic.js b/content-scripts/src/modules/features/dynamic.js index d61d9c0f..0af7dd96 100644 --- a/content-scripts/src/modules/features/dynamic.js +++ b/content-scripts/src/modules/features/dynamic.js @@ -24,7 +24,7 @@ import { KeyNavigationButtonsLabels } from "../../../../storage-keys"; import changeHideViewCounts from "../options/hideViewCount"; -import { addAnalyticsButton, addCommunitiesButton, addListsButton, addTopicsButton, addXPremiumButton, hideGrokDrawer, changeNavigationButtonsLabels } from "../options/navigation"; +import { addAnalyticsButton, addCommunitiesButton, addListsButton, addTopicsButton, addXPremiumButton, addZenWriterModeButton, hideGrokDrawer, changeNavigationButtonsLabels } from "../options/navigation"; import { changeFollowingTimeline, changeRecentMedia, changeTimelineTabs, changeTopicsToFollow, changeTrendsHomeTimeline, enableGrokDrawerOnGrokButtonClick } from "../options/timeline"; import { changeWriterMode } from "../options/writerMode"; import { addTypefullyComposerPlug, addTypefullyReplyPlug, saveCurrentReplyToLink, addTypefullySecurityAndAccountAccessPlug, addTypefullySchedulePlug } from "../typefullyPlugs"; @@ -55,7 +55,7 @@ export const dynamicFeatures = { navigation: (data) => { changeNavigationButtonsLabels(data[KeyNavigationButtonsLabels]); }, - sidebarButtons: async () => { + sidebarButtons: async (writerMode) => { const data = await getStorage([KeyListsButton, KeyCommunitiesButton, KeyTopicsButton, KeyXPremiumButton, KeyTypefullyGrowTab]); if (!data) return; @@ -65,6 +65,7 @@ export const dynamicFeatures = { if (data[KeyTopicsButton] === "on") addTopicsButton(); if (data[KeyXPremiumButton] === "on") addXPremiumButton(); if (data[KeyTypefullyGrowTab] === "on") addAnalyticsButton(); + addZenWriterModeButton(writerMode); }, writerMode: async (data) => { if (data[KeyWriterMode] === "on") { @@ -92,7 +93,7 @@ export const runDynamicFeatures = throttle(async () => { if (data) { dynamicFeatures.general(); dynamicFeatures.typefullyPlugs(); - await dynamicFeatures.sidebarButtons(); + await dynamicFeatures.sidebarButtons(data[KeyWriterMode]); await dynamicFeatures.writerMode(data); dynamicFeatures.navigation(data); diff --git a/content-scripts/src/modules/features/static.js b/content-scripts/src/modules/features/static.js index 1ad0f9ae..1dbb21e4 100644 --- a/content-scripts/src/modules/features/static.js +++ b/content-scripts/src/modules/features/static.js @@ -72,6 +72,7 @@ import { changeSidebarLogo, changeTopicsButton, changeUnreadCountBadge, + updateZenWriterModeButtonState, changeVerifiedOrgsButton, changeXPremiumButton, hideGrokDrawer, @@ -106,6 +107,7 @@ export const staticFeatures = { changeTopicsToFollow(data[KeyRemoveTopicsToFollow]); changeTimelineTabs(data[KeyRemoveTimelineTabs], data[KeyWriterMode]); changeTypefullyEnhancementsButtons(data[KeyTypefullyEnhancementsButtons]); + updateZenWriterModeButtonState(data[KeyWriterMode]); changeFollowingAndFollowersCounts(data[KeyFollowCount]); changeReplyCount(data[KeyReplyCount]); changeRetweetCount(data[KeyRetweetCount]); diff --git a/content-scripts/src/modules/options/navigation.js b/content-scripts/src/modules/options/navigation.js index 60a61a4e..5e14da3d 100644 --- a/content-scripts/src/modules/options/navigation.js +++ b/content-scripts/src/modules/options/navigation.js @@ -1,8 +1,10 @@ +import { KeyWriterMode } from "../../../../storage-keys"; import selectors from "../../selectors"; import svgAssets from "../svgAssets"; import addStyles, { removeStyles } from "../utilities/addStyles"; import { createTypefullyUrl } from "../utilities/createTypefullyUrl"; import { addSidebarButton } from "../utilities/sidebar"; +import { getStorage, setStorage } from "../utilities/storage"; // Utilities @@ -43,6 +45,30 @@ export const changeCommunitiesButton = (state) => changeSidebarSetting("communit export const changeListsButton = (state) => changeSidebarSetting("lists", state, addListsButton); export const changeAnalyticsButton = (state) => changeSidebarSetting("analytics", state, addAnalyticsButton); +export const addZenWriterModeButton = (writerMode) => { + addSidebarButton({ + name: "Zen Writer Mode", + svgAsset: svgAssets.zenWriterMode.normal, + onClick: async () => { + const writerMode = await getStorage(KeyWriterMode); + await setStorage({ [KeyWriterMode]: writerMode === "on" ? "off" : "on" }); + updateZenWriterModeButtonState(writerMode === "on" ? "off" : "on"); + }, + }); + + updateZenWriterModeButtonState(writerMode); +}; + +export const updateZenWriterModeButtonState = async (writerMode) => { + const button = document.querySelector('nav[role="navigation"] > [aria-label="Zen Writer Mode"]'); + + if (!button) return; + + const state = writerMode ?? (await getStorage(KeyWriterMode)); + button.classList.add("mt-zen-writer-mode-button"); + button.classList.toggle("mt-zen-writer-mode-button-active", state === "on"); +}; + let tm1; export const addXPremiumButton = () => { clearTimeout(tm1); diff --git a/content-scripts/src/modules/options/writerMode.js b/content-scripts/src/modules/options/writerMode.js index 9471ff20..5632b92e 100644 --- a/content-scripts/src/modules/options/writerMode.js +++ b/content-scripts/src/modules/options/writerMode.js @@ -50,7 +50,6 @@ export const changeWriterMode = (writerMode) => { width: 100%; max-width: 100%; } - ${selectors.leftSidebar}, ${selectors.rightSidebar}, ${selectors.mainColumn} > div > div:not(:nth-of-type(1)):not(:nth-of-type(2)):not(:nth-of-type(3)) { overflow: hidden; @@ -59,6 +58,15 @@ export const changeWriterMode = (writerMode) => { width: 0; height: 0; } + ${selectors.leftSidebar} nav[role="navigation"] > *:not([aria-label="Zen Writer Mode"]), + ${selectors.accountSwitcherButton}, + ${selectors.tweetButton} { + overflow: hidden; + visibility: hidden; + opacity: 0; + width: 0; + height: 0; + } ${selectors.topHeader} { visibility: hidden; } diff --git a/content-scripts/src/modules/svgAssets.js b/content-scripts/src/modules/svgAssets.js index 55929135..f783b2eb 100644 --- a/content-scripts/src/modules/svgAssets.js +++ b/content-scripts/src/modules/svgAssets.js @@ -3,6 +3,9 @@ const svgAssets = { normal: ``, selected: ``, }, + zenWriterMode: { + normal: ``, + }, lists: { normal: ``, selected: ``, diff --git a/css/main.css b/css/main.css index f24d9897..97cee97b 100644 --- a/css/main.css +++ b/css/main.css @@ -125,6 +125,23 @@ a[href*="quick_promote_web"] { background-color: rgba(var(--glyphs-color-rgb), 0.1); } +.mt-zen-writer-mode-button > div { + transition: background-color 0.2s ease, box-shadow 0.2s ease; +} + +.mt-zen-writer-mode-button-active > div { + background-color: rgba(var(--accent-color-rgb), 0.1); + box-shadow: 0 0 0 1px rgba(var(--accent-color-rgb), 0.22), 0 0 18px rgba(var(--accent-color-rgb), 0.18); +} + +.mt-zen-writer-mode-button-active svg { + color: rgb(var(--accent-color-rgb)); +} + +.mt-zen-writer-mode-button svg path { + fill: currentColor; +} + /* Don't show label at smaller screens */ @media only screen and (max-width: 1264px) { .mt-sidebar-button > div > div[dir] { From 30d924ecebd932845e2282c54d911752e0fe6d2c Mon Sep 17 00:00:00 2001 From: Anatolii Date: Sun, 24 May 2026 22:17:55 -0700 Subject: [PATCH 03/16] Add settings import and export --- popup/components/sections/Footer.js | 117 +++++++++++++++++++++++++++- 1 file changed, 116 insertions(+), 1 deletion(-) diff --git a/popup/components/sections/Footer.js b/popup/components/sections/Footer.js index ed0e4c10..55fe23e9 100644 --- a/popup/components/sections/Footer.js +++ b/popup/components/sections/Footer.js @@ -1,6 +1,121 @@ +import { useRef, useState } from "react"; +import { allSettingsKeys, defaultPreferences } from "../../../storage-keys"; + +const SETTINGS_EXPORT_VERSION = 1; + const Footer = () => { + const fileInputRef = useRef(null); + const [status, setStatus] = useState(""); + + const getSettings = () => { + return new Promise((resolve) => { + chrome.storage.local.get(allSettingsKeys, (data) => { + const settings = allSettingsKeys.reduce((acc, key) => { + acc[key] = data[key] ?? defaultPreferences[key]; + return acc; + }, {}); + + resolve(settings); + }); + }); + }; + + const exportSettings = async () => { + const settings = await getSettings(); + const payload = { + version: SETTINGS_EXPORT_VERSION, + exportedAt: new Date().toISOString(), + settings, + }; + const blob = new Blob([JSON.stringify(payload, null, 2)], { + type: "application/json", + }); + const url = URL.createObjectURL(blob); + const link = document.createElement("a"); + + link.href = url; + link.download = "minimal-twitter-settings.json"; + document.body.appendChild(link); + link.click(); + link.remove(); + window.setTimeout(() => URL.revokeObjectURL(url), 0); + setStatus("Settings exported."); + }; + + const importSettings = async (event) => { + const file = event.target.files?.[0]; + + if (!file) return; + + try { + const text = await file.text(); + const parsed = JSON.parse(text); + const sourceSettings = parsed.settings ?? parsed; + + if (!sourceSettings || typeof sourceSettings !== "object") { + setStatus("No settings found."); + return; + } + + const settings = allSettingsKeys.reduce((acc, key) => { + if (Object.prototype.hasOwnProperty.call(sourceSettings, key)) { + acc[key] = sourceSettings[key]; + } + + return acc; + }, {}); + + if (!Object.keys(settings).length) { + setStatus("No settings found."); + return; + } + + chrome.storage.local.set(settings, () => { + if (chrome.runtime.lastError) { + setStatus("Import failed."); + return; + } + + setStatus("Settings imported."); + }); + } catch (error) { + console.error(error); + setStatus("Import failed."); + } finally { + event.target.value = ""; + } + }; + return ( -