From 7960134e54c111d9d3d6465750fbe808b7cb1042 Mon Sep 17 00:00:00 2001 From: Akira Matsuda Date: Sun, 12 Oct 2025 10:27:07 +0900 Subject: [PATCH 1/8] Message#children --- app/models/message.rb | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/models/message.rb b/app/models/message.rb index c6d051f..c65e6f4 100644 --- a/app/models/message.rb +++ b/app/models/message.rb @@ -10,6 +10,8 @@ class Message < ApplicationRecord # https://blade.ruby-lang.org/ruby-talk/410000 is not. self.skip_time_zone_conversion_for_attributes = [:published_at] + attr_accessor :children + class << self def from_mail(mail, list, list_seq) body = Kconv.toutf8 mail.body.raw_source @@ -85,6 +87,10 @@ def from_string(str) end end + def count_recursively(count = 0) + count + 1 + (children&.sum(&:count_recursively) || 0) + end + def reload_from_s3(s3_client = Aws::S3::Client.new(region: BLADE_BUCKET_REGION)) m = Message.from_s3(List.find_by_id(self.list_id).name, self.list_seq, s3_client) From ca329d1f456e70509f79a3a35a0ea84477e16d2c Mon Sep 17 00:00:00 2001 From: Akira Matsuda Date: Sun, 12 Oct 2025 10:24:55 +0900 Subject: [PATCH 2/8] Select thread messages with recursive query --- app/controllers/messages_controller.rb | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/app/controllers/messages_controller.rb b/app/controllers/messages_controller.rb index 0af40a8..e9101db 100644 --- a/app/controllers/messages_controller.rb +++ b/app/controllers/messages_controller.rb @@ -5,7 +5,10 @@ class MessagesController < ApplicationController def index if (list_name = params[:list_name]) @list = List.find_by_name list_name - @messages = Message.where(list_id: @list.id).order(:id) + + messages = Message.with_recursive(parent_and_children: [Message.where(list_id: @list.id, parent_id: nil).order(:id).limit(100), Message.joins('inner join parent_and_children on messages.parent_id = parent_and_children.id')]) + .joins('inner join parent_and_children on parent_and_children.id = messages.id') + @messages = compose_tree(messages) elsif (query = params[:q]) search query @@ -47,4 +50,17 @@ def search(query) message_where = Message.where('body %> ? AND list_id IN (?)', query, list_ids).order(Arel.sql('body <-> ?', query)) @messages = message_where.offset(page * PER_PAGE).limit(PER_PAGE) end + + def compose_tree(messages) + [].tap do |ret| + messages.each do |m| + if m.parent_id && (parent = messages.detect { it.id == m.parent_id }) + (parent.children ||= []) << m + else + ret << m + end + end + ret.sort_by!(&:id) + end + end end From 2d57227f5fc48151edb15ed4373621d02fd0ee64 Mon Sep 17 00:00:00 2001 From: Akira Matsuda Date: Sun, 12 Oct 2025 10:27:23 +0900 Subject: [PATCH 3/8] Show thread messages count --- app/views/messages/index.html.erb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/messages/index.html.erb b/app/views/messages/index.html.erb index c1b3181..cdfc2b3 100644 --- a/app/views/messages/index.html.erb +++ b/app/views/messages/index.html.erb @@ -6,7 +6,7 @@ <% @messages.each do |message| %>

- <%= message.list_seq %>: + <%= message.list_seq %>: (<%= message.count_recursively %>) <%= link_to without_list_prefix(message.subject), "/#{@list.name}/#{message.list_seq}" %>— <%= message.from %>

<%= message.body %>
From cbd1c227f418ec044b532073f748b9e2611ff497 Mon Sep 17 00:00:00 2001 From: Akira Matsuda Date: Sun, 12 Oct 2025 10:35:34 +0900 Subject: [PATCH 4/8] Render threads --- app/views/messages/_thread.html.erb | 22 ++++++++++++++++++++++ app/views/messages/index.html.erb | 2 ++ 2 files changed, 24 insertions(+) create mode 100644 app/views/messages/_thread.html.erb diff --git a/app/views/messages/_thread.html.erb b/app/views/messages/_thread.html.erb new file mode 100644 index 0000000..c8a7217 --- /dev/null +++ b/app/views/messages/_thread.html.erb @@ -0,0 +1,22 @@ +
+ <% if depth == 0 %> +

+ <%= message.list_seq %>: (<%= message.count_recursively %>) + <%= link_to without_list_prefix(message.subject), "/#{list.name}/#{message.list_seq}" %> +

+ <% else %> +
+ + <%= link_to "[#{message.list_seq}]", "/#{list.name}/#{message.list_seq}" %> + <%= without_list_prefix(message.subject) %> + - <%= message.from&.first || message.from %> + +
+ <% end %> + + <% if message.children&.any? %> + <% message.children.each do |child| %> + <%= render partial: 'thread', locals: { message: child, list: list, depth: depth + 1 } %> + <% end %> + <% end %> +
diff --git a/app/views/messages/index.html.erb b/app/views/messages/index.html.erb index cdfc2b3..f4823d7 100644 --- a/app/views/messages/index.html.erb +++ b/app/views/messages/index.html.erb @@ -10,4 +10,6 @@ <%= link_to without_list_prefix(message.subject), "/#{@list.name}/#{message.list_seq}" %>— <%= message.from %>
<%= message.body %>
+ + <%= render partial: 'thread', locals: { message: message, list: @list, depth: 0 } %> <% end %> From 1276689c58e30997fda4450a6274f3eace0208e9 Mon Sep 17 00:00:00 2001 From: Akira Matsuda Date: Sun, 12 Oct 2025 11:46:23 +0900 Subject: [PATCH 5/8] bundle tailwindcss-rails --- Gemfile | 2 ++ Gemfile.lock | 11 +++++++++++ 2 files changed, 13 insertions(+) diff --git a/Gemfile b/Gemfile index de247cf..10deeda 100644 --- a/Gemfile +++ b/Gemfile @@ -24,6 +24,8 @@ gem "turbo-rails" # Hotwire's modest JavaScript framework [https://stimulus.hotwired.dev] gem "stimulus-rails" +gem 'tailwindcss-rails' + # Use Redis adapter to run Action Cable in production # gem "redis", ">= 4.0.1" diff --git a/Gemfile.lock b/Gemfile.lock index 2832e3f..b3f2dea 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -296,6 +296,16 @@ GEM stimulus-rails (1.3.4) railties (>= 6.0.0) stringio (3.1.7) + tailwindcss-rails (4.3.0) + railties (>= 7.0.0) + tailwindcss-ruby (~> 4.0) + tailwindcss-ruby (4.1.13) + tailwindcss-ruby (4.1.13-aarch64-linux-gnu) + tailwindcss-ruby (4.1.13-aarch64-linux-musl) + tailwindcss-ruby (4.1.13-arm64-darwin) + tailwindcss-ruby (4.1.13-x86_64-darwin) + tailwindcss-ruby (4.1.13-x86_64-linux-gnu) + tailwindcss-ruby (4.1.13-x86_64-linux-musl) thor (1.4.0) timeout (0.4.3) tsort (0.2.0) @@ -353,6 +363,7 @@ DEPENDENCIES simplecov sprockets-rails stimulus-rails + tailwindcss-rails turbo-rails tzinfo-data web-console From 4daa2140260403f29f987690eba955a745deab23 Mon Sep 17 00:00:00 2001 From: Akira Matsuda Date: Sun, 12 Oct 2025 11:48:59 +0900 Subject: [PATCH 6/8] bin/rails tailwindcss:install --- .gitignore | 3 +++ Procfile.dev | 2 ++ app/assets/builds/.keep | 0 app/assets/config/manifest.js | 1 + app/assets/tailwind/application.css | 1 + app/views/layouts/application.html.erb | 5 ++++- bin/dev | 18 ++++++++++++++++-- 7 files changed, 27 insertions(+), 3 deletions(-) create mode 100644 Procfile.dev create mode 100644 app/assets/builds/.keep create mode 100644 app/assets/tailwind/application.css diff --git a/.gitignore b/.gitignore index 1a68f5b..be8cf26 100644 --- a/.gitignore +++ b/.gitignore @@ -35,3 +35,6 @@ /config/master.key /coverage + +/app/assets/builds/* +!/app/assets/builds/.keep diff --git a/Procfile.dev b/Procfile.dev new file mode 100644 index 0000000..da151fe --- /dev/null +++ b/Procfile.dev @@ -0,0 +1,2 @@ +web: bin/rails server +css: bin/rails tailwindcss:watch diff --git a/app/assets/builds/.keep b/app/assets/builds/.keep new file mode 100644 index 0000000..e69de29 diff --git a/app/assets/config/manifest.js b/app/assets/config/manifest.js index 5918193..338a0e8 100644 --- a/app/assets/config/manifest.js +++ b/app/assets/config/manifest.js @@ -1,2 +1,3 @@ //= link_tree ../images //= link_directory ../stylesheets .css +//= link_tree ../builds diff --git a/app/assets/tailwind/application.css b/app/assets/tailwind/application.css new file mode 100644 index 0000000..f1d8c73 --- /dev/null +++ b/app/assets/tailwind/application.css @@ -0,0 +1 @@ +@import "tailwindcss"; diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index 9d58673..7b3ab38 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -11,11 +11,14 @@ <%= csrf_meta_tags %> <%= csp_meta_tag %> + <%= stylesheet_link_tag "tailwind", "data-turbo-track": "reload" %> <%= stylesheet_link_tag "application", "data-turbo-track": "reload" %> - <%= yield %> +
+ <%= yield %> +
diff --git a/bin/dev b/bin/dev index 5f91c20..ad72c7d 100755 --- a/bin/dev +++ b/bin/dev @@ -1,2 +1,16 @@ -#!/usr/bin/env ruby -exec "./bin/rails", "server", *ARGV +#!/usr/bin/env sh + +if ! gem list foreman -i --silent; then + echo "Installing foreman..." + gem install foreman +fi + +# Default to port 3000 if not specified +export PORT="${PORT:-3000}" + +# Let the debug gem allow remote connections, +# but avoid loading until `debugger` is called +export RUBY_DEBUG_OPEN="true" +export RUBY_DEBUG_LAZY="true" + +exec foreman start -f Procfile.dev "$@" From dfc7a777658b20fa3bae29e0dfcc763911fb6a61 Mon Sep 17 00:00:00 2001 From: Akira Matsuda Date: Sun, 12 Oct 2025 12:25:29 +0900 Subject: [PATCH 7/8] Brief design with Tailwind --- app/views/layouts/application.html.erb | 12 +++++- app/views/messages/_message.html.erb | 32 ++++++++++++---- app/views/messages/_thread.html.erb | 51 +++++++++++++++++++------- app/views/messages/index.html.erb | 23 ++++++------ app/views/messages/show.html.erb | 10 ++++- 5 files changed, 93 insertions(+), 35 deletions(-) diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index 7b3ab38..3fadb48 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -16,8 +16,16 @@ <%= stylesheet_link_tag "application", "data-turbo-track": "reload" %> - -
+ +
+
+

+ <%= link_to "blade.ruby-lang.org", root_path, class: "hover:text-red-600 transition-colors" %> +

+
+
+ +
<%= yield %>
diff --git a/app/views/messages/_message.html.erb b/app/views/messages/_message.html.erb index 29675d2..01c737a 100644 --- a/app/views/messages/_message.html.erb +++ b/app/views/messages/_message.html.erb @@ -1,8 +1,26 @@ -
-
    -
  • From: <%= message.from %>
  • -
  • Date: <%= message.published_at %>
  • -
  • Subject: <%= message.subject %>
  • -
-
<%= message.body %>
+
+
+

<%= message.subject %>

+
+
+ From: + <%= message.from %> +
+
+ Date: + <%= message.published_at&.strftime("%Y-%m-%d %H:%M:%S %Z") || "N/A" %> +
+ <% if message.list %> +
+ List: + + <%= message.list.name %> #<%= message.list_seq %> + +
+ <% end %> +
+
+
+
<%= message.body %>
+
diff --git a/app/views/messages/_thread.html.erb b/app/views/messages/_thread.html.erb index c8a7217..9c1081d 100644 --- a/app/views/messages/_thread.html.erb +++ b/app/views/messages/_thread.html.erb @@ -1,22 +1,45 @@ -
+
<% if depth == 0 %> -

- <%= message.list_seq %>: (<%= message.count_recursively %>) - <%= link_to without_list_prefix(message.subject), "/#{list.name}/#{message.list_seq}" %> -

+
+
+
+
+

+ [#<%= message.list_seq %>] + <%= link_to without_list_prefix(message.subject), "/#{list.name}/#{message.list_seq}" %> + — <%= message.from %> +

+
+ + + + + <%= count = message.count_recursively %> <%= count == 1 ? 'message' : 'messages' %> + +
+
+
+
+
<% else %> -
- - <%= link_to "[#{message.list_seq}]", "/#{list.name}/#{message.list_seq}" %> - <%= without_list_prefix(message.subject) %> - - <%= message.from&.first || message.from %> - +
+
+ + + +
+ <%= link_to "/#{list.name}/#{message.list_seq}", class: "text-gray-900 hover:text-red-600 transition-colors" do %> + [#<%= message.list_seq %>] <%= without_list_prefix(message.subject) %> + <% end %> + — <%= message.from %> +
+
<% end %> <% if message.children&.any? %> - <% message.children.each do |child| %> - <%= render partial: 'thread', locals: { message: child, list: list, depth: depth + 1 } %> - <% end %> +
+ <%= render partial: 'thread', collection: message.children, as: :message, locals: {list: list, depth: depth + 1} %> +
<% end %>
diff --git a/app/views/messages/index.html.erb b/app/views/messages/index.html.erb index f4823d7..65d7f4d 100644 --- a/app/views/messages/index.html.erb +++ b/app/views/messages/index.html.erb @@ -1,15 +1,16 @@ <% content_for :title, @list.name %> -

<%= notice %>

- -

<%= @list.name %>

+<% if notice %> +
+ <%= notice %> +
+<% end %> -<% @messages.each do |message| %> -

- <%= message.list_seq %>: (<%= message.count_recursively %>) - <%= link_to without_list_prefix(message.subject), "/#{@list.name}/#{message.list_seq}" %>— <%= message.from %> -

-
<%= message.body %>
+
+

<%= @list.name %>

+

Mailing list archive

+
- <%= render partial: 'thread', locals: { message: message, list: @list, depth: 0 } %> -<% end %> +
+ <%= render partial: 'thread', collection: @messages, as: :message, locals: {list: @list, depth: 0} %> +
diff --git a/app/views/messages/show.html.erb b/app/views/messages/show.html.erb index f67d490..d778389 100644 --- a/app/views/messages/show.html.erb +++ b/app/views/messages/show.html.erb @@ -1,5 +1,13 @@ <% content_for :title, @message.subject %> -

<%= notice %>

+<% if notice %> +
+ <%= notice %> +
+<% end %> + +
+ <%= link_to "← Back to #{@message.list.name}", "/#{@message.list.name}/", class: "text-red-600 hover:text-red-800 font-medium" %> +
<%= render @message %> From 8e9efb77f4f9bad4fd667be0511482fa58d8d951 Mon Sep 17 00:00:00 2001 From: Akira Matsuda Date: Tue, 21 Oct 2025 19:32:51 +0900 Subject: [PATCH 8/8] Don't reference to a mail in another ML because the thread page is going to be per-ML --- app/models/message.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/models/message.rb b/app/models/message.rb index c65e6f4..fc561a9 100644 --- a/app/models/message.rb +++ b/app/models/message.rb @@ -35,13 +35,13 @@ def from_mail(mail, list, list_seq) # mail.in_reply_to returns strange Array object in some cases (?), so let's use the raw value parent_message_id_header = extract_message_id_from_in_reply_to(mail.header[:in_reply_to]&.value) - parent_message_id = Message.where(message_id_header: parent_message_id_header).pick(:id) if parent_message_id_header + parent_message_id = Message.where(list_id: list.id, message_id_header: parent_message_id_header).pick(:id) if parent_message_id_header if !parent_message_id && (String === mail.references) - parent_message_id = Message.where(message_id_header: mail.references).pick(:id) + parent_message_id = Message.where(list_id: list.id, message_id_header: mail.references).pick(:id) end if !parent_message_id && (Array === mail.references) mail.references.compact.each do |ref| - break if (parent_message_id = Message.where(message_id_header: ref).pick(:id)) + break if (parent_message_id = Message.where(list_id: list.id, message_id_header: ref).pick(:id)) end end