Conversation
|
I have read the CLA Document and I hereby sign the CLA 1 out of 2 committers have signed the CLA. |
|
I have read the CLA Document and I hereby sign the CLA |
|
recheck |
|
Here's a netlify link, if you wanted to test: |
Rooms with unread activity but no notification count (muted rooms, push-rule-downgraded rooms) showed a small grey dot in the room list and kept appearing in the collapsed "unread only" nav section even though nothing actually needed attention. This adds a "Hide Unread Activity Dots" toggle (off by default) under Settings › Notifications › System with three coordinated effects: 1. UnreadBadge: when the setting is on, skip rendering the dot entirely for rooms where count=0 and highlight=false. 2. Nav collapse filter (Direct, Home, Space): reads the same setting reactively — when on, the collapsed category shows only rooms with total>0 or highlight>0; when off, it reverts to the original behaviour of showing all rooms that have any unread activity. Toggling the setting in Settings updates the nav instantly without a page reload. 3. Settings UI: adds the toggle under Settings › Notifications › System.
|
Uhm, maybe I'm misunderstanding the purpose of this PR, but your "After" screenshot just seems to hide all the rooms that had this Unread Indicator |
|
@CrazyNicc thx for answering so quickly! Yes that's exactly the point. A lot of irrelevant events trigger that unread marker and I think it makes sense to hide them for some users who get a lot of messages 🙈 Also it is shown when the room is muted I think and probably in other scenarios. There are also a lot of bugs that are related to them. |
|
@chagai95 Oh, afaik muting the room also removed all the read indicators.
But your screenshot completely hides the rooms themselves, not just the unread markers of these rooms. Or did I miss something? |
It hides them from the unread view, when the section is closed (arrow not down), maybe muting the room helps with that, didn't check but I don't want to start muting all my rooms just to avoid seeing this, on fluffy chat it's not shown by default in the unread tab. |
|
Hi everyone, just wondering if there is something missing here? |
|
See the notice in the Project README:
|
…message" Replying to a voice note (m.audio .ogg, e.g. a bridged WhatsApp "Voice message.ogg") — or any media without a text body — rendered the quoted preview as "Failed to load message" even though the replied-to event loaded fine. Root cause: Reply.tsx only used the event's `body`. When `body` was missing/empty (or was only the stripped reply-quote), it fell through to the generic MessageFailedContent fallback, which reads "Failed to load message". Fix: when a loaded reply target has no usable text body, derive a msgtype-based preview (🎤 Voice message / 🔊 Audio / 🖼️ Image / 🎬 Video / 📄 File / 📍 Location) instead of the failure fallback. Voice notes are detected via the MSC3245/MSC1767 voice extension on m.audio. Only genuinely un-renderable events (redacted → deleted notice; unknown/no content → failure) still hit the fallbacks. General across all media msgtypes, not a voice-only band-aid. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VPNRaVnHM9hmpwYyewbeVj
…e estimates The emoji board virtualizes at the group level but used a flat estimateSize of 40px per group. Before any group was measured, the virtualizer therefore believed all ~10 groups fit in the 400px viewport and eagerly mounted every group — ~1,900 emoji <button> nodes — on the very first paint, defeating the virtualization and making the popover slow to open. Estimate each group's real rendered height from its item count (48px emoji / 112px sticker buttons, ~6/3 per row + group chrome) so only the groups that actually fit the viewport mount on open (~700 nodes now, dominated by the first "People" category), and drop overscan 2 -> 1 so an off-screen 500+ item category isn't eagerly built. Scroll/search, categories, recents and custom emoji/stickers are unchanged.
…guage setting
Add an in-app, client-side transcription action for voice/audio messages
(m.audio / m.voice). A microphone button next to the audio player opens an
app-only popover that requests a transcript from the local Whisper backend
(POST /transcribe with { room_id, event_id, mxc, language? }) and shows the
returned text plus the detected language, with copy-to-clipboard. The
transcript is cached in component state and is NEVER posted to the chat or
forwarded anywhere — it is a purely client-side overlay. Transcript text is
rendered strictly as text (untrusted content).
Adds a per-user default transcription language setting (General settings) and
optional backend URL/secret overrides. The API base and shared secret are
configurable via Vite env vars VITE_TRANSCRIBE_API_BASE and
VITE_TRANSCRIBE_API_SECRET (with settings overrides taking precedence), so the
endpoint can be re-pointed without a code change; no secret is hardcoded.
New files live under src/app/features/transcription/ to avoid overlap with the
parallel in-app translation feature. Shared files touched additively:
settings.ts (new fields), RenderMessageContent/AudioContent (thread roomId/
eventId to the button), General.tsx (settings section), RoomTimeline.tsx (pass
identifiers at the two message render sites).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add per-message translate button + auto-translate-all toggle to Cinny, backed by Chagai's on-box argos/"Libre" translation API (offline, no cloud translator). New self-contained `translation` feature (kept separate from the voice-note transcription UI to minimize merge conflicts): - Per-message "Translate" trigger under each text message with a language picker; shows the translation inline. Result cached per event+target. - Global "auto-translate everywhere into <lang>" toggle + language in a new Settings > Translation page; per-chat override in room settings General. - Settings persist in Matrix account data (`in.cinny.translation`) so they sync across devices; room override beats the global setting. - Backend URL + shared token read from public/config.json (ClientConfig.translation); feature hides itself when unconfigured. - Message text is only translated, never interpreted (injection-safe). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VPNRaVnHM9hmpwYyewbeVj
…lback to Settings) Ship the on-box transcription API base+secret in public/config.json (mirrors the translation block) so the 🎤 transcribe button works out of the box on the static Netlify site. TranscribeButton falls back to config.json when the per-user Settings overrides are empty (precedence: Settings > config.json > built-in default). Also fill the translation token in config.json. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VPNRaVnHM9hmpwYyewbeVj
useRooms() excludes DM rooms (mDirects check), so WA portal room IDs were stripped from the URL filter — producing [] (not undefined) — causing Synapse to receive `rooms: []` and return zero results. Fix 1: replace useRooms with new useAllJoinedRooms (no mDirects exclusion) so WA portals resolve correctly in the URL param filter. Fix 2: harden the resolved list — return undefined when all requested rooms fail to resolve (silent-empty guard) so the search falls back to the default scope instead of sending an empty room list. Fix 3: pass the full allRooms list (rooms + DMs) to SearchFilters so the room picker shows WA chats as selectable options. Co-Authored-By: Claude <noreply@anthropic.com>
Include DM/WhatsApp rooms in message search room filter. Fix: useAllJoinedRooms hook to bypass mDirects exclusion, harden empty-room-list → undefined fallback so Synapse gets correct scope.
There was a problem hiding this comment.
Pull request overview
This PR introduces a new user setting to suppress “dot-only” unread indicators (keeping numeric unread counts), and also bundles several additional UI and feature changes (translation, transcription, search scoping, reply preview improvements, and emoji board virtualization tuning).
Changes:
- Add
hideUnreadActivityDotssetting and apply it to unread badge rendering and “show room anyway” filtering when categories are collapsed. - Add in-app translation (account-data backed config, per-message translation UI, global/per-room auto-translate controls) and local Whisper transcription UI/actions.
- Adjust message search room scoping and tune EmojiBoard virtualization/overscan behavior.
Reviewed changes
Copilot reviewed 33 out of 33 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
src/app/state/settings.ts |
Adds new settings keys (hideUnreadActivityDots, transcription overrides) and defaults. |
src/app/state/hooks/roomList.ts |
Adds useAllJoinedRooms hook for broader “joined rooms” selection. |
src/app/pages/client/space/Space.tsx |
Applies hideUnreadActivityDots when deciding whether unread rooms keep categories open/visible. |
src/app/pages/client/home/Home.tsx |
Applies hideUnreadActivityDots when filtering rooms shown under collapsed categories. |
src/app/pages/client/direct/Direct.tsx |
Applies hideUnreadActivityDots when filtering directs shown under collapsed categories. |
src/app/hooks/useClientConfig.ts |
Extends client config type with translation and transcription configuration blocks. |
src/app/features/translation/useTranslation.ts |
Adds translation hooks (account-data config + per-event translation state + auto-translate logic). |
src/app/features/translation/translationConfig.ts |
Adds account-data persistence/read/write + resolution logic for per-room overrides. |
src/app/features/translation/translateApi.ts |
Adds translation backend client and in-memory caching. |
src/app/features/translation/RoomAutoTranslate.tsx |
Adds per-room auto-translate controls in room settings. |
src/app/features/translation/MessageTranslation.tsx |
Adds per-message translation UI (toggle, language picker, result rendering). |
src/app/features/translation/MessageTranslation.css.ts |
Styles for translation UI elements. |
src/app/features/translation/languages.ts |
Static language list + helpers for translation feature. |
src/app/features/transcription/TranscriptionSettings.tsx |
Adds general settings section for transcription language and backend overrides. |
src/app/features/transcription/TranscribeLanguageSelect.tsx |
Adds dropdown selector for transcription default language. |
src/app/features/transcription/TranscribeButton.tsx |
Adds per-audio-message “Transcribe” popover action with copy/retry handling. |
src/app/features/transcription/transcribeApi.ts |
Adds transcription backend client and error handling. |
src/app/features/transcription/languages.ts |
Static language list + helper for transcription feature. |
src/app/features/settings/translation/Translation.tsx |
Adds a settings page for global translation/auto-translate configuration. |
src/app/features/settings/translation/index.ts |
Exports the translation settings page. |
src/app/features/settings/Settings.tsx |
Adds “Translation” to the settings navigation and routes it to a new page. |
src/app/features/settings/notifications/SystemNotification.tsx |
Adds toggle UI for hideUnreadActivityDots. |
src/app/features/settings/general/General.tsx |
Includes the new TranscriptionSettings section. |
src/app/features/room/RoomTimeline.tsx |
Passes roomId/eventId down to message content rendering for transcription actions. |
src/app/features/room/message/Message.tsx |
Injects per-message translation UI under non-edit message content. |
src/app/features/room-settings/general/General.tsx |
Adds per-room auto-translate controls to room settings. |
src/app/features/message-search/MessageSearch.tsx |
Switches room source for search scoping + tweaks URL-room resolution behavior and filter list. |
src/app/components/unread-badge/UnreadBadge.tsx |
Hides dot-only unread badges when hideUnreadActivityDots is enabled. |
src/app/components/RenderMessageContent.tsx |
Adds optional roomId/eventId props and forwards them into AudioContent. |
src/app/components/message/Reply.tsx |
Improves reply previews for media messages (voice/audio/image/file/etc.) when no body is present. |
src/app/components/message/content/AudioContent.tsx |
Adds transcription button next to audio playback controls when identifiers are available. |
src/app/components/emoji-board/EmojiBoard.tsx |
Adjusts virtualizer overscan and adds better height estimation for emoji/sticker groups. |
config.json |
Adds translation/transcription config defaults (including endpoint/token fields). |
| "translation": { | ||
| "endpoint": "https://mx.chagai.website/translate-api", | ||
| "token": "oMBdfX9qUcwBl2TZMTNjYkC9YHuSdI2Cfne-57h8Pks", | ||
| "defaultTargetLang": "en" | ||
| }, | ||
|
|
||
| "transcription": { | ||
| "endpoint": "https://mx.chagai.website/apptranscribe", | ||
| "token": "UwTuImsqajz9PTXg7dMQb839PEPMKsaTWMRmde4V1NQ" | ||
| } |
| "translation": { | ||
| "endpoint": "https://mx.chagai.website/translate-api", | ||
| "token": "oMBdfX9qUcwBl2TZMTNjYkC9YHuSdI2Cfne-57h8Pks", | ||
| "defaultTargetLang": "en" | ||
| }, |
| export const useRoomAutoTranslate = (roomId: string): { auto: boolean; targetLang: string } => { | ||
| const { config } = useTranslationConfig(); | ||
| return useMemo(() => resolveRoomAutoTranslate(config, roomId), [config, roomId]); | ||
| }; |
| {t.error && ( | ||
| <Text size="T200" style={{ color: config.color?.critical, marginTop: config.space.S100 }}> | ||
| {`Translation failed: ${t.error}`} | ||
| </Text> |
|
Sorry I accidently kept on using the same branch 🤦🏻 |
…ehind When the server-side read-up-to event is too old to exist in any loaded timeline (receipt pinned behind by a bridge or a bot posting as the user), tryAutoMarkAsRead found no timeline for it and silently sent no receipt at all - so opening the room could never clear its notification badge. Now, if the user is at the live timeline's end, advance the receipt to the latest event. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PeNxHGMwGxiB48UEMkYuGd
matrix-js-sdk synthesizes an implicit read receipt for the sender of every
live event it processes, including our own messages (typed here, or bridged
in from the phone as the same user). `markAsRead()` read the read-up-to
event with the default `ignoreSynthesized = false`, so as soon as our own
message was the newest event in the room the read-up-to event WAS that
message: `getLatestValidEvent()` matched it on its first iteration, returned
null, and no receipt was sent at all. The server's `notif_count` therefore
stayed stuck and the room kept a phantom unread badge that neither opening
the room nor the "Mark as read" menu item could clear.
Pass `ignoreSynthesized = true` in `markAsRead()` so only server-sent
receipts decide whether anything is left to acknowledge, and the same flag
in `getRoomUnreadInfo()` (RoomTimeline) so `tryAutoMarkAsRead` compares
against the real receipt too instead of a local synthetic one.
`roomHaveUnread()` in utils/room.ts deliberately keeps the non-ignoring
lookup: it is a local display heuristic for the room list, and its early
return ("the newest live event is ours -> not unread") means the synthesized
receipt can never reach the read-up-to comparison. In every other case the
newest event is somebody else's, so the walk back through the timeline
decides on notification events before the receipt id matters. Changing it
would alter sidebar behaviour without fixing anything, so it is left alone.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NH86ZDX9tKwS1Yv7f8VkEv
Authenticated media (MSC3916) can only be fetched with an Authorization header, so the service worker re-issues every media request with the access token. It held that token in an in-memory Map keyed by client id, which the browser throws away every time it terminates the idle worker. The respawned worker then asked the page for the session and, if the page's main thread did not answer within 3s (routine on a large account), fell through to `fetch(event.request)` - the original request with no credentials. Synapse answered 401 in about a millisecond, and because `downloadMedia` never checked `res.ok` the 60-byte JSON error body was wrapped in an object URL and handed to the audio element, which can never decode it: a voice note that spins forever with no error, plus blank avatars, until the tab is reloaded. - sw.ts: also persist the last known session (access token + homeserver base URL) in the worker's own Cache storage, so a respawned worker restores it without a page round-trip; writes are serialized so the newest session always wins, and are only made when the session actually changed. On a miss the persisted session is tried first, then the page is asked with a 10s (was 3s) budget, and only a genuine absence of any session falls through to an unauthenticated request. The token is still never attached to a URL that does not belong to the session's homeserver. Requests without a client id can now be served too, instead of going out bare. The token already lives in this origin's localStorage, so a copy in this origin's cache storage is not a new exposure; it is deleted as soon as the page reports no session (logout, or any load while logged out). - sw-session.ts / index.tsx: push the session on `controllerchange` and when the tab becomes visible, and fall back to the registration's active worker when this page has no controller yet - previously the very first load after a registration pushed nothing at all. - initMatrix.ts: clear the worker's session in `clearLoginData` too, next to the existing call in `logoutClient`. - utils/matrix.ts: `downloadMedia` now throws on a non-ok response, so a failed download reaches the existing error handling instead of producing a "successful" broken blob. - AudioContent.tsx: show the failure - the play chip turns critical and reads "Retry" instead of spinning forever; clicking it already retried. Tested out of band with a node harness that runs the built sw.js against a faked ServiceWorkerGlobalScope and Cache storage: 9 checks covering persist, restore after respawn without asking the page, cross-origin token isolation, waiting for a slow page instead of firing a bare request, and clearing on logout. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NH86ZDX9tKwS1Yv7f8VkEv
Read receipts: stop treating the js-sdk's synthesized receipt for our own messages as an acknowledgement, and send a receipt anyway when the stored read position is too old to be found in any loaded timeline. Media: keep the service worker's access token across worker respawns and surface a failed download instead of feeding an error body to the player. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NH86ZDX9tKwS1Yv7f8VkEv
The idea is to get rid of showing those type of unread rooms and keep only the ones with the number which are a lot more reliable and important.
This is how it looks before:

and this is after:
