From 15f56134e789e5e8031d9335d8b51af660bd7d0b Mon Sep 17 00:00:00 2001 From: Marcello Costagliola Date: Tue, 6 Oct 2026 15:40:27 +0200 Subject: [PATCH 1/2] Show a file in a message's rich text without making its preview on view Files are posted as a message's own attachment, and the composer puts none in the rich text: it permits only mentions and link embeds. The server still keeps any attachment whose signed id resolves, a blob included, and Action Text's blob partial links to a representation URL that makes the preview on the first request, without the limits the POST has, and on every request while it fails. The app's own partial shows an image's preview only if it was already made, and otherwise the file's name and size, as it does for a file that isn't an image. The cached presentation's version goes up, since Action Text renders the partial by name and the template digest doesn't see it. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Bj8KnxpTf9sj2Ysa8aLAVa --- app/views/active_storage/blobs/_blob.html.erb | 20 ++++++++++++ app/views/messages/_message.html.erb | 2 +- test/helpers/messages_helper_test.rb | 31 +++++++++++++++++++ 3 files changed, 52 insertions(+), 1 deletion(-) create mode 100644 app/views/active_storage/blobs/_blob.html.erb diff --git a/app/views/active_storage/blobs/_blob.html.erb b/app/views/active_storage/blobs/_blob.html.erb new file mode 100644 index 0000000..cbec938 --- /dev/null +++ b/app/views/active_storage/blobs/_blob.html.erb @@ -0,0 +1,20 @@ +<%# Files are posted as a message's own attachment, which gets its preview when it's posted; the composer adds none to + the rich text. One that's there anyway shows a preview only if it was already made, rather than linking to a URL + that makes it, on any view, for as long as it fails. %> +<% preview = blob.variant(resize_to_limit: local_assigns[:in_gallery] ? [ 800, 600 ] : [ 1024, 768 ]) if blob.variable? %> +<% preview = nil unless preview&.processed? %> + +
attachment--<%= blob.filename.extension %>"> + <% if preview %> + <%= image_tag preview, alt: blob.try(:alt) %> + <% end %> + +
+ <% if caption = blob.try(:caption) %> + <%= caption %> + <% else %> + <%= blob.filename %> + <%= number_to_human_size blob.byte_size %> + <% end %> +
+
diff --git a/app/views/messages/_message.html.erb b/app/views/messages/_message.html.erb index d0c491f..77732cd 100644 --- a/app/views/messages/_message.html.erb +++ b/app/views/messages/_message.html.erb @@ -1,7 +1,7 @@ <%# Be sure to check/update messages/_template.html.erb when changing this file %> <%# Bump this version when the message presentation filters change what they emit. Editing this line changes the template digest, which busts BOTH this fragment cache and the collection cache that keys on this partial's digest (helper Ruby changes alone don't). %> -<% cache [ message, "presentation-v3" ] do %> +<% cache [ message, "presentation-v4" ] do %> <%= message_tag message do %>

<%= local_datetime_tag message.created_at, style: :date %>

diff --git a/test/helpers/messages_helper_test.rb b/test/helpers/messages_helper_test.rb index d2655a8..b49465f 100644 --- a/test/helpers/messages_helper_test.rb +++ b/test/helpers/messages_helper_test.rb @@ -24,4 +24,35 @@ class MessagesHelperTest < ActionView::TestCase assert_match /]*>example<\/a>/, presentation assert_match /bold<\/strong>/, presentation end + + test "message_presentation shows an image in the rich text as a file, rather than making its preview on view" do + presentation = view.message_presentation(message_with_file_in_rich_text("moon.jpg", "image/jpeg")) + + assert_no_match %r{/representations/}, presentation + assert_match %r{moon\.jpg}, presentation + end + + test "message_presentation shows an image in the rich text whose preview was already made" do + message = message_with_file_in_rich_text("moon.jpg", "image/jpeg") + message.body.embeds.first.blob.variant(resize_to_limit: [ 1024, 768 ]).processed + + presentation = view.message_presentation(message.reload) + + assert_match %r{]+src="[^"]*/representations/[^"]*moon\.jpg"}, presentation + end + + test "message_presentation shows a video in the rich text as a file" do + presentation = view.message_presentation(message_with_file_in_rich_text("alpha-centuri.mov", "video/quicktime")) + + assert_no_match %r{/representations/}, presentation + assert_match %r{alpha-centuri\.mov}, presentation + end + + private + def message_with_file_in_rich_text(file, content_type) + blob = ActiveStorage::Blob.create_and_upload!(io: file_fixture(file).open, filename: file, content_type: content_type) + body = %(
Here:
) + + Message.create! room: rooms(:pets), body: body, client_message_id: "0015", creator: users(:jason) + end end From c48083dcfe472a02bbe8f421fc1e4a777057338a Mon Sep 17 00:00:00 2001 From: Marcello Costagliola Date: Tue, 6 Oct 2026 16:02:35 +0200 Subject: [PATCH 2/2] Show a file in a message's rich text by its name, whatever its variants Showing an image's preview when its variant had already been made tied the cached message to state that changes without touching it: a variant made after the message was cached would never show. It also cost a query per embedded image, since Action Text loads the embeds without their variant records. Nothing in the app makes those variants, so the partial no longer looks at them. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Bj8KnxpTf9sj2Ysa8aLAVa --- app/views/active_storage/blobs/_blob.html.erb | 13 +++---------- test/helpers/messages_helper_test.rb | 12 +++++++++--- 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/app/views/active_storage/blobs/_blob.html.erb b/app/views/active_storage/blobs/_blob.html.erb index cbec938..d0d43b3 100644 --- a/app/views/active_storage/blobs/_blob.html.erb +++ b/app/views/active_storage/blobs/_blob.html.erb @@ -1,14 +1,7 @@ <%# Files are posted as a message's own attachment, which gets its preview when it's posted; the composer adds none to - the rich text. One that's there anyway shows a preview only if it was already made, rather than linking to a URL - that makes it, on any view, for as long as it fails. %> -<% preview = blob.variant(resize_to_limit: local_assigns[:in_gallery] ? [ 800, 600 ] : [ 1024, 768 ]) if blob.variable? %> -<% preview = nil unless preview&.processed? %> - -
attachment--<%= blob.filename.extension %>"> - <% if preview %> - <%= image_tag preview, alt: blob.try(:alt) %> - <% end %> - + the rich text. One that's there anyway is shown by its name, with no preview: Action Text's partial links to a URL + that makes one on view, and nothing else in the app would make it. %> +
<% if caption = blob.try(:caption) %> <%= caption %> diff --git a/test/helpers/messages_helper_test.rb b/test/helpers/messages_helper_test.rb index b49465f..976d54e 100644 --- a/test/helpers/messages_helper_test.rb +++ b/test/helpers/messages_helper_test.rb @@ -1,6 +1,8 @@ require "test_helper" class MessagesHelperTest < ActionView::TestCase + include ActiveRecord::Assertions::QueryAssertions + test "message_presentation neutralizes unsafe URI schemes in links" do message = Message.create! room: rooms(:pets), body: '', client_message_id: "0015", creator: users(:jason) @@ -26,19 +28,23 @@ class MessagesHelperTest < ActionView::TestCase end test "message_presentation shows an image in the rich text as a file, rather than making its preview on view" do - presentation = view.message_presentation(message_with_file_in_rich_text("moon.jpg", "image/jpeg")) + message = Message.with_attachment_details.find(message_with_file_in_rich_text("moon.jpg", "image/jpeg").id) + + presentation = nil + assert_no_queries_match(/active_storage_variant_records/) { presentation = view.message_presentation(message) } assert_no_match %r{/representations/}, presentation assert_match %r{moon\.jpg}, presentation end - test "message_presentation shows an image in the rich text whose preview was already made" do + test "message_presentation shows an image in the rich text as a file even when its preview was made" do message = message_with_file_in_rich_text("moon.jpg", "image/jpeg") message.body.embeds.first.blob.variant(resize_to_limit: [ 1024, 768 ]).processed presentation = view.message_presentation(message.reload) - assert_match %r{]+src="[^"]*/representations/[^"]*moon\.jpg"}, presentation + assert_no_match %r{/representations/}, presentation + assert_match %r{moon\.jpg}, presentation end test "message_presentation shows a video in the rich text as a file" do