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/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 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/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 diff --git a/app/models/message.rb b/app/models/message.rb index c6d051f..fc561a9 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 @@ -33,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 @@ -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) diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index 9d58673..3fadb48 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -11,11 +11,22 @@ <%= csrf_meta_tags %> <%= csp_meta_tag %> + <%= stylesheet_link_tag "tailwind", "data-turbo-track": "reload" %> <%= stylesheet_link_tag "application", "data-turbo-track": "reload" %> - - <%= yield %> + +
+
+

+ <%= 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 @@ -
- -
<%= 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 new file mode 100644 index 0000000..9c1081d --- /dev/null +++ b/app/views/messages/_thread.html.erb @@ -0,0 +1,45 @@ +
+ <% if depth == 0 %> +
+
+
+
+

+ [#<%= 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 "/#{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? %> +
+ <%= 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 c1b3181..65d7f4d 100644 --- a/app/views/messages/index.html.erb +++ b/app/views/messages/index.html.erb @@ -1,13 +1,16 @@ <% content_for :title, @list.name %> -

<%= notice %>

+<% if notice %> +
+ <%= notice %> +
+<% end %> -

<%= @list.name %>

+
+

<%= @list.name %>

+

Mailing list archive

+
-<% @messages.each do |message| %> -

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

-
<%= message.body %>
-<% 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 %> 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 "$@"