Skip to content

Add setting to hide unread activity dot indicators - #2923

Open
chagai95 wants to merge 17 commits into
cinnyapp:devfrom
chagai95:dev
Open

chagai95 wants to merge 17 commits into
cinnyapp:devfrom
chagai95:dev

Conversation

@chagai95

@chagai95 chagai95 commented May 2, 2026

Copy link
Copy Markdown

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:
image

and this is after:
image

@github-actions

github-actions Bot commented May 2, 2026

Copy link
Copy Markdown


Thank you for your submission, we really appreciate it. Like many open-source projects, we ask that you all sign our Contributor License Agreement before we can accept your contribution. You can sign the CLA by just posting a Pull Request Comment same as the below format.


I have read the CLA Document and I hereby sign the CLA


1 out of 2 committers have signed the CLA.
✅ (chagai95)[https://github.com/chagai95]
@claude
You can retrigger this bot by commenting recheck in this Pull Request. Posted by the CLA Assistant Lite bot.

@chagai95
chagai95 marked this pull request as ready for review May 2, 2026 01:19
@chagai95
chagai95 marked this pull request as draft May 2, 2026 01:20
@chagai95

chagai95 commented May 2, 2026

Copy link
Copy Markdown
Author

I have read the CLA Document and I hereby sign the CLA

ajbura added a commit to cinnyapp/cla that referenced this pull request May 2, 2026
@chagai95

chagai95 commented May 2, 2026

Copy link
Copy Markdown
Author

recheck

@chagai95

chagai95 commented May 2, 2026

Copy link
Copy Markdown
Author

Here's a netlify link, if you wanted to test:
https://dev--cinny-chagai.netlify.app/home/

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.
@chagai95
chagai95 marked this pull request as ready for review May 6, 2026 23:22
@CrazyNicc

Copy link
Copy Markdown

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

@chagai95

chagai95 commented May 7, 2026

Copy link
Copy Markdown
Author

@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.

@CrazyNicc

Copy link
Copy Markdown

@chagai95 Oh, afaik muting the room also removed all the read indicators.

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

But your screenshot completely hides the rooms themselves, not just the unread markers of these rooms. Or did I miss something?

@chagai95

chagai95 commented May 7, 2026

Copy link
Copy Markdown
Author

@chagai95 Oh, afaik muting the room also removed all the read indicators.

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

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.

@chagai95

Copy link
Copy Markdown
Author

Hi everyone, just wondering if there is something missing here?

@CrazyNicc

Copy link
Copy Markdown

See the notice in the Project README:

We are currently in the process of replacing the matrix-js-sdk with our own SDK. As a result, we will not be accepting any pull requests until further notice. Thank you for your understanding.

claude added 11 commits July 16, 2026 15:22
…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.
Copilot AI review requested due to automatic review settings August 3, 2026 22:34

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 hideUnreadActivityDots setting 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).

Comment thread config.json
Comment on lines +33 to 42
"translation": {
"endpoint": "https://mx.chagai.website/translate-api",
"token": "oMBdfX9qUcwBl2TZMTNjYkC9YHuSdI2Cfne-57h8Pks",
"defaultTargetLang": "en"
},

"transcription": {
"endpoint": "https://mx.chagai.website/apptranscribe",
"token": "UwTuImsqajz9PTXg7dMQb839PEPMKsaTWMRmde4V1NQ"
}
Comment thread config.json
Comment on lines +33 to +37
"translation": {
"endpoint": "https://mx.chagai.website/translate-api",
"token": "oMBdfX9qUcwBl2TZMTNjYkC9YHuSdI2Cfne-57h8Pks",
"defaultTargetLang": "en"
},
Comment on lines +94 to +97
export const useRoomAutoTranslate = (roomId: string): { auto: boolean; targetLang: string } => {
const { config } = useTranslationConfig();
return useMemo(() => resolveRoomAutoTranslate(config, roomId), [config, roomId]);
};
Comment on lines +58 to +61
{t.error && (
<Text size="T200" style={{ color: config.color?.critical, marginTop: config.space.S100 }}>
{`Translation failed: ${t.error}`}
</Text>
@chagai95

chagai95 commented Aug 4, 2026

Copy link
Copy Markdown
Author

Sorry I accidently kept on using the same branch 🤦🏻

claude added 4 commits August 5, 2026 08:14
…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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants