Skip to content

Commit bd8deca

Browse files
committed
Improve long thread loading time
* Inline attachments are loaded lazyly when opened * Only new messages are rendered open, already read messages are collapsed and lazy loaded
1 parent ae20d25 commit bd8deca

File tree

13 files changed

+290
-60
lines changed

13 files changed

+290
-60
lines changed

app/assets/stylesheets/components/messages.css

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -409,3 +409,30 @@ summary.attachment-info {
409409
.message-branch-3 .message-header { border-left-color: #10b981; }
410410
.message-branch-4 .message-header { border-left-color: #ef4444; }
411411
.message-branch-5 .message-header { border-left-color: #0ea5e9; }
412+
413+
/* Collapse toggle */
414+
.message-collapse-toggle {
415+
border: none;
416+
background: none;
417+
color: var(--color-text-muted);
418+
cursor: pointer;
419+
padding: var(--spacing-1) var(--spacing-2);
420+
display: inline-flex;
421+
align-items: center;
422+
border-radius: var(--border-radius-sm);
423+
margin-left: auto;
424+
}
425+
426+
.message-collapse-toggle:hover {
427+
color: var(--color-text-primary);
428+
background: var(--color-bg-hover);
429+
}
430+
431+
/* Collapsed state */
432+
.message-card.is-collapsed .message-body-wrapper {
433+
display: none;
434+
}
435+
436+
.message-card.is-collapsed .message-header {
437+
border-bottom: none;
438+
}

app/controllers/attachments_controller.rb

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,9 @@ def show
99

1010
send_data data, filename: filename, type: content_type, disposition: "attachment"
1111
end
12+
13+
def content
14+
@attachment = Attachment.find(params[:id])
15+
render layout: false
16+
end
1217
end

app/controllers/messages_controller.rb

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,38 @@ def by_message_id
1616
end
1717
end
1818

19+
def content
20+
@message = Message.eager_load(
21+
:sender,
22+
:sender_person,
23+
{ sender_person: :default_alias },
24+
:attachments
25+
).find(params[:id])
26+
@topic = @message.topic
27+
28+
if user_signed_in?
29+
ranges = MessageReadRange.where(user: current_user, topic: @topic)
30+
.order(:range_start_message_id)
31+
.pluck(:range_start_message_id, :range_end_message_id)
32+
@read_message_ids = {}
33+
@read_message_ids[@message.id] = ranges.any? { |(s, e)| s <= @message.id && @message.id <= e }
34+
35+
notes = Note.active.visible_to(current_user)
36+
.where(topic: @topic, message: @message)
37+
.includes(
38+
:note_tags,
39+
{ author: { person: :default_alias } },
40+
{ last_editor: { person: :default_alias } },
41+
{ note_mentions: :mentionable }
42+
)
43+
.order(:created_at)
44+
@notes_by_message = Hash.new { |h, k| h[k] = [] }
45+
notes.each { |note| @notes_by_message[note.message_id] << note }
46+
end
47+
48+
render layout: false
49+
end
50+
1951
def read
2052
message = Message.find(params[:id])
2153
MessageReadRange.add_range(user: current_user, topic: message.topic, start_id: message.id, end_id: message.id)
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { Controller } from "@hotwired/stimulus"
2+
3+
export default class extends Controller {
4+
static values = { collapsed: Boolean }
5+
6+
connect() {
7+
this.applyState()
8+
}
9+
10+
collapsedValueChanged() {
11+
this.applyState()
12+
}
13+
14+
toggle() {
15+
this.collapsedValue = !this.collapsedValue
16+
}
17+
18+
applyState() {
19+
this.element.classList.toggle("is-collapsed", this.collapsedValue)
20+
const icon = this.element.querySelector(".message-collapse-toggle i")
21+
if (icon) {
22+
icon.classList.toggle("fa-chevron-down", this.collapsedValue)
23+
icon.classList.toggle("fa-chevron-up", !this.collapsedValue)
24+
}
25+
}
26+
}

app/javascript/controllers/thread_actions_controller.js

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,14 @@ export default class extends Controller {
88

99
markAllRead(event) {
1010
event.preventDefault()
11-
this.post(this.readAllUrlValue, () => {
12-
document.querySelectorAll(".message-content").forEach(el => el.classList.add("is-read"))
11+
document.querySelectorAll(".message-content").forEach(el => el.classList.add("is-read"))
12+
document.querySelectorAll(".message-card").forEach(card => {
13+
const controller = this.application.getControllerForElementAndIdentifier(card, "message-collapse")
14+
if (controller) {
15+
controller.collapsedValue = true
16+
}
1317
})
18+
this.post(this.readAllUrlValue)
1419
}
1520

1621
post(url, onSuccess) {
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
= turbo_frame_tag "attachment-content-#{@attachment.id}" do
2+
pre.attachment-content
3+
code data-controller=("diff-highlight" if @attachment.patch?)
4+
= @attachment.decoded_body_utf8
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
= turbo_frame_tag "message-body-#{@message.id}" do
2+
= render "topics/message_body", message: @message
Lines changed: 10 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,12 @@
11
- 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
2+
- render_mode = local_assigns.fetch(:render_mode, :inline)
3+
- is_collapsed = render_mode != :inline
4+
.message-card id=message_dom_id(message) class=message_classes data-controller="message-collapse" data-message-collapse-collapsed-value=is_collapsed
35
- if (mid_anchor = message_id_anchor(message))
46
a.message-id-anchor id=mid_anchor aria-hidden="true"
57
- if local_assigns[:is_first_unread]
68
a.message-id-anchor id="first-unread" aria-hidden="true"
79
- display_number = number || (@message_numbers && @message_numbers[message.id])
8-
- is_read = defined?(@read_message_ids) && @read_message_ids[message.id]
9-
- if user_signed_in?
10-
- read_data = { controller: "read-status", "read-status-message-id-value": message.id, "read-status-read-url-value": read_message_path(message, format: :json), "read-status-delay-seconds-value": read_visibility_seconds }
11-
- read_classes = ["message-content", ("is-read" if is_read)].compact
12-
- else
13-
- read_data = {}
14-
- read_classes = ["message-content"]
1510
.message-header
1611
.message-author
1712
.author-row
@@ -53,47 +48,11 @@
5348
i.fa-solid.fa-building-columns
5449
= link_to "https://www.postgresql.org/message-id/resend/#{ERB::Util.url_encode(message.message_id)}", target: "_blank", rel: "noopener", data: { turbo: false }, title: "Resend from postgresql.org archive", "aria-label": "Resend from postgresql.org archive" do
5550
i.fa-solid.fa-paper-plane
51+
button.message-collapse-toggle type="button" data-action="click->message-collapse#toggle" aria-label="Toggle message"
52+
i.fa-solid class=(is_collapsed ? "fa-chevron-down" : "fa-chevron-up")
5653

57-
.message-content class=read_classes.join(" ") data=read_data
58-
- if message.subject != @topic.title && message.subject.present?
59-
.message-subject = message.subject
60-
61-
.message-body
62-
= render_message_body(message.body)
63-
64-
- if message.attachments.any?
65-
.message-attachments id="message-#{message.id}-attachments"
66-
h4 Attachments:
67-
- message.attachments.each do |attachment|
68-
- if attachment.patch? || attachment.text?
69-
details.attachment
70-
summary.attachment-info
71-
span.attachment-summary-row
72-
span.filename = attachment.file_name
73-
span.content-type = attachment.content_type if attachment.content_type
74-
= link_to "Download", attachment_path(attachment), class: "attachment-download", download: attachment.file_name, data: { turbo: false }
75-
- if attachment.patch?
76-
- stats = attachment.diff_line_stats
77-
- if stats[:added].positive? || stats[:removed].positive?
78-
span.patchset-stats aria-label="Patch line changes" title="Lines added and removed by this patch"
79-
span.patchset-added +#{stats[:added]}
80-
span.patchset-removed -#{stats[:removed]}
81-
pre.attachment-content
82-
code data-controller=("diff-highlight" if attachment.patch?)
83-
= attachment.decoded_body_utf8
84-
- else
85-
.attachment
86-
.attachment-info
87-
span.filename = attachment.file_name
88-
span.content-type = attachment.content_type if attachment.content_type
89-
= link_to "Download", attachment_path(attachment), class: "attachment-download", download: attachment.file_name, data: { turbo: false }
90-
91-
- if message.import_log.present?
92-
.import-metadata
93-
details
94-
summary Import Notes
95-
pre.import-log = message.import_log
96-
97-
- notes_for_message = @notes_by_message&.[](message.id) || []
98-
- if user_signed_in?
99-
= render "notes/note_stack", topic: @topic, message: message, notes: notes_for_message
54+
.message-body-wrapper
55+
- if render_mode == :inline
56+
= render "topics/message_body", message: message
57+
- elsif render_mode == :collapsed
58+
= turbo_frame_tag "message-body-#{message.id}", src: message_content_path(message)
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
- is_read = defined?(@read_message_ids) && @read_message_ids[message.id]
2+
- if user_signed_in?
3+
- read_data = { controller: "read-status", "read-status-message-id-value": message.id, "read-status-read-url-value": read_message_path(message, format: :json), "read-status-delay-seconds-value": read_visibility_seconds }
4+
- read_classes = ["message-content", ("is-read" if is_read)].compact
5+
- else
6+
- read_data = {}
7+
- read_classes = ["message-content"]
8+
.message-content class=read_classes.join(" ") data=read_data
9+
- if message.subject != @topic.title && message.subject.present?
10+
.message-subject = message.subject
11+
12+
.message-body
13+
= render_message_body(message.body)
14+
15+
- if message.attachments.any?
16+
.message-attachments id="message-#{message.id}-attachments"
17+
h4 Attachments:
18+
- message.attachments.each do |attachment|
19+
- if attachment.patch? || attachment.text?
20+
details.attachment
21+
summary.attachment-info
22+
span.attachment-summary-row
23+
span.filename = attachment.file_name
24+
span.content-type = attachment.content_type if attachment.content_type
25+
= link_to "Download", attachment_path(attachment), class: "attachment-download", download: attachment.file_name, data: { turbo: false }
26+
- if attachment.patch?
27+
- stats = attachment.diff_line_stats
28+
- if stats[:added].positive? || stats[:removed].positive?
29+
span.patchset-stats aria-label="Patch line changes" title="Lines added and removed by this patch"
30+
span.patchset-added +#{stats[:added]}
31+
span.patchset-removed -#{stats[:removed]}
32+
= turbo_frame_tag "attachment-content-#{attachment.id}", src: content_attachment_path(attachment), loading: :lazy
33+
- else
34+
.attachment
35+
.attachment-info
36+
span.filename = attachment.file_name
37+
span.content-type = attachment.content_type if attachment.content_type
38+
= link_to "Download", attachment_path(attachment), class: "attachment-download", download: attachment.file_name, data: { turbo: false }
39+
40+
- if message.import_log.present?
41+
.import-metadata
42+
details
43+
summary Import Notes
44+
pre.import-log = message.import_log
45+
46+
- notes_for_message = @notes_by_message&.[](message.id) || []
47+
- if user_signed_in?
48+
= render "notes/note_stack", topic: @topic, message: message, notes: notes_for_message

app/views/topics/show.html.slim

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -209,12 +209,14 @@
209209

210210
- first_unread_found = false
211211
- @messages.each do |message|
212+
- is_read = user_signed_in? && @read_message_ids&.dig(message.id)
212213
- is_first_unread = false
213-
- if user_signed_in? && !first_unread_found && !@read_message_ids&.dig(message.id)
214+
- if user_signed_in? && !is_read && !first_unread_found
214215
- is_first_unread = true
215216
- first_unread_found = true
217+
- render_mode = (user_signed_in? && is_read) ? :collapsed : :inline
216218
- branch_index = @message_branch_index&.dig(message.id)
217-
= render 'message', message: message, number: @message_numbers[message.id], is_first_unread: is_first_unread, branch_index: branch_index
219+
= render 'message', message: message, number: @message_numbers[message.id], is_first_unread: is_first_unread, branch_index: branch_index, render_mode: render_mode
218220

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

0 commit comments

Comments
 (0)