From f16bd3acd6c7a31e448c230a2ee4209d7ed7ff4c Mon Sep 17 00:00:00 2001 From: trichoplax Date: Thu, 23 Apr 2026 15:26:06 +0100 Subject: [PATCH 01/13] Temporary commit to confirm what should be cherry picked from the mod tools branch --- app/assets/javascripts/application.js | 9 + app/assets/stylesheets/application.scss | 31 ++ app/assets/stylesheets/tabs.scss | 6 +- app/assets/stylesheets/users.scss | 15 + app/controllers/admin_controller.rb | 6 +- app/controllers/moderator_controller.rb | 1 + app/controllers/users_controller.rb | 24 +- app/views/mod_warning/log.html.erb | 89 ++--- app/views/mod_warning/new.html.erb | 135 ++++---- .../moderator/user_vote_summary.html.erb | 107 +++--- app/views/shared/_user_mod_sidebar.html.erb | 88 +++++ app/views/users/_tabs.html.erb | 18 + app/views/users/annotations.html.erb | 62 ++-- app/views/users/full_log.html.erb | 60 ++-- app/views/users/mod.html.erb | 223 ++++++++++-- app/views/users/mod_delete.html.erb | 27 ++ .../users/mod_delete_network_account.html.erb | 27 ++ app/views/users/mod_failban.html.erb | 26 ++ app/views/users/mod_privileges.html.erb | 325 +++++++++--------- app/views/users/show.html.erb | 23 +- config/routes.rb | 5 +- test/controllers/admin_controller_test.rb | 8 +- 22 files changed, 884 insertions(+), 431 deletions(-) create mode 100644 app/views/shared/_user_mod_sidebar.html.erb create mode 100644 app/views/users/mod_delete.html.erb create mode 100644 app/views/users/mod_delete_network_account.html.erb create mode 100644 app/views/users/mod_failban.html.erb diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index 4eb8247fb..1c80254af 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -37,6 +37,15 @@ document.addEventListener('DOMContentLoaded', async () => { dialog.classList.toggle('is-active'); }); + QPixel.DOM.addSelectorListener('click', '.is-partial-only:not(.open)', (ev) => { + if (ev.target.classList.contains("open")) { + return; + } + + ev.target.classList.add("open"); + ev.stopPropagation(); + }); + if (document.cookie.indexOf('dismiss_fvn') === -1) { QPixel.DOM.addSelectorListener('click', '#fvn-dismiss', (_ev) => { document.cookie = 'dismiss_fvn=true; path=/; expires=Fri, 31 Dec 9999 23:59:59 GMT'; diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss index e178458a3..441471e64 100644 --- a/app/assets/stylesheets/application.scss +++ b/app/assets/stylesheets/application.scss @@ -265,3 +265,34 @@ kbd { right: 0.15em; }; } + +.is-partial-only:not(.open) { + max-height: 100px; + overflow: hidden; + position: relative; + + &::after { + content: ''; + position: absolute; + right: 0; left: 0; + bottom: 0; + height: 75px; + background-color: rgba(255, 255, 255, 0.5); + background: linear-gradient(#ffffff66, #fffffffa); + z-index: 10000000; + } + + &::before { + content: 'expand'; + position: absolute; + left: 50%; + transform: translate(-50%); + bottom: 10px; + z-index: 10000001; + padding: 5px 20px; + background-color: #ddd; + border: 1px solid #666; + border-radius: 15px; + cursor: pointer; + } +} diff --git a/app/assets/stylesheets/tabs.scss b/app/assets/stylesheets/tabs.scss index 108168625..ce2c1345a 100644 --- a/app/assets/stylesheets/tabs.scss +++ b/app/assets/stylesheets/tabs.scss @@ -2,4 +2,8 @@ .tabs { margin-bottom: 1em; -} \ No newline at end of file + + .tabs--push { + flex-grow: 1; + } +} diff --git a/app/assets/stylesheets/users.scss b/app/assets/stylesheets/users.scss index 90d9fadc2..7dc2ce359 100644 --- a/app/assets/stylesheets/users.scss +++ b/app/assets/stylesheets/users.scss @@ -241,3 +241,18 @@ $sizes: (16, 32, 40, 48, 64, 128, 256); } } } + +.modtools--sidebar { + margin-right: 1rem; +} +.modtools--usercard { + padding: 0.5rem; +} +.modtools-tbl-noborder { + th { + border-bottom-width: 1px !important; + width: 150px; + } +} + +.mod-warnings-clear-form { display: inline; } diff --git a/app/controllers/admin_controller.rb b/app/controllers/admin_controller.rb index f17661088..b5aace580 100644 --- a/app/controllers/admin_controller.rb +++ b/app/controllers/admin_controller.rb @@ -2,9 +2,9 @@ class AdminController < ApplicationController before_action :verify_admin, except: [:change_back, :verify_elevation] before_action :verify_global_admin, only: [:admin_email, :send_admin_email, :new_site, :create_site, :setup, - :setup_save, :hellban, :all_email, :send_all_email] + :setup_save, :failban, :all_email, :send_all_email] before_action :verify_developer, only: [:change_users, :impersonate] - before_action :set_user, only: [:change_users, :hellban, :impersonate] + before_action :set_user, only: [:change_users, :failban, :impersonate] skip_before_action :check_if_warning_or_suspension_pending, only: [:change_back, :verify_elevation] @@ -204,7 +204,7 @@ def setup_save render end - def hellban + def failban @user.block("user manually blocked by admin ##{current_user.id}") flash[:success] = t 'admin.user_fed_stat' redirect_back fallback_location: admin_path diff --git a/app/controllers/moderator_controller.rb b/app/controllers/moderator_controller.rb index c2f649e7f..60e5ee4fe 100644 --- a/app/controllers/moderator_controller.rb +++ b/app/controllers/moderator_controller.rb @@ -69,6 +69,7 @@ def user_vote_summary total: Vote.for(@user).count ) ) + render layout: 'without_sidebar' end def spammy_users diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 9a81f07bc..65e5777d8 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -12,9 +12,10 @@ class UsersController < ApplicationController before_action :redirect_to_sign_in, only: [:filters], unless: [:user_signed_in?, :json_request?] - before_action :verify_moderator, only: [:mod, :destroy, :soft_delete, :role_toggle, :full_log, - :annotate, :annotations, :mod_privileges, :mod_privilege_action] - before_action :set_user, only: [:show, :mod, :destroy, :soft_delete, :posts, :role_toggle, :full_log, :activity, + before_action :verify_moderator, only: [:mod, :soft_delete, :role_toggle, :full_log, + :annotate, :annotations, :mod_privileges, :mod_privilege_action, :mod_delete,] + before_action :verify_global_moderator, only: [:mod_failban, :mod_delete_network_account] + before_action :set_user, only: [:show, :mod, :mod_delete, :mod_failban, :mod_delete_network_account, :soft_delete, :posts, :role_toggle, :full_log, :activity, :annotate, :annotations, :mod_privileges, :mod_privilege_action, :vote_summary, :network, :avatar] before_action :check_deleted, only: [:show, :posts, :activity] @@ -275,7 +276,9 @@ def activity render layout: 'without_sidebar' end - def mod; end + def mod + render layout: 'without_sidebar' + end def full_log @posts = Post.by(@user).count @@ -319,8 +322,21 @@ def full_log render layout: 'without_sidebar' end + def mod_delete_network_account + render layout: 'without_sidebar' + end + + def mod_failban + render layout: 'without_sidebar' + end + def mod_privileges @abilities = Ability.all + render layout: 'without_sidebar' + end + + def mod_delete + render layout: 'without_sidebar' end def soft_delete diff --git a/app/views/mod_warning/log.html.erb b/app/views/mod_warning/log.html.erb index 5a019feb4..9ba5c5b89 100644 --- a/app/views/mod_warning/log.html.erb +++ b/app/views/mod_warning/log.html.erb @@ -1,42 +1,49 @@ -

Warnings sent to <%= user_link @user %>

+<% content_for :title, "Moderator Tools: #{rtl_safe_username(@user)}" %> - - - - - - - - - <% @warnings.each do |w| %> - - - - - - - - <% end %> -
DateTypeFromExcerptStatus
- <%= time_ago_in_words(w.created_at) %> ago - - <% if w.suspension? %> - <% diff = ((w.suspension_end - w.created_at) / (3600 * 24)).to_i %> - Suspension (<%= diff %>d) - <% else %> - Warning - <% end %> - <%= user_link w.author %><%= raw(sanitize(render_markdown(w.body), scrubber: scrubber)) %> - <% if w.suspension_active? %> - Current - <%= form_tag lift_mod_warning_url(@user.id), method: :post do %> - <%= submit_tag '(lift)', class: 'link is-red' %> - <% end %> - <% elsif w.active %> - Unread - <% elsif w.read %> - Read - <% else %> - Lifted - <% end %> -
+<%= render 'users/tabs', user: @user %> + +
+ <%= render 'shared/user_mod_sidebar', user: @user %> + +
+

Previously Sent Warnings

+ + <% if @warnings.size == 0 %> +

No warnings found for this user.

+ <% end %> + + <% @warnings.each do |w| %> +
+
+
+ <% if w.suspension_active? %> + <%= form_tag lift_mod_warning_url(@user.id), method: :post, class: 'mod-warnings-clear-form' do %> + <%= submit_tag '(lift)', class: 'link is-red' %> + <% end %> · + Current + <% elsif w.active %> + Unread + <% elsif w.read %> + Read + <% else %> + Lifted + <% end %> +
+
+ <% if w.is_suspension %> + <% diff = ((w.suspension_end - w.created_at) / (3600 * 24)).to_i %> + Suspension + length: <%= diff %>d + <% else %> + Warning + <% end %> +
+
<%= time_ago_in_words(w.created_at) %> ago by <%= user_link w.author %>
+
+
+ <%= raw(sanitize(render_markdown(w.body), scrubber: scrubber)) %> +
+
+ <% end %> +
+
diff --git a/app/views/mod_warning/new.html.erb b/app/views/mod_warning/new.html.erb index 63f91ca38..797164d94 100644 --- a/app/views/mod_warning/new.html.erb +++ b/app/views/mod_warning/new.html.erb @@ -2,80 +2,93 @@ <%= render 'posts/markdown_script' %> <% end %> -<%= render 'posts/image_upload' %> +<% content_for :title, "Moderator Tools: #{rtl_safe_username(@user)}" %> -

Warn or suspend <%= user_link @user %>

+<%= render 'users/tabs', user: @user %> -
-

Use the warning tool only against users who have violated the site rules. Prefer other measurements, such as friendly asking the user to stop certain behaviors in a comment.

-
+
+ <%= render 'shared/user_mod_sidebar', user: @user %> + +
+

Warn or Suspend User

-<%= form_for @warning, url: create_mod_warning_path(@user.id), method: :post do |f| %> -
-
- 1. Choose a template +
+

Use the warning tool only against users who have violated the site rules. Prefer other measures where possible, such as a public + comment.

-
-
-

Choose a template, which explains, why you are contacting the user. If none is applicable, choose to send a custom message.

- - + + <%= form_for @warning, url: create_mod_warning_path(@user.id), method: :post do |f| %> +
+
+ 1. Choose a template
-
-
- 2. Review the message -
-
-
-

Review the generated message and add details. Do not add salutations or information about possible suspensions, as they are generated automatically.

+
+
+

Choose a template that explains why you are contacting the user. If none are applicable, choose to send a + custom message.

+ + +
+
+
+ 2. Review the message +
+
+
+

Review the generated message and add details. Do not add salutations or information about possible + suspensions, as they are generated automatically.

-
- <%= render 'shared/body_field', f: f, field_name: :body, field_label: 'Body' %> -
+
+ <%= render 'shared/body_field', f: f, field_name: :body, field_label: 'Body' %> +
+
-
-
- 3. Choose optional suspension -
-
-
-

Decide, whether or not to suspend the user, and if, for how long. Choose an optional message shown publicly on the user profile.

+
+ 3. Choose optional suspension +
+
+
+

Decide whether or not to suspend the user, and if so for how long. Choose an optional message shown + publicly on the user profile.

- <% if @prior_warning_count == 0 %> -

Info: This user has no prior warnings. The system recommends issuing only a warning, unless the user is destructive and needs to be stopped immediately.

- <% elsif @prior_warning_count >= 5 %> -

Info: This user has <%= @prior_warning_count %> prior warnings. The system recommends suspending them for 365 days (the maximum).

- <% else %> - <% lengths = { 1 => 3, 2 => 7, 3 => 30, 4 => 180 } %> -

Info: This user has <%= @prior_warning_count %> prior warnings. The system recommends suspending them for <%= lengths[@prior_warning_count] %> days.

- <% end %> + <% if @prior_warning_count == 0 %> +

Info: This user has no prior warnings. The system recommends issuing only a warning, unless the user is destructive and needs to be stopped immediately.

+ <% elsif @prior_warning_count >= 5 %> +

Info: This user has <%= @prior_warning_count %> prior warnings. The system recommends suspending them for 365 days (the maximum).

+ <% else %> + <% lengths = { 1 => 3, 2 => 7, 3 => 30, 4 => 180 } %> +

Info: This user has <%= @prior_warning_count %> prior warnings. The system recommends suspending them for <%= lengths[@prior_warning_count] %> days.

+ <% end %> -
- <%= f.label :is_suspension, 'Suspend this user account?', class: 'form-element' %> - - -
+
+ <%= f.label :is_suspension, 'Suspend this user account?', class: 'form-element' %> + + +
-
- <%= f.label :suspension_duration, 'If suspending, for how long?', class: 'form-element' %> -
Enter the number of days. At least 1, at most 365.
- <%= f.number_field :suspension_duration, in: 1..365, class: 'form-element' %> -
+
+ <%= f.label :suspension_duration, 'If suspending, for how long?', class: 'form-element' %> +
Enter the number of days. At least 1, at most 365.
+ <%= f.number_field :suspension_duration, in: 1..365, class: 'form-element' %> +
-
- <%= f.label :suspension_public_notice, 'If suspending, what public notice, if any, do you want to show?', class: 'form-element' %> - <%= f.select :suspension_public_notice, options_for_select([['for rule violations', 'for rule violations'], ['to cool down', 'to cool down']]), { include_blank: true }, class: 'form-element' %> +
+ <%= f.label :suspension_public_notice, 'If suspending, what public notice, if any, do you want to show?', class: 'form-element' %> + <%= f.select :suspension_public_notice, options_for_select([['for rule violations', 'for rule violations'], ['to cool down', 'to cool down']]), { include_blank: true }, class: 'form-element' %> +
+
- + <% end %> + +
-<% end %> diff --git a/app/views/moderator/user_vote_summary.html.erb b/app/views/moderator/user_vote_summary.html.erb index 65fb666a3..9c2d3e7b1 100644 --- a/app/views/moderator/user_vote_summary.html.erb +++ b/app/views/moderator/user_vote_summary.html.erb @@ -1,53 +1,62 @@ -

Vote Summary: <%= user_link @user %>>

-

- This is a summary of votes cast and received by this user. This may help you to identify voting patterns and - sock puppets, but use caution: what you see as a pattern may also be coincidence. Look for conclusive undeniable - patterns before using this data for sanctions. -

+<% content_for :title, "Moderator Tools: #{rtl_safe_username(@user)}" %> -

- Key: - <%= text_bg 'yellow-200', '> 20%', class: 'has-padding-1 has-margin-horizontal-1' %> - <%= text_bg 'yellow-700', '> 30%', class: 'has-padding-1 has-margin-horizontal-1' %> - <%= text_bg 'red-200', '> 40%', class: 'has-padding-1 has-margin-horizontal-1' %> - <%= text_bg 'red-700', '> 50%', class: 'has-color-white has-padding-1 has-margin-horizontal-1' %> -

+<%= render 'users/tabs', user: @user %> -<% [:cast, :received].each do |type| %> -

Votes <%= type %>

+
+ <%= render 'shared/user_mod_sidebar', user: @user %> - - - - - - - - - - - <% @vote_data[type].breakdown.each do |key, count| %> - - - - - <% pct = count * 100.0 / @vote_data[type].total %> - - - <% end %> - -
<%= type == :cast ? 'To' : 'From' %> userVote typeVote count% of total
<%= user_link @users.select { |x| x.id == key[0] }[0] %><%= key[1] %><%= count %> - <% if pct >= 50 %> - <%= text_bg 'red-700', number_to_percentage(pct, precision: 2), class: 'has-color-white has-padding-1' %> - <% elsif pct >= 40 %> - <%= text_bg 'red-200', number_to_percentage(pct, precision: 2), class: 'has-padding-1' %> - <% elsif pct >= 30 %> - <%= text_bg 'yellow-700', number_to_percentage(pct, precision: 2), class: 'has-padding-1' %> - <% elsif pct >= 20 %> - <%= text_bg 'yellow-200', number_to_percentage(pct, precision: 2), class: 'has-padding-1' %> - <% else %> - <%= number_to_percentage(pct, precision: 2) %> - <% end %> -
-<% end %> +
+

Vote Summary

+

+ This is a summary of votes cast and received by this user. This may help you to identify voting patterns and + sock puppets, but use caution: what you see as a pattern may also be coincidence. Look for conclusive undeniable + patterns before using this data for sanctions. +

+ +

+ Key: + <%= text_bg 'yellow-200', '> 20%', class: 'has-padding-1 has-margin-horizontal-1' %> + <%= text_bg 'yellow-700', '> 30%', class: 'has-padding-1 has-margin-horizontal-1' %> + <%= text_bg 'red-200', '> 40%', class: 'has-padding-1 has-margin-horizontal-1' %> + <%= text_bg 'red-700', '> 50%', class: 'has-color-white has-padding-1 has-margin-horizontal-1' %> +

+ <% [:cast, :received].each do |type| %> +

Votes <%= type %>

+ + + + + + + + + + + + <% @vote_data[type].breakdown.each do |key, count| %> + + + + + <% pct = count * 100.0 / @vote_data[type].total %> + + + <% end %> + +
<%= type == :cast ? 'To' : 'From' %> userVote typeVote count% of total
<%= user_link @users.select { |x| x.id == key[0] }[0] %><%= key[1] %><%= count %> + <% if pct >= 50 %> + <%= text_bg 'red-700', number_to_percentage(pct, precision: 2), class: 'has-color-white has-padding-1' %> + <% elsif pct >= 40 %> + <%= text_bg 'red-200', number_to_percentage(pct, precision: 2), class: 'has-padding-1' %> + <% elsif pct >= 30 %> + <%= text_bg 'yellow-700', number_to_percentage(pct, precision: 2), class: 'has-padding-1' %> + <% elsif pct >= 20 %> + <%= text_bg 'yellow-200', number_to_percentage(pct, precision: 2), class: 'has-padding-1' %> + <% else %> + <%= number_to_percentage(pct, precision: 2) %> + <% end %> +
+ <% end %> +
+
diff --git a/app/views/shared/_user_mod_sidebar.html.erb b/app/views/shared/_user_mod_sidebar.html.erb new file mode 100644 index 000000000..6c890e4c8 --- /dev/null +++ b/app/views/shared/_user_mod_sidebar.html.erb @@ -0,0 +1,88 @@ +
+
+
+ User Moderation Tools +
+
+
+ <%= render 'users/common_card', user: user, ckb: false %> +
+
+
+ +
+
+
diff --git a/app/views/users/_tabs.html.erb b/app/views/users/_tabs.html.erb index fb2c9d191..8047eae2b 100644 --- a/app/views/users/_tabs.html.erb +++ b/app/views/users/_tabs.html.erb @@ -28,4 +28,22 @@ <%= link_to network_path(user), class: "tabs--item #{current_page?(network_path(user)) ? 'is-active' : ''}" do %> All Communities <% end %> +
+ <% if current_user&.moderator? %> + <%= link_to mod_user_path(user), class: "tabs--item #{( + current_page?(mod_user_path(user)) || + current_page?(full_user_log_path(user)) || + current_page?(user_annotations_path(user)) || + current_page?(mod_vote_summary_path(user)) || + current_page?(user_privileges_path(user)) || + current_page?(mod_warning_log_path(user)) || + current_page?(new_mod_warning_path(user)) || + current_page?(mod_delete_path(user)) || + current_page?(mod_delete_network_account_path(user)) || + current_page?(mod_failban_path(user)) || + current_page?(start_impersonating_path(user)) + ) ? 'is-active' : ''}" do %> + Moderator Tools <% if @user&.community_user&.mod_warnings&.size&.positive? %> (<%= pluralize(@user.community_user.mod_warnings.count, 'message') %>) <% end %> + <% end %> + <% end %>
diff --git a/app/views/users/annotations.html.erb b/app/views/users/annotations.html.erb index 858a5fdab..26c5b784a 100644 --- a/app/views/users/annotations.html.erb +++ b/app/views/users/annotations.html.erb @@ -1,35 +1,39 @@ -<% - title = 'User annotations' -%> +<% content_for :title, "Moderator Tools: #{rtl_safe_username(@user)}" %> -<% content_for :title, title %> +<%= render 'tabs', user: @user %> -

<%= title %>

+
+ <%= render 'shared/user_mod_sidebar', user: @user %> -
- Add an annotation - <% if defined?(@log) && @log.errors.any? %> -
- There was an error while trying to save your annotation. -
    - <% @log.errors.full_messages.each do |msg| %> -
  • <%= msg %>
  • - <% end %> -
-
- <% end %> - <%= form_tag annotate_user_path(@user), method: :post do %> -
- <%= label_tag :comment, 'Comment', class: 'form-element' %> - <%= text_field_tag :comment, params[:comment], class: 'form-element' %> -
+
+

User annotations

- <%= submit_tag 'Save', class: 'button is-filled' %> - <% end %> -
+
+ Add an annotation + <% if defined?(@log) && @log.errors.any? %> +
+ There was an error while trying to save your annotation. +
    + <% @log.errors.full_messages.each do |msg| %> +
  • <%= msg %>
  • + <% end %> +
+
+ <% end %> + <%= form_tag annotate_user_path(@user), method: :post do %> +
+ <%= label_tag :comment, 'Comment', class: 'form-element' %> + <%= text_field_tag :comment, params[:comment], class: 'form-element' %> +
-
-

<%= pluralize(@logs.count, 'log') %>

-
+ <%= submit_tag 'Save', class: 'button is-filled' %> + <% end %> +
-<%= render 'admin/log_table' %> +
+

<%= pluralize(@logs.count, 'log') %>

+
+ + <%= render 'admin/log_table' %> +
+
diff --git a/app/views/users/full_log.html.erb b/app/views/users/full_log.html.erb index bbd01e835..7c1f8cde8 100644 --- a/app/views/users/full_log.html.erb +++ b/app/views/users/full_log.html.erb @@ -1,44 +1,52 @@ <% content_for :title, "Full Activity Log: #{rtl_safe_username(@user)}" %> -

Full activity log for <%= user_link @user %>

+<%= render 'tabs', user: @user %> -

This is a filterable log for all activity by the user. You can consult it for moderation decisions. Do not share this information to people, who do not have access to it.

+
+ <%= render 'shared/user_mod_sidebar', user: @user %> -<% if params[:filter] == 'interesting' %> -

You are looking at negative interactions the user had with this site. These are not necessarily bad, just actions at which you should look more closely. This list includes deleted comments, rejected flags and edit suggestions and negatively received posts.

-<% end %> +
+

Full activity log

-
- - Show all events - - <% if @interesting > 0 %> - - Negative - <%= @interesting %> - +

This is a filterable log for all activity by the user. You can consult it for moderation decisions. Do not share this information to people, who do not have access to it.

+ + <% if params[:filter] == 'interesting' %> +

You are looking at negative interactions the user had with this site. These are not necessarily bad, just actions at which you should look more closely. This list includes deleted comments, rejected flags and edit suggestions and negatively received posts.

<% end %> - + + + +
-<%= render 'activity_items', mod: true %> + <%= render 'activity_items', mod: true %> +
+
diff --git a/app/views/users/mod.html.erb b/app/views/users/mod.html.erb index 647bd7e1e..18562929c 100644 --- a/app/views/users/mod.html.erb +++ b/app/views/users/mod.html.erb @@ -1,39 +1,198 @@ <% content_for :title, "Moderator Tools: #{rtl_safe_username(@user)}" %> -

Moderator Tools: <%= user_link @user %>

+<%= render 'tabs', user: @user %> -
-
Links
-
-
    -
  • full activity log
  • -
  • <%= link_to 'annotations on user', user_annotations_path(@user) %>
  • -
  • privileges
  • -
  • warnings and suspensions sent to user <% if @user.community_user.suspended? %>(includes lifting the suspension)<% end %>
  • -
  • warn or suspend user
  • -
  • <%= link_to 'vote summary', mod_vote_summary_path(@user) %>
  • - <% if current_user.developer %> -
  • <%= link_to 'impersonate', start_impersonating_path(@user), class: 'is-yellow' %>
  • - <% end %> -
-
-
+
+ <%= render 'shared/user_mod_sidebar', user: @user %> + +
+

Dashboard

+

Please note that information shown in these user moderation tools is sensitive and should not be shared with anyone outside the moderator and admin team.

+ +
+
Account Information
+
+ + + + + + + + + + + + + +
User Name<%= rtl_safe_username(@user) %>
Account ID#<%= @user.id %>
Joined<%= @user.created_at.strftime("%Y-%m-%d") %> (network), <%= @user.community_user.created_at.strftime("%Y-%m-%d") %> (community)
+
+
+ +
+
Activity Summary
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
BehaviorSince CreationLast YearLast MonthMost Recent
Posts Written + <%= @user.posts.count %> + + <%= @user.posts.where(created_at: 360.days.ago..DateTime.now).count %> + + <%= @user.posts.where(created_at: 30.days.ago..DateTime.now).count %> + + <% last_post = @user.posts.last %> + <% if last_post %> + <%= time_ago_in_words(last_post.created_at, locale: :en_abbrev) %> ago + <% else %> + never + <% end %> +
Votes Cast + <%= @user.votes.count %> + + <%= @user.votes.where(created_at: 360.days.ago..DateTime.now).count %> + + <%= @user.votes.where(created_at: 30.days.ago..DateTime.now).count %> + + <% last_vote = @user.votes.last %> + <% if last_vote %> + <%= time_ago_in_words(last_vote.created_at, locale: :en_abbrev) %> ago + <% else %> + never + <% end %> +
Comments written + <%= @user.comments.count %> + + <%= @user.comments.where(created_at: 360.days.ago..DateTime.now).count %> + + <%= @user.comments.where(created_at: 30.days.ago..DateTime.now).count %> + + <% last_comment = @user.comments.last %> + <% if last_comment %> + <%= time_ago_in_words(last_comment.created_at, locale: :en_abbrev) %> ago + <% else %> + never + <% end %> +
Edits Suggested + <%= @user.suggested_edits.count %> + + <%= @user.suggested_edits.where(created_at: 360.days.ago..DateTime.now).count %> + + <%= @user.suggested_edits.where(created_at: 30.days.ago..DateTime.now).count %> + + <% last_suggested_edit = @user.suggested_edits.last %> + <% if last_suggested_edit %> + <%= time_ago_in_words(last_suggested_edit.created_at, locale: :en_abbrev) %> ago + <% else %> + never + <% end %> +
Flags Raised + <%= @user.flags.count %> + + <%= @user.flags.where(created_at: 360.days.ago..DateTime.now).count %> + + <%= @user.votes.where(created_at: 30.days.ago..DateTime.now).count %> + + <% last_flag = @user.flags.last %> + <% if last_flag %> + <%= time_ago_in_words(last_flag.created_at, locale: :en_abbrev) %> ago + <% else %> + never + <% end %> +
+
+
-
-
Danger Zone
-
-

Take care! Actions in this section may not be reversible, and you will not be asked to confirm - after initiating an action.

-
- <%= link_to 'Delete community profile', soft_delete_user_path(@user.id, type: 'profile'), remote: true, - method: :delete, class: 'js-soft-delete button is-danger is-filled', role: 'button' %> - <% if current_user.is_global_moderator || current_user.is_global_admin %> - <%= link_to 'Delete user network-wide', soft_delete_user_path(@user.id, type: 'user'), remote: true, - method: :delete, class: 'js-soft-delete button is-danger is-filled', role: 'button' %> - <% end %> - <% if current_user.is_global_admin %> - <%= link_to 'Feed to STAT (180 days)', hellban_user_path(@user), method: :post, class: 'button is-danger is-filled', role: 'button' %> - <% end %> +
+
Moderation Summary
+
+ <% annotations_count = AuditLog.where(log_type: 'user_annotation', related: @user).count %> + <% warnings_count = ModWarning.where(community_user: @user.community_user, is_suspension: false).count %> + <% suspensions_count = ModWarning.where(community_user: @user.community_user, is_suspension: true).count %> + + + + + + + + + + + + + + + + + +
Annotations + <% if annotations_count > 0 %> + <%= annotations_count %> + <% else %> + 0 + <% end %> +
Currently Suspended? + <% if @user.community_user.suspended? %> + yes + <% else %> + no + <% end %> +
Warnings + <% if warnings_count > 0 %> + <%= warnings_count %> + <% else %> + 0 + <% end %> +
Suspensions + <% if suspensions_count > 0 %> + <%= suspensions_count %> + <% else %> + 0 + <% end %> +
+
diff --git a/app/views/users/mod_delete.html.erb b/app/views/users/mod_delete.html.erb new file mode 100644 index 000000000..518f10a0b --- /dev/null +++ b/app/views/users/mod_delete.html.erb @@ -0,0 +1,27 @@ +<% content_for :title, "Moderator Tools: #{rtl_safe_username(@user)}" %> + +<%= render 'tabs', user: @user %> + +
+ <%= render 'shared/user_mod_sidebar', user: @user %> + +
+

Delete Account

+ +

Some users are just blatant spammers or trolls and some users are just unwilling to follow site rules, even after repeated warnings and suspensions. As a moderator, you may delete the user account in these cases.

+ +
+

Take care! These actions may not be reversible and you will not be asked to confirm after initiating an action.

+
+ +
+
+

Delete Community Profile

+

Delete the community profile of users who are unwilling to follow the rules of this site, even after repeated warnings and suspensions. Choose this option if a user has requested deletion of their profile on one site, once you have confirmed their identity and request.

+ + <%= link_to 'Delete community profile', soft_delete_user_path(@user.id, type: 'profile'), remote: true, + method: :delete, class: 'js-soft-delete button is-danger is-filled' %> +
+
+
+
diff --git a/app/views/users/mod_delete_network_account.html.erb b/app/views/users/mod_delete_network_account.html.erb new file mode 100644 index 000000000..6df3c2409 --- /dev/null +++ b/app/views/users/mod_delete_network_account.html.erb @@ -0,0 +1,27 @@ +<% content_for :title, "Moderator Tools: #{rtl_safe_username(@user)}" %> + +<%= render 'tabs', user: @user %> + +
+ <%= render 'shared/user_mod_sidebar', user: @user %> + +
+

Network-wide Account Deletion

+ +

As a global moderator, you may delete the user account network-wide.

+ +
+

Take care! These actions may not be reversible and you will not be asked to confirm after initiating an action.

+
+ +
+
+

Delete User Network-wide

+

Delete the account network-wide for users who are unwilling to follow the rules of this network, even after repeated warnings and suspensions. Choose this option if a user has requested deletion of their profile on all sites, once you have confirmed their identity and request.

+ + <%= link_to 'Delete user network-wide', soft_delete_user_path(@user.id, type: 'user'), remote: true, + method: :delete, class: 'js-soft-delete button is-danger is-filled' %> +
+
+
+
diff --git a/app/views/users/mod_failban.html.erb b/app/views/users/mod_failban.html.erb new file mode 100644 index 000000000..b7a2b95ce --- /dev/null +++ b/app/views/users/mod_failban.html.erb @@ -0,0 +1,26 @@ +<% content_for :title, "Moderator Tools: #{rtl_safe_username(@user)}" %> + +<%= render 'tabs', user: @user %> + +
+ <%= render 'shared/user_mod_sidebar', user: @user %> + +
+

Fail-ban

+ +
+

Take care! These actions may not be reversible and you will not be asked to confirm after initiating an action.

+
+ +
+ <% if current_user.is_global_admin %> +
+

Feed to STAT

+

Blatant spammers and trolls may be fed into STAT ("Stop The Awful Troll") which causes the system to fail-ban them and limits the amount of damage they might possibly do. Only use for accounts where you are sure that they are never going to constructively use this site.

+ + <%= link_to 'Feed to STAT (180 days)', failban_user_path(@user), method: :post, class: 'button is-danger is-filled' %> +
+ <% end %> +
+
+
diff --git a/app/views/users/mod_privileges.html.erb b/app/views/users/mod_privileges.html.erb index ed62efb6d..0f0428ea3 100644 --- a/app/views/users/mod_privileges.html.erb +++ b/app/views/users/mod_privileges.html.erb @@ -1,176 +1,185 @@ -<% content_for :title, "Moderator Tools: #{@user.username}" %> +<% content_for :title, "Moderator Tools: #{rtl_safe_username(@user)}" %> -

Privileges of <%= link_to @user.username, user_path(@user) %>

+<%= render 'tabs', user: @user %> -
-
- ability - page - Abilities -
- <% @abilities.each do |a| %> - <% next if a.internal_id == 'mod' %> - <% ua = @user.privilege a.internal_id %> -
-
-
- -
-
-

- <%= a.name %> -

-

<%= a.summary %>

- <% unless ua.nil? %> -

- Delete -

- <% end %> -
-
- <% if ua.nil? %> - - <% elsif ua.suspended? %> - - <% else %> - -
-
suspend ability to <%= a.name %>
- -
in days; leave blank for permanent
- +
+ <%= render 'shared/user_mod_sidebar', user: @user %> - -
will be privately shown to user
- +
+

User Privileges

- +
+
+ ability + page + Abilities +
+ <% @abilities.each do |a| %> + <% next if a.internal_id == 'mod' %> + <% ua = @user.privilege a.internal_id %> +
+
+
+ +
+
+

+ <%= a.name %> +

+

<%= a.summary %>

+ <% unless ua.nil? %> +

+ Delete +

+ <% end %>
- <% end %> +
+ <% if ua.nil? %> + + <% elsif ua.suspended? %> + + <% else %> + +
+
suspend ability to <%= a.name %>
+ +
in days; leave blank for permanent
+ + + +
will be privately shown to user
+ + + +
+ <% end %> +
+
+ <% end %> +
- <% end %> - -
-<% if current_user.admin? %> -
-
- Roles -
-
-
-
- + <% if current_user.admin? %> +
+
+ Roles
-
-

- Moderator -

-

Moderators can unilaterally close and delete posts, can feature and lock posts - and may impose restrictions on user accounts.

+
+
+
+ +
+
+

+ Moderator +

+

Moderators can unilaterally close and delete posts, can feature and lock posts + and may impose restrictions on user accounts.

+
+
+ <% if @user.moderator? %> + + <% else %> + + <% end %> +
+
-
- <% if @user.community_user&.is_moderator %> - - <% else %> - - <% end %> + <% end %> + <% if current_user.is_global_admin %> +
+
+
+ +
+
+

+ Administrator +

+

Administrators can edit site settings and user roles.

+
+
+ <% if @user.is_global_moderator %> + + <% else %> + + <% end %> +
-
-<% end %> -<% if current_user.global_admin? %> -
-
-
- -
-
-

- Administrator -

-

Administrators can edit site settings and user roles.

-
-
- <% if @user.community_user&.is_admin %> - - <% else %> - - <% end %> -
-
-
-
-
-
- -
-
-

- Network-wide Moderator -

-

This user will have moderator status on every site in this network.

-
-
- <% if @user.global_moderator? %> - - <% else %> - - <% end %> -
-
-
-
-
-
- -
-
-

- Network-wide Admin -

-

This user will have admin status on every site in this network.

-
-
- <% if @user.global_admin? %> - <% if @user.id == current_user.id %> - - <% else %> - - <% end %> - <% else %> - - <% end %> -
-
-
-<% end %> -<% if current_user.global_admin? && current_user.staff? %> -
-
-
- +
+
+
+ +
+
+

+ Network-wide Moderator +

+

This user will have moderator status on every site in this network.

+
+
+ <% if @user.is_global_moderator %> + + <% else %> + + <% end %> +
+
-
-

- Staff -

-

The staff role doesn't carry any privileges, but designates the staff running this - site.

+
+
+
+ +
+
+

+ Network-wide Admin +

+

This user will have admin status on every site in this network.

+
+
+ <% if @user.is_global_admin %> + <% if @user.id == current_user.id %> + + <% else %> + + <% end %> + <% else %> + + <% end %> +
+
-
- <% if @user.staff? %> - - <% else %> - - <% end %> + <% end %> + <% if current_user.is_global_admin && current_user.staff? %> +
+
+
+ +
+
+

+ Staff +

+

The staff role doesn't carry any privileges, but designates the staff running this + site.

+
+
+ <% if @user.staff? %> + + <% else %> + + <% end %> +
+
+ <% end %>
+
-<% end %>
diff --git a/app/views/users/show.html.erb b/app/views/users/show.html.erb index 74a8ae1b4..627f663bb 100644 --- a/app/views/users/show.html.erb +++ b/app/views/users/show.html.erb @@ -64,27 +64,6 @@ Subscribe to user <% end %> <% end %> - <% if current_user&.at_least_moderator? %> - Moderator Tools <% if @user.community_user.mod_warnings&.size.positive? %> (<%= pluralize(@user.community_user.mod_warnings.count, 'message') %>) <% end %> - - <% end %> <% if current_user&.same_as?(@user) %> <%= link_to qr_login_code_path, class: 'button is-outlined is-small' do %> Mobile Sign In @@ -117,7 +96,7 @@
<% if @user.staff? %>
diff --git a/config/routes.rb b/config/routes.rb index fc4c5ca46..276fe870a 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -224,7 +224,10 @@ get '/:id/mod/annotations', to: 'users#annotations', as: :user_annotations post '/:id/mod/annotations', to: 'users#annotate', as: :annotate_user get '/:id/mod/activity-log', to: 'users#full_log', as: :full_user_log - post '/:id/hellban', to: 'admin#hellban', as: :hellban_user + post '/:id/failban', to: 'admin#failban', as: :failban_user + get '/:id/mod/failban', to: 'users#mod_failban', as: :mod_failban + get '/:id/mod/delete', to: 'users#mod_delete', as: :mod_delete + get '/:id/mod/delete-network-account', to: 'users#mod_delete_network_account', as: :mod_delete_network_account get '/:id/avatar/:size', to: 'users#avatar', as: :user_auto_avatar end diff --git a/test/controllers/admin_controller_test.rb b/test/controllers/admin_controller_test.rb index ba2dcbfd4..7ea0f47f2 100644 --- a/test/controllers/admin_controller_test.rb +++ b/test/controllers/admin_controller_test.rb @@ -196,11 +196,11 @@ class AdminControllerTest < ActionController::TestCase end end - test 'hellban should correctly block the user' do + test 'failban should correctly block the user' do sign_in users(:global_admin) user = users(:standard_user) - try_hellban_user(user) + try_failban_user(user) user.reload assert_response(:found) @@ -223,8 +223,8 @@ def try_audit_logs(**params) get :audit_logs, params: params end - def try_hellban_user(user) - post :hellban, params: { id: user.id } + def try_failban_user(user) + post :failban, params: { id: user.id } end def try_impersonate_user(user) From 6c29d6d6ffe34df1af33b250a9d1a0a1e309cc24 Mon Sep 17 00:00:00 2001 From: trichoplax Date: Thu, 23 Apr 2026 16:00:20 +0100 Subject: [PATCH 02/13] Add missing image upload modal for warnings --- app/views/mod_warning/new.html.erb | 1 + 1 file changed, 1 insertion(+) diff --git a/app/views/mod_warning/new.html.erb b/app/views/mod_warning/new.html.erb index 797164d94..d45550f27 100644 --- a/app/views/mod_warning/new.html.erb +++ b/app/views/mod_warning/new.html.erb @@ -4,6 +4,7 @@ <% content_for :title, "Moderator Tools: #{rtl_safe_username(@user)}" %> +<%= render 'posts/image_upload' %> <%= render 'users/tabs', user: @user %>
From ed3e5d9b798728b166c438264bee9e69c1acc0a8 Mon Sep 17 00:00:00 2001 From: trichoplax Date: Thu, 23 Apr 2026 18:03:24 +0100 Subject: [PATCH 03/13] Formatting improvements thanks to Rubocop --- app/controllers/users_controller.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 65e5777d8..2af983ee0 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -13,11 +13,11 @@ class UsersController < ApplicationController before_action :redirect_to_sign_in, only: [:filters], unless: [:user_signed_in?, :json_request?] before_action :verify_moderator, only: [:mod, :soft_delete, :role_toggle, :full_log, - :annotate, :annotations, :mod_privileges, :mod_privilege_action, :mod_delete,] + :annotate, :annotations, :mod_privileges, :mod_privilege_action, :mod_delete] before_action :verify_global_moderator, only: [:mod_failban, :mod_delete_network_account] - before_action :set_user, only: [:show, :mod, :mod_delete, :mod_failban, :mod_delete_network_account, :soft_delete, :posts, :role_toggle, :full_log, :activity, - :annotate, :annotations, :mod_privileges, :mod_privilege_action, - :vote_summary, :network, :avatar] + before_action :set_user, only: [:show, :mod, :mod_delete, :mod_failban, :mod_delete_network_account, :soft_delete, + :posts, :role_toggle, :full_log, :activity, :annotate, :annotations, :mod_privileges, + :mod_privilege_action, :vote_summary, :network, :avatar] before_action :check_deleted, only: [:show, :posts, :activity] def index From bcfedf9a181cd9c3bd9c079ccc31f20f854a837a Mon Sep 17 00:00:00 2001 From: trichoplax Date: Fri, 24 Apr 2026 17:43:46 +0100 Subject: [PATCH 04/13] Make 'without_sidebar' the default layout for the users controller --- app/controllers/users_controller.rb | 43 ++++------------------------- 1 file changed, 5 insertions(+), 38 deletions(-) diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 2af983ee0..df5dfc6bc 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -2,6 +2,8 @@ # rubocop:disable Metrics/ClassLength class UsersController < ApplicationController + layout 'without_sidebar' + include Devise::Controllers::Rememberable before_action :authenticate_user!, only: [:edit_profile, :update_profile, :stack_redirect, @@ -38,6 +40,7 @@ def index respond_to do |format| format.html + render layout: 'application' format.json do render json: @users end @@ -61,7 +64,6 @@ def show .count end @posts = @posts.first(@limit) - render layout: 'without_sidebar' end def me @@ -93,7 +95,6 @@ def preferences prefs = current_user.preferences @preferences = prefs[:global] @community_prefs = prefs[:community] - render layout: 'without_sidebar' end format.json do render json: current_user.preferences @@ -136,9 +137,7 @@ def filters_json def filters respond_to do |format| - format.html do - render layout: 'without_sidebar' - end + format.html format.json do render json: filters_json end @@ -226,7 +225,7 @@ def posts respond_to do |format| format.html do - render :posts + render :posts, layout: 'application' end format.json do render json: @posts @@ -240,7 +239,6 @@ def my_network def network @communities = Community.all - render layout: 'without_sidebar' end def my_activity @@ -273,11 +271,6 @@ def activity end @items = items.sort_by(&:created_at).reverse.paginate(page: params[:page], per_page: 50) - render layout: 'without_sidebar' - end - - def mod - render layout: 'without_sidebar' end def full_log @@ -318,25 +311,10 @@ def full_log SuggestedEdit.by(@user).all + PostHistory.by(@user).all + ModWarning.to(@user).all end).sort_by(&:created_at).reverse.paginate(page: params[:page], per_page: 50) - - render layout: 'without_sidebar' - end - - def mod_delete_network_account - render layout: 'without_sidebar' - end - - def mod_failban - render layout: 'without_sidebar' end def mod_privileges @abilities = Ability.all - render layout: 'without_sidebar' - end - - def mod_delete - render layout: 'without_sidebar' end def soft_delete @@ -365,10 +343,6 @@ def soft_delete render json: { status: 'success', user: @user.id } end - def edit_profile - render layout: 'without_sidebar' - end - def cleaned_profile_websites(profile_params) sites = profile_params[:user_websites_attributes] @@ -591,7 +565,6 @@ def annotations @logs = AuditLog.where(log_type: 'user_annotation', related: @user) .newest_first .paginate(page: params[:page], per_page: 20) - render layout: 'without_sidebar' end def annotate @@ -621,8 +594,6 @@ def vote_summary [k, vl.group_by(&:post), vl.sum { |v| v.vote_type * v.vote_count }] end .paginate(page: params[:page], per_page: 15) - - render layout: 'without_sidebar' end def avatar @@ -644,10 +615,6 @@ def specific_avatar end end - def disconnect_sso - render layout: 'without_sidebar' - end - def confirm_disconnect_sso if current_user.sso_profile.blank? || !helpers.devise_sign_in_enabled? || !SiteSetting['AllowSsoDisconnect'] flash[:danger] = 'You cannot disable Single Sign-On.' From 588c019a4c3887c9c73f9ee51112ff458c719046 Mon Sep 17 00:00:00 2001 From: trichoplax Date: Fri, 24 Apr 2026 17:58:16 +0100 Subject: [PATCH 05/13] Add missing do block (thanks Rubocop!) --- app/controllers/users_controller.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index df5dfc6bc..38ba9eb29 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -39,8 +39,9 @@ def index @post_counts = Post.where(user_id: @users.pluck(:id).uniq).group(:user_id).count respond_to do |format| - format.html + format.html do render layout: 'application' + end format.json do render json: @users end From c2acb3d235fcab50feb001a74b7bb038abae211d Mon Sep 17 00:00:00 2001 From: trichoplax Date: Mon, 27 Apr 2026 19:12:17 +0100 Subject: [PATCH 06/13] Remove expand buttons in mod warning log --- app/assets/javascripts/application.js | 9 ---- app/assets/stylesheets/application.scss | 31 ------------- app/views/mod_warning/log.html.erb | 60 ++++++++++++------------- 3 files changed, 29 insertions(+), 71 deletions(-) diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index 1c80254af..4eb8247fb 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -37,15 +37,6 @@ document.addEventListener('DOMContentLoaded', async () => { dialog.classList.toggle('is-active'); }); - QPixel.DOM.addSelectorListener('click', '.is-partial-only:not(.open)', (ev) => { - if (ev.target.classList.contains("open")) { - return; - } - - ev.target.classList.add("open"); - ev.stopPropagation(); - }); - if (document.cookie.indexOf('dismiss_fvn') === -1) { QPixel.DOM.addSelectorListener('click', '#fvn-dismiss', (_ev) => { document.cookie = 'dismiss_fvn=true; path=/; expires=Fri, 31 Dec 9999 23:59:59 GMT'; diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss index 441471e64..e178458a3 100644 --- a/app/assets/stylesheets/application.scss +++ b/app/assets/stylesheets/application.scss @@ -265,34 +265,3 @@ kbd { right: 0.15em; }; } - -.is-partial-only:not(.open) { - max-height: 100px; - overflow: hidden; - position: relative; - - &::after { - content: ''; - position: absolute; - right: 0; left: 0; - bottom: 0; - height: 75px; - background-color: rgba(255, 255, 255, 0.5); - background: linear-gradient(#ffffff66, #fffffffa); - z-index: 10000000; - } - - &::before { - content: 'expand'; - position: absolute; - left: 50%; - transform: translate(-50%); - bottom: 10px; - z-index: 10000001; - padding: 5px 20px; - background-color: #ddd; - border: 1px solid #666; - border-radius: 15px; - cursor: pointer; - } -} diff --git a/app/views/mod_warning/log.html.erb b/app/views/mod_warning/log.html.erb index 9ba5c5b89..2aaa4d904 100644 --- a/app/views/mod_warning/log.html.erb +++ b/app/views/mod_warning/log.html.erb @@ -9,41 +9,39 @@

Previously Sent Warnings

<% if @warnings.size == 0 %> -

No warnings found for this user.

+

No warnings found for this user.

<% end %> <% @warnings.each do |w| %> -
-
-
- <% if w.suspension_active? %> - <%= form_tag lift_mod_warning_url(@user.id), method: :post, class: 'mod-warnings-clear-form' do %> - <%= submit_tag '(lift)', class: 'link is-red' %> - <% end %> · - Current - <% elsif w.active %> - Unread - <% elsif w.read %> - Read - <% else %> - Lifted - <% end %> -
-
- <% if w.is_suspension %> - <% diff = ((w.suspension_end - w.created_at) / (3600 * 24)).to_i %> - Suspension - length: <%= diff %>d - <% else %> - Warning - <% end %> -
-
<%= time_ago_in_words(w.created_at) %> ago by <%= user_link w.author %>
-
-
- <%= raw(sanitize(render_markdown(w.body), scrubber: scrubber)) %> -
+
+
+
+ <% if w.suspension_active? %> + <%= form_tag lift_mod_warning_url(@user.id), method: :post, class: 'mod-warnings-clear-form' do %> + <%= submit_tag '(lift)', class: 'link is-red' %> + <% end %> · + Current + <% elsif w.active %> + Unread + <% elsif w.read %> + Read + <% else %> + Lifted + <% end %> +
+
+ <% if w.is_suspension %> + <% diff = ((w.suspension_end - w.created_at) / (3600 * 24)).to_i %> + Suspension + length: <%= diff %>d + <% else %> + Warning + <% end %> +
+
<%= time_ago_in_words(w.created_at) %> ago by <%= user_link w.author %>
+ <%= raw(sanitize(render_markdown(w.body), scrubber: scrubber)) %> +
<% end %>
From ae78b0b739704fa9795badb673348a42014bd5b3 Mon Sep 17 00:00:00 2001 From: trichoplax Date: Mon, 27 Apr 2026 19:45:59 +0100 Subject: [PATCH 07/13] Restore formatting of mod tools previous warning log --- app/views/mod_warning/log.html.erb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/views/mod_warning/log.html.erb b/app/views/mod_warning/log.html.erb index 2aaa4d904..c1c45525a 100644 --- a/app/views/mod_warning/log.html.erb +++ b/app/views/mod_warning/log.html.erb @@ -40,7 +40,9 @@
<%= time_ago_in_words(w.created_at) %> ago by <%= user_link w.author %>
- <%= raw(sanitize(render_markdown(w.body), scrubber: scrubber)) %> +
+ <%= raw(sanitize(render_markdown(w.body), scrubber: scrubber)) %> +
<% end %>
From efd7158b173ee2a54a2cc49bbcf9489bdb8d9e29 Mon Sep 17 00:00:00 2001 From: trichoplax Date: Fri, 15 May 2026 10:19:42 +0100 Subject: [PATCH 08/13] Make user mod tools left panel move to top on mobile --- app/assets/stylesheets/application.scss | 8 ++ app/assets/stylesheets/users.scss | 15 +++ app/controllers/admin_controller.rb | 2 + app/views/admin/impersonate.html.erb | 62 +++++++----- app/views/mod_warning/log.html.erb | 4 +- app/views/mod_warning/new.html.erb | 4 +- .../moderator/user_vote_summary.html.erb | 4 +- .../shared/_user_mod_mobile_list.html.erb | 13 +++ app/views/shared/_user_mod_sidebar.html.erb | 97 +++---------------- .../shared/_user_mod_tools_list.html.erb | 74 ++++++++++++++ app/views/users/annotations.html.erb | 4 +- app/views/users/full_log.html.erb | 4 +- app/views/users/mod.html.erb | 4 +- app/views/users/mod_delete.html.erb | 4 +- .../users/mod_delete_network_account.html.erb | 4 +- app/views/users/mod_failban.html.erb | 4 +- app/views/users/mod_privileges.html.erb | 4 +- 17 files changed, 191 insertions(+), 120 deletions(-) create mode 100644 app/views/shared/_user_mod_mobile_list.html.erb create mode 100644 app/views/shared/_user_mod_tools_list.html.erb diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss index e178458a3..acef111a7 100644 --- a/app/assets/stylesheets/application.scss +++ b/app/assets/stylesheets/application.scss @@ -265,3 +265,11 @@ kbd { right: 0.15em; }; } + +.is-9-desktop-12-mobile { + width: 75%; + + @media screen and (max-width:850px) { + width: 100%; + } +} diff --git a/app/assets/stylesheets/users.scss b/app/assets/stylesheets/users.scss index 7dc2ce359..32c54548b 100644 --- a/app/assets/stylesheets/users.scss +++ b/app/assets/stylesheets/users.scss @@ -256,3 +256,18 @@ $sizes: (16, 32, 40, 48, 64, 128, 256); } .mod-warnings-clear-form { display: inline; } + +.modtools--top-list { + display: none; +} + + +@media screen and (max-width: 850px) { + .modtools--top-list { + display: block; + } + + .modtools--sidebar { + display: none; + } +} diff --git a/app/controllers/admin_controller.rb b/app/controllers/admin_controller.rb index b5aace580..e24ea4967 100644 --- a/app/controllers/admin_controller.rb +++ b/app/controllers/admin_controller.rb @@ -214,6 +214,8 @@ def impersonate if Rails.env.development? change_users end + + render layout: 'without_sidebar' end def change_users diff --git a/app/views/admin/impersonate.html.erb b/app/views/admin/impersonate.html.erb index 19fe6e3f0..91025e29a 100644 --- a/app/views/admin/impersonate.html.erb +++ b/app/views/admin/impersonate.html.erb @@ -1,31 +1,43 @@ -

Impersonate <%= @user.username %>

-

- As a developer, you have access to impersonate users to help in reproducing bug reports, among other things. -

+<% content_for :title, "Moderator Tools: #{rtl_safe_username(@user)}" %> -
-

Caution

-

- Using this tool may give you access to a user's personally identifiable information (PII). You are reminded of your - obligations under data protection laws to protect this information and not to use or disclose it without permission - or reasonable justification. This impersonation will be logged. -

-
+<%= render 'users/tabs', user: @user %> -
-
- <%= render 'users/common_card', user: @user, ckb: false %> -
-
+<%= render 'shared/user_mod_mobile_list', user: @user %> + +
+ <%= render 'shared/user_mod_sidebar', user: @user %> -<%= form_tag impersonate_path(@user) do %> -
-
- <%= label_tag :comment, 'Why are you impersonating this user?', class: 'form-element' %> - <%= text_field_tag :comment, nil, class: 'form-element', required: true %> +
+

Impersonate <%= @user.username %>

+

+ As a developer, you have access to impersonate users to help in reproducing bug reports, among other things. +

+ +
+

Caution

+

+ Using this tool may give you access to a user's personally identifiable information (PII). You are reminded of your + obligations under data protection laws to protect this information and not to use or disclose it without permission + or reasonable justification. This impersonation will be logged. +

-
- <%= submit_tag 'Impersonate', class: 'button is-danger h-m-b-2' %> + +
+
+ <%= render 'users/common_card', user: @user, ckb: false %> +
+ + <%= form_tag impersonate_path(@user) do %> +
+
+ <%= label_tag :comment, 'Why are you impersonating this user?', class: 'form-element' %> + <%= text_field_tag :comment, nil, class: 'form-element', required: true %> +
+
+ <%= submit_tag 'Impersonate', class: 'button is-danger h-m-b-2' %> +
+
+ <% end %>
-<% end %> +
diff --git a/app/views/mod_warning/log.html.erb b/app/views/mod_warning/log.html.erb index c1c45525a..9e06f7d9c 100644 --- a/app/views/mod_warning/log.html.erb +++ b/app/views/mod_warning/log.html.erb @@ -2,10 +2,12 @@ <%= render 'users/tabs', user: @user %> +<%= render 'shared/user_mod_mobile_list', user: @user %> +
<%= render 'shared/user_mod_sidebar', user: @user %> -
+

Previously Sent Warnings

<% if @warnings.size == 0 %> diff --git a/app/views/mod_warning/new.html.erb b/app/views/mod_warning/new.html.erb index d45550f27..8195f8890 100644 --- a/app/views/mod_warning/new.html.erb +++ b/app/views/mod_warning/new.html.erb @@ -7,10 +7,12 @@ <%= render 'posts/image_upload' %> <%= render 'users/tabs', user: @user %> +<%= render 'shared/user_mod_mobile_list', user: @user %> +
<%= render 'shared/user_mod_sidebar', user: @user %> -
+

Warn or Suspend User

diff --git a/app/views/moderator/user_vote_summary.html.erb b/app/views/moderator/user_vote_summary.html.erb index 9c2d3e7b1..e9936093a 100644 --- a/app/views/moderator/user_vote_summary.html.erb +++ b/app/views/moderator/user_vote_summary.html.erb @@ -2,10 +2,12 @@ <%= render 'users/tabs', user: @user %> +<%= render 'shared/user_mod_mobile_list', user: @user %> +
<%= render 'shared/user_mod_sidebar', user: @user %> -
+

Vote Summary

This is a summary of votes cast and received by this user. This may help you to identify voting patterns and diff --git a/app/views/shared/_user_mod_mobile_list.html.erb b/app/views/shared/_user_mod_mobile_list.html.erb new file mode 100644 index 000000000..032c499a4 --- /dev/null +++ b/app/views/shared/_user_mod_mobile_list.html.erb @@ -0,0 +1,13 @@ +

+
+ + User Moderation Tools + + <%= render 'shared/user_mod_tools_list', user: user %> +
+
+
+ <%= render 'users/common_card', user: user, ckb: false %> +
+
+
diff --git a/app/views/shared/_user_mod_sidebar.html.erb b/app/views/shared/_user_mod_sidebar.html.erb index 6c890e4c8..ee9646e80 100644 --- a/app/views/shared/_user_mod_sidebar.html.erb +++ b/app/views/shared/_user_mod_sidebar.html.erb @@ -1,88 +1,15 @@
-
-
- User Moderation Tools -
-
-
- <%= render 'users/common_card', user: user, ckb: false %> -
-
-
- -
+
+
+ User Moderation Tools
+
+
+ <%= render 'users/common_card', user: user, ckb: false %> +
+
+
+ <%= render 'shared/user_mod_tools_list', user: user %> +
+
diff --git a/app/views/shared/_user_mod_tools_list.html.erb b/app/views/shared/_user_mod_tools_list.html.erb new file mode 100644 index 000000000..7a73b3140 --- /dev/null +++ b/app/views/shared/_user_mod_tools_list.html.erb @@ -0,0 +1,74 @@ + diff --git a/app/views/users/annotations.html.erb b/app/views/users/annotations.html.erb index 26c5b784a..2555e8d60 100644 --- a/app/views/users/annotations.html.erb +++ b/app/views/users/annotations.html.erb @@ -2,10 +2,12 @@ <%= render 'tabs', user: @user %> +<%= render 'shared/user_mod_mobile_list', user: @user %> +
<%= render 'shared/user_mod_sidebar', user: @user %> -
+

User annotations

diff --git a/app/views/users/full_log.html.erb b/app/views/users/full_log.html.erb index 7c1f8cde8..8ece5fb67 100644 --- a/app/views/users/full_log.html.erb +++ b/app/views/users/full_log.html.erb @@ -2,10 +2,12 @@ <%= render 'tabs', user: @user %> +<%= render 'shared/user_mod_mobile_list', user: @user %> +
<%= render 'shared/user_mod_sidebar', user: @user %> -
+

Full activity log

This is a filterable log for all activity by the user. You can consult it for moderation decisions. Do not share this information to people, who do not have access to it.

diff --git a/app/views/users/mod.html.erb b/app/views/users/mod.html.erb index 18562929c..17453185c 100644 --- a/app/views/users/mod.html.erb +++ b/app/views/users/mod.html.erb @@ -2,10 +2,12 @@ <%= render 'tabs', user: @user %> +<%= render 'shared/user_mod_mobile_list', user: @user %> +
<%= render 'shared/user_mod_sidebar', user: @user %> -
+

Dashboard

Please note that information shown in these user moderation tools is sensitive and should not be shared with anyone outside the moderator and admin team.

diff --git a/app/views/users/mod_delete.html.erb b/app/views/users/mod_delete.html.erb index 518f10a0b..086dc9ecc 100644 --- a/app/views/users/mod_delete.html.erb +++ b/app/views/users/mod_delete.html.erb @@ -2,10 +2,12 @@ <%= render 'tabs', user: @user %> +<%= render 'shared/user_mod_mobile_list', user: @user %> +
<%= render 'shared/user_mod_sidebar', user: @user %> -
+

Delete Account

Some users are just blatant spammers or trolls and some users are just unwilling to follow site rules, even after repeated warnings and suspensions. As a moderator, you may delete the user account in these cases.

diff --git a/app/views/users/mod_delete_network_account.html.erb b/app/views/users/mod_delete_network_account.html.erb index 6df3c2409..42a1dbb03 100644 --- a/app/views/users/mod_delete_network_account.html.erb +++ b/app/views/users/mod_delete_network_account.html.erb @@ -2,10 +2,12 @@ <%= render 'tabs', user: @user %> +<%= render 'shared/user_mod_mobile_list', user: @user %> +
<%= render 'shared/user_mod_sidebar', user: @user %> -
+

Network-wide Account Deletion

As a global moderator, you may delete the user account network-wide.

diff --git a/app/views/users/mod_failban.html.erb b/app/views/users/mod_failban.html.erb index b7a2b95ce..22208e071 100644 --- a/app/views/users/mod_failban.html.erb +++ b/app/views/users/mod_failban.html.erb @@ -2,10 +2,12 @@ <%= render 'tabs', user: @user %> +<%= render 'shared/user_mod_mobile_list', user: @user %> +
<%= render 'shared/user_mod_sidebar', user: @user %> -
+

Fail-ban

diff --git a/app/views/users/mod_privileges.html.erb b/app/views/users/mod_privileges.html.erb index 0f0428ea3..380260c04 100644 --- a/app/views/users/mod_privileges.html.erb +++ b/app/views/users/mod_privileges.html.erb @@ -2,10 +2,12 @@ <%= render 'tabs', user: @user %> +<%= render 'shared/user_mod_mobile_list', user: @user %> +
<%= render 'shared/user_mod_sidebar', user: @user %> -
+

User Privileges

From 126c2c414c35cce79b57f84c2fffd85b18a1deda Mon Sep 17 00:00:00 2001 From: trichoplax Date: Fri, 15 May 2026 21:52:00 +0100 Subject: [PATCH 09/13] Only render once in impersonate method --- app/controllers/admin_controller.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/controllers/admin_controller.rb b/app/controllers/admin_controller.rb index e24ea4967..7615464c8 100644 --- a/app/controllers/admin_controller.rb +++ b/app/controllers/admin_controller.rb @@ -213,9 +213,9 @@ def failban def impersonate if Rails.env.development? change_users + else + render layout: 'without_sidebar' end - - render layout: 'without_sidebar' end def change_users From 586c9c9f0073fa770c0cd36e32771292399d753e Mon Sep 17 00:00:00 2001 From: trichoplax Date: Sun, 24 May 2026 18:06:32 +0100 Subject: [PATCH 10/13] Avoid separate views for desktop and mobile mod tools lists --- app/assets/stylesheets/application.scss | 8 -- app/assets/stylesheets/users.scss | 41 +++++++-- app/views/admin/impersonate.html.erb | 4 +- app/views/mod_warning/log.html.erb | 4 +- app/views/mod_warning/new.html.erb | 4 +- .../moderator/user_vote_summary.html.erb | 4 +- .../shared/_user_mod_mobile_list.html.erb | 13 --- app/views/shared/_user_mod_sidebar.html.erb | 83 ++++++++++++++++++- .../shared/_user_mod_tools_list.html.erb | 74 ----------------- app/views/users/annotations.html.erb | 4 +- app/views/users/full_log.html.erb | 4 +- app/views/users/mod.html.erb | 4 +- app/views/users/mod_delete.html.erb | 4 +- .../users/mod_delete_network_account.html.erb | 4 +- app/views/users/mod_failban.html.erb | 4 +- app/views/users/mod_privileges.html.erb | 4 +- 16 files changed, 125 insertions(+), 138 deletions(-) delete mode 100644 app/views/shared/_user_mod_mobile_list.html.erb delete mode 100644 app/views/shared/_user_mod_tools_list.html.erb diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss index acef111a7..e178458a3 100644 --- a/app/assets/stylesheets/application.scss +++ b/app/assets/stylesheets/application.scss @@ -265,11 +265,3 @@ kbd { right: 0.15em; }; } - -.is-9-desktop-12-mobile { - width: 75%; - - @media screen and (max-width:850px) { - width: 100%; - } -} diff --git a/app/assets/stylesheets/users.scss b/app/assets/stylesheets/users.scss index 32c54548b..c6a71183e 100644 --- a/app/assets/stylesheets/users.scss +++ b/app/assets/stylesheets/users.scss @@ -242,12 +242,10 @@ $sizes: (16, 32, 40, 48, 64, 128, 256); } } -.modtools--sidebar { - margin-right: 1rem; -} .modtools--usercard { padding: 0.5rem; } + .modtools-tbl-noborder { th { border-bottom-width: 1px !important; @@ -257,17 +255,44 @@ $sizes: (16, 32, 40, 48, 64, 128, 256); .mod-warnings-clear-form { display: inline; } -.modtools--top-list { - display: none; +.modtools--sidebar { + margin-right: 1rem; } +$grid-medium-to-large-breakpoint: 768px !default; -@media screen and (max-width: 850px) { - .modtools--top-list { +.modtools--sidebar { + > .widget--header { + display: none; + } + + label { display: block; } + #mobile-mod-tools-toggle { + + .widget--body { + display: none; + } + + &:checked + .widget--body { + display: block; + } + } +} + +@media screen and (min-width: $grid-medium-to-large-breakpoint) { .modtools--sidebar { - display: none; + > .widget--header { + display: block; + } + + label { + display: none; + } + + #mobile-mod-tools-toggle + .widget--body { + display: block; + } } } diff --git a/app/views/admin/impersonate.html.erb b/app/views/admin/impersonate.html.erb index 91025e29a..1c23a7271 100644 --- a/app/views/admin/impersonate.html.erb +++ b/app/views/admin/impersonate.html.erb @@ -2,12 +2,10 @@ <%= render 'users/tabs', user: @user %> -<%= render 'shared/user_mod_mobile_list', user: @user %> -
<%= render 'shared/user_mod_sidebar', user: @user %> -
+

Impersonate <%= @user.username %>

As a developer, you have access to impersonate users to help in reproducing bug reports, among other things. diff --git a/app/views/mod_warning/log.html.erb b/app/views/mod_warning/log.html.erb index 9e06f7d9c..9d4ab8333 100644 --- a/app/views/mod_warning/log.html.erb +++ b/app/views/mod_warning/log.html.erb @@ -2,12 +2,10 @@ <%= render 'users/tabs', user: @user %> -<%= render 'shared/user_mod_mobile_list', user: @user %> -

<%= render 'shared/user_mod_sidebar', user: @user %> -
+

Previously Sent Warnings

<% if @warnings.size == 0 %> diff --git a/app/views/mod_warning/new.html.erb b/app/views/mod_warning/new.html.erb index 8195f8890..210f03aec 100644 --- a/app/views/mod_warning/new.html.erb +++ b/app/views/mod_warning/new.html.erb @@ -7,12 +7,10 @@ <%= render 'posts/image_upload' %> <%= render 'users/tabs', user: @user %> -<%= render 'shared/user_mod_mobile_list', user: @user %> -
<%= render 'shared/user_mod_sidebar', user: @user %> -
+

Warn or Suspend User

diff --git a/app/views/moderator/user_vote_summary.html.erb b/app/views/moderator/user_vote_summary.html.erb index e9936093a..f86f0e384 100644 --- a/app/views/moderator/user_vote_summary.html.erb +++ b/app/views/moderator/user_vote_summary.html.erb @@ -2,12 +2,10 @@ <%= render 'users/tabs', user: @user %> -<%= render 'shared/user_mod_mobile_list', user: @user %> -
<%= render 'shared/user_mod_sidebar', user: @user %> -
+

Vote Summary

This is a summary of votes cast and received by this user. This may help you to identify voting patterns and diff --git a/app/views/shared/_user_mod_mobile_list.html.erb b/app/views/shared/_user_mod_mobile_list.html.erb deleted file mode 100644 index 032c499a4..000000000 --- a/app/views/shared/_user_mod_mobile_list.html.erb +++ /dev/null @@ -1,13 +0,0 @@ -

-
- - User Moderation Tools - - <%= render 'shared/user_mod_tools_list', user: user %> -
-
-
- <%= render 'users/common_card', user: user, ckb: false %> -
-
-
diff --git a/app/views/shared/_user_mod_sidebar.html.erb b/app/views/shared/_user_mod_sidebar.html.erb index ee9646e80..d63b2a712 100644 --- a/app/views/shared/_user_mod_sidebar.html.erb +++ b/app/views/shared/_user_mod_sidebar.html.erb @@ -1,4 +1,4 @@ -
+
User Moderation Tools @@ -8,8 +8,87 @@ <%= render 'users/common_card', user: user, ckb: false %>
+ +
- <%= render 'shared/user_mod_tools_list', user: user %> +
diff --git a/app/views/shared/_user_mod_tools_list.html.erb b/app/views/shared/_user_mod_tools_list.html.erb deleted file mode 100644 index 7a73b3140..000000000 --- a/app/views/shared/_user_mod_tools_list.html.erb +++ /dev/null @@ -1,74 +0,0 @@ - diff --git a/app/views/users/annotations.html.erb b/app/views/users/annotations.html.erb index 2555e8d60..547f3d271 100644 --- a/app/views/users/annotations.html.erb +++ b/app/views/users/annotations.html.erb @@ -2,12 +2,10 @@ <%= render 'tabs', user: @user %> -<%= render 'shared/user_mod_mobile_list', user: @user %> -
<%= render 'shared/user_mod_sidebar', user: @user %> -
+

User annotations

diff --git a/app/views/users/full_log.html.erb b/app/views/users/full_log.html.erb index 8ece5fb67..727a4db82 100644 --- a/app/views/users/full_log.html.erb +++ b/app/views/users/full_log.html.erb @@ -2,12 +2,10 @@ <%= render 'tabs', user: @user %> -<%= render 'shared/user_mod_mobile_list', user: @user %> -
<%= render 'shared/user_mod_sidebar', user: @user %> -
+

Full activity log

This is a filterable log for all activity by the user. You can consult it for moderation decisions. Do not share this information to people, who do not have access to it.

diff --git a/app/views/users/mod.html.erb b/app/views/users/mod.html.erb index 17453185c..84ac4e004 100644 --- a/app/views/users/mod.html.erb +++ b/app/views/users/mod.html.erb @@ -2,12 +2,10 @@ <%= render 'tabs', user: @user %> -<%= render 'shared/user_mod_mobile_list', user: @user %> -
<%= render 'shared/user_mod_sidebar', user: @user %> -
+

Dashboard

Please note that information shown in these user moderation tools is sensitive and should not be shared with anyone outside the moderator and admin team.

diff --git a/app/views/users/mod_delete.html.erb b/app/views/users/mod_delete.html.erb index 086dc9ecc..19a31cfa8 100644 --- a/app/views/users/mod_delete.html.erb +++ b/app/views/users/mod_delete.html.erb @@ -2,12 +2,10 @@ <%= render 'tabs', user: @user %> -<%= render 'shared/user_mod_mobile_list', user: @user %> -
<%= render 'shared/user_mod_sidebar', user: @user %> -
+

Delete Account

Some users are just blatant spammers or trolls and some users are just unwilling to follow site rules, even after repeated warnings and suspensions. As a moderator, you may delete the user account in these cases.

diff --git a/app/views/users/mod_delete_network_account.html.erb b/app/views/users/mod_delete_network_account.html.erb index 42a1dbb03..30ce811d1 100644 --- a/app/views/users/mod_delete_network_account.html.erb +++ b/app/views/users/mod_delete_network_account.html.erb @@ -2,12 +2,10 @@ <%= render 'tabs', user: @user %> -<%= render 'shared/user_mod_mobile_list', user: @user %> -
<%= render 'shared/user_mod_sidebar', user: @user %> -
+

Network-wide Account Deletion

As a global moderator, you may delete the user account network-wide.

diff --git a/app/views/users/mod_failban.html.erb b/app/views/users/mod_failban.html.erb index 22208e071..9dec61958 100644 --- a/app/views/users/mod_failban.html.erb +++ b/app/views/users/mod_failban.html.erb @@ -2,12 +2,10 @@ <%= render 'tabs', user: @user %> -<%= render 'shared/user_mod_mobile_list', user: @user %> -
<%= render 'shared/user_mod_sidebar', user: @user %> -
+

Fail-ban

diff --git a/app/views/users/mod_privileges.html.erb b/app/views/users/mod_privileges.html.erb index 380260c04..40ae1f026 100644 --- a/app/views/users/mod_privileges.html.erb +++ b/app/views/users/mod_privileges.html.erb @@ -2,12 +2,10 @@ <%= render 'tabs', user: @user %> -<%= render 'shared/user_mod_mobile_list', user: @user %> -
<%= render 'shared/user_mod_sidebar', user: @user %> -
+

User Privileges

From 6ad16aeeb9a28a6de1130de85e1d784a145ebe68 Mon Sep 17 00:00:00 2001 From: trichoplax Date: Sun, 24 May 2026 18:31:42 +0100 Subject: [PATCH 11/13] Improve code formatting --- app/assets/stylesheets/tabs.scss | 2 +- app/views/mod_warning/new.html.erb | 103 +++++++++--------- .../moderator/user_vote_summary.html.erb | 42 +++---- app/views/shared/_user_mod_sidebar.html.erb | 34 +++--- app/views/users/mod_delete.html.erb | 17 +-- .../users/mod_delete_network_account.html.erb | 17 +-- 6 files changed, 110 insertions(+), 105 deletions(-) diff --git a/app/assets/stylesheets/tabs.scss b/app/assets/stylesheets/tabs.scss index ce2c1345a..110ee644f 100644 --- a/app/assets/stylesheets/tabs.scss +++ b/app/assets/stylesheets/tabs.scss @@ -4,6 +4,6 @@ margin-bottom: 1em; .tabs--push { - flex-grow: 1; + flex-grow: 1; } } diff --git a/app/views/mod_warning/new.html.erb b/app/views/mod_warning/new.html.erb index 210f03aec..302a795b1 100644 --- a/app/views/mod_warning/new.html.erb +++ b/app/views/mod_warning/new.html.erb @@ -14,82 +14,81 @@

Warn or Suspend User

-

Use the warning tool only against users who have violated the site rules. Prefer other measures where possible, such as a public - comment.

+

Use the warning tool only against users who have violated the site rules. Prefer other measures where possible, such as a public + comment.

<%= form_for @warning, url: create_mod_warning_path(@user.id), method: :post do |f| %> -
+
- 1. Choose a template + 1. Choose a template
-
-

Choose a template that explains why you are contacting the user. If none are applicable, choose to send a - custom message.

- - -
+
+

Choose a template that explains why you are contacting the user. If none are applicable, choose to send a + custom message.

+ + +
- 2. Review the message + 2. Review the message
-
-

Review the generated message and add details. Do not add salutations or information about possible - suspensions, as they are generated automatically.

+
+

Review the generated message and add details. Do not add salutations or information about possible + suspensions, as they are generated automatically.

-
- <%= render 'shared/body_field', f: f, field_name: :body, field_label: 'Body' %> -
-
+
+ <%= render 'shared/body_field', f: f, field_name: :body, field_label: 'Body' %> +
+
- 3. Choose optional suspension + 3. Choose optional suspension
-
-

Decide whether or not to suspend the user, and if so for how long. Choose an optional message shown - publicly on the user profile.

+
+

Decide whether or not to suspend the user, and if so for how long. Choose an optional message shown + publicly on the user profile.

- <% if @prior_warning_count == 0 %> -

Info: This user has no prior warnings. The system recommends issuing only a warning, unless the user is destructive and needs to be stopped immediately.

- <% elsif @prior_warning_count >= 5 %> -

Info: This user has <%= @prior_warning_count %> prior warnings. The system recommends suspending them for 365 days (the maximum).

- <% else %> - <% lengths = { 1 => 3, 2 => 7, 3 => 30, 4 => 180 } %> -

Info: This user has <%= @prior_warning_count %> prior warnings. The system recommends suspending them for <%= lengths[@prior_warning_count] %> days.

- <% end %> + <% if @prior_warning_count == 0 %> +

Info: This user has no prior warnings. The system recommends issuing only a warning, unless the user is destructive and needs to be stopped immediately.

+ <% elsif @prior_warning_count >= 5 %> +

Info: This user has <%= @prior_warning_count %> prior warnings. The system recommends suspending them for 365 days (the maximum).

+ <% else %> + <% lengths = { 1 => 3, 2 => 7, 3 => 30, 4 => 180 } %> +

Info: This user has <%= @prior_warning_count %> prior warnings. The system recommends suspending them for <%= lengths[@prior_warning_count] %> days.

+ <% end %> -
- <%= f.label :is_suspension, 'Suspend this user account?', class: 'form-element' %> - - -
+
+ <%= f.label :is_suspension, 'Suspend this user account?', class: 'form-element' %> + + +
-
- <%= f.label :suspension_duration, 'If suspending, for how long?', class: 'form-element' %> -
Enter the number of days. At least 1, at most 365.
- <%= f.number_field :suspension_duration, in: 1..365, class: 'form-element' %> -
+
+ <%= f.label :suspension_duration, 'If suspending, for how long?', class: 'form-element' %> +
Enter the number of days. At least 1, at most 365.
+ <%= f.number_field :suspension_duration, in: 1..365, class: 'form-element' %> +
-
- <%= f.label :suspension_public_notice, 'If suspending, what public notice, if any, do you want to show?', class: 'form-element' %> - <%= f.select :suspension_public_notice, options_for_select([['for rule violations', 'for rule violations'], ['to cool down', 'to cool down']]), { include_blank: true }, class: 'form-element' %> -
+
+ <%= f.label :suspension_public_notice, 'If suspending, what public notice, if any, do you want to show?', class: 'form-element' %> + <%= f.select :suspension_public_notice, options_for_select([['for rule violations', 'for rule violations'], ['to cool down', 'to cool down']]), { include_blank: true }, class: 'form-element' %>
+
-
+
<% end %> -
diff --git a/app/views/moderator/user_vote_summary.html.erb b/app/views/moderator/user_vote_summary.html.erb index f86f0e384..165487637 100644 --- a/app/views/moderator/user_vote_summary.html.erb +++ b/app/views/moderator/user_vote_summary.html.erb @@ -34,27 +34,27 @@ - <% @vote_data[type].breakdown.each do |key, count| %> - - <%= user_link @users.select { |x| x.id == key[0] }[0] %> - <%= key[1] %> - <%= count %> - <% pct = count * 100.0 / @vote_data[type].total %> - - <% if pct >= 50 %> - <%= text_bg 'red-700', number_to_percentage(pct, precision: 2), class: 'has-color-white has-padding-1' %> - <% elsif pct >= 40 %> - <%= text_bg 'red-200', number_to_percentage(pct, precision: 2), class: 'has-padding-1' %> - <% elsif pct >= 30 %> - <%= text_bg 'yellow-700', number_to_percentage(pct, precision: 2), class: 'has-padding-1' %> - <% elsif pct >= 20 %> - <%= text_bg 'yellow-200', number_to_percentage(pct, precision: 2), class: 'has-padding-1' %> - <% else %> - <%= number_to_percentage(pct, precision: 2) %> - <% end %> - - - <% end %> + <% @vote_data[type].breakdown.each do |key, count| %> + + <%= user_link @users.select { |x| x.id == key[0] }[0] %> + <%= key[1] %> + <%= count %> + <% pct = count * 100.0 / @vote_data[type].total %> + + <% if pct >= 50 %> + <%= text_bg 'red-700', number_to_percentage(pct, precision: 2), class: 'has-color-white has-padding-1' %> + <% elsif pct >= 40 %> + <%= text_bg 'red-200', number_to_percentage(pct, precision: 2), class: 'has-padding-1' %> + <% elsif pct >= 30 %> + <%= text_bg 'yellow-700', number_to_percentage(pct, precision: 2), class: 'has-padding-1' %> + <% elsif pct >= 20 %> + <%= text_bg 'yellow-200', number_to_percentage(pct, precision: 2), class: 'has-padding-1' %> + <% else %> + <%= number_to_percentage(pct, precision: 2) %> + <% end %> + + + <% end %> <% end %> diff --git a/app/views/shared/_user_mod_sidebar.html.erb b/app/views/shared/_user_mod_sidebar.html.erb index d63b2a712..8ce7c9198 100644 --- a/app/views/shared/_user_mod_sidebar.html.erb +++ b/app/views/shared/_user_mod_sidebar.html.erb @@ -68,25 +68,25 @@ <% end %> <% if current_user.is_global_moderator || current_user.is_global_admin %> - - <%= link_to mod_delete_network_account_path(user), - class: "menu--item #{current_page?(mod_delete_network_account_path(user)) ? 'is-active' : ''}" do %> - - Delete Network Account - <% end %> - <%= link_to mod_failban_path(user), - class: "menu--item #{current_page?(mod_failban_path(user)) ? 'is-active' : ''}" do %> - - Fail-ban - <% end %> + + <%= link_to mod_delete_network_account_path(user), + class: "menu--item #{current_page?(mod_delete_network_account_path(user)) ? 'is-active' : ''}" do %> + + Delete Network Account + <% end %> + <%= link_to mod_failban_path(user), + class: "menu--item #{current_page?(mod_failban_path(user)) ? 'is-active' : ''}" do %> + + Fail-ban + <% end %> <% end %> <% if current_user.developer %> - - <%= link_to start_impersonating_path(user), - class: "menu--item #{current_page?(start_impersonating_path(user)) ? 'is-active' : ''}" do %> - - Impersonate - <% end %> + + <%= link_to start_impersonating_path(user), + class: "menu--item #{current_page?(start_impersonating_path(user)) ? 'is-active' : ''}" do %> + + Impersonate + <% end %> <% end %>
diff --git a/app/views/users/mod_delete.html.erb b/app/views/users/mod_delete.html.erb index 19a31cfa8..20027b3b4 100644 --- a/app/views/users/mod_delete.html.erb +++ b/app/views/users/mod_delete.html.erb @@ -11,17 +11,20 @@

Some users are just blatant spammers or trolls and some users are just unwilling to follow site rules, even after repeated warnings and suspensions. As a moderator, you may delete the user account in these cases.

-

Take care! These actions may not be reversible and you will not be asked to confirm after initiating an action.

+

Take care! These actions may not be reversible and you will not be asked to confirm after initiating an action.

-
-

Delete Community Profile

-

Delete the community profile of users who are unwilling to follow the rules of this site, even after repeated warnings and suspensions. Choose this option if a user has requested deletion of their profile on one site, once you have confirmed their identity and request.

+
+

Delete Community Profile

+

Delete the community profile of users who are unwilling to follow the rules of this site, even after repeated warnings and suspensions. Choose this option if a user has requested deletion of their profile on one site, once you have confirmed their identity and request.

- <%= link_to 'Delete community profile', soft_delete_user_path(@user.id, type: 'profile'), remote: true, - method: :delete, class: 'js-soft-delete button is-danger is-filled' %> -
+ <%= link_to 'Delete community profile', + soft_delete_user_path(@user.id, type: 'profile'), + remote: true, + method: :delete, + class: 'js-soft-delete button is-danger is-filled' %> +
diff --git a/app/views/users/mod_delete_network_account.html.erb b/app/views/users/mod_delete_network_account.html.erb index 30ce811d1..06836eaa4 100644 --- a/app/views/users/mod_delete_network_account.html.erb +++ b/app/views/users/mod_delete_network_account.html.erb @@ -11,17 +11,20 @@

As a global moderator, you may delete the user account network-wide.

-

Take care! These actions may not be reversible and you will not be asked to confirm after initiating an action.

+

Take care! These actions may not be reversible and you will not be asked to confirm after initiating an action.

-
-

Delete User Network-wide

-

Delete the account network-wide for users who are unwilling to follow the rules of this network, even after repeated warnings and suspensions. Choose this option if a user has requested deletion of their profile on all sites, once you have confirmed their identity and request.

+
+

Delete User Network-wide

+

Delete the account network-wide for users who are unwilling to follow the rules of this network, even after repeated warnings and suspensions. Choose this option if a user has requested deletion of their profile on all sites, once you have confirmed their identity and request.

- <%= link_to 'Delete user network-wide', soft_delete_user_path(@user.id, type: 'user'), remote: true, - method: :delete, class: 'js-soft-delete button is-danger is-filled' %> -
+ <%= link_to 'Delete user network-wide', + soft_delete_user_path(@user.id, type: 'user'), + remote: true, + method: :delete, + class: 'js-soft-delete button is-danger is-filled' %> +
From 7c87bb12ef301efcc24f600189e7bc8b438fedd9 Mon Sep 17 00:00:00 2001 From: trichoplax Date: Tue, 26 May 2026 18:29:11 +0100 Subject: [PATCH 12/13] Match left panel layout change in mod tools to right panel in other places --- app/views/admin/impersonate.html.erb | 2 +- app/views/mod_warning/log.html.erb | 2 +- app/views/mod_warning/new.html.erb | 2 +- app/views/moderator/user_vote_summary.html.erb | 2 +- app/views/users/annotations.html.erb | 2 +- app/views/users/full_log.html.erb | 2 +- app/views/users/mod.html.erb | 2 +- app/views/users/mod_delete.html.erb | 2 +- app/views/users/mod_delete_network_account.html.erb | 2 +- app/views/users/mod_failban.html.erb | 2 +- app/views/users/mod_privileges.html.erb | 2 +- 11 files changed, 11 insertions(+), 11 deletions(-) diff --git a/app/views/admin/impersonate.html.erb b/app/views/admin/impersonate.html.erb index 1c23a7271..9167e265c 100644 --- a/app/views/admin/impersonate.html.erb +++ b/app/views/admin/impersonate.html.erb @@ -5,7 +5,7 @@
<%= render 'shared/user_mod_sidebar', user: @user %> -
+

Impersonate <%= @user.username %>

As a developer, you have access to impersonate users to help in reproducing bug reports, among other things. diff --git a/app/views/mod_warning/log.html.erb b/app/views/mod_warning/log.html.erb index 9d4ab8333..c124c9e27 100644 --- a/app/views/mod_warning/log.html.erb +++ b/app/views/mod_warning/log.html.erb @@ -5,7 +5,7 @@

<%= render 'shared/user_mod_sidebar', user: @user %> -
+

Previously Sent Warnings

<% if @warnings.size == 0 %> diff --git a/app/views/mod_warning/new.html.erb b/app/views/mod_warning/new.html.erb index 302a795b1..09eb22360 100644 --- a/app/views/mod_warning/new.html.erb +++ b/app/views/mod_warning/new.html.erb @@ -10,7 +10,7 @@
<%= render 'shared/user_mod_sidebar', user: @user %> -
+

Warn or Suspend User

diff --git a/app/views/moderator/user_vote_summary.html.erb b/app/views/moderator/user_vote_summary.html.erb index 165487637..1a0504157 100644 --- a/app/views/moderator/user_vote_summary.html.erb +++ b/app/views/moderator/user_vote_summary.html.erb @@ -5,7 +5,7 @@
<%= render 'shared/user_mod_sidebar', user: @user %> -
+

Vote Summary

This is a summary of votes cast and received by this user. This may help you to identify voting patterns and diff --git a/app/views/users/annotations.html.erb b/app/views/users/annotations.html.erb index 547f3d271..82a7e0136 100644 --- a/app/views/users/annotations.html.erb +++ b/app/views/users/annotations.html.erb @@ -5,7 +5,7 @@

<%= render 'shared/user_mod_sidebar', user: @user %> -
+

User annotations

diff --git a/app/views/users/full_log.html.erb b/app/views/users/full_log.html.erb index 727a4db82..bfa1dc555 100644 --- a/app/views/users/full_log.html.erb +++ b/app/views/users/full_log.html.erb @@ -5,7 +5,7 @@
<%= render 'shared/user_mod_sidebar', user: @user %> -
+

Full activity log

This is a filterable log for all activity by the user. You can consult it for moderation decisions. Do not share this information to people, who do not have access to it.

diff --git a/app/views/users/mod.html.erb b/app/views/users/mod.html.erb index 84ac4e004..e503ed84b 100644 --- a/app/views/users/mod.html.erb +++ b/app/views/users/mod.html.erb @@ -5,7 +5,7 @@
<%= render 'shared/user_mod_sidebar', user: @user %> -
+

Dashboard

Please note that information shown in these user moderation tools is sensitive and should not be shared with anyone outside the moderator and admin team.

diff --git a/app/views/users/mod_delete.html.erb b/app/views/users/mod_delete.html.erb index 20027b3b4..5b70225bb 100644 --- a/app/views/users/mod_delete.html.erb +++ b/app/views/users/mod_delete.html.erb @@ -5,7 +5,7 @@
<%= render 'shared/user_mod_sidebar', user: @user %> -
+

Delete Account

Some users are just blatant spammers or trolls and some users are just unwilling to follow site rules, even after repeated warnings and suspensions. As a moderator, you may delete the user account in these cases.

diff --git a/app/views/users/mod_delete_network_account.html.erb b/app/views/users/mod_delete_network_account.html.erb index 06836eaa4..ff6ba554f 100644 --- a/app/views/users/mod_delete_network_account.html.erb +++ b/app/views/users/mod_delete_network_account.html.erb @@ -5,7 +5,7 @@
<%= render 'shared/user_mod_sidebar', user: @user %> -
+

Network-wide Account Deletion

As a global moderator, you may delete the user account network-wide.

diff --git a/app/views/users/mod_failban.html.erb b/app/views/users/mod_failban.html.erb index 9dec61958..4e3c562dc 100644 --- a/app/views/users/mod_failban.html.erb +++ b/app/views/users/mod_failban.html.erb @@ -5,7 +5,7 @@
<%= render 'shared/user_mod_sidebar', user: @user %> -
+

Fail-ban

diff --git a/app/views/users/mod_privileges.html.erb b/app/views/users/mod_privileges.html.erb index 40ae1f026..999852360 100644 --- a/app/views/users/mod_privileges.html.erb +++ b/app/views/users/mod_privileges.html.erb @@ -5,7 +5,7 @@
<%= render 'shared/user_mod_sidebar', user: @user %> -
+

User Privileges

From 8de70f0f2b5590052954a1d9ca861321f85dd59d Mon Sep 17 00:00:00 2001 From: trichoplax Date: Tue, 26 May 2026 19:49:49 +0100 Subject: [PATCH 13/13] Add temporary CSS variable for mod tools --- app/assets/stylesheets/_variables.scss | 2 ++ app/assets/stylesheets/users.scss | 4 +--- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/assets/stylesheets/_variables.scss b/app/assets/stylesheets/_variables.scss index d412b3ec7..edba4b743 100644 --- a/app/assets/stylesheets/_variables.scss +++ b/app/assets/stylesheets/_variables.scss @@ -6,6 +6,8 @@ $screen-md: 780px; $screen-lg: 992px; $screen-xl: 1200px; +$mod-tools-medium-to-large-breakpoint: 768px !default; + // Colors $key: #335; $muted-text: #666; diff --git a/app/assets/stylesheets/users.scss b/app/assets/stylesheets/users.scss index c6a71183e..22c71a25d 100644 --- a/app/assets/stylesheets/users.scss +++ b/app/assets/stylesheets/users.scss @@ -259,8 +259,6 @@ $sizes: (16, 32, 40, 48, 64, 128, 256); margin-right: 1rem; } -$grid-medium-to-large-breakpoint: 768px !default; - .modtools--sidebar { > .widget--header { display: none; @@ -281,7 +279,7 @@ $grid-medium-to-large-breakpoint: 768px !default; } } -@media screen and (min-width: $grid-medium-to-large-breakpoint) { +@media screen and (min-width: $mod-tools-medium-to-large-breakpoint) { .modtools--sidebar { > .widget--header { display: block;