From 3245f41073ec52fc0df4ee7bf8b270e48ad1b03d Mon Sep 17 00:00:00 2001 From: Max Litruv Boonzaayer Date: Thu, 9 Jul 2026 03:40:51 +1000 Subject: [PATCH] refactor: remove Android typography fix and associated styles from ClientNonUIFeatures --- .../app/pages/client/ClientNonUIFeatures.tsx | 11 - overlay/src/app/styles/android-typography.css | 8 - overlay/src/app/utils/tauri.ts | 21 +- overlay/src/font-setup.ts | 18 + overlay/src/index.css | 506 ++++++++++++++++++ overlay/src/index.tsx | 112 ++++ 6 files changed, 638 insertions(+), 38 deletions(-) delete mode 100644 overlay/src/app/styles/android-typography.css create mode 100644 overlay/src/font-setup.ts create mode 100644 overlay/src/index.css create mode 100644 overlay/src/index.tsx diff --git a/overlay/src/app/pages/client/ClientNonUIFeatures.tsx b/overlay/src/app/pages/client/ClientNonUIFeatures.tsx index 1bb3893..1387ffa 100644 --- a/overlay/src/app/pages/client/ClientNonUIFeatures.tsx +++ b/overlay/src/app/pages/client/ClientNonUIFeatures.tsx @@ -35,11 +35,9 @@ import { isTauri, isElectron, isCapacitorNative, - applyAndroidTypographyFix, sendNotification, setupNotificationTapListener, } from '../../utils/tauri'; -import '../../styles/android-typography.css'; import { setPaarrotNavigate, initPaarrotAPI } from '../../paarrot-api'; import { startBackgroundSync, @@ -86,14 +84,6 @@ function EmojiStyleFeature() { return null; } -function AndroidTypographyFeature() { - useEffect(() => { - applyAndroidTypographyFix(); - }, []); - - return null; -} - function PageZoomFeature() { const [pageZoom] = useSetting(settingsAtom, 'pageZoom'); @@ -694,7 +684,6 @@ export function ClientNonUIFeatures({ children }: ClientNonUIFeaturesProps) { return ( <> - diff --git a/overlay/src/app/styles/android-typography.css b/overlay/src/app/styles/android-typography.css deleted file mode 100644 index 0d3ef4f..0000000 --- a/overlay/src/app/styles/android-typography.css +++ /dev/null @@ -1,8 +0,0 @@ -/* - * Android WebView renders Inter Variable with broken word/letter spacing. - * Applied when html.android-capacitor is set (see applyAndroidTypographyFix). - */ -html.android-capacitor body { - letter-spacing: normal; - word-spacing: normal; -} diff --git a/overlay/src/app/utils/tauri.ts b/overlay/src/app/utils/tauri.ts index 9d7f05d..3703936 100644 --- a/overlay/src/app/utils/tauri.ts +++ b/overlay/src/app/utils/tauri.ts @@ -256,31 +256,14 @@ export const isCapacitorAndroid = (): boolean => { }; /** - * Android WebView mishandles Inter Variable — swap to static Inter / system fonts. + * Android WebView: keep emoji fonts out of the body text stack and use system UI fonts. */ export const applyAndroidTypographyFix = (): void => { if (!isCapacitorAndroid()) return; const root = document.documentElement; root.classList.add('android-capacitor'); - - void (async () => { - try { - await import('@fontsource/inter/400.css'); - await import('@fontsource/inter/500.css'); - await import('@fontsource/inter/600.css'); - root.style.setProperty( - '--font-secondary', - `'Inter', system-ui, Roboto, 'Noto Sans', var(--font-emoji), sans-serif` - ); - } catch (err) { - console.warn('[applyAndroidTypographyFix] Falling back to system font:', err); - root.style.setProperty( - '--font-secondary', - `system-ui, Roboto, 'Noto Sans', var(--font-emoji), sans-serif` - ); - } - })(); + root.style.setProperty('--font-secondary', 'system-ui, Roboto, "Noto Sans", sans-serif'); }; /** diff --git a/overlay/src/font-setup.ts b/overlay/src/font-setup.ts new file mode 100644 index 0000000..8a0d080 --- /dev/null +++ b/overlay/src/font-setup.ts @@ -0,0 +1,18 @@ +const cap = + typeof window !== 'undefined' + ? (window as { Capacitor?: { isNativePlatform?: () => boolean; getPlatform?: () => string } }).Capacitor + : undefined; + +export const isCapacitorAndroid = Boolean( + cap?.isNativePlatform?.() && cap?.getPlatform?.() === 'android' +); + +if (isCapacitorAndroid) { + document.documentElement.classList.add('android-capacitor'); + document.documentElement.style.setProperty( + '--font-secondary', + 'system-ui, Roboto, "Noto Sans", sans-serif' + ); +} else { + await import('@fontsource-variable/inter'); +} diff --git a/overlay/src/index.css b/overlay/src/index.css new file mode 100644 index 0000000..7aa5f36 --- /dev/null +++ b/overlay/src/index.css @@ -0,0 +1,506 @@ +/* Twemoji font (Twitter emoji) */ +@font-face { + font-family: Twemoji; + src: url('../public/font/Twemoji.Mozilla.v15.1.0.woff2'), + url('../public/font/Twemoji.Mozilla.v15.1.0.ttf'); + font-display: swap; + unicode-range: U+1F300-1F9FF, U+2600-26FF, U+2700-27BF, U+FE0F, U+200D, U+1F1E6-1F1FF; +} + +/* Apple Color Emoji - uses local Apple Color Emoji if available (macOS/iOS), + falls back to Segoe UI Emoji (Windows) or Noto (Linux/Android) */ +@font-face { + font-family: AppleColorEmoji; + src: local('Apple Color Emoji'), + local('Segoe UI Emoji'), + local('Noto Color Emoji'); + font-display: swap; + unicode-range: U+1F300-1F9FF, U+2600-26FF, U+2700-27BF, U+FE0F, U+200D, U+1F1E6-1F1FF; +} + +/* System emoji - uses native OS emoji */ +@font-face { + font-family: SystemEmoji; + src: local('Apple Color Emoji'), + local('Segoe UI Emoji'), + local('Segoe UI Symbol'), + local('Noto Color Emoji'), + local('Android Emoji'), + local('EmojiOne Color'); + font-display: swap; + unicode-range: U+1F300-1F9FF, U+2600-26FF, U+2700-27BF, U+FE0F, U+200D, U+1F1E6-1F1FF; +} + +:root { + --tc-link: hsl(213deg 100% 45%); + + /* user mxid colors */ + --mx-uc-1: hsl(208, 100%, 45%); + --mx-uc-2: hsl(302, 100%, 30%); + --mx-uc-3: hsl(163, 100%, 30%); + --mx-uc-4: hsl(343, 100%, 45%); + --mx-uc-5: hsl(24, 100%, 45%); + --mx-uc-6: hsl(181, 100%, 30%); + --mx-uc-7: hsl(242, 100%, 45%); + --mx-uc-8: hsl(94, 100%, 35%); + + --font-emoji: 'AppleColorEmoji'; + --font-secondary: 'InterVariable', sans-serif; +} + +.dark-theme, +.butter-theme, +.discord-theme, +.discord-darker-theme, +.twilight-theme, +.mocha-theme { + --tc-link: hsl(213deg 100% 80%); + + --mx-uc-1: hsl(208, 100%, 75%); + --mx-uc-2: hsl(301, 100%, 80%); + --mx-uc-3: hsl(163, 100%, 70%); + --mx-uc-4: hsl(343, 100%, 75%); + --mx-uc-5: hsl(24, 100%, 70%); + --mx-uc-6: hsl(181, 100%, 60%); + --mx-uc-7: hsl(243, 100%, 80%); + --mx-uc-8: hsl(94, 100%, 80%); + + --font-secondary: 'InterVariable', sans-serif; +} + +html { + height: 100%; + overflow: hidden; + /* Enable safe area variables */ + --safe-area-inset-top: env(safe-area-inset-top, 0px); + --safe-area-inset-bottom: env(safe-area-inset-bottom, 0px); + --safe-area-inset-left: env(safe-area-inset-left, 0px); + --safe-area-inset-right: env(safe-area-inset-right, 0px); +} + +body { + margin: 0; + padding: 0; + /* Safe area insets for mobile devices (notch, navigation bar) */ + padding-top: var(--safe-area-inset-top); + padding-bottom: var(--safe-area-inset-bottom); + padding-left: var(--safe-area-inset-left); + padding-right: var(--safe-area-inset-right); + height: 100%; + font-family: var(--font-secondary); + font-size: 16px; + font-weight: 400; + /* Default to dark theme background for safe areas */ + background-color: #262626; + + /*Why font-variant-ligatures => https://github.com/rsms/inter/issues/222 */ + font-variant-ligatures: no-contextual; +} + +/* Prevent zalgo text and combining diacritics from overflowing line boundaries */ +p, span, div, a, button, li, td, th, h1, h2, h3, h4, h5, h6 { + line-height: 1.5; +} + +/* Theme-specific body backgrounds for safe area padding */ +body.light-theme { + background-color: #F0F0F0; +} +body.silver-theme { + background-color: #DEDEDE; +} +body.dark-theme { + background-color: #262626; +} +body.butter-theme { + background-color: #1A1916; +} +body.discord-theme { + background-color: #2B2D31; +} +body.discord-darker-theme { + background-color: #1E1F22; +} +body.twilight-theme { + background: linear-gradient(135deg, #14121F 0%, #1C1A2E 50%, #24223D 100%); +} +body.mocha-theme { + background: linear-gradient(135deg, #1A1614 0%, #242019 50%, #2D2721 100%); +} +#root { + width: 100%; + height: 100%; + display: flex; + flex-direction: column; + background-color: #262626; +} + +.twilight-theme #root { + background: linear-gradient(180deg, rgba(28, 26, 46, 0.5) 0%, rgba(36, 34, 61, 0.3) 100%); +} + +.mocha-theme #root { + background: linear-gradient(180deg, rgba(36, 32, 25, 0.5) 0%, rgba(45, 39, 33, 0.3) 100%); +} + +/* Twilight theme gradient overlays */ +.twilight-theme [role="navigation"], +.twilight-theme aside { + background: linear-gradient(180deg, rgba(20, 18, 31, 0.8) 0%, rgba(28, 26, 46, 0.9) 100%); + backdrop-filter: blur(8px); +} + +.twilight-theme main { + background: linear-gradient(135deg, rgba(24, 22, 45, 0.4) 0%, rgba(36, 34, 61, 0.3) 100%); +} + +.twilight-theme header { + background: linear-gradient(90deg, rgba(20, 18, 31, 0.9) 0%, rgba(28, 26, 46, 0.8) 100%); +} + +/* CSS animations for route transitions */ +@keyframes fadeSlideIn { + from { + opacity: 0; + transform: translateX(20px); + } + to { + opacity: 1; + transform: translateX(0); + } +} + +@keyframes fadeIn { + from { + opacity: 0; + } + to { + opacity: 1; + } +} + +/* Apply transition to route changes - works on ALL themes */ +[data-route-transition="true"] { + animation: fadeSlideIn 0.3s cubic-bezier(0.4, 0, 0.2, 1); +} + +/* Twilight theme enhanced transitions */ +.twilight-theme [data-route-transition="true"] { + animation: fadeSlideIn 0.35s cubic-bezier(0.4, 0, 0.2, 1); +} + +/* Also apply to main content areas for double coverage */ +.twilight-theme [data-room-view], +.twilight-theme [data-space-view], +.twilight-theme [data-inbox-view], +.twilight-theme [data-explore-view] { + animation: fadeSlideIn 0.3s cubic-bezier(0.4, 0, 0.2, 1); +} + +/* Smooth transitions for interactive elements */ +.twilight-theme button, +.twilight-theme [role="button"], +.twilight-theme a, +.twilight-theme input, +.twilight-theme textarea { + transition: background-color 0.2s ease, color 0.2s ease, transform 0.15s ease; +} + +.twilight-theme button:hover, +.twilight-theme [role="button"]:hover { + transform: translateY(-1px); +} + +.twilight-theme button:active, +.twilight-theme [role="button"]:active { + transform: translateY(0); +} + +/* Message transitions */ +.twilight-theme [data-message-item] { + transition: background-color 0.2s ease, transform 0.2s ease; +} + +.twilight-theme [data-message-item]:hover { + background: linear-gradient(90deg, rgba(62, 58, 106, 0.15) 0%, rgba(86, 80, 150, 0.1) 100%); + transform: translateX(2px); +} + +/* Selected item indicator */ +.twilight-theme [data-selected="true"], +.twilight-theme [aria-selected="true"], +.twilight-theme [aria-current="true"] { + position: relative; + transition: all 0.2s ease; +} + +.twilight-theme [data-selected="true"]::before, +.twilight-theme [aria-selected="true"]::before, +.twilight-theme [aria-current="true"]::before { + content: ''; + position: absolute; + left: 0; + top: 0; + bottom: 0; + width: 3px; + background: linear-gradient(180deg, #8B7FFF 0%, #6E62E8 100%); + transition: width 0.2s ease; +} + +/* Scrollbar styling */ +.twilight-theme ::-webkit-scrollbar { + width: 10px; + height: 10px; +} + +.twilight-theme ::-webkit-scrollbar-track { + background: rgba(20, 18, 31, 0.5); +} + +.twilight-theme ::-webkit-scrollbar-thumb { + background: linear-gradient(180deg, #3E3A6A 0%, #565096 100%); + border-radius: 5px; + border: 2px solid rgba(20, 18, 31, 0.5); + transition: background 0.2s ease; +} + +.twilight-theme ::-webkit-scrollbar-thumb:hover { + background: linear-gradient(180deg, #4A4580 0%, #625BAC 100%); +} + +/* Mocha theme gradient overlays */ +.mocha-theme [role="navigation"], +.mocha-theme aside { + background: linear-gradient(180deg, rgba(26, 22, 20, 0.8) 0%, rgba(36, 32, 25, 0.9) 100%); + backdrop-filter: blur(8px); +} + +.mocha-theme main { + background: linear-gradient(135deg, rgba(36, 32, 25, 0.4) 0%, rgba(45, 39, 33, 0.3) 100%); +} + +.mocha-theme header { + background: linear-gradient(90deg, rgba(26, 22, 20, 0.9) 0%, rgba(36, 32, 25, 0.8) 100%); +} + +/* Mocha theme enhanced transitions */ +.mocha-theme [data-route-transition="true"] { + animation: fadeSlideIn 0.35s cubic-bezier(0.4, 0, 0.2, 1); +} + +.mocha-theme [data-room-view], +.mocha-theme [data-space-view], +.mocha-theme [data-inbox-view], +.mocha-theme [data-explore-view] { + animation: fadeSlideIn 0.3s cubic-bezier(0.4, 0, 0.2, 1); +} + +/* Smooth transitions for interactive elements */ +.mocha-theme button, +.mocha-theme [role="button"], +.mocha-theme a, +.mocha-theme input, +.mocha-theme textarea { + transition: background-color 0.2s ease, color 0.2s ease, transform 0.15s ease; +} + +.mocha-theme button:hover, +.mocha-theme [role="button"]:hover { + transform: translateY(-1px); +} + +.mocha-theme button:active, +.mocha-theme [role="button"]:active { + transform: translateY(0); +} + +/* Message transitions */ +.mocha-theme [data-message-item] { + transition: background-color 0.2s ease, transform 0.2s ease; +} + +.mocha-theme [data-message-item]:hover { + background: linear-gradient(90deg, rgba(84, 73, 63, 0.15) 0%, rgba(112, 97, 81, 0.1) 100%); + transform: translateX(2px); +} + +/* Selected item indicator */ +.mocha-theme [data-selected="true"], +.mocha-theme [aria-selected="true"], +.mocha-theme [aria-current="true"] { + position: relative; + transition: all 0.2s ease; +} + +.mocha-theme [data-selected="true"]::before, +.mocha-theme [aria-selected="true"]::before, +.mocha-theme [aria-current="true"]::before { + content: ''; + position: absolute; + left: 0; + top: 0; + bottom: 0; + width: 3px; + background: linear-gradient(180deg, #D4A574 0%, #B48660 100%); + transition: width 0.2s ease; +} + +/* Scrollbar styling */ +.mocha-theme ::-webkit-scrollbar { + width: 10px; + height: 10px; +} + +.mocha-theme ::-webkit-scrollbar-track { + background: rgba(26, 22, 20, 0.5); +} + +.mocha-theme ::-webkit-scrollbar-thumb { + background: linear-gradient(180deg, #54493F 0%, #706151 100%); + border-radius: 5px; + border: 2px solid rgba(26, 22, 20, 0.5); + transition: background 0.2s ease; +} + +.mocha-theme ::-webkit-scrollbar-thumb:hover { + background: linear-gradient(180deg, #625548 0%, #7E6D5A 100%); +} + +/* Subtle dot pattern overlay */ +.mocha-theme::after { + content: ''; + position: fixed; + inset: 0; + background-image: radial-gradient(rgba(212, 165, 116, 0.03) 1px, transparent 1px); + background-size: 24px 24px; + pointer-events: none; + z-index: 0; +} + +.mocha-theme #root { + position: relative; + z-index: 1; +} + +/* Focus states */ +.mocha-theme input:focus, +.mocha-theme textarea:focus, +.mocha-theme button:focus-visible { + outline: 2px solid rgba(212, 165, 116, 0.5); + outline-offset: 2px; + transition: outline 0.2s ease; +} + +/* Subtle dot pattern overlay */ +.twilight-theme::after { + content: ''; + position: fixed; + inset: 0; + background-image: radial-gradient(rgba(139, 127, 255, 0.03) 1px, transparent 1px); + background-size: 24px 24px; + pointer-events: none; + z-index: 0; +} + +.twilight-theme #root { + position: relative; + z-index: 1; +} + +/* Focus states */ +.twilight-theme input:focus, +.twilight-theme textarea:focus, +.twilight-theme button:focus-visible { + outline: 2px solid rgba(139, 127, 255, 0.5); + outline-offset: 2px; + transition: outline 0.2s ease; +} + +*, +*::before, +*::after { + box-sizing: border-box; + -webkit-tap-highlight-color: rgba(0, 0, 0, 0); + -webkit-tap-highlight-color: transparent; +} +a { + color: var(--tc-link); + text-decoration: none; +} +a:hover { + text-decoration: underline; +} + +[data-mx-spoiler][aria-pressed='true'] a { + color: transparent; + pointer-events: none; +} + +b { + font-weight: 500; +} +label { + margin: 0; + padding: 0; +} +button, +textarea { + margin: 0; + padding: 0; + background-color: transparent; + color: inherit; + font-family: inherit; + font-size: inherit; + font-weight: inherit; + line-height: inherit; + letter-spacing: inherit; + border: none; +} +button { + max-width: 100%; + text-transform: none; + text-align: inherit; + overflow: visible; + -webkit-appearance: button; +} +textarea, +input, +input[type], +input[type='text'], +input[type='username'], +input[type='password'], +input[type='email'], +input[type='checkbox'] { + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; +} + +textarea { + color: inherit; + word-spacing: inherit; +} + +audio:not([controls]) { + display: none !important; +} + +/* Android WebView: Inter Variable + emoji fonts in the text stack break space metrics */ +html.android-capacitor, +html.android-capacitor.dark-theme, +html.android-capacitor.butter-theme, +html.android-capacitor.discord-theme, +html.android-capacitor.discord-darker-theme, +html.android-capacitor.twilight-theme, +html.android-capacitor.mocha-theme, +html.android-capacitor.light-theme, +html.android-capacitor.silver-theme { + --font-secondary: system-ui, Roboto, 'Noto Sans', sans-serif; +} + +html.android-capacitor body { + font-family: system-ui, Roboto, 'Noto Sans', sans-serif; + letter-spacing: normal; + word-spacing: normal; + font-variant-ligatures: normal; +} diff --git a/overlay/src/index.tsx b/overlay/src/index.tsx new file mode 100644 index 0000000..d710701 --- /dev/null +++ b/overlay/src/index.tsx @@ -0,0 +1,112 @@ +/* eslint-disable import/first */ +import React from 'react'; +import { createRoot } from 'react-dom/client'; +import { enableMapSet } from 'immer'; + +await import('./font-setup'); + +import 'folds/dist/style.css'; +import { configClass, varsClass } from 'folds'; + +enableMapSet(); + +import './index.css'; + +import { trimTrailingSlash } from './app/utils/common'; +import App from './app/pages/App'; +import { applySafeAreaInsets, isTauri } from './app/utils/tauri'; +import { enableViewTransitionsForNavigation } from './app/utils/viewTransitions'; + +// import i18n (needs to be bundled ;)) +import './app/i18n'; + +document.body.classList.add(configClass, varsClass); + +// Apply safe area insets for mobile devices +applySafeAreaInsets(); + +// Enable View Transitions API for smooth navigation +enableViewTransitionsForNavigation(); + +// Register Service Worker +if ('serviceWorker' in navigator) { + const swUrl = + import.meta.env.MODE === 'production' + ? `${trimTrailingSlash(import.meta.env.BASE_URL)}/sw.js` + : `/dev-sw.js?dev-sw`; + + navigator.serviceWorker.register(swUrl); + navigator.serviceWorker.addEventListener('message', (event) => { + if (event.data?.type === 'token' && event.data?.responseKey) { + const getCurrentAccessToken = async () => { + const { getCurrentAccessToken: getToken } = await import('./app/utils/auth'); + return getToken(); + }; + + getCurrentAccessToken().then((token) => { + event.source!.postMessage({ + responseKey: event.data.responseKey, + token, + }); + }); + } + }); +} + +const isElectron = (): boolean => 'electron' in window; + +async function checkForUpdates() { + if (isElectron()) { + console.log('Update check skipped - Electron handles updates natively'); + return; + } + + if (!isTauri() || isElectron()) { + console.log('Update check skipped - not running in Tauri'); + return; + } + + console.log('Checking for updates...'); + try { + const { check } = await import('@tauri-apps/plugin-updater'); + const { ask } = await import('@tauri-apps/plugin-dialog'); + const { relaunch } = await import('@tauri-apps/plugin-process'); + + const update = await check(); + console.log('Update check result:', update); + if (update) { + console.log(`Update available: ${update.version}`); + const shouldUpdate = await ask( + `A new version (${update.version}) is available. Would you like to update now?`, + { title: 'Update Available', kind: 'info' } + ); + if (shouldUpdate) { + console.log('User chose to update, downloading...'); + await update.downloadAndInstall(); + await relaunch(); + } + } else { + console.log('App is up to date'); + } + } catch (error) { + console.error('Failed to check for updates:', error); + } +} + +const mountApp = () => { + const rootContainer = document.getElementById('root'); + + if (rootContainer === null) { + console.error('Root container element not found!'); + return; + } + + const root = createRoot(rootContainer); + root.render(); +}; + +mountApp(); + +setTimeout(() => { + checkForUpdates(); +}, 3000);