CI / Build & Quality Checks (pull_request) Successful in 2m43s
CI / Trigger Desktop Build (pull_request) Skipped
CI / Docker image build & smoke test (pull_request) Skipped
CI / Secret scan (gitleaks) (pull_request) Successful in 12s
CI / Playwright smoke (e2e) (pull_request) Successful in 16m49s
The service worker posts inline notification replies in the clear (it cannot encrypt). The Reply action was only withheld for rooms that were encrypted when the notification was shown, so a reply typed hours later into a room that has since turned on encryption went out unencrypted. It also posted with the newest session the worker knew, not the notification's account. The worker now GETs the room's m.room.encryption state first and only sends on a definite 404; notifications carry the user id and the worker uses that account's session (or refuses). "Reply not sent" says why. Fixes #299 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017PCb59uY3UKqDqt7B6hzd5
137 lines
5.5 KiB
TypeScript
137 lines
5.5 KiB
TypeScript
/* eslint-disable import/first */
|
|
import React from 'react';
|
|
import { createRoot } from 'react-dom/client';
|
|
import { enableMapSet } from 'immer';
|
|
import '@fontsource-variable/inter/index.css';
|
|
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 i18n (needs to be bundled ;))
|
|
import './app/i18n';
|
|
import { pushSessionToSW } from './sw-session';
|
|
import { getFallbackSession } from './app/state/sessions';
|
|
import { startKeychainMirror } from './app/state/keychainMirror';
|
|
import { tauriInvoke } from './app/hooks/useTauri';
|
|
import { cleanupSearchCacheIfSignedOut } from './client/initMatrix';
|
|
|
|
document.body.classList.add(configClass, varsClass);
|
|
|
|
// [Gitea #105] Desktop: keep a copy of the login tokens in the OS keychain
|
|
// (step 1: mirror only; the session is still read from localStorage).
|
|
startKeychainMirror(tauriInvoke());
|
|
|
|
// Register Service Worker
|
|
// Service workers only register on http(s) pages. The desktop app loads from
|
|
// `tauri://localhost` in debug builds (and on any platform where the localhost
|
|
// plugin isn't used), where register() rejects: that surfaced in Sentry as an
|
|
// unhandled "must be called with a script URL whose protocol is either HTTP or
|
|
// HTTPS". Skip it there; the app works without a SW, only authenticated media
|
|
// falls back to the client's own fetch.
|
|
const swProtocolOk = window.location.protocol === 'https:' || window.location.protocol === 'http:';
|
|
if ('serviceWorker' in navigator && swProtocolOk) {
|
|
const swUrl =
|
|
import.meta.env.MODE === 'production'
|
|
? `${trimTrailingSlash(import.meta.env.BASE_URL)}/sw.js`
|
|
: `/dev-sw.js?dev-sw`;
|
|
|
|
const sendSessionToSW = () => {
|
|
const session = getFallbackSession();
|
|
pushSessionToSW(session?.baseUrl, session?.accessToken, session?.userId);
|
|
};
|
|
|
|
// The dev worker is an ES module (it imports workbox), so it must be
|
|
// registered as one — otherwise "script evaluation failed" and the dev
|
|
// client has no SW: authenticated media 401s and notification clicks
|
|
// never route. Production sw.js is a classic bundled script.
|
|
navigator.serviceWorker
|
|
.register(swUrl, import.meta.env.MODE === 'production' ? undefined : { type: 'module' })
|
|
.then(sendSessionToSW)
|
|
.catch((err) => {
|
|
// e.g. a private window with SWs disabled; the app still works.
|
|
console.warn('Service worker registration failed', err);
|
|
});
|
|
navigator.serviceWorker.ready.then(sendSessionToSW);
|
|
|
|
navigator.serviceWorker.addEventListener('message', (ev) => {
|
|
const { type } = ev.data ?? {};
|
|
|
|
if (type === 'requestSession') {
|
|
sendSessionToSW();
|
|
}
|
|
});
|
|
}
|
|
|
|
// Request persistent storage so the browser can't evict the IndexedDB
|
|
// rust-crypto store under storage pressure. Eviction (while the localStorage
|
|
// session/device-id survives) resurrects the device with a blank crypto store,
|
|
// which then re-uploads OTKs the server already holds → the "one time key
|
|
// already exists" upload storm and E2EE breakage. Only ask for sessions worth
|
|
// protecting (skip anonymous/landing visitors to avoid a needless Firefox
|
|
// prompt); check persisted() first so we don't re-prompt. Best-effort.
|
|
// [Gitea #45] If a logout's search-index wipe lost the multi-tab race (another
|
|
// tab still held the DB), finish it now that no session exists.
|
|
if (!getFallbackSession()) {
|
|
cleanupSearchCacheIfSignedOut().catch(() => undefined);
|
|
}
|
|
|
|
if (navigator.storage?.persist && getFallbackSession()) {
|
|
navigator.storage
|
|
.persisted()
|
|
.then((already) => (already ? undefined : navigator.storage.persist()))
|
|
.catch(() => undefined);
|
|
}
|
|
|
|
// Reload once if a lazy-loaded chunk is missing (stale deployment)
|
|
window.addEventListener('vite:preloadError', () => {
|
|
if (!sessionStorage.getItem('chunk-reload-attempted')) {
|
|
sessionStorage.setItem('chunk-reload-attempted', '1');
|
|
window.location.reload();
|
|
}
|
|
});
|
|
// Clear the reload flag after a successful load so future deploys can still trigger a reload
|
|
window.addEventListener('load', () => sessionStorage.removeItem('chunk-reload-attempted'));
|
|
|
|
// Filter out known-benign, high-volume matrix-js-sdk console warnings that we
|
|
// can't fix client-side and that would otherwise flood the console:
|
|
// - "Adding default global …": the SDK patches MSC3786/MSC3914 push rules on
|
|
// every login (one warn each) until Synapse ships them as server defaults.
|
|
// - "EventTimelineSet…": the SDK loudly warns whenever a decrypted event
|
|
// references a thread/room the current timeline set doesn't hold, then
|
|
// discards it harmlessly. This fires constantly in E2EE rooms with threads.
|
|
// - "Decrypted event … is not in room …": same family — a late decryption for
|
|
// an event the room's timeline no longer tracks; ignored by the SDK.
|
|
// These are informational SDK bookkeeping, not errors; real warnings still log.
|
|
{
|
|
const suppressedPrefixes = ['Adding default global ', 'EventTimelineSet'];
|
|
const _warn = console.warn.bind(console);
|
|
console.warn = (...args: unknown[]) => {
|
|
const first = args[0];
|
|
if (typeof first === 'string') {
|
|
if (suppressedPrefixes.some((p) => first.startsWith(p))) return;
|
|
if (first.startsWith('Decrypted event ') && first.includes('is not in room')) return;
|
|
}
|
|
_warn(...args);
|
|
};
|
|
}
|
|
|
|
const mountApp = () => {
|
|
const rootContainer = document.getElementById('root');
|
|
|
|
if (rootContainer === null) {
|
|
console.error('Root container element not found!');
|
|
return;
|
|
}
|
|
|
|
const root = createRoot(rootContainer);
|
|
root.render(<App />);
|
|
};
|
|
|
|
mountApp();
|