Skip to content

Commit 2713e4b

Browse files
committed
Connect messages with outline items
This commit visually connects the actual messages with the thread outline using two markers: * Message headers display the same colored markers as outline items * The outline item/branch for the currently visible message is highlighted
1 parent 75a63f8 commit 2713e4b

File tree

6 files changed

+115
-3
lines changed

6 files changed

+115
-3
lines changed

app/assets/stylesheets/components/messages.css

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -346,3 +346,21 @@ summary.attachment-info {
346346
white-space: pre-wrap;
347347
overflow-x: auto;
348348
}
349+
350+
.message-branch-0 .message-header,
351+
.message-branch-1 .message-header,
352+
.message-branch-2 .message-header,
353+
.message-branch-3 .message-header,
354+
.message-branch-4 .message-header,
355+
.message-branch-5 .message-header {
356+
border-left-width: 4px;
357+
border-left-style: solid;
358+
padding-left: calc(var(--spacing-5) - 4px);
359+
}
360+
361+
.message-branch-0 .message-header { border-left-color: #2563eb; }
362+
.message-branch-1 .message-header { border-left-color: #7c3aed; }
363+
.message-branch-2 .message-header { border-left-color: #f59e0b; }
364+
.message-branch-3 .message-header { border-left-color: #10b981; }
365+
.message-branch-4 .message-header { border-left-color: #ef4444; }
366+
.message-branch-5 .message-header { border-left-color: #0ea5e9; }

app/assets/stylesheets/layouts/topic-view.css

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,17 @@
209209
.branch-details.branch-4 { border-left-color: #ef4444; }
210210
.branch-details.branch-5 { border-left-color: #0ea5e9; }
211211

212+
.outline-item.outline-active {
213+
background: var(--color-bg-hover);
214+
border-left-width: 4px;
215+
font-weight: var(--font-weight-medium);
216+
}
217+
218+
summary.outline-active {
219+
background: var(--color-bg-hover);
220+
border-radius: var(--border-radius-sm);
221+
}
222+
212223
.outline-content {
213224
display: flex;
214225
justify-content: space-between;

app/controllers/topics_controller.rb

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -309,6 +309,11 @@ def build_thread_outline(messages_scope)
309309

310310
assign_branch_segments!
311311
@has_multiple_branches = @thread_outline.map { |entry| entry[:branch_segment_index] }.uniq.size > 1
312+
313+
# Build message_id -> branch_index mapping for message rendering
314+
@message_branch_index = @thread_outline.each_with_object({}) do |entry, hash|
315+
hash[entry[:message].id] = entry[:branch_index]
316+
end
312317
end
313318

314319
def preload_read_state!
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import { Controller } from "@hotwired/stimulus"
2+
3+
export default class extends Controller {
4+
connect() {
5+
this.activeItem = null
6+
this.setupObserver()
7+
}
8+
9+
disconnect() {
10+
if (this.observer) {
11+
this.observer.disconnect()
12+
}
13+
}
14+
15+
setupObserver() {
16+
const options = {
17+
root: null,
18+
rootMargin: "-20% 0px -60% 0px",
19+
threshold: 0
20+
}
21+
22+
this.observer = new IntersectionObserver((entries) => {
23+
this.handleIntersection(entries)
24+
}, options)
25+
26+
document.querySelectorAll(".message-card[id^='message-']").forEach((card) => {
27+
this.observer.observe(card)
28+
})
29+
}
30+
31+
handleIntersection(entries) {
32+
let visibleMessages = []
33+
34+
entries.forEach((entry) => {
35+
if (entry.isIntersecting) {
36+
visibleMessages.push({
37+
element: entry.target,
38+
top: entry.boundingClientRect.top
39+
})
40+
}
41+
})
42+
43+
if (visibleMessages.length === 0) return
44+
45+
visibleMessages.sort((a, b) => a.top - b.top)
46+
const topMessage = visibleMessages[0].element
47+
const messageId = topMessage.id.replace("message-", "")
48+
49+
this.highlightOutlineItem(messageId)
50+
}
51+
52+
highlightOutlineItem(messageId) {
53+
if (this.activeItem) {
54+
this.activeItem.classList.remove("outline-active")
55+
}
56+
if (this.activeSummary) {
57+
this.activeSummary.classList.remove("outline-active")
58+
}
59+
60+
const outlineItem = this.element.querySelector(`a[href="#message-${messageId}"]`)
61+
if (outlineItem) {
62+
const itemContainer = outlineItem.closest(".outline-item") || outlineItem
63+
itemContainer.classList.add("outline-active")
64+
this.activeItem = itemContainer
65+
66+
const parentDetails = outlineItem.closest("details.branch-details")
67+
if (parentDetails) {
68+
const summary = parentDetails.querySelector(":scope > summary")
69+
if (summary) {
70+
summary.classList.add("outline-active")
71+
this.activeSummary = summary
72+
}
73+
}
74+
}
75+
}
76+
}

app/views/topics/_message.html.slim

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
.message-card id=message_dom_id(message) class=("reply-message" if message.reply_to_id)
1+
- message_classes = [("reply-message" if message.reply_to_id), ("message-branch-#{local_assigns[:branch_index]}" unless local_assigns[:branch_index].nil?)].compact.join(" ")
2+
.message-card id=message_dom_id(message) class=message_classes
23
- if (mid_anchor = message_id_anchor(message))
34
a.message-id-anchor id=mid_anchor aria-hidden="true"
45
- if local_assigns[:is_first_unread]

app/views/topics/show.html.slim

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@
100100
details.sidebar-section open=true
101101
summary.sidebar-heading Thread Outline
102102
- if @thread_outline.present?
103-
.thread-outline
103+
.thread-outline data-controller="thread-outline"
104104
- @thread_outline.chunk_while { |a, b| a[:branch_segment_index] == b[:branch_segment_index] }.each do |segment|
105105
- first = segment.first
106106
- first_msg = first[:message]
@@ -199,7 +199,8 @@
199199
- if user_signed_in? && !first_unread_found && !@read_message_ids&.dig(message.id)
200200
- is_first_unread = true
201201
- first_unread_found = true
202-
= render 'message', message: message, number: @message_numbers[message.id], is_first_unread: is_first_unread
202+
- branch_index = @message_branch_index&.dig(message.id)
203+
= render 'message', message: message, number: @message_numbers[message.id], is_first_unread: is_first_unread, branch_index: branch_index
203204

204205
.topic-navigation
205206
= link_to "← Back to Topics", topics_path, class: "back-link"

0 commit comments

Comments
 (0)