From 206a927f30f8b29eea4445f4f599b1f16f66b85e Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 14 Feb 2026 17:10:43 +1100 Subject: [PATCH 1/4] fix(deps): update dependency react-router-dom to v6.30.3 (#2612) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package-lock.json | 29 ++++++++++++++++------------- package.json | 2 +- 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4c12e9db..823c38b1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -56,7 +56,7 @@ "react-google-recaptcha": "2.1.0", "react-i18next": "15.0.0", "react-range": "1.8.14", - "react-router-dom": "6.20.0", + "react-router-dom": "6.30.3", "sanitize-html": "2.12.1", "slate": "0.112.0", "slate-dom": "0.112.2", @@ -3699,9 +3699,10 @@ } }, "node_modules/@remix-run/router": { - "version": "1.13.0", - "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.13.0.tgz", - "integrity": "sha512-5dMOnVnefRsl4uRnAdoWjtVTdh8e6aZqgM4puy9nmEADH72ck+uXwzpJLEKE9Q6F8ZljNewLgmTfkxUrBdv4WA==", + "version": "1.23.2", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.2.tgz", + "integrity": "sha512-Ic6m2U/rMjTkhERIa/0ZtXJP17QUi2CbWE7cqx4J58M8aA3QTfW+2UlQ4psvTX9IO1RfNVhK3pcpdjej7L+t2w==", + "license": "MIT", "engines": { "node": ">=14.0.0" } @@ -9605,11 +9606,12 @@ } }, "node_modules/react-router": { - "version": "6.20.0", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.20.0.tgz", - "integrity": "sha512-pVvzsSsgUxxtuNfTHC4IxjATs10UaAtvLGVSA1tbUE4GDaOSU1Esu2xF5nWLz7KPiMuW8BJWuPFdlGYJ7/rW0w==", + "version": "6.30.3", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.3.tgz", + "integrity": "sha512-XRnlbKMTmktBkjCLE8/XcZFlnHvr2Ltdr1eJX4idL55/9BbORzyZEaIkBFDhFGCEWBBItsVrDxwx3gnisMitdw==", + "license": "MIT", "dependencies": { - "@remix-run/router": "1.13.0" + "@remix-run/router": "1.23.2" }, "engines": { "node": ">=14.0.0" @@ -9619,12 +9621,13 @@ } }, "node_modules/react-router-dom": { - "version": "6.20.0", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.20.0.tgz", - "integrity": "sha512-CbcKjEyiSVpA6UtCHOIYLUYn/UJfwzp55va4yEfpk7JBN3GPqWfHrdLkAvNCcpXr8QoihcDMuk0dzWZxtlB/mQ==", + "version": "6.30.3", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.3.tgz", + "integrity": "sha512-pxPcv1AczD4vso7G4Z3TKcvlxK7g7TNt3/FNGMhfqyntocvYKj+GCatfigGDjbLozC4baguJ0ReCigoDJXb0ag==", + "license": "MIT", "dependencies": { - "@remix-run/router": "1.13.0", - "react-router": "6.20.0" + "@remix-run/router": "1.23.2", + "react-router": "6.30.3" }, "engines": { "node": ">=14.0.0" diff --git a/package.json b/package.json index 00535bd8..9438330a 100644 --- a/package.json +++ b/package.json @@ -67,7 +67,7 @@ "react-google-recaptcha": "2.1.0", "react-i18next": "15.0.0", "react-range": "1.8.14", - "react-router-dom": "6.20.0", + "react-router-dom": "6.30.3", "sanitize-html": "2.12.1", "slate": "0.112.0", "slate-dom": "0.112.2", From 074c5552943206ff47df0aa0f85b50b4d79af656 Mon Sep 17 00:00:00 2001 From: Ajay Bura <32841439+ajbura@users.noreply.github.com> Date: Sat, 14 Feb 2026 11:41:36 +0530 Subject: [PATCH 2/4] Post session info to service worker instead of asking from sw (#2605) post session info to service worker instead of asking from sw on each request --- src/app/pages/Router.tsx | 5 ++- src/client/initMatrix.ts | 2 + src/index.tsx | 15 +++---- src/sw-session.ts | 10 +++++ src/sw.ts | 94 +++++++++++++++++++++++++++------------- 5 files changed, 84 insertions(+), 42 deletions(-) create mode 100644 src/sw-session.ts diff --git a/src/app/pages/Router.tsx b/src/app/pages/Router.tsx index 18591f29..04d14a07 100644 --- a/src/app/pages/Router.tsx +++ b/src/app/pages/Router.tsx @@ -68,6 +68,7 @@ import { Create } from './client/create'; import { CreateSpaceModalRenderer } from '../features/create-space'; import { SearchModalRenderer } from '../features/search'; import { getFallbackSession } from '../state/sessions'; +import { pushSessionToSW } from '../../sw-session'; export const createRouter = (clientConfig: ClientConfig, screenSize: ScreenSize) => { const { hashRouter } = clientConfig; @@ -106,7 +107,8 @@ export const createRouter = (clientConfig: ClientConfig, screenSize: ScreenSize) { - if (!getFallbackSession()) { + const session = getFallbackSession(); + if (!session) { const afterLoginPath = getAppPathFromHref( getOriginBaseUrl(hashRouter), window.location.href @@ -114,6 +116,7 @@ export const createRouter = (clientConfig: ClientConfig, screenSize: ScreenSize) if (afterLoginPath) setAfterLoginRedirectPath(afterLoginPath); return redirect(getLoginPath()); } + pushSessionToSW(session.baseUrl, session.accessToken); return null; }} element={ diff --git a/src/client/initMatrix.ts b/src/client/initMatrix.ts index 487c3f13..498d4f75 100644 --- a/src/client/initMatrix.ts +++ b/src/client/initMatrix.ts @@ -2,6 +2,7 @@ import { createClient, MatrixClient, IndexedDBStore, IndexedDBCryptoStore } from import { cryptoCallbacks } from './secretStorageKeys'; import { clearNavToActivePathStore } from '../app/state/navToActivePath'; +import { pushSessionToSW } from '../sw-session'; type Session = { baseUrl: string; @@ -53,6 +54,7 @@ export const clearCacheAndReload = async (mx: MatrixClient) => { }; export const logoutClient = async (mx: MatrixClient) => { + pushSessionToSW(); mx.stopClient(); try { await mx.logout(); diff --git a/src/index.tsx b/src/index.tsx index ddfd30b4..0019a224 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -15,6 +15,8 @@ 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'; document.body.classList.add(configClass, varsClass); @@ -25,16 +27,9 @@ if ('serviceWorker' in navigator) { ? `${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) { - // Get the token for SW. - const token = localStorage.getItem('cinny_access_token') ?? undefined; - event.source!.postMessage({ - responseKey: event.data.responseKey, - token, - }); - } + navigator.serviceWorker.register(swUrl).then(() => { + const session = getFallbackSession(); + pushSessionToSW(session?.baseUrl, session?.accessToken); }); } diff --git a/src/sw-session.ts b/src/sw-session.ts new file mode 100644 index 00000000..4b2ec055 --- /dev/null +++ b/src/sw-session.ts @@ -0,0 +1,10 @@ +export function pushSessionToSW(baseUrl?: string, accessToken?: string) { + if (!('serviceWorker' in navigator)) return; + if (!navigator.serviceWorker.controller) return; + + navigator.serviceWorker.controller.postMessage({ + type: 'setSession', + accessToken, + baseUrl, + }); +} diff --git a/src/sw.ts b/src/sw.ts index 2179dfcb..d4eebc02 100644 --- a/src/sw.ts +++ b/src/sw.ts @@ -3,22 +3,64 @@ export type {}; declare const self: ServiceWorkerGlobalScope; -async function askForAccessToken(client: Client): Promise { - return new Promise((resolve) => { - const responseKey = Math.random().toString(36); - const listener = (event: ExtendableMessageEvent) => { - if (event.data.responseKey !== responseKey) return; - resolve(event.data.token); - self.removeEventListener('message', listener); - }; - self.addEventListener('message', listener); - client.postMessage({ responseKey, type: 'token' }); +self.addEventListener('install', () => { + self.skipWaiting(); +}); + +self.addEventListener('activate', (event: ExtendableEvent) => { + event.waitUntil(self.clients.claim()); +}); + +type SessionInfo = { + accessToken: string; + baseUrl: string; +}; + +/** + * Store session per client (tab) + */ +const sessions = new Map(); + +async function cleanupDeadClients() { + const activeClients = await self.clients.matchAll(); + const activeIds = new Set(activeClients.map((c) => c.id)); + + Array.from(sessions.keys()).forEach((id) => { + if (!activeIds.has(id)) { + sessions.delete(id); + } }); } -function fetchConfig(token?: string): RequestInit | undefined { - if (!token) return undefined; +/** + * Receive session updates from clients + */ +self.addEventListener('message', (event: ExtendableMessageEvent) => { + const client = event.source as Client | null; + if (!client) return; + const { type, accessToken, baseUrl } = event.data || {}; + + if (type !== 'setSession') return; + + cleanupDeadClients(); + + if (typeof accessToken === 'string' && typeof baseUrl === 'string') { + sessions.set(client.id, { accessToken, baseUrl }); + } else { + // Logout or invalid session + sessions.delete(client.id); + } +}); + +function validMediaRequest(url: string, baseUrl: string): boolean { + const downloadUrl = new URL('/_matrix/client/v1/media/download', baseUrl); + const thumbnailUrl = new URL('/_matrix/client/v1/media/thumbnail', baseUrl); + + return url.startsWith(downloadUrl.href) || url.startsWith(thumbnailUrl.href); +} + +function fetchConfig(token: string): RequestInit { return { headers: { Authorization: `Bearer ${token}`, @@ -27,26 +69,16 @@ function fetchConfig(token?: string): RequestInit | undefined { }; } -self.addEventListener('activate', (event: ExtendableEvent) => { - event.waitUntil(clients.claim()); -}); - self.addEventListener('fetch', (event: FetchEvent) => { const { url, method } = event.request; - if (method !== 'GET') return; - if ( - !url.includes('/_matrix/client/v1/media/download') && - !url.includes('/_matrix/client/v1/media/thumbnail') - ) { - return; - } - event.respondWith( - (async (): Promise => { - const client = await self.clients.get(event.clientId); - let token: string | undefined; - if (client) token = await askForAccessToken(client); - return fetch(url, fetchConfig(token)); - })() - ); + if (method !== 'GET') return; + if (!event.clientId) return; + + const session = sessions.get(event.clientId); + if (!session) return; + + if (!validMediaRequest(url, session.baseUrl)) return; + + event.respondWith(fetch(url, fetchConfig(session.accessToken))); }); From 3522751a15ae194e1d9cf49512796ee5f4f3d74a Mon Sep 17 00:00:00 2001 From: Ajay Bura <32841439+ajbura@users.noreply.github.com> Date: Sat, 14 Feb 2026 11:42:28 +0530 Subject: [PATCH 3/4] Prevent invalid mxc from getting used (#2609) --- .../editor/autocomplete/EmoticonAutocomplete.tsx | 6 ++++-- src/app/components/emoji-board/EmojiBoard.tsx | 5 ++--- src/app/components/emoji-board/components/Item.tsx | 4 ++-- src/app/components/message/FileHeader.tsx | 3 ++- src/app/components/message/content/AudioContent.tsx | 3 ++- src/app/components/message/content/FileContent.tsx | 9 ++++++--- src/app/components/message/content/ImageContent.tsx | 3 ++- src/app/components/message/content/ThumbnailContent.tsx | 3 ++- src/app/components/message/content/VideoContent.tsx | 3 ++- 9 files changed, 24 insertions(+), 15 deletions(-) diff --git a/src/app/components/editor/autocomplete/EmoticonAutocomplete.tsx b/src/app/components/editor/autocomplete/EmoticonAutocomplete.tsx index cc0dff19..d358ff7d 100644 --- a/src/app/components/editor/autocomplete/EmoticonAutocomplete.tsx +++ b/src/app/components/editor/autocomplete/EmoticonAutocomplete.tsx @@ -88,6 +88,8 @@ export function EmoticonAutocomplete({ {autoCompleteEmoticon.map((emoticon) => { const isCustomEmoji = 'url' in emoticon; const key = isCustomEmoji ? emoticon.url : emoticon.unicode; + const customEmojiUrl = mxcUrlToHttp(mx, key, useAuthentication); + return ( handleAutocomplete(key, emoticon.shortcode)} before={ - isCustomEmoji ? ( + isCustomEmoji && customEmojiUrl ? ( diff --git a/src/app/components/emoji-board/EmojiBoard.tsx b/src/app/components/emoji-board/EmojiBoard.tsx index 3db27e2a..d5a76c71 100644 --- a/src/app/components/emoji-board/EmojiBoard.tsx +++ b/src/app/components/emoji-board/EmojiBoard.tsx @@ -202,8 +202,7 @@ function EmojiSidebar({ activeGroupAtom, packs, onScrollToGroup }: EmojiSidebarP if (!label) label = isUserId(pack.id) ? 'Personal Pack' : mx.getRoom(pack.id)?.name; const url = - mxcUrlToHttp(mx, pack.getAvatarUrl(usage) ?? '', useAuthentication) || - pack.meta.avatar; + mxcUrlToHttp(mx, pack.getAvatarUrl(usage) ?? '', useAuthentication) ?? undefined; return ( ); @@ -98,7 +98,7 @@ export function StickerItem({ mx, useAuthentication, image }: StickerItemProps) loading="lazy" className={css.StickerImg} alt={image.body || image.shortcode} - src={mxcUrlToHttp(mx, image.url, useAuthentication) ?? image.url} + src={mxcUrlToHttp(mx, image.url, useAuthentication) ?? ''} /> ); diff --git a/src/app/components/message/FileHeader.tsx b/src/app/components/message/FileHeader.tsx index 0248862d..2ffc9ec4 100644 --- a/src/app/components/message/FileHeader.tsx +++ b/src/app/components/message/FileHeader.tsx @@ -27,7 +27,8 @@ export function FileDownloadButton({ filename, url, mimeType, encInfo }: FileDow const [downloadState, download] = useAsyncCallback( useCallback(async () => { - const mediaUrl = mxcUrlToHttp(mx, url, useAuthentication) ?? url; + const mediaUrl = mxcUrlToHttp(mx, url, useAuthentication); + if (!mediaUrl) throw new Error('Invalid media URL'); const fileContent = encInfo ? await downloadEncryptedMedia(mediaUrl, (encBuf) => decryptFile(encBuf, mimeType, encInfo)) : await downloadMedia(mediaUrl); diff --git a/src/app/components/message/content/AudioContent.tsx b/src/app/components/message/content/AudioContent.tsx index 71551b12..478486a4 100644 --- a/src/app/components/message/content/AudioContent.tsx +++ b/src/app/components/message/content/AudioContent.tsx @@ -54,7 +54,8 @@ export function AudioContent({ const [srcState, loadSrc] = useAsyncCallback( useCallback(async () => { - const mediaUrl = mxcUrlToHttp(mx, url, useAuthentication) ?? url; + const mediaUrl = mxcUrlToHttp(mx, url, useAuthentication); + if (!mediaUrl) throw new Error('Invalid media URL'); const fileContent = encInfo ? await downloadEncryptedMedia(mediaUrl, (encBuf) => decryptFile(encBuf, mimeType, encInfo)) : await downloadMedia(mediaUrl); diff --git a/src/app/components/message/content/FileContent.tsx b/src/app/components/message/content/FileContent.tsx index ad54c531..7e127f2a 100644 --- a/src/app/components/message/content/FileContent.tsx +++ b/src/app/components/message/content/FileContent.tsx @@ -86,7 +86,8 @@ export function ReadTextFile({ body, mimeType, url, encInfo, renderViewer }: Rea const [textState, loadText] = useAsyncCallback( useCallback(async () => { - const mediaUrl = mxcUrlToHttp(mx, url, useAuthentication) ?? url; + const mediaUrl = mxcUrlToHttp(mx, url, useAuthentication); + if (!mediaUrl) throw new Error('Invalid media URL'); const fileContent = encInfo ? await downloadEncryptedMedia(mediaUrl, (encBuf) => decryptFile(encBuf, mimeType, encInfo)) : await downloadMedia(mediaUrl); @@ -176,7 +177,8 @@ export function ReadPdfFile({ body, mimeType, url, encInfo, renderViewer }: Read const [pdfState, loadPdf] = useAsyncCallback( useCallback(async () => { - const mediaUrl = mxcUrlToHttp(mx, url, useAuthentication) ?? url; + const mediaUrl = mxcUrlToHttp(mx, url, useAuthentication); + if (!mediaUrl) throw new Error('Invalid media URL'); const fileContent = encInfo ? await downloadEncryptedMedia(mediaUrl, (encBuf) => decryptFile(encBuf, mimeType, encInfo)) : await downloadMedia(mediaUrl); @@ -253,7 +255,8 @@ export function DownloadFile({ body, mimeType, url, info, encInfo }: DownloadFil const [downloadState, download] = useAsyncCallback( useCallback(async () => { - const mediaUrl = mxcUrlToHttp(mx, url, useAuthentication) ?? url; + const mediaUrl = mxcUrlToHttp(mx, url, useAuthentication); + if (!mediaUrl) throw new Error('Invalid media URL'); const fileContent = encInfo ? await downloadEncryptedMedia(mediaUrl, (encBuf) => decryptFile(encBuf, mimeType, encInfo)) : await downloadMedia(mediaUrl); diff --git a/src/app/components/message/content/ImageContent.tsx b/src/app/components/message/content/ImageContent.tsx index 84e3709e..4f1d9c75 100644 --- a/src/app/components/message/content/ImageContent.tsx +++ b/src/app/components/message/content/ImageContent.tsx @@ -87,7 +87,8 @@ export const ImageContent = as<'div', ImageContentProps>( const [srcState, loadSrc] = useAsyncCallback( useCallback(async () => { - const mediaUrl = mxcUrlToHttp(mx, url, useAuthentication) ?? url; + const mediaUrl = mxcUrlToHttp(mx, url, useAuthentication); + if (!mediaUrl) throw new Error('Invalid media URL'); if (encInfo) { const fileContent = await downloadEncryptedMedia(mediaUrl, (encBuf) => decryptFile(encBuf, mimeType ?? FALLBACK_MIMETYPE, encInfo) diff --git a/src/app/components/message/content/ThumbnailContent.tsx b/src/app/components/message/content/ThumbnailContent.tsx index 0746137c..aa9171aa 100644 --- a/src/app/components/message/content/ThumbnailContent.tsx +++ b/src/app/components/message/content/ThumbnailContent.tsx @@ -23,7 +23,8 @@ export function ThumbnailContent({ info, renderImage }: ThumbnailContentProps) { throw new Error('Failed to load thumbnail'); } - const mediaUrl = mxcUrlToHttp(mx, thumbMxcUrl, useAuthentication) ?? thumbMxcUrl; + const mediaUrl = mxcUrlToHttp(mx, thumbMxcUrl, useAuthentication); + if (!mediaUrl) throw new Error('Invalid media URL'); if (encInfo) { const fileContent = await downloadEncryptedMedia(mediaUrl, (encBuf) => decryptFile(encBuf, thumbInfo.mimetype ?? FALLBACK_MIMETYPE, encInfo) diff --git a/src/app/components/message/content/VideoContent.tsx b/src/app/components/message/content/VideoContent.tsx index 52073ac1..b33ec272 100644 --- a/src/app/components/message/content/VideoContent.tsx +++ b/src/app/components/message/content/VideoContent.tsx @@ -81,7 +81,8 @@ export const VideoContent = as<'div', VideoContentProps>( const [srcState, loadSrc] = useAsyncCallback( useCallback(async () => { - const mediaUrl = mxcUrlToHttp(mx, url, useAuthentication) ?? url; + const mediaUrl = mxcUrlToHttp(mx, url, useAuthentication); + if (!mediaUrl) throw new Error('Invalid media URL'); const fileContent = encInfo ? await downloadEncryptedMedia(mediaUrl, (encBuf) => decryptFile(encBuf, mimeType, encInfo) From 9d49418a1f81d3b368bba8e8454d33a9ee588770 Mon Sep 17 00:00:00 2001 From: Andrew Murphy Date: Sat, 14 Feb 2026 17:32:10 +1100 Subject: [PATCH 4/4] Set m.fully_read marker when marking rooms as read (#2587) Previously markAsRead() only sent m.read receipts via sendReadReceipt(). This meant the read position was not persisted across page refreshes, especially noticeable in bridged rooms. Now uses setRoomReadMarkers() which sets both: - m.fully_read marker (persistent read position) - m.read receipt Fixes issue where rooms would still show as unread after refresh. --- src/app/utils/notifications.ts | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/app/utils/notifications.ts b/src/app/utils/notifications.ts index a23bd1a4..edab9196 100644 --- a/src/app/utils/notifications.ts +++ b/src/app/utils/notifications.ts @@ -1,6 +1,6 @@ import { MatrixClient, ReceiptType } from 'matrix-js-sdk'; -export async function markAsRead(mx: MatrixClient, roomId: string, privateReceipt: boolean) { +export async function markAsRead(mx: MatrixClient, roomId: string, privateReceipt?: boolean) { const room = mx.getRoom(roomId); if (!room) return; @@ -19,8 +19,15 @@ export async function markAsRead(mx: MatrixClient, roomId: string, privateReceip const latestEvent = getLatestValidEvent(); if (latestEvent === null) return; - await mx.sendReadReceipt( - latestEvent, - privateReceipt ? ReceiptType.ReadPrivate : ReceiptType.Read + const latestEventId = latestEvent.getId(); + if (!latestEventId) return; + + // Set both the read receipt AND the fully_read marker + // The fully_read marker is what persists your read position across sessions + await mx.setRoomReadMarkers( + roomId, + latestEventId, // m.fully_read marker + latestEvent, // m.read receipt event + privateReceipt ? { receiptType: ReceiptType.ReadPrivate } : undefined ); }