mirror of
https://github.com/basecamp/once-campfire.git
synced 2026-10-08 07:40:08 +09:00
Merge branch 'main' into cursor/split-sidebar-membership-queries-8545
This commit is contained in:
@@ -51,15 +51,15 @@ If you'd rather run the Docker image yourself, you can read more about that in t
|
||||
|
||||
## Other implementations
|
||||
|
||||
Campfire also has implementations in Django, Laravel, Elixir, Go and Rust:
|
||||
Campfire also has implementations in Django, Laravel, Express, Elixir, Go and Rust:
|
||||
|
||||
| HTTP workload (requests/sec) | Rails | [Django](https://github.com/basecamp/once-campfire-django) | [Laravel](https://github.com/basecamp/once-campfire-laravel) | [Elixir](https://github.com/basecamp/once-campfire-elixir) | [Go](https://github.com/basecamp/once-campfire-go) | [Rust](https://github.com/basecamp/once-campfire-rust) |
|
||||
|---|---:|---:|---:|---:|---:|---:|
|
||||
| Room page | 242 | 170 | 164 | 722 | 3,860 | 36,260 |
|
||||
| Messages page | 402 | 196 | 175 | 1,053 | 5,573 | 40,872 |
|
||||
| Sidebar | 541 | 615 | 715 | 1,275 | 19,753 | 34,672 |
|
||||
| Search | 424 | 315 | 305 | 1,156 | 7,053 | 33,299 |
|
||||
| Post a message | 225 | 154 | 137 | 801 | 4,767 | 6,896 |
|
||||
| HTTP workload (requests/sec) | Rails | [Django](https://github.com/basecamp/once-campfire-django) | [Laravel](https://github.com/basecamp/once-campfire-laravel) | [Express](https://github.com/basecamp/once-campfire-express) | [Elixir](https://github.com/basecamp/once-campfire-elixir) | [Go](https://github.com/basecamp/once-campfire-go) | [Rust](https://github.com/basecamp/once-campfire-rust) |
|
||||
|---|---:|---:|---:|---:|---:|---:|---:|
|
||||
| Room page | 241 | 170 | 164 | 559 | 722 | 3,860 | 36,260 |
|
||||
| Messages page | 413 | 196 | 175 | 777 | 1,053 | 5,573 | 40,872 |
|
||||
| Sidebar | 552 | 615 | 715 | 4,125 | 1,275 | 19,753 | 34,672 |
|
||||
| Search | 435 | 315 | 305 | 1,294 | 1,156 | 7,053 | 33,299 |
|
||||
| Post a message | 273 | 154 | 137 | 256 | 801 | 4,767 | 6,896 |
|
||||
|
||||
Measured with 16 concurrent clients on an AMD Ryzen AI MAX+ 395,
|
||||
with four hardware threads allocated to each app.
|
||||
|
||||
@@ -120,7 +120,7 @@ export default class BaseAutocompleteHandler {
|
||||
|
||||
#fetchAutocompletables(url) {
|
||||
if (url) {
|
||||
return fetch(url, { as: "json" }).then(response => response.json())
|
||||
return fetch(url, { headers: { Accept: "application/json" } }).then(response => response.json())
|
||||
} else {
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
@@ -49,7 +49,7 @@ module Message::Pagination
|
||||
end
|
||||
|
||||
def paged?
|
||||
count > PAGE_SIZE
|
||||
offset(PAGE_SIZE).exists?
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -6,7 +6,15 @@ module Message::Searchable
|
||||
after_update_commit :update_in_index
|
||||
after_destroy_commit :remove_from_index
|
||||
|
||||
scope :search, ->(query) { joins("join message_search_index idx on messages.id = idx.rowid").where("idx.body match ?", query).ordered }
|
||||
scope :search, ->(query) { joins("join message_search_index idx on messages.id = idx.rowid").where("idx.body match ?", match_terms(query)).ordered }
|
||||
end
|
||||
|
||||
class_methods do
|
||||
# Quotes each word, so that FTS5 searches for AND, OR, NOT and NEAR rather than
|
||||
# parsing them as operators, which fails on a query like "AND" or "salt AND".
|
||||
def match_terms(query)
|
||||
query.split.map { |word| %("#{word.gsub('"', '""')}") }.join(" ")
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
@@ -40,9 +40,9 @@
|
||||
<div class="flash" data-controller="element-removal" data-action="animationend->element-removal#remove">
|
||||
<div class="flash__inner shadow" style="<%= "--flash-background: var(--color-negative)" if flash[:alert] %>">
|
||||
<% if flash[:alert] %>
|
||||
<%= image_tag "alert.svg", aria: { hidden: true }, size: 24, class: "colorize--white" %></span>
|
||||
<%= image_tag "alert.svg", aria: { hidden: true }, size: 24, class: "colorize--white" %>
|
||||
<% else %>
|
||||
<%= image_tag "check.svg", aria: { hidden: true }, size: 24, class: "colorize--white" %></span>
|
||||
<%= image_tag "check.svg", aria: { hidden: true }, size: 24, class: "colorize--white" %>
|
||||
<% end %>
|
||||
</div>
|
||||
<span class="for-screen-reader" role="alert" aria-atomic="true"><%= notice %></span>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
<%= turbo_frame_tag message, :new_boost do %>
|
||||
<div class="flex-inline message__boost-inline" data-controller="soft-keyboard">
|
||||
<%= link_to new_message_boost_path(message), class: "boost__action txt-small btn", action: "soft-keyboard#open" do %>
|
||||
<%= link_to new_message_boost_path(message), class: "boost__action txt-small btn", data: { action: "soft-keyboard#open" } do %>
|
||||
<%= image_tag "boost.svg", size: 20, aria: { hidden: "true" } %>
|
||||
<span class="for-screen-reader">Add a boost</span>
|
||||
<% end %>
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
class AddRoomIdAndCreatedAtIndexToMessages < ActiveRecord::Migration[8.2]
|
||||
def change
|
||||
add_index :messages, %i[ room_id created_at ]
|
||||
end
|
||||
end
|
||||
Generated
+2
-1
@@ -10,7 +10,7 @@
|
||||
#
|
||||
# It's strongly recommended that you check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema[8.2].define(version: 2025_12_12_154340) do
|
||||
ActiveRecord::Schema[8.2].define(version: 2026_10_04_225000) do
|
||||
create_table "accounts", force: :cascade do |t|
|
||||
t.datetime "created_at", null: false
|
||||
t.text "custom_styles"
|
||||
@@ -101,6 +101,7 @@ ActiveRecord::Schema[8.2].define(version: 2025_12_12_154340) do
|
||||
t.integer "room_id", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.index ["creator_id"], name: "index_messages_on_creator_id"
|
||||
t.index ["room_id", "created_at"], name: "index_messages_on_room_id_and_created_at"
|
||||
t.index ["room_id"], name: "index_messages_on_room_id"
|
||||
end
|
||||
|
||||
|
||||
@@ -6,6 +6,13 @@ class Messages::BoostsControllerTest < ActionDispatch::IntegrationTest
|
||||
@message = messages(:first)
|
||||
end
|
||||
|
||||
test "index wires the new boost link to the soft keyboard" do
|
||||
get message_boosts_url(@message)
|
||||
|
||||
assert_response :success
|
||||
assert_select ".message__boost-inline a.boost__action[data-action='soft-keyboard#open']"
|
||||
end
|
||||
|
||||
test "create" do
|
||||
assert_turbo_stream_broadcasts [ @message.room, :messages ], count: 1 do
|
||||
assert_difference -> { @message.boosts.count }, 1 do
|
||||
|
||||
@@ -20,6 +20,24 @@ class SearchesControllerTest < ActionDispatch::IntegrationTest
|
||||
assert_select ".message", text: /Hello world!/
|
||||
end
|
||||
|
||||
test "operator words are searched for as words" do
|
||||
rooms(:designers).messages.create! body: "Salt and pepper", client_message_id: "operators", creator: users(:david)
|
||||
|
||||
get searches_url, params: { q: "salt AND" }
|
||||
|
||||
assert_response :success
|
||||
assert_select ".message", text: /Salt and pepper/
|
||||
end
|
||||
|
||||
test "searching for an operator word alone doesn't fail" do
|
||||
%w[ AND OR NOT ].each do |word|
|
||||
get searches_url, params: { q: word }
|
||||
|
||||
assert_response :success
|
||||
assert_select ".message", count: 0
|
||||
end
|
||||
end
|
||||
|
||||
test "unreachable messages are not found" do
|
||||
memberships(:david_designers).destroy!
|
||||
|
||||
|
||||
@@ -65,6 +65,24 @@ class MessageTest < ActiveSupport::TestCase
|
||||
assert_equal room.messages.ordered.first(Message::Pagination::PAGE_SIZE).map(&:id), room.messages.first_page.map(&:id)
|
||||
end
|
||||
|
||||
test "paged? tells whether a room has more than a page of messages without counting them all" do
|
||||
room = Rooms::Closed.create!(name: "Paging", creator: users(:david))
|
||||
Message.insert_all Array.new(Message::Pagination::PAGE_SIZE) { |i| { room_id: room.id, creator_id: users(:david).id, client_message_id: "paging-#{i}" } }
|
||||
|
||||
assert_no_queries_match(/COUNT/i) do
|
||||
assert_not room.messages.paged?
|
||||
|
||||
Message.insert_all [ { room_id: room.id, creator_id: users(:david).id, client_message_id: "paging-next" } ]
|
||||
assert room.messages.paged?
|
||||
end
|
||||
end
|
||||
|
||||
test "search reads quotes and operator words in the query as text" do
|
||||
message = rooms(:designers).messages.create!(body: "Say hi, NOT bye", client_message_id: "quoted", creator: users(:david))
|
||||
|
||||
assert_equal [ message ], Message.search(%(say "hi NOT)).to_a
|
||||
end
|
||||
|
||||
private
|
||||
def create_new_message_in(room)
|
||||
room.messages.create!(creator: users(:jason), body: "Hello", client_message_id: "123")
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
require "application_system_test_case"
|
||||
|
||||
class StartingPingsTest < ApplicationSystemTestCase
|
||||
setup do
|
||||
sign_in "kevin@37signals.com"
|
||||
end
|
||||
|
||||
test "suggesting people to ping" do
|
||||
visit new_rooms_direct_url
|
||||
|
||||
find("[data-autocomplete-target='input']").send_keys("Jas")
|
||||
|
||||
assert_selector "suggestion-option[role='option']", text: "Jason"
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user