# Require rails
gem "rails", "~> 7.1.0"
+gem "turbo-rails"
# Require json for multi_json
gem "json"
thor (1.3.1)
tilt (2.3.0)
timeout (0.4.1)
+ turbo-rails (2.0.4)
+ actionpack (>= 6.0.0)
+ activejob (>= 6.0.0)
+ railties (>= 6.0.0)
tzinfo (2.0.6)
concurrent-ruby (~> 1.0)
unicode-display_width (2.5.0)
sprockets-exporters_pack
strong_migrations
terser
+ turbo-rails
unicode-display_width
validates_email_format_of (>= 1.5.1)
vendorer
};
$(document).ready(function () {
+ // NB: Turns Turbo Drive off by default. Turbo Drive must be opt-in on a per-link and per-form basis
+ // See https://turbo.hotwired.dev/reference/drive#turbo.session.drive
+ Turbo.session.drive = false;
+
var headerWidth = 0,
compactWidth = 0;
updateHeader();
$(window).resize(updateHeader);
+ $(document).on("turbo:render", updateHeader);
}, 0);
$("#menu-icon").on("click", function (e) {
$(document).ready(function () {
- $(".inbox-mark-unread").on("ajax:success", function (event, data) {
- updateHtml(data);
- updateReadState(this, false);
+ $(".messages-table .destroy-message").on("turbo:submit-end", function (event) {
+ if (event.detail.success) {
+ event.target.dataset.isDestroyed = true;
+ }
});
- $(".inbox-mark-read").on("ajax:success", function (event, data) {
- updateHtml(data);
- updateReadState(this, true);
+ $(".messages-table .message-summary").on("turbo:before-morph-element", function (event) {
+ if ($(event.target).find("[data-is-destroyed]").length > 0) {
+ event.preventDefault(); // NB: prevent Turbo from morhping/removing this element
+ $(event.target).fadeOut(800, "linear", function () {
+ $(this).remove();
+ });
+ }
});
-
- $(".inbox-destroy").on("ajax:success", function (event, data) {
- updateHtml(data);
-
- $(this).closest("tr").fadeOut(800, "linear", function () {
- $(this).remove();
- });
- });
-
- function updateHtml(data) {
- $("#inboxanchor").remove();
- $(".user-button").before(data.inboxanchor);
-
- $("#inbox-count").replaceWith(data.inbox_count);
- $("#outbox-count").replaceWith(data.outbox_count);
- $("#muted-count").replaceWith(data.muted_count);
- }
-
- function updateReadState(target, isRead) {
- $(target).closest("tr")
- .toggleClass("inbox-row", isRead)
- .toggleClass("inbox-row-unread", !isRead)
- .find(".inbox-mark-unread").prop("hidden", !isRead).end()
- .find(".inbox-mark-read").prop("hidden", isRead);
- }
});
@message = Message.where(:recipient => current_user).or(Message.where(:sender => current_user.id)).find(params[:id])
@message.from_user_visible = false if @message.sender == current_user
@message.to_user_visible = false if @message.recipient == current_user
- if @message.save && !request.xhr?
+ if @message.save
flash[:notice] = t ".destroyed"
referer = safe_referer(params[:referer]) if params[:referer]
- redirect_to referer || { :action => :inbox }
+ redirect_to referer || { :action => :inbox }, :status => :see_other
end
rescue ActiveRecord::RecordNotFound
@title = t "messages.no_such_message.title"
notice = t ".as_read"
end
@message.message_read = message_read
- if @message.save && !request.xhr?
+ if @message.save
flash[:notice] = notice
- redirect_to :action => :inbox
+ redirect_back_or_to inbox_messages_path, :status => :see_other
end
rescue ActiveRecord::RecordNotFound
@title = t "messages.no_such_message.title"
<% set_title(changeset_index_title(params, current_user))
@heading = if params[:display_name]
- t("changesets.index.title_user_link_html", :user_link => link_to(params[:display_name], user_path(:display_name => params[:display_name])))
+ t("changesets.index.title_user_link_html", :user_link => link_to(params[:display_name], user_path(params[:display_name])))
else
@title
end %>
<%= user_thumbnail diary_comment.user %>
</div>
<div class="col">
- <p class="text-muted m-0" id="comment<%= diary_comment.id %>"><%= t(".comment_from_html", :link_user => (link_to diary_comment.user.display_name, user_path(diary_comment.user)), :comment_created_at => link_to(l(diary_comment.created_at, :format => :friendly), :anchor => "comment#{diary_comment.id}")) %>
+ <p class="text-muted m-0" id="comment<%= diary_comment.id %>"><%= t(".comment_from_html", :link_user => (link_to diary_comment.user.display_name, diary_comment.user), :comment_created_at => link_to(l(diary_comment.created_at, :format => :friendly), :anchor => "comment#{diary_comment.id}")) %>
<% if current_user and diary_comment.user.id != current_user.id %>
| <%= report_link(t(".report"), diary_comment) %>
<% end %>
<% if can? :hidecomment, DiaryEntry %>
<span>
<% if diary_comment.visible? %>
- <%= link_to t(".hide_link"), hide_diary_comment_path(:display_name => diary_comment.diary_entry.user.display_name, :id => diary_comment.diary_entry.id, :comment => diary_comment.id), :method => :post, :data => { :confirm => t(".confirm") } %>
+ <%= link_to t(".hide_link"), hide_diary_comment_path(diary_comment.diary_entry.user, diary_comment.diary_entry, diary_comment), :method => :post, :data => { :confirm => t(".confirm") } %>
<% else %>
- <%= link_to t(".unhide_link"), unhide_diary_comment_path(:display_name => diary_comment.diary_entry.user.display_name, :id => diary_comment.diary_entry.id, :comment => diary_comment.id), :method => :post, :data => { :confirm => t(".confirm") } %>
+ <%= link_to t(".unhide_link"), unhide_diary_comment_path(diary_comment.diary_entry.user, diary_comment.diary_entry, diary_comment), :method => :post, :data => { :confirm => t(".confirm") } %>
<% end %>
</span>
<% end %>
<% end %>
<% if current_user && current_user == diary_entry.user %>
- <li><%= link_to t(".edit_link"), :action => "edit", :display_name => diary_entry.user.display_name, :id => diary_entry.id %></li>
+ <li><%= link_to t(".edit_link"), edit_diary_entry_path(diary_entry.user, diary_entry) %></li>
<% end %>
<% if current_user and diary_entry.user != current_user %>
<% if can? :hide, DiaryEntry %>
<li>
<% if diary_entry.visible %>
- <%= link_to t(".hide_link"), hide_diary_entry_path(:display_name => diary_entry.user.display_name, :id => diary_entry.id), :method => :post, :data => { :confirm => t(".confirm") } %>
+ <%= link_to t(".hide_link"), hide_diary_entry_path(diary_entry.user, diary_entry), :method => :post, :data => { :confirm => t(".confirm") } %>
<% else %>
- <%= link_to t(".unhide_link"), unhide_diary_entry_path(:display_name => diary_entry.user.display_name, :id => diary_entry.id), :method => :post, :data => { :confirm => t(".confirm") } %>
+ <%= link_to t(".unhide_link"), unhide_diary_entry_path(diary_entry.user, diary_entry), :method => :post, :data => { :confirm => t(".confirm") } %>
<% end %>
</li>
<% end %>
<% end %>
<small class='text-muted'>
- <%= t("diary_entries.diary_entry.posted_by_html", :link_user => (link_to diary_entry.user.display_name, user_path(diary_entry.user)), :created => l(diary_entry.created_at, :format => :blog), :language_link => (link_to diary_entry.language.name, :controller => "diary_entries", :action => "index", :display_name => nil, :language => diary_entry.language_code)) %>
+ <%= t("diary_entries.diary_entry.posted_by_html", :link_user => (link_to diary_entry.user.display_name, diary_entry.user), :created => l(diary_entry.created_at, :format => :blog), :language_link => (link_to diary_entry.language.name, :controller => "diary_entries", :action => "index", :display_name => nil, :language => diary_entry.language_code)) %>
<% if (l(diary_entry.updated_at, :format => :blog) != l(diary_entry.created_at, :format => :blog)) %>
<%= t("diary_entries.diary_entry.updated_at_html", :updated => l(diary_entry.updated_at, :format => :blog)) %>
<% end %>
<%= t ".location" %>
-<a href="<%= url_for :controller => "site", :action => "index", :anchor => "map=14/#{location.latitude}/#{location.longitude}" %>">
-<abbr class="geo" title="<%= t ".coordinates", :latitude => number_with_precision(location.latitude, :precision => 4), :longitude => number_with_precision(location.longitude, :precision => 4) %>">
-<%= describe_location location.latitude, location.longitude, 14, location.language_code %>
-</abbr>
-</a>
+<%= link_to root_path(:anchor => "map=14/#{location.latitude}/#{location.longitude}") do
+ tag.abbr :class => "geo",
+ :title => t(".coordinates", :latitude => number_with_precision(location.latitude, :precision => 4),
+ :longitude => number_with_precision(location.longitude, :precision => 4)) do
+ describe_location location.latitude, location.longitude, 14, location.language_code
+ end
+ end %>
<% content_for :heading do %>
<h1><%= t ".heading", :user => @user.display_name %></h1>
- <p><%= t ".subheading_html", :user => link_to(@user.display_name, user_path(@user)) %></p>
+ <p><%= t ".subheading_html", :user => link_to(@user.display_name, @user) %></p>
<% end %>
<% if @comments.empty? %>
<% if current_user %>
<div class="col-auto">
<% if @entry.subscribers.exists?(current_user.id) %>
- <%= link_to t("javascripts.changesets.show.unsubscribe"), diary_entry_unsubscribe_path(:display_name => @entry.user.display_name, :id => @entry.id), :method => :post, :class => "btn btn-sm btn-primary" %>
+ <%= link_to t("javascripts.changesets.show.unsubscribe"), diary_entry_unsubscribe_path(@entry.user, @entry), :method => :post, :class => "btn btn-sm btn-primary" %>
<% else %>
- <%= link_to t("javascripts.changesets.show.subscribe"), diary_entry_subscribe_path(:display_name => @entry.user.display_name, :id => @entry.id), :method => :post, :class => "btn btn-sm btn-primary" %>
+ <%= link_to t("javascripts.changesets.show.subscribe"), diary_entry_subscribe_path(@entry.user, @entry.id), :method => :post, :class => "btn btn-sm btn-primary" %>
<% end %>
</div>
<% end %>
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1">
<%= javascript_include_tag "es6" unless browser.es6? %>
+ <%= javascript_include_tag "turbo", :type => "module" %>
<%= javascript_include_tag "application" %>
<%= javascript_include_tag "i18n/#{I18n.locale}" %>
<%= stylesheet_link_tag "screen-#{dir}", :media => "screen" %>
<%= t("users.show.my messages") %>
<span class='badge count-number'><%= number_with_delimiter(current_user.new_messages.size) %></span>
<% end %>
- <%= link_to t("users.show.my profile"), user_path(current_user), :class => "dropdown-item" %>
+ <%= link_to t("users.show.my profile"), current_user, :class => "dropdown-item" %>
<%= link_to t("users.show.my settings"), edit_account_path, :class => "dropdown-item" %>
<%= link_to t("users.show.my_preferences"), preferences_path, :class => "dropdown-item" %>
<div class="dropdown-divider"></div>
<%= tag.meta :name => "msapplication-TileColor", :content => "#00a300" %>
<%= tag.meta :name => "msapplication-TileImage", :content => image_path("mstile-144x144.png") %>
<%= tag.meta :name => "theme-color", :content => "#ffffff" %>
+<%= turbo_refresh_method_tag :morph %>
+<%= turbo_refresh_scroll_tag :preserve %>
<%= canonical_tag %>
<% if Settings.key?(:publisher_url) -%>
<%= tag.link :rel => "publisher", :href => Settings.publisher_url %>
+++ /dev/null
-<h4 id="inbox-count">
-<%= t "messages.inbox.messages",
- :new_messages => t("messages.inbox.new_messages",
- :count => current_user.new_messages.size),
- :old_messages => t("messages.inbox.old_messages",
- :count => current_user.messages.size - current_user.new_messages.size) %>
-</h4>
-<tr id="inbox-<%= message.id %>" class="inbox-row<%= "-unread" unless message.message_read? %>">
- <td><%= link_to message.sender.display_name, message.sender %></td>
- <td><%= link_to message.title, message %></td>
+<tr id="inbox-<%= message.id %>" class="message-summary inbox-row<%= "-unread" unless message.message_read? %>">
+ <td><%= link_to message.sender.display_name, user_path(message.sender) %></td>
+ <td><%= link_to message.title, message_path(message) %></td>
<td class="text-nowrap"><%= l message.sent_on, :format => :friendly %></td>
<td class="text-nowrap d-flex justify-content-end gap-1">
- <%= button_to t(".unread_button"), message_mark_path(message, :mark => "unread"), :remote => true, :class => "btn btn-sm btn-primary", :form => { :class => "inbox-mark-unread", :hidden => !message.message_read? } %>
- <%= button_to t(".read_button"), message_mark_path(message, :mark => "read"), :remote => true, :class => "btn btn-sm btn-primary", :form => { :class => "inbox-mark-read", :hidden => message.message_read? } %>
- <%= button_to t(".destroy_button"), message_path(message, :referer => request.fullpath), :method => :delete, :remote => true, :class => "btn btn-sm btn-danger", :form_class => "inbox-destroy" %>
+ <%= button_to t(".unread_button"), message_mark_path(message, :mark => "unread"), :class => "btn btn-sm btn-primary", :form => { :data => { :turbo => true }, :class => "inbox-mark-unread", :hidden => !message.message_read? } %>
+ <%= button_to t(".read_button"), message_mark_path(message, :mark => "read"), :class => "btn btn-sm btn-primary", :form => { :data => { :turbo => true }, :class => "inbox-mark-read", :hidden => message.message_read? } %>
+ <%= button_to t(".destroy_button"), message_path(message, :referer => request.fullpath), :method => :delete, :class => "btn btn-sm btn-danger", :form => { :data => { :turbo => true }, :class => "destroy-message" } %>
<% if message.muted? %>
- <%= button_to t(".unmute_button"), message_unmute_path(message), :method => :patch, :class => "btn btn-sm btn-secondary" %>
+ <%= button_to t(".unmute_button"), message_unmute_path(message), :method => :patch, :class => "btn btn-sm btn-secondary", :form => { :data => { :turbo => true } } %>
<% end %>
</td>
</tr>
-<table class="table table-sm align-middle">
+<table class="table table-sm align-middle messages-table">
<thead>
<tr>
<% columns.each do |column| %>
+++ /dev/null
-<h4 id="muted-count">
-<%= t "messages.muted.messages", :count => current_user.muted_messages.size %>
-</h4>
+++ /dev/null
-<h4 id="outbox-count">
-<%= t "messages.outbox.messages", :count => current_user.sent_messages.size %>
-</h4>
-<tr class="inbox-row">
- <td><%= link_to message.recipient.display_name, message.recipient %></td>
- <td><%= link_to message.title, message %></td>
+<tr id="outbox-<%= message.id %>" class="message-summary inbox-row">
+ <td><%= link_to message.recipient.display_name, user_path(message.recipient) %></td>
+ <td><%= link_to message.title, message_path(message) %></td>
<td class="text-nowrap"><%= l message.sent_on, :format => :friendly %></td>
<td class="text-nowrap d-flex justify-content-end gap-1">
- <%= button_to t(".destroy_button"), message_path(message, :referer => request.fullpath), :method => :delete, :remote => true, :class => "btn btn-sm btn-danger", :form_class => "inbox-destroy" %>
+ <%= button_to t(".destroy_button"), message_path(message, :referer => request.fullpath), :method => :delete, :class => "btn btn-sm btn-danger", :form => { :data => { :turbo => true }, :class => "destroy-message" } %>
</td>
</tr>
+++ /dev/null
-json.inboxanchor render(:partial => "layouts/inbox")
-json.inbox_count render(:partial => "inbox_count")
-json.outbox_count render(:partial => "outbox_count")
-json.muted_count render(:partial => "muted_count")
<%= render :partial => "heading", :locals => { :active_link_path => inbox_messages_path } %>
-<%= render :partial => "inbox_count" %>
+<h4><%= t "messages.inbox.messages", :new_messages => t(".new_messages", :count => current_user.new_messages.size), :old_messages => t(".old_messages", :count => current_user.messages.size - current_user.new_messages.size) %></h4>
<% if current_user.messages.size > 0 %>
<%= render :partial => "messages_table", :locals => { :columns => %w[from subject date], :messages => current_user.messages, :inner_partial => "message_summary" } %>
+++ /dev/null
-json.inboxanchor render(:partial => "layouts/inbox")
-json.inbox_count render(:partial => "inbox_count")
-json.outbox_count render(:partial => "outbox_count")
-json.muted_count render(:partial => "muted_count")
<%= render :partial => "heading", :locals => { :active_link_path => muted_messages_path } %>
-<%= render :partial => "muted_count" %>
+<h4><%= t ".messages", :count => current_user.muted_messages.size %></h4>
<%= render :partial => "messages_table", :locals => { :columns => %w[from subject date], :messages => current_user.muted_messages, :inner_partial => "message_summary" } %>
<%= render :partial => "heading", :locals => { :active_link_path => outbox_messages_path } %>
-<%= render :partial => "outbox_count" %>
+<h4><%= t ".messages", :count => current_user.sent_messages.size %></h4>
<% if current_user.sent_messages.size > 0 %>
<%= render :partial => "messages_table", :locals => { :columns => %w[to subject date], :messages => current_user.sent_messages, :inner_partial => "sent_message_summary" } %>
<% content_for :heading do %>
<h1><%= t ".heading", :user => @user.display_name %></h1>
<p><%= t ".subheading_html",
- :user => link_to(@user.display_name, user_path(@user)),
+ :user => link_to(@user.display_name, @user),
:submitted => tag.span(t(".subheading_submitted"), :class => "px-2 py-1 bg-primary bg-opacity-25"),
:commented => tag.span(t(".subheading_commented"), :class => "px-2 py-1 bg-white") %></p>
<% end %>
<h1><%= t ".title" %></h1>
<% end %>
-<p><%= t(".request_access_html", :app_name => link_to(@token.client_application.name, @token.client_application.url), :user => link_to(current_user.display_name, user_path(current_user))) %></p>
+<p><%= t(".request_access_html", :app_name => link_to(@token.client_application.name, @token.client_application.url), :user => link_to(current_user.display_name, current_user)) %></p>
<%= bootstrap_form_tag do |f| %>
<%= f.hidden_field :oauth_token, :value => @token.token %>
</fieldset>
<%= f.primary t(".save") %>
- <%= link_to t(".cancel"), user_path(current_user), :class => "btn btn-link" %>
+ <%= link_to t(".cancel"), current_user, :class => "btn btn-link" %>
<% end %>
<p>
<b><%= t ".user" %></b>
- <%= link_to(@redaction.user.display_name, user_path(@redaction.user)) %>
+ <%= link_to @redaction.user.display_name, @redaction.user %>
</p>
<div class="richtext text-break">
<b><%= t ".description" %></b>
<tr>
<td>
<%= user_thumbnail_tiny user %>
- <%= link_to user.display_name, user_path(user) %>
+ <%= link_to user.display_name, user %>
</td>
<td class="text-nowrap text-end">
<%= link_to t(".table.tbody.unmute"), user_mute_path(user), :method => :delete, :class => "btn btn-sm btn-primary" %>
<p>
<% if user.creation_ip %>
<%= t "users.index.summary_html",
- :name => link_to(user.display_name, user_path(user)),
+ :name => link_to(user.display_name, user),
:ip_address => link_to(user.creation_ip, :ip => user.creation_ip),
:date => l(user.created_at, :format => :friendly) %>
<% else %>
<%= t "users.index.summary_no_ip_html",
- :name => link_to(user.display_name, user_path(user)),
+ :name => link_to(user.display_name, user),
:date => l(user.created_at, :format => :friendly) %>
<% end %>
</p>
"OSM": "writable",
"Matomo": "readonly",
"Qs": "readonly",
- "updateLinks": "readonly"
+ "updateLinks": "readonly",
+ "Turbo": "readonly"
},
"rules": {
"accessor-pairs": "error",
assert_response :success
assert_template "history"
assert_template :layout => "map"
- assert_select "h2", :text => "Changesets by #{user.display_name}", :count => 1
+ assert_select "h2", :text => "Changesets by #{user.display_name}", :count => 1 do
+ assert_select "a[href=?]", user_path(user)
+ end
assert_select "link[rel='alternate'][type='application/atom+xml']", :count => 1 do
assert_select "[href=?]", "http://www.example.com/user/#{ERB::Util.url_encode(user.display_name)}/history/feed"
end
get diary_comments_path(:display_name => other_user.display_name)
assert_response :success
assert_template :comments
- assert_select "table.table-striped" do
- assert_select "tr", :count => 2 # header and one comment
+ assert_dom "a[href='#{user_path(other_user)}']", :text => other_user.display_name
+ assert_select "table.table-striped tbody" do
+ assert_select "tr", :count => 1
end
# Test a suspended user
assert_not Message.find(unread_message.id).message_read
# Check that the marking a message read via XHR works
- post message_mark_path(:message_id => unread_message, :mark => "read"), :xhr => true
- assert_response :success
- assert_template "mark"
+ post message_mark_path(:message_id => unread_message, :mark => "read")
+ assert_response :see_other
assert Message.find(unread_message.id).message_read
# Check that the marking a message unread via XHR works
- post message_mark_path(:message_id => unread_message, :mark => "unread"), :xhr => true
- assert_response :success
- assert_template "mark"
+ post message_mark_path(:message_id => unread_message, :mark => "unread")
+ assert_response :see_other
assert_not Message.find(unread_message.id).message_read
# Asking to mark a message with no ID should fail
get user_notes_path(first_user)
assert_response :success
+ assert_select ".content-heading a[href='#{user_path first_user}']", :text => first_user.display_name
assert_select "table.note_list tbody tr", :count => 1
get user_notes_path(second_user)
assert_response :success
+ assert_select ".content-heading a[href='#{user_path second_user}']", :text => second_user.display_name
assert_select "table.note_list tbody tr", :count => 1
get user_notes_path("non-existent")
end
end
+ def test_show
+ redaction = create(:redaction, :title => "tested-redaction")
+
+ get redaction_path(redaction)
+ assert_response :success
+ assert_dom "h1", :text => /tested-redaction/
+ assert_dom "a[href='#{user_path redaction.user}']", :text => redaction.user.display_name
+ end
+
def test_new
get new_redaction_path
assert_redirected_to login_path(:referer => new_redaction_path)
def test_index
user = create(:user)
- user.mutes.create(:subject => create(:user))
+ muted_user = create(:user)
+ user.mutes.create(:subject => muted_user)
session_for(user)
get user_mutes_path
assert_match "You have muted 1 User", @response.body
+ assert_dom "tr a[href='#{user_path muted_user}']", :text => muted_user.display_name
end
def test_create
assert_text "1 muted message"
click_on "Delete"
- assert_text "0 muted messages"
+ refute_text "1 muted message"
+ assert_text "You have 0 new messages and 0 old messages"
end
end