diff --git a/app/assets/stylesheets/components/messages.css b/app/assets/stylesheets/components/messages.css index 479f543..926869a 100644 --- a/app/assets/stylesheets/components/messages.css +++ b/app/assets/stylesheets/components/messages.css @@ -229,6 +229,45 @@ font-size: var(--font-size-sm); } +summary.attachment-info { + cursor: pointer; +} + +.attachment-summary-row { + display: flex; + gap: var(--spacing-3); + align-items: center; + width: 100%; +} + +.attachment-download { + margin-left: auto; + font-size: var(--font-size-xs); + color: var(--color-text-link); + text-decoration: none; +} + +.attachment-download:hover { + color: var(--color-text-link-hover); + text-decoration: underline; +} + +.attachment-content { + margin-top: var(--spacing-2); + padding: var(--spacing-2); + background: var(--color-bg-container); + border: var(--border-width) solid var(--color-border); + border-radius: var(--border-radius-sm); + font-size: var(--font-size-xs); + overflow-x: auto; +} + +.attachment-content code { + display: block; + font-family: var(--font-family-mono); + white-space: pre; +} + .filename { font-weight: var(--font-weight-medium); color: var(--color-text-primary); diff --git a/app/controllers/attachments_controller.rb b/app/controllers/attachments_controller.rb new file mode 100644 index 0000000..7ca7332 --- /dev/null +++ b/app/controllers/attachments_controller.rb @@ -0,0 +1,13 @@ +class AttachmentsController < ApplicationController + def show + attachment = Attachment.find(params[:id]) + data = attachment.decoded_body + return head :not_found unless data + + filename = attachment.file_name.presence || "attachment-#{attachment.id}" + content_type = attachment.content_type.presence || "application/octet-stream" + + send_data data, filename: filename, type: content_type, disposition: "attachment" + end +end + diff --git a/app/views/topics/_message.html.slim b/app/views/topics/_message.html.slim index 7a8c7c3..b6298fa 100644 --- a/app/views/topics/_message.html.slim +++ b/app/views/topics/_message.html.slim @@ -41,10 +41,22 @@ .message-attachments h4 Attachments: - message.attachments.each do |attachment| - .attachment - .attachment-info - span.filename = attachment.file_name - span.content-type = attachment.content_type if attachment.content_type + - if attachment.patch? + details.attachment + summary.attachment-info + span.attachment-summary-row + span.filename = attachment.file_name + span.content-type = attachment.content_type if attachment.content_type + = link_to "Download", attachment_path(attachment), class: "attachment-download", download: attachment.file_name, data: { turbo: false } + pre.attachment-content + code.language-diff + = attachment.decoded_body + - else + .attachment + .attachment-info + span.filename = attachment.file_name + span.content-type = attachment.content_type if attachment.content_type + = link_to "Download", attachment_path(attachment), class: "attachment-download", download: attachment.file_name, data: { turbo: false } - if message.import_log.present? .import-metadata diff --git a/config/routes.rb b/config/routes.rb index bb017a0..195122a 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -55,6 +55,7 @@ get '/auth/:provider/callback', to: 'omniauth_callbacks#google_oauth2' post "messages/:id/read", to: "messages#read", as: :read_message + resources :attachments, only: [:show] if defined?(PgHero) constraints AdminConstraint.new do diff --git a/spec/requests/topics_spec.rb b/spec/requests/topics_spec.rb index 8d7ecdb..0f1175d 100644 --- a/spec/requests/topics_spec.rb +++ b/spec/requests/topics_spec.rb @@ -79,6 +79,17 @@ reply_position = response.body.index(reply_message.body) expect(root_position).to be < reply_position end + + it "renders patch attachments inline as expandable diff blocks" do + create(:attachment, :patch_file, message: root_message) + + get topic_path(topic) + + expect(response).to have_http_status(:success) + expect(response.body).to include("Attachments:") + expect(response.body).to include('class="language-diff"') + expect(response.body).to include("diff --git") + end end context "with nonexistent topic" do @@ -89,6 +100,31 @@ end end + describe "GET /attachments/:id" do + let!(:creator) { create(:alias) } + let!(:topic) { create(:topic, creator: creator) } + let!(:message) { create(:message, topic: topic, sender: creator, reply_to: nil, created_at: 2.hours.ago) } + + it "streams the attachment as a download with the right filename" do + attachment = create(:attachment, :patch_file, message: message) + + get attachment_path(attachment) + + expect(response).to have_http_status(:success) + expect(response.headers["Content-Disposition"]).to include("attachment") + expect(response.headers["Content-Disposition"]).to include(attachment.file_name) + expect(response.body).to include("diff --git") + end + + it "returns 404 when the attachment body is missing" do + attachment = create(:attachment, body: nil, message: message) + + get attachment_path(attachment) + + expect(response).to have_http_status(:not_found) + end + end + describe "GET /topics/search" do let!(:creator1) { create(:alias) } let!(:creator2) { create(:alias) }