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/9] 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/9] 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/9] 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/9] 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 ); } From 4ba7b9162da75615eb1c4850b8eb4de5c063ab55 Mon Sep 17 00:00:00 2001 From: Krishan <33421343+kfiven@users.noreply.github.com> Date: Mon, 16 Feb 2026 06:03:37 +1100 Subject: [PATCH 5/9] Revert "fix: set m.fully_read marker when marking rooms as read" (#2629) Revert "Set m.fully_read marker when marking rooms as read (#2587)" This reverts commit 9d49418a1f81d3b368bba8e8454d33a9ee588770. --- src/app/utils/notifications.ts | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/src/app/utils/notifications.ts b/src/app/utils/notifications.ts index edab9196..a23bd1a4 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,15 +19,8 @@ export async function markAsRead(mx: MatrixClient, roomId: string, privateReceip const latestEvent = getLatestValidEvent(); if (latestEvent === null) return; - 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 + await mx.sendReadReceipt( + latestEvent, + privateReceipt ? ReceiptType.ReadPrivate : ReceiptType.Read ); } From fbde1a20306f2a6f4d270aa94f673b3b5c96a615 Mon Sep 17 00:00:00 2001 From: Ajay Bura <32841439+ajbura@users.noreply.github.com> Date: Mon, 16 Feb 2026 14:21:09 +0530 Subject: [PATCH 6/9] fix: image not loading on mobile after lock/unlock (#2631) image not loading on mobile after lock/unlock --- src/index.tsx | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/index.tsx b/src/index.tsx index 0019a224..71e723ab 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -27,10 +27,24 @@ if ('serviceWorker' in navigator) { ? `${trimTrailingSlash(import.meta.env.BASE_URL)}/sw.js` : `/dev-sw.js?dev-sw`; - navigator.serviceWorker.register(swUrl).then(() => { + const sendSessionToSW = () => { const session = getFallbackSession(); pushSessionToSW(session?.baseUrl, session?.accessToken); + }; + + navigator.serviceWorker.register(swUrl).then(sendSessionToSW); + navigator.serviceWorker.ready.then(sendSessionToSW); + window.addEventListener('load', sendSessionToSW); + + // When returning from background + document.addEventListener('visibilitychange', () => { + if (document.visibilityState === 'visible') { + sendSessionToSW(); + } }); + + // When restored from bfcache (important on iOS) + window.addEventListener('pageshow', sendSessionToSW); } const mountApp = () => { From d866c1b903cdba58804dc16e2a964e7a25965f18 Mon Sep 17 00:00:00 2001 From: Ajay Bura <32841439+ajbura@users.noreply.github.com> Date: Mon, 16 Feb 2026 14:21:55 +0530 Subject: [PATCH 7/9] fix room back button not working after router update (#2630) --- src/app/components/BackRouteHandler.tsx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/app/components/BackRouteHandler.tsx b/src/app/components/BackRouteHandler.tsx index fa3d7592..3b13e487 100644 --- a/src/app/components/BackRouteHandler.tsx +++ b/src/app/components/BackRouteHandler.tsx @@ -51,8 +51,12 @@ export function BackRouteHandler({ children }: BackRouteHandlerProps) { }, location.pathname ); - if (spaceMatch?.params.spaceIdOrAlias) { - navigate(getSpacePath(spaceMatch.params.spaceIdOrAlias)); + const encodedSpaceIdOrAlias = spaceMatch?.params.spaceIdOrAlias; + const decodedSpaceIdOrAlias = + encodedSpaceIdOrAlias && decodeURIComponent(encodedSpaceIdOrAlias); + + if (decodedSpaceIdOrAlias) { + navigate(getSpacePath(decodedSpaceIdOrAlias)); return; } if ( From 0f220f50d67a09ed401c37dabc3fe852a350ef47 Mon Sep 17 00:00:00 2001 From: Rin Date: Mon, 16 Feb 2026 15:54:05 +0700 Subject: [PATCH 8/9] fix: add noreferrer to sanitized links for improved privacy consistency (#2628) Enhance privacy by adding noreferrer to sanitized links --- src/app/utils/sanitize.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/utils/sanitize.ts b/src/app/utils/sanitize.ts index 985c47b1..28e0db59 100644 --- a/src/app/utils/sanitize.ts +++ b/src/app/utils/sanitize.ts @@ -100,7 +100,7 @@ const transformATag: Transformer = (tagName, attribs) => ({ tagName, attribs: { ...attribs, - rel: 'noopener', + rel: 'noreferrer noopener', target: '_blank', }, }); @@ -112,7 +112,7 @@ const transformImgTag: Transformer = (tagName, attribs) => { tagName: 'a', attribs: { href: src, - rel: 'noopener', + rel: 'noreferrer noopener', target: '_blank', }, text: attribs.alt || src, From 29ec172c8be7e72c1b2f0ef85bc212ac9c73231d Mon Sep 17 00:00:00 2001 From: Krishan <33421343+kfiven@users.noreply.github.com> Date: Mon, 16 Feb 2026 22:19:21 +1100 Subject: [PATCH 9/9] Release v4.10.3 (#2608) --- package-lock.json | 4 ++-- package.json | 2 +- src/app/features/settings/about/About.tsx | 2 +- src/app/pages/auth/AuthFooter.tsx | 2 +- src/app/pages/client/WelcomePage.tsx | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index 823c38b1..582f02f4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "cinny", - "version": "4.10.2", + "version": "4.10.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "cinny", - "version": "4.10.2", + "version": "4.10.3", "license": "AGPL-3.0-only", "dependencies": { "@atlaskit/pragmatic-drag-and-drop": "1.1.6", diff --git a/package.json b/package.json index 9438330a..601323ad 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cinny", - "version": "4.10.2", + "version": "4.10.3", "description": "Yet another matrix client", "main": "index.js", "type": "module", diff --git a/src/app/features/settings/about/About.tsx b/src/app/features/settings/about/About.tsx index f2cabf1d..7cf121d5 100644 --- a/src/app/features/settings/about/About.tsx +++ b/src/app/features/settings/about/About.tsx @@ -46,7 +46,7 @@ export function About({ requestClose }: AboutProps) { Cinny - v4.10.2 + v4.10.3 Yet another matrix client. diff --git a/src/app/pages/auth/AuthFooter.tsx b/src/app/pages/auth/AuthFooter.tsx index d53ef9a6..124b0384 100644 --- a/src/app/pages/auth/AuthFooter.tsx +++ b/src/app/pages/auth/AuthFooter.tsx @@ -15,7 +15,7 @@ export function AuthFooter() { target="_blank" rel="noreferrer" > - v4.10.2 + v4.10.3 Twitter diff --git a/src/app/pages/client/WelcomePage.tsx b/src/app/pages/client/WelcomePage.tsx index 6eb33a16..969a5cd4 100644 --- a/src/app/pages/client/WelcomePage.tsx +++ b/src/app/pages/client/WelcomePage.tsx @@ -24,7 +24,7 @@ export function WelcomePage() { target="_blank" rel="noreferrer noopener" > - v4.10.2 + v4.10.3 }