Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,6 @@
/config/master.key

/coverage

/app/assets/builds/*
!/app/assets/builds/.keep
2 changes: 2 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down
11 changes: 11 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -353,6 +363,7 @@ DEPENDENCIES
simplecov
sprockets-rails
stimulus-rails
tailwindcss-rails
turbo-rails
tzinfo-data
web-console
Expand Down
2 changes: 2 additions & 0 deletions Procfile.dev
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
web: bin/rails server
css: bin/rails tailwindcss:watch
Empty file added app/assets/builds/.keep
Empty file.
1 change: 1 addition & 0 deletions app/assets/config/manifest.js
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
//= link_tree ../images
//= link_directory ../stylesheets .css
//= link_tree ../builds
1 change: 1 addition & 0 deletions app/assets/tailwind/application.css
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
@import "tailwindcss";
18 changes: 17 additions & 1 deletion app/controllers/messages_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
12 changes: 9 additions & 3 deletions app/models/message.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand Down Expand Up @@ -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)

Expand Down
15 changes: 13 additions & 2 deletions app/views/layouts/application.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,22 @@
<meta name="viewport" content="width=device-width,initial-scale=1">
<%= csrf_meta_tags %>
<%= csp_meta_tag %>
<%= stylesheet_link_tag "tailwind", "data-turbo-track": "reload" %>

<%= stylesheet_link_tag "application", "data-turbo-track": "reload" %>
</head>

<body>
<%= yield %>
<body class="bg-gray-50">
<header class="bg-white shadow-sm border-b border-gray-200">
<div class="container mx-auto px-4 py-4">
<h1 class="text-2xl font-bold text-gray-900">
<%= link_to "blade.ruby-lang.org", root_path, class: "hover:text-red-600 transition-colors" %>
</h1>
</div>
</header>

<main class="container mx-auto px-4 py-8">
<%= yield %>
</main>
</body>
</html>
32 changes: 25 additions & 7 deletions app/views/messages/_message.html.erb
Original file line number Diff line number Diff line change
@@ -1,8 +1,26 @@
<div class="message" id="<%= dom_id message %>">
<ul class="headers">
<li>From: <%= message.from %></li>
<li>Date: <%= message.published_at %></li>
<li>Subject: <%= message.subject %></li>
</ul>
<pre><%= message.body %></pre>
<div class="bg-white rounded-lg shadow-md overflow-hidden" id="<%= dom_id message %>">
<div class="border-b border-gray-200 bg-gray-50 px-6 py-4">
<h2 class="text-xl font-semibold text-gray-900 mb-3"><%= message.subject %></h2>
<div class="space-y-1 text-sm text-gray-600">
<div class="flex items-center gap-2">
<span class="font-medium text-gray-700">From:</span>
<span><%= message.from %></span>
</div>
<div class="flex items-center gap-2">
<span class="font-medium text-gray-700">Date:</span>
<span><%= message.published_at&.strftime("%Y-%m-%d %H:%M:%S %Z") || "N/A" %></span>
</div>
<% if message.list %>
<div class="flex items-center gap-2">
<span class="font-medium text-gray-700">List:</span>
<span class="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-red-100 text-red-800">
<%= message.list.name %> #<%= message.list_seq %>
</span>
</div>
<% end %>
</div>
</div>
<div class="px-6 py-4">
<pre class="whitespace-pre-wrap font-mono text-sm text-gray-800 leading-relaxed"><%= message.body %></pre>
</div>
</div>
45 changes: 45 additions & 0 deletions app/views/messages/_thread.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<div class="thread-message" style="margin-left: <%= depth * 6 %>px;">
<% if depth == 0 %>
<div class="bg-white rounded-lg shadow-md border border-gray-200 overflow-hidden hover:shadow-lg transition-shadow">
<div class="p-5">
<div class="flex items-start justify-between gap-4">
<div class="flex-1 min-w-0">
<h2 class="text-lg font-semibold text-gray-900 hover:text-red-600 transition-colors mb-2">
<span class="px-0.5">[#<%= message.list_seq %>]</span>
<%= link_to without_list_prefix(message.subject), "/#{list.name}/#{message.list_seq}" %>
<span class="text-sm text-gray-500 font-normal ml-2">— <%= message.from %></span>
</h2>
<div class="flex items-center gap-3 text-sm text-gray-600">
<span class="inline-flex items-center gap-1">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 10h.01M12 10h.01M16 10h.01M9 16H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-5l-5 5v-5z"></path>
</svg>
<%= count = message.count_recursively %> <%= count == 1 ? 'message' : 'messages' %>
</span>
</div>
</div>
</div>
</div>
</div>
<% else %>
<div class="py-2 border-l-2 border-gray-200 pl-4 hover:border-red-400 transition-colors">
<div class="flex items-start gap-2 text-sm">
<svg class="w-4 h-4 text-gray-400 mt-0.5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 10h10a8 8 0 018 8v2M3 10l6 6m-6-6l6-6"></path>
</svg>
<div class="flex-1 min-w-0">
<%= link_to "/#{list.name}/#{message.list_seq}", class: "text-gray-900 hover:text-red-600 transition-colors" do %>
<span class="px-0.5">[#<%= message.list_seq %>] <%= without_list_prefix(message.subject) %></span>
<% end %>
<span class="text-gray-500">— <%= message.from %></span>
</div>
</div>
</div>
<% end %>

<% if message.children&.any? %>
<div class="mt-2">
<%= render partial: 'thread', collection: message.children, as: :message, locals: {list: list, depth: depth + 1} %>
</div>
<% end %>
</div>
21 changes: 12 additions & 9 deletions app/views/messages/index.html.erb
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
<% content_for :title, @list.name %>

<p style="color: green"><%= notice %></p>
<% if notice %>
<div class="mb-4 p-4 bg-green-50 border border-green-200 text-green-800 rounded-lg">
<%= notice %>
</div>
<% end %>

<h1><%= @list.name %></h1>
<div class="mb-8">
<h1 class="text-3xl font-bold text-gray-900"><%= @list.name %></h1>
<p class="text-gray-600 mt-2">Mailing list archive</p>
</div>

<% @messages.each do |message| %>
<h2>
<span><%= message.list_seq %>:</span>
<%= link_to without_list_prefix(message.subject), "/#{@list.name}/#{message.list_seq}" %>— <%= message.from %>
</h2>
<div><pre><%= message.body %></pre></div>
<% end %>
<div class="space-y-6">
<%= render partial: 'thread', collection: @messages, as: :message, locals: {list: @list, depth: 0} %>
</div>
10 changes: 9 additions & 1 deletion app/views/messages/show.html.erb
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
<% content_for :title, @message.subject %>

<p style="color: green"><%= notice %></p>
<% if notice %>
<div class="mb-4 p-4 bg-green-50 border border-green-200 text-green-800 rounded-lg">
<%= notice %>
</div>
<% end %>

<div class="mb-4">
<%= link_to "← Back to #{@message.list.name}", "/#{@message.list.name}/", class: "text-red-600 hover:text-red-800 font-medium" %>
</div>

<%= render @message %>
18 changes: 16 additions & 2 deletions bin/dev
Original file line number Diff line number Diff line change
@@ -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 "$@"