Skip to content

Commit f4e08e2

Browse files
dvassalloclaude
andcommitted
Add real-time unread mentions indicator to Mentions icon
Display a red dot on the Mentions icon in the sidebar when there are unread mentions in any room. The indicator updates in real-time via: - WebSocket events when other clients mark messages as unread - Turbo Stream updates when the current user marks messages as unread - MutationObserver to catch all DOM changes to room badge classes Changes: - Add mentions_indicator_controller.js with MutationObserver to watch for .badge class changes - Update rooms_list_controller.js to dispatch "addBadge" event for consistency - Add red dot styling to sidebar__tool matching the sidebar toggle button style - Wrap sidebar in controller scope to access both rooms list and icon target 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
1 parent 5f17cd9 commit f4e08e2

File tree

4 files changed

+75
-7
lines changed

4 files changed

+75
-7
lines changed

app/assets/stylesheets/application/sidebar.css

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,14 +66,29 @@
6666
transition: background-color 0.2s ease;
6767
margin-bottom: calc(var(--block-space) * 2);
6868
position: relative;
69-
69+
7070
&:hover {
7171
background-color: var(--color-border);
7272
}
73-
73+
7474
img {
7575
margin: 0;
7676
}
77+
78+
&.has-unread-mentions::after {
79+
--size: 1em;
80+
81+
aspect-ratio: 1;
82+
background-color: var(--color-negative);
83+
block-size: var(--size);
84+
border-radius: calc(var(--size) * 2);
85+
content: "";
86+
flex-shrink: 0;
87+
inline-size: var(--size);
88+
position: absolute;
89+
inset-block-start: calc(var(--size) / -4);
90+
inset-inline-end: calc(var(--size) / -4);
91+
}
7792
}
7893

7994
.sidebar__tool-label {
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { Controller } from "@hotwired/stimulus"
2+
3+
export default class extends Controller {
4+
static targets = [ "icon" ]
5+
6+
connect() {
7+
this.updateIndicator()
8+
9+
// Use MutationObserver to watch for any class changes in the sidebar
10+
this.observer = new MutationObserver(() => {
11+
this.updateIndicator()
12+
})
13+
14+
// Observe the sidebar container for changes to class attributes
15+
const sidebarContainer = this.element.querySelector('.sidebar__container')
16+
if (sidebarContainer) {
17+
this.observer.observe(sidebarContainer, {
18+
attributes: true,
19+
attributeFilter: ['class'],
20+
subtree: true,
21+
childList: true
22+
})
23+
}
24+
}
25+
26+
disconnect() {
27+
if (this.observer) {
28+
this.observer.disconnect()
29+
}
30+
}
31+
32+
update() {
33+
this.updateIndicator()
34+
}
35+
36+
updateIndicator() {
37+
// Check if there are any rooms with the badge class (unread mentions)
38+
const hasUnreadMentions = this.element.querySelectorAll('.room.badge').length > 0
39+
40+
console.log('Mentions indicator update:', hasUnreadMentions, 'badge count:', this.element.querySelectorAll('.room.badge').length)
41+
42+
if (hasUnreadMentions) {
43+
this.iconTarget.classList.add('has-unread-mentions')
44+
} else {
45+
this.iconTarget.classList.remove('has-unread-mentions')
46+
}
47+
}
48+
}

app/javascript/controllers/rooms_list_controller.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,8 @@ export default class extends Controller {
114114
unreadRoom.classList.add(this.badgeClass)
115115
}
116116
})
117+
118+
this.dispatch("addBadge", { detail: { roomId: roomId } })
117119
}
118120

119121
#roomUpdated({ roomId, sortableName }) {

app/views/users/sidebars/show.html.erb

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,12 @@
22
<%= turbo_stream_from :rooms %>
33
<%= turbo_stream_from Current.user, :rooms %>
44

5-
<div class="sidebar__container overflow-y overflow-hide-scrollbar"
6-
data-controller="badge-dot maintain-scroll debounce-updates"
7-
data-badge-dot-unread-class="badge"
8-
data-action="rooms-list:unread@window->badge-dot#update rooms-list:read@window->badge-dot#update rooms-list:addBadge@window->badge-dot#update turbo:submit-start->turbo-frame#unpermanize turbo:before-render@document->maintain-scroll#beforeRender">
5+
<div data-controller="mentions-indicator"
6+
data-action="rooms-list:unread@window->mentions-indicator#update rooms-list:read@window->mentions-indicator#update rooms-list:addBadge@window->mentions-indicator#update">
7+
<div class="sidebar__container overflow-y overflow-hide-scrollbar"
8+
data-controller="badge-dot maintain-scroll debounce-updates"
9+
data-badge-dot-unread-class="badge"
10+
data-action="rooms-list:unread@window->badge-dot#update rooms-list:read@window->badge-dot#update rooms-list:addBadge@window->badge-dot#update turbo:submit-start->turbo-frame#unpermanize turbo:before-render@document->maintain-scroll#beforeRender">
911
<turbo-frame id="direct_rooms_control" target="_top">
1012
<div class="directs gap overflow-x overflow-hide-scrollbar">
1113
<%= link_to new_rooms_direct_path, class: "direct direct__new", data: { turbo_frame: "_self" } do %>
@@ -74,7 +76,7 @@
7476

7577
<div class="sidebar__tools d-hotwire-native-none">
7678
<div class="sidebar__tool-container">
77-
<%= link_to inbox_path, class: "btn sidebar__tool" do %>
79+
<%= link_to inbox_path, class: "btn sidebar__tool", data: { mentions_indicator_target: "icon" } do %>
7880
<%= image_tag "mentions.svg", size: 20, aria: { hidden: "true" } %>
7981
<span class="for-screen-reader">Mentions</span>
8082
<span class="sidebar__tool-label">Mentions</span>
@@ -134,4 +136,5 @@
134136
<% end %>
135137
</div>
136138
</div>
139+
</div>
137140
<% end %>

0 commit comments

Comments
 (0)