about summary refs log tree commit diff
path: root/app/views
diff options
context:
space:
mode:
Diffstat (limited to 'app/views')
-rw-r--r--app/views/accounts/show.html.haml2
-rw-r--r--app/views/accounts/show.rss.ruby2
-rw-r--r--app/views/admin/accounts/index.html.haml55
-rw-r--r--app/views/admin/accounts/show.html.haml9
-rw-r--r--app/views/admin/action_logs/index.html.haml2
-rw-r--r--app/views/admin/instances/show.html.haml49
-rw-r--r--app/views/admin/roles/_form.html.haml40
-rw-r--r--app/views/admin/roles/_role.html.haml30
-rw-r--r--app/views/admin/roles/edit.html.haml8
-rw-r--r--app/views/admin/roles/index.html.haml17
-rw-r--r--app/views/admin/roles/new.html.haml4
-rw-r--r--app/views/admin/settings/edit.html.haml6
-rw-r--r--app/views/admin/tags/show.html.haml93
-rw-r--r--app/views/admin/trends/tags/_tag.html.haml2
-rw-r--r--app/views/admin/users/roles/show.html.haml9
-rw-r--r--app/views/admin_mailer/_new_trending_tags.text.erb4
-rw-r--r--app/views/application/_sidebar.html.haml2
-rw-r--r--app/views/custom_css/show.css.erb12
-rw-r--r--app/views/filters/_fields.html.haml16
-rw-r--r--app/views/filters/_filter.html.haml32
-rw-r--r--app/views/filters/_filter_fields.html.haml33
-rw-r--r--app/views/filters/_keyword_fields.html.haml8
-rw-r--r--app/views/filters/edit.html.haml2
-rw-r--r--app/views/filters/index.html.haml17
-rw-r--r--app/views/filters/new.html.haml4
-rwxr-xr-xapp/views/layouts/application.html.haml3
-rw-r--r--app/views/settings/featured_tags/index.html.haml2
-rw-r--r--app/views/settings/preferences/notifications/show.html.haml14
-rw-r--r--app/views/tags/_og.html.haml4
-rw-r--r--app/views/tags/show.html.haml6
-rw-r--r--app/views/tags/show.rss.ruby6
31 files changed, 322 insertions, 171 deletions
diff --git a/app/views/accounts/show.html.haml b/app/views/accounts/show.html.haml
index 72e9c6611..7fa688bd3 100644
--- a/app/views/accounts/show.html.haml
+++ b/app/views/accounts/show.html.haml
@@ -75,7 +75,7 @@
           = link_to short_account_tag_path(@account, featured_tag.tag) do
             %h4
               = fa_icon 'hashtag'
-              = featured_tag.name
+              = featured_tag.display_name
               %small
                 - if featured_tag.last_status_at.nil?
                   = t('accounts.nothing_here')
diff --git a/app/views/accounts/show.rss.ruby b/app/views/accounts/show.rss.ruby
index fd45a8b2b..34e29d483 100644
--- a/app/views/accounts/show.rss.ruby
+++ b/app/views/accounts/show.rss.ruby
@@ -28,7 +28,7 @@ RSS::Builder.build do |doc|
       end
 
       status.tags.each do |tag|
-        item.category(tag.name)
+        item.category(tag.display_name)
       end
     end
   end
diff --git a/app/views/admin/accounts/index.html.haml b/app/views/admin/accounts/index.html.haml
index 0290df7de..84040e480 100644
--- a/app/views/admin/accounts/index.html.haml
+++ b/app/views/admin/accounts/index.html.haml
@@ -1,45 +1,36 @@
 - content_for :page_title do
   = t('admin.accounts.title')
 
-.filters
-  .filter-subset
-    %strong= t('admin.accounts.location.title')
-    %ul
-      %li= filter_link_to t('generic.all'), origin: nil
-      %li= filter_link_to t('admin.accounts.location.local'), origin: 'local'
-      %li= filter_link_to t('admin.accounts.location.remote'), origin: 'remote'
-  .filter-subset
-    %strong= t('admin.accounts.moderation.title')
-    %ul
-      %li= filter_link_to t('generic.all'), status: nil
-      %li= filter_link_to t('admin.accounts.moderation.active'), status: 'active'
-      %li= filter_link_to t('admin.accounts.moderation.suspended'), status: 'suspended'
-      %li= filter_link_to safe_join([t('admin.accounts.moderation.pending'), "(#{number_with_delimiter(User.pending.count)})"], ' '), status: 'pending'
-  .filter-subset
-    %strong= t('admin.accounts.role')
-    %ul
-      %li= filter_link_to t('admin.accounts.moderation.all'), permissions: nil
-      %li= filter_link_to t('admin.accounts.roles.staff'), permissions: 'staff'
-  .filter-subset
-    %strong= t 'generic.order_by'
-    %ul
-      %li= filter_link_to t('relationships.most_recent'), order: nil
-      %li= filter_link_to t('relationships.last_active'), order: 'active'
-
 = form_tag admin_accounts_url, method: 'GET', class: 'simple_form' do
-  .fields-group
-    - (AccountFilter::KEYS - %i(origin status permissions)).each do |key|
-      - if params[key].present?
-        = hidden_field_tag key, params[key]
+  .filters
+    .filter-subset.filter-subset--with-select
+      %strong= t('admin.accounts.location.title')
+      .input.select.optional
+        = select_tag :origin, options_for_select([[t('admin.accounts.location.local'), 'local'], [t('admin.accounts.location.remote'), 'remote']], params[:origin]), prompt: I18n.t('generic.all')
+    .filter-subset.filter-subset--with-select
+      %strong= t('admin.accounts.moderation.title')
+      .input.select.optional
+        = select_tag :status, options_for_select([[t('admin.accounts.moderation.active'), 'active'], [t('admin.accounts.moderation.silenced'), 'silenced'], [t('admin.accounts.moderation.suspended'), 'suspended'], [safe_join([t('admin.accounts.moderation.pending'), "(#{number_with_delimiter(User.pending.count)})"], ' '), 'pending']], params[:status]), prompt: I18n.t('generic.all')
+    .filter-subset.filter-subset--with-select
+      %strong= t('admin.accounts.role')
+      .input.select.optional
+        = select_tag :role_ids, options_from_collection_for_select(UserRole.assignable, :id, :name, params[:role_ids]), prompt: I18n.t('admin.accounts.moderation.all')
+    .filter-subset.filter-subset--with-select
+      %strong= t 'generic.order_by'
+      .input.select
+        = select_tag :order, options_for_select([[t('relationships.most_recent'), nil], [t('relationships.last_active'), 'active']], params[:order])
 
+  .fields-group
     - %i(username by_domain display_name email ip).each do |key|
       - unless key == :by_domain && params[:origin] != 'remote'
         .input.string.optional
           = text_field_tag key, params[key], class: 'string optional', placeholder: I18n.t("admin.accounts.#{key}")
 
-    .actions
-      %button.button= t('admin.accounts.search')
-      = link_to t('admin.accounts.reset'), admin_accounts_path, class: 'button negative'
+  .actions
+    %button.button= t('admin.accounts.search')
+    = link_to t('admin.accounts.reset'), admin_accounts_path, class: 'button negative'
+
+%hr.spacer/
 
 = form_for(@form, url: batch_admin_accounts_path) do |f|
   = hidden_field_tag :page, params[:page] || 1
diff --git a/app/views/admin/accounts/show.html.haml b/app/views/admin/accounts/show.html.haml
index a69832b04..dc3b35956 100644
--- a/app/views/admin/accounts/show.html.haml
+++ b/app/views/admin/accounts/show.html.haml
@@ -92,10 +92,13 @@
 
           %tr
             %th= t('admin.accounts.role')
-            %td= t("admin.accounts.roles.#{@account.user&.role}")
             %td
-              = table_link_to 'angle-double-up', t('admin.accounts.promote'), promote_admin_account_role_path(@account.id), method: :post, data: { confirm: t('admin.accounts.are_you_sure') } if can?(:promote, @account.user)
-              = table_link_to 'angle-double-down', t('admin.accounts.demote'), demote_admin_account_role_path(@account.id), method: :post, data: { confirm: t('admin.accounts.are_you_sure') } if can?(:demote, @account.user)
+              - if @account.user_role&.everyone?
+                = t('admin.accounts.no_role_assigned')
+              - else
+                = @account.user_role&.name
+            %td
+              = table_link_to 'vcard', t('admin.accounts.change_role.label'), admin_user_role_path(@account.user) if can?(:change_role, @account.user)
 
           %tr
             %th{ rowspan: can?(:create, :email_domain_block) ? 3 : 2 }= t('admin.accounts.email')
diff --git a/app/views/admin/action_logs/index.html.haml b/app/views/admin/action_logs/index.html.haml
index 03d5bffb9..7869570e6 100644
--- a/app/views/admin/action_logs/index.html.haml
+++ b/app/views/admin/action_logs/index.html.haml
@@ -8,7 +8,7 @@
     .filter-subset.filter-subset--with-select
       %strong= t('admin.action_logs.filter_by_user')
       .input.select.optional
-        = select_tag :account_id, options_from_collection_for_select(Account.joins(:user).merge(User.staff), :id, :username, params[:account_id]), prompt: I18n.t('admin.accounts.moderation.all')
+        = select_tag :account_id, options_from_collection_for_select(@auditable_accounts, :id, :username, params[:account_id]), prompt: I18n.t('admin.accounts.moderation.all')
 
     .filter-subset.filter-subset--with-select
       %strong= t('admin.action_logs.filter_by_action')
diff --git a/app/views/admin/instances/show.html.haml b/app/views/admin/instances/show.html.haml
index e8cc1c400..00c1927df 100644
--- a/app/views/admin/instances/show.html.haml
+++ b/app/views/admin/instances/show.html.haml
@@ -1,32 +1,33 @@
 - content_for :page_title do
   = @instance.domain
 
-- content_for :heading_actions do
-  = l(@time_period.first)
-  = ' - '
-  = l(@time_period.last)
+- if current_user.can?(:view_dashboard)
+  - content_for :heading_actions do
+    = l(@time_period.first)
+    = ' - '
+    = l(@time_period.last)
 
-%p
-  = fa_icon 'info fw'
-  = t('admin.instances.totals_time_period_hint_html')
+  %p
+    = fa_icon 'info fw'
+    = t('admin.instances.totals_time_period_hint_html')
 
-.dashboard
-  .dashboard__item
-    = react_admin_component :counter, measure: 'instance_accounts', start_at: @time_period.first, end_at: @time_period.last, params: { domain: @instance.domain }, label: t('admin.instances.dashboard.instance_accounts_measure'), href: admin_accounts_path(origin: 'remote', by_domain: @instance.domain)
-  .dashboard__item
-    = react_admin_component :counter, measure: 'instance_statuses', start_at: @time_period.first, end_at: @time_period.last, params: { domain: @instance.domain }, label: t('admin.instances.dashboard.instance_statuses_measure')
-  .dashboard__item
-    = react_admin_component :counter, measure: 'instance_media_attachments', start_at: @time_period.first, end_at: @time_period.last, params: { domain: @instance.domain }, label: t('admin.instances.dashboard.instance_media_attachments_measure')
-  .dashboard__item
-    = react_admin_component :counter, measure: 'instance_follows', start_at: @time_period.first, end_at: @time_period.last, params: { domain: @instance.domain }, label: t('admin.instances.dashboard.instance_follows_measure')
-  .dashboard__item
-    = react_admin_component :counter, measure: 'instance_followers', start_at: @time_period.first, end_at: @time_period.last, params: { domain: @instance.domain }, label: t('admin.instances.dashboard.instance_followers_measure')
-  .dashboard__item
-    = react_admin_component :counter, measure: 'instance_reports', start_at: @time_period.first, end_at: @time_period.last, params: { domain: @instance.domain }, label: t('admin.instances.dashboard.instance_reports_measure'), href: admin_reports_path(by_target_domain: @instance.domain)
-  .dashboard__item
-    = react_admin_component :dimension, dimension: 'instance_accounts', start_at: @time_period.first, end_at: @time_period.last, params: { domain: @instance.domain }, limit: 8, label: t('admin.instances.dashboard.instance_accounts_dimension')
-  .dashboard__item
-    = react_admin_component :dimension, dimension: 'instance_languages', start_at: @time_period.first, end_at: @time_period.last, params: { domain: @instance.domain }, limit: 8, label: t('admin.instances.dashboard.instance_languages_dimension')
+  .dashboard
+    .dashboard__item
+      = react_admin_component :counter, measure: 'instance_accounts', start_at: @time_period.first, end_at: @time_period.last, params: { domain: @instance.domain }, label: t('admin.instances.dashboard.instance_accounts_measure'), href: admin_accounts_path(origin: 'remote', by_domain: @instance.domain)
+    .dashboard__item
+      = react_admin_component :counter, measure: 'instance_statuses', start_at: @time_period.first, end_at: @time_period.last, params: { domain: @instance.domain }, label: t('admin.instances.dashboard.instance_statuses_measure')
+    .dashboard__item
+      = react_admin_component :counter, measure: 'instance_media_attachments', start_at: @time_period.first, end_at: @time_period.last, params: { domain: @instance.domain }, label: t('admin.instances.dashboard.instance_media_attachments_measure')
+    .dashboard__item
+      = react_admin_component :counter, measure: 'instance_follows', start_at: @time_period.first, end_at: @time_period.last, params: { domain: @instance.domain }, label: t('admin.instances.dashboard.instance_follows_measure')
+    .dashboard__item
+      = react_admin_component :counter, measure: 'instance_followers', start_at: @time_period.first, end_at: @time_period.last, params: { domain: @instance.domain }, label: t('admin.instances.dashboard.instance_followers_measure')
+    .dashboard__item
+      = react_admin_component :counter, measure: 'instance_reports', start_at: @time_period.first, end_at: @time_period.last, params: { domain: @instance.domain }, label: t('admin.instances.dashboard.instance_reports_measure'), href: admin_reports_path(by_target_domain: @instance.domain)
+    .dashboard__item
+      = react_admin_component :dimension, dimension: 'instance_accounts', start_at: @time_period.first, end_at: @time_period.last, params: { domain: @instance.domain }, limit: 8, label: t('admin.instances.dashboard.instance_accounts_dimension')
+    .dashboard__item
+      = react_admin_component :dimension, dimension: 'instance_languages', start_at: @time_period.first, end_at: @time_period.last, params: { domain: @instance.domain }, limit: 8, label: t('admin.instances.dashboard.instance_languages_dimension')
 
 %hr.spacer/
 
diff --git a/app/views/admin/roles/_form.html.haml b/app/views/admin/roles/_form.html.haml
new file mode 100644
index 000000000..9beaf619f
--- /dev/null
+++ b/app/views/admin/roles/_form.html.haml
@@ -0,0 +1,40 @@
+= simple_form_for @role, url: @role.new_record? ? admin_roles_path : admin_role_path(@role) do |f|
+  = render 'shared/error_messages', object: @role
+
+  - if @role.everyone?
+    .flash-message.info
+      = t('admin.roles.everyone_full_description_html')
+  - else
+    .fields-group
+      = f.input :name, wrapper: :with_label
+
+    - unless current_user.role.id == @role.id
+      .fields-group
+        = f.input :position, wrapper: :with_label, input_html: { max: current_user.role.position - 1 }
+
+    .fields-group
+      = f.input :color, wrapper: :with_label, input_html: { placeholder: '#000000' }
+
+    %hr.spacer/
+
+    .fields-group
+      = f.input :highlighted, wrapper: :with_label
+
+    %hr.spacer/
+
+  - unless current_user.role.id == @role.id
+
+    .field-group
+      .input.with_block_label
+        %label= t('simple_form.labels.user_role.permissions_as_keys')
+        %span.hint= t('simple_form.hints.user_role.permissions_as_keys')
+
+      - (@role.everyone? ? UserRole::Flags::CATEGORIES.slice(:invites) : UserRole::Flags::CATEGORIES).each do |category, permissions|
+        %h4= t(category, scope: 'admin.roles.categories')
+
+        = f.input :permissions_as_keys, collection: permissions, wrapper: :with_block_label, include_blank: false, label_method: lambda { |privilege| safe_join([t("admin.roles.privileges.#{privilege}"), content_tag(:span, t("admin.roles.privileges.#{privilege}_description"), class: 'hint')]) }, required: false, as: :check_boxes, collection_wrapper_tag: 'ul', item_wrapper_tag: 'li', label: false, hint: false, disabled: permissions.filter { |privilege| UserRole::FLAGS[privilege] & current_user.role.computed_permissions == 0 }
+
+    %hr.spacer/
+
+  .actions
+    = f.button :button, @role.new_record? ? t('admin.roles.add_new') : t('generic.save_changes'), type: :submit
diff --git a/app/views/admin/roles/_role.html.haml b/app/views/admin/roles/_role.html.haml
new file mode 100644
index 000000000..798d8d8b4
--- /dev/null
+++ b/app/views/admin/roles/_role.html.haml
@@ -0,0 +1,30 @@
+.announcements-list__item
+  - if can?(:update, role)
+    = link_to edit_admin_role_path(role), class: 'announcements-list__item__title' do
+      %span.user-role{ class: "user-role-#{role.id}" }
+        = fa_icon 'users fw'
+
+        - if role.everyone?
+          = t('admin.roles.everyone')
+        - else
+          = role.name
+  - else
+    %span.announcements-list__item__title
+      %span.user-role{ class: "user-role-#{role.id}" }
+        = fa_icon 'users fw'
+
+        - if role.everyone?
+          = t('admin.roles.everyone')
+        - else
+          = role.name
+
+  .announcements-list__item__action-bar
+    .announcements-list__item__meta
+      - if role.everyone?
+        = t('admin.roles.everyone_full_description_html')
+      - else
+        = link_to t('admin.roles.assigned_users', count: role.users.count), admin_accounts_path(role_ids: role.id)
+        •
+        %abbr{ title: role.permissions_as_keys.map { |privilege| I18n.t("admin.roles.privileges.#{privilege}") }.join(', ') }= t('admin.roles.permissions_count', count: role.permissions_as_keys.size)
+    %div
+      = table_link_to 'pencil', t('admin.accounts.edit'), edit_admin_role_path(role) if can?(:update, role)
diff --git a/app/views/admin/roles/edit.html.haml b/app/views/admin/roles/edit.html.haml
new file mode 100644
index 000000000..659ccb8dc
--- /dev/null
+++ b/app/views/admin/roles/edit.html.haml
@@ -0,0 +1,8 @@
+- content_for :page_title do
+  = t('admin.roles.edit', name: @role.everyone? ? t('admin.roles.everyone') : @role.name)
+
+- content_for :heading_actions do
+  = link_to t('admin.roles.delete'), admin_role_path(@role), method: :delete, data: { confirm: t('admin.accounts.are_you_sure') }, class: 'button button--destructive' if can?(:destroy, @role)
+
+= render partial: 'form'
+
diff --git a/app/views/admin/roles/index.html.haml b/app/views/admin/roles/index.html.haml
new file mode 100644
index 000000000..4f6c511b4
--- /dev/null
+++ b/app/views/admin/roles/index.html.haml
@@ -0,0 +1,17 @@
+- content_for :page_title do
+  = t('admin.roles.title')
+
+- content_for :heading_actions do
+  = link_to t('admin.roles.add_new'), new_admin_role_path, class: 'button' if can?(:create, :user_role)
+
+%p= t('admin.roles.description_html')
+
+%hr.spacer/
+
+.applications-list
+  = render partial: 'role', collection: @roles.select(&:everyone?)
+
+%hr.spacer/
+
+.applications-list
+  = render partial: 'role', collection: @roles.reject(&:everyone?)
diff --git a/app/views/admin/roles/new.html.haml b/app/views/admin/roles/new.html.haml
new file mode 100644
index 000000000..821079271
--- /dev/null
+++ b/app/views/admin/roles/new.html.haml
@@ -0,0 +1,4 @@
+- content_for :page_title do
+  = t('admin.roles.add_new')
+
+= render partial: 'form'
diff --git a/app/views/admin/settings/edit.html.haml b/app/views/admin/settings/edit.html.haml
index dd794b727..98af7e718 100644
--- a/app/views/admin/settings/edit.html.haml
+++ b/app/views/admin/settings/edit.html.haml
@@ -62,9 +62,6 @@
       = f.input :show_known_fediverse_at_about_page, as: :boolean, wrapper: :with_label, label: t('admin.settings.show_known_fediverse_at_about_page.title'), hint: t('admin.settings.show_known_fediverse_at_about_page.desc_html')
 
   .fields-group
-    = f.input :show_staff_badge, as: :boolean, wrapper: :with_label, label: t('admin.settings.show_staff_badge.title'), hint: t('admin.settings.show_staff_badge.desc_html')
-
-  .fields-group
     = f.input :open_deletion, as: :boolean, wrapper: :with_label, label: t('admin.settings.registrations.deletion.title'), hint: t('admin.settings.registrations.deletion.desc_html')
 
   - unless whitelist_mode?
@@ -103,9 +100,6 @@
 
   %hr.spacer/
 
-  .fields-group
-    = f.input :min_invite_role, wrapper: :with_label, collection: %i(disabled user moderator admin), label: t('admin.settings.registrations.min_invite_role.title'), label_method: lambda { |role| role == :disabled ? t('admin.settings.registrations.min_invite_role.disabled') : t("admin.accounts.roles.#{role}") }, include_blank: false, collection_wrapper_tag: 'ul', item_wrapper_tag: 'li'
-
   .fields-row
     .fields-row__column.fields-row__column-6.fields-group
       = f.input :show_domain_blocks, wrapper: :with_label, collection: %i(disabled users all), label: t('admin.settings.domain_blocks.title'), label_method: lambda { |value| t("admin.settings.domain_blocks.#{value}") }, include_blank: false, collection_wrapper_tag: 'ul', item_wrapper_tag: 'li'
diff --git a/app/views/admin/tags/show.html.haml b/app/views/admin/tags/show.html.haml
index 5ac57e1f2..71bce0c0c 100644
--- a/app/views/admin/tags/show.html.haml
+++ b/app/views/admin/tags/show.html.haml
@@ -1,55 +1,56 @@
 - content_for :page_title do
-  = "##{@tag.name}"
-
-- content_for :heading_actions do
-  = l(@time_period.first)
-  = ' - '
-  = l(@time_period.last)
-
-.dashboard
-  .dashboard__item
-    = react_admin_component :counter, measure: 'tag_accounts', start_at: @time_period.first, end_at: @time_period.last, params: { id: @tag.id }, label: t('admin.trends.tags.dashboard.tag_accounts_measure'), href: tag_url(@tag), target: '_blank'
-  .dashboard__item
-    = react_admin_component :counter, measure: 'tag_uses', start_at: @time_period.first, end_at: @time_period.last, params: { id: @tag.id }, label: t('admin.trends.tags.dashboard.tag_uses_measure')
-  .dashboard__item
-    = react_admin_component :counter, measure: 'tag_servers', start_at: @time_period.first, end_at: @time_period.last, params: { id: @tag.id }, label: t('admin.trends.tags.dashboard.tag_servers_measure')
-  .dashboard__item
-    = react_admin_component :dimension, dimension: 'tag_servers', start_at: @time_period.first, end_at: @time_period.last, params: { id: @tag.id }, limit: 8, label: t('admin.trends.tags.dashboard.tag_servers_dimension')
-  .dashboard__item
-    = react_admin_component :dimension, dimension: 'tag_languages', start_at: @time_period.first, end_at: @time_period.last, params: { id: @tag.id }, limit: 8, label: t('admin.trends.tags.dashboard.tag_languages_dimension')
-  .dashboard__item
-    = link_to admin_tag_path(@tag.id), class: ['dashboard__quick-access', @tag.usable? ? 'positive' : 'negative'] do
-      - if @tag.usable?
-        %span= t('admin.trends.tags.usable')
-        = fa_icon 'check fw'
-      - else
-        %span= t('admin.trends.tags.not_usable')
-        = fa_icon 'lock fw'
-
-    = link_to admin_tag_path(@tag.id), class: ['dashboard__quick-access', @tag.trendable? ? 'positive' : 'negative'] do
-      - if @tag.trendable?
-        %span= t('admin.trends.tags.trendable')
-        = fa_icon 'check fw'
-      - else
-        %span= t('admin.trends.tags.not_trendable')
-        = fa_icon 'lock fw'
-
-
-    = link_to admin_tag_path(@tag.id), class: ['dashboard__quick-access', @tag.listable? ? 'positive' : 'negative'] do
-      - if @tag.listable?
-        %span= t('admin.trends.tags.listable')
-        = fa_icon 'check fw'
-      - else
-        %span= t('admin.trends.tags.not_listable')
-        = fa_icon 'lock fw'
-
-%hr.spacer/
+  = "##{@tag.display_name}"
+
+- if current_user.can?(:view_dashboard)
+  - content_for :heading_actions do
+    = l(@time_period.first)
+    = ' - '
+    = l(@time_period.last)
+
+  .dashboard
+    .dashboard__item
+      = react_admin_component :counter, measure: 'tag_accounts', start_at: @time_period.first, end_at: @time_period.last, params: { id: @tag.id }, label: t('admin.trends.tags.dashboard.tag_accounts_measure'), href: tag_url(@tag), target: '_blank'
+    .dashboard__item
+      = react_admin_component :counter, measure: 'tag_uses', start_at: @time_period.first, end_at: @time_period.last, params: { id: @tag.id }, label: t('admin.trends.tags.dashboard.tag_uses_measure')
+    .dashboard__item
+      = react_admin_component :counter, measure: 'tag_servers', start_at: @time_period.first, end_at: @time_period.last, params: { id: @tag.id }, label: t('admin.trends.tags.dashboard.tag_servers_measure')
+    .dashboard__item
+      = react_admin_component :dimension, dimension: 'tag_servers', start_at: @time_period.first, end_at: @time_period.last, params: { id: @tag.id }, limit: 8, label: t('admin.trends.tags.dashboard.tag_servers_dimension')
+    .dashboard__item
+      = react_admin_component :dimension, dimension: 'tag_languages', start_at: @time_period.first, end_at: @time_period.last, params: { id: @tag.id }, limit: 8, label: t('admin.trends.tags.dashboard.tag_languages_dimension')
+    .dashboard__item
+      = link_to admin_tag_path(@tag.id), class: ['dashboard__quick-access', @tag.usable? ? 'positive' : 'negative'] do
+        - if @tag.usable?
+          %span= t('admin.trends.tags.usable')
+          = fa_icon 'check fw'
+        - else
+          %span= t('admin.trends.tags.not_usable')
+          = fa_icon 'lock fw'
+
+      = link_to admin_tag_path(@tag.id), class: ['dashboard__quick-access', @tag.trendable? ? 'positive' : 'negative'] do
+        - if @tag.trendable?
+          %span= t('admin.trends.tags.trendable')
+          = fa_icon 'check fw'
+        - else
+          %span= t('admin.trends.tags.not_trendable')
+          = fa_icon 'lock fw'
+
+
+      = link_to admin_tag_path(@tag.id), class: ['dashboard__quick-access', @tag.listable? ? 'positive' : 'negative'] do
+        - if @tag.listable?
+          %span= t('admin.trends.tags.listable')
+          = fa_icon 'check fw'
+        - else
+          %span= t('admin.trends.tags.not_listable')
+          = fa_icon 'lock fw'
+
+  %hr.spacer/
 
 = simple_form_for @tag, url: admin_tag_path(@tag.id) do |f|
   = render 'shared/error_messages', object: @tag
 
   .fields-group
-    = f.input :name, wrapper: :with_block_label
+    = f.input :display_name, wrapper: :with_block_label
 
   .fields-group
     = f.input :usable, as: :boolean, wrapper: :with_label
diff --git a/app/views/admin/trends/tags/_tag.html.haml b/app/views/admin/trends/tags/_tag.html.haml
index 7bb99b158..a30666a08 100644
--- a/app/views/admin/trends/tags/_tag.html.haml
+++ b/app/views/admin/trends/tags/_tag.html.haml
@@ -6,7 +6,7 @@
     .pending-account__header
       = link_to admin_tag_path(tag.id) do
         = fa_icon 'hashtag'
-        = tag.name
+        = tag.display_name
 
       %br/
 
diff --git a/app/views/admin/users/roles/show.html.haml b/app/views/admin/users/roles/show.html.haml
new file mode 100644
index 000000000..821618060
--- /dev/null
+++ b/app/views/admin/users/roles/show.html.haml
@@ -0,0 +1,9 @@
+- content_for :page_title do
+  = t('admin.accounts.change_role.title', username: @user.account.username)
+
+= simple_form_for @user, url: admin_user_role_path(@user) do |f|
+  .fields-group
+    = f.association :role, wrapper: :with_block_label, collection: UserRole.assignable, label_method: :name, include_blank: I18n.t('admin.accounts.change_role.no_role')
+
+  .actions
+    = f.button :button, t('generic.save_changes'), type: :submit
diff --git a/app/views/admin_mailer/_new_trending_tags.text.erb b/app/views/admin_mailer/_new_trending_tags.text.erb
index cde5af4e4..363df369d 100644
--- a/app/views/admin_mailer/_new_trending_tags.text.erb
+++ b/app/views/admin_mailer/_new_trending_tags.text.erb
@@ -1,12 +1,12 @@
 <%= raw t('admin_mailer.new_trends.new_trending_tags.title') %>
 
 <% @tags.each do |tag| %>
-- #<%= tag.name %>
+- #<%= tag.display_name %>
   <%= raw t('admin.trends.tags.usage_comparison', today: tag.history.get(Time.now.utc).accounts, yesterday: tag.history.get(Time.now.utc - 1.day).accounts) %> • <%= t('admin.trends.tags.current_score', score: Trends.tags.score(tag.id).round(2)) %>
 <% end %>
 
 <% if @lowest_trending_tag %>
-<%= raw t('admin_mailer.new_trends.new_trending_tags.requirements', lowest_tag_name: @lowest_trending_tag.name, lowest_tag_score: Trends.tags.score(@lowest_trending_tag.id).round(2), rank: Trends.tags.options[:review_threshold]) %>
+<%= raw t('admin_mailer.new_trends.new_trending_tags.requirements', lowest_tag_name: @lowest_trending_tag.display_name, lowest_tag_score: Trends.tags.score(@lowest_trending_tag.id).round(2), rank: Trends.tags.options[:review_threshold]) %>
 <% else %>
 <%= raw t('admin_mailer.new_trends.new_trending_tags.no_approved_tags') %>
 <% end %>
diff --git a/app/views/application/_sidebar.html.haml b/app/views/application/_sidebar.html.haml
index 0a952add0..cc157bf47 100644
--- a/app/views/application/_sidebar.html.haml
+++ b/app/views/application/_sidebar.html.haml
@@ -13,4 +13,4 @@
       %h4.emojify= t('footer.trending_now')
 
       - trends.each do |tag|
-        = react_component :hashtag, hashtag: ActiveModelSerializers::SerializableResource.new(tag, serializer: REST::TagSerializer).as_json
+        = react_component :hashtag, hashtag: ActiveModelSerializers::SerializableResource.new(tag, serializer: REST::TagSerializer, scope: current_user, scope_name: :current_user).as_json
diff --git a/app/views/custom_css/show.css.erb b/app/views/custom_css/show.css.erb
new file mode 100644
index 000000000..bcbe81962
--- /dev/null
+++ b/app/views/custom_css/show.css.erb
@@ -0,0 +1,12 @@
+<%- if Setting.custom_css.present? %>
+<%= raw Setting.custom_css %>
+
+<%- end %>
+<%- UserRole.where(highlighted: true).select { |role| role.color.present? }.each do |role| %>
+.user-role-<%= role.id %> {
+  --user-role-accent: <%= role.color %>;
+  --user-role-background: <%= role.color + '19' %>;
+  --user-role-border: <%= role.color + '80' %>;
+}
+
+<%- end %>
diff --git a/app/views/filters/_fields.html.haml b/app/views/filters/_fields.html.haml
deleted file mode 100644
index 84dcdcca5..000000000
--- a/app/views/filters/_fields.html.haml
+++ /dev/null
@@ -1,16 +0,0 @@
-.fields-row
-  .fields-row__column.fields-row__column-6.fields-group
-    = f.input :phrase, as: :string, wrapper: :with_label, hint: false
-  .fields-row__column.fields-row__column-6.fields-group
-    = f.input :expires_in, wrapper: :with_label, collection: [30.minutes, 1.hour, 6.hours, 12.hours, 1.day, 1.week].map(&:to_i), label_method: lambda { |i| I18n.t("invites.expires_in.#{i}") }, include_blank: I18n.t('invites.expires_in_prompt')
-
-.fields-group
-  = f.input :context, wrapper: :with_block_label, collection: CustomFilter::VALID_CONTEXTS, as: :check_boxes, collection_wrapper_tag: 'ul', item_wrapper_tag: 'li', label_method: lambda { |context| I18n.t("filters.contexts.#{context}") }, include_blank: false
-
-%hr.spacer/
-
-.fields-group
-  = f.input :irreversible, wrapper: :with_label
-
-.fields-group
-  = f.input :whole_word, wrapper: :with_label
diff --git a/app/views/filters/_filter.html.haml b/app/views/filters/_filter.html.haml
new file mode 100644
index 000000000..2ab014081
--- /dev/null
+++ b/app/views/filters/_filter.html.haml
@@ -0,0 +1,32 @@
+.filters-list__item{ class: [filter.expired? && 'expired'] }
+  = link_to edit_filter_path(filter), class: 'filters-list__item__title' do
+    = filter.title
+
+    - if filter.expires?
+      .expiration{ title: t('filters.index.expires_on', date: l(filter.expires_at)) }
+        - if filter.expired?
+          = t('invites.expired')
+        - else
+          = t('filters.index.expires_in', distance: distance_of_time_in_words_to_now(filter.expires_at))
+
+  .filters-list__item__permissions
+    %ul.permissions-list
+      - unless filter.keywords.empty?
+        %li.permissions-list__item
+          .permissions-list__item__icon
+            = fa_icon('paragraph')
+          .permissions-list__item__text
+            .permissions-list__item__text__title
+              = t('filters.index.keywords', count: filter.keywords.size)
+            .permissions-list__item__text__type
+              - keywords = filter.keywords.map(&:keyword)
+              - keywords = keywords.take(5) + ['…'] if keywords.size > 5 # TODO
+              = keywords.join(', ')
+
+  .announcements-list__item__action-bar
+    .announcements-list__item__meta
+      = t('filters.index.contexts', contexts: filter.context.map { |context| I18n.t("filters.contexts.#{context}") }.join(', '))
+
+    %div
+      = table_link_to 'pencil', t('filters.edit.title'), edit_filter_path(filter)
+      = table_link_to 'times', t('filters.index.delete'), filter_path(filter), method: :delete, data: { confirm: t('admin.accounts.are_you_sure') }
diff --git a/app/views/filters/_filter_fields.html.haml b/app/views/filters/_filter_fields.html.haml
new file mode 100644
index 000000000..1a52faa7a
--- /dev/null
+++ b/app/views/filters/_filter_fields.html.haml
@@ -0,0 +1,33 @@
+.fields-row
+  .fields-row__column.fields-row__column-6.fields-group
+    = f.input :title, as: :string, wrapper: :with_label, hint: false
+  .fields-row__column.fields-row__column-6.fields-group
+    = f.input :expires_in, wrapper: :with_label, collection: [30.minutes, 1.hour, 6.hours, 12.hours, 1.day, 1.week].map(&:to_i), label_method: lambda { |i| I18n.t("invites.expires_in.#{i}") }, include_blank: I18n.t('invites.expires_in_prompt')
+
+.fields-group
+  = f.input :context, wrapper: :with_block_label, collection: CustomFilter::VALID_CONTEXTS, as: :check_boxes, collection_wrapper_tag: 'ul', item_wrapper_tag: 'li', label_method: lambda { |context| I18n.t("filters.contexts.#{context}") }, include_blank: false
+
+%hr.spacer/
+
+.fields-group
+  = f.input :filter_action, as: :radio_buttons, collection: %i(warn hide), include_blank: false, wrapper: :with_block_label, label_method: ->(action) { safe_join([t("simple_form.labels.filters.actions.#{action}"), content_tag(:span, t("simple_form.hints.filters.actions.#{action}"), class: 'hint')]) }, hint: t('simple_form.hints.filters.action'), required: true
+
+%hr.spacer/
+
+%h4= t('filters.edit.keywords')
+
+.table-wrapper
+  %table.table.keywords-table
+    %thead
+      %tr
+        %th= t('simple_form.labels.defaults.phrase')
+        %th= t('simple_form.labels.defaults.whole_word')
+        %th
+    %tbody
+      = f.simple_fields_for :keywords do |keyword|
+        = render 'keyword_fields', f: keyword
+    %tfoot
+      %tr
+        %td{ colspan: 3}
+          = link_to_add_association f, :keywords, class: 'table-action-link', partial: 'keyword_fields', 'data-association-insertion-node': '.keywords-table tbody', 'data-association-insertion-method': 'append' do
+            = safe_join([fa_icon('plus'), t('filters.edit.add_keyword')])
diff --git a/app/views/filters/_keyword_fields.html.haml b/app/views/filters/_keyword_fields.html.haml
new file mode 100644
index 000000000..eedd514ef
--- /dev/null
+++ b/app/views/filters/_keyword_fields.html.haml
@@ -0,0 +1,8 @@
+%tr.nested-fields
+  %td= f.input :keyword, as: :string
+  %td
+    .label_input__wrapper= f.input_field :whole_word
+  %td
+    = f.hidden_field :id if f.object&.persisted? # Required so Rails doesn't put the field outside of the <tr/>
+    = link_to_remove_association(f, class: 'table-action-link') do
+      = safe_join([fa_icon('times'), t('filters.index.delete')])
diff --git a/app/views/filters/edit.html.haml b/app/views/filters/edit.html.haml
index e971215ac..3dc3f07b7 100644
--- a/app/views/filters/edit.html.haml
+++ b/app/views/filters/edit.html.haml
@@ -2,7 +2,7 @@
   = t('filters.edit.title')
 
 = simple_form_for @filter, url: filter_path(@filter), method: :put do |f|
-  = render 'fields', f: f
+  = render 'filter_fields', f: f
 
   .actions
     = f.button :button, t('generic.save_changes'), type: :submit
diff --git a/app/views/filters/index.html.haml b/app/views/filters/index.html.haml
index b4d5333aa..0227526a4 100644
--- a/app/views/filters/index.html.haml
+++ b/app/views/filters/index.html.haml
@@ -7,18 +7,5 @@
 - if @filters.empty?
   %div.muted-hint.center-text= t 'filters.index.empty'
 - else
-  .table-wrapper
-    %table.table
-      %thead
-        %tr
-          %th= t('simple_form.labels.defaults.phrase')
-          %th= t('simple_form.labels.defaults.context')
-          %th
-      %tbody
-        - @filters.each do |filter|
-          %tr
-            %td= filter.phrase
-            %td= filter.context.map { |context| I18n.t("filters.contexts.#{context}") }.join(', ')
-            %td
-              = table_link_to 'pencil', t('filters.edit.title'), edit_filter_path(filter)
-              = table_link_to 'times', t('filters.index.delete'), filter_path(filter), method: :delete
+  .applications-list
+    = render partial: 'filter', collection: @filters
diff --git a/app/views/filters/new.html.haml b/app/views/filters/new.html.haml
index 05bec343f..5f400e604 100644
--- a/app/views/filters/new.html.haml
+++ b/app/views/filters/new.html.haml
@@ -2,7 +2,7 @@
   = t('filters.new.title')
 
 = simple_form_for @filter, url: filters_path do |f|
-  = render 'fields', f: f
+  = render 'filter_fields', f: f
 
   .actions
-    = f.button :button, t('filters.new.title'), type: :submit
+    = f.button :button, t('filters.new.save'), type: :submit
diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml
index ee444c070..40c38cecb 100755
--- a/app/views/layouts/application.html.haml
+++ b/app/views/layouts/application.html.haml
@@ -43,8 +43,7 @@
     = render partial: 'layouts/theme', object: @core
     = render partial: 'layouts/theme', object: @theme
 
-    - if Setting.custom_css.present?
-      = stylesheet_link_tag custom_css_path, host: request.host, media: 'all'
+    = stylesheet_link_tag custom_css_path, host: request.host, media: 'all'
 
   %body{ class: body_classes }
     = content_for?(:content) ? yield(:content) : yield
diff --git a/app/views/settings/featured_tags/index.html.haml b/app/views/settings/featured_tags/index.html.haml
index 65de7f8f3..5d87e2862 100644
--- a/app/views/settings/featured_tags/index.html.haml
+++ b/app/views/settings/featured_tags/index.html.haml
@@ -9,7 +9,7 @@
   = render 'shared/error_messages', object: @featured_tag
 
   .fields-group
-    = f.input :name, wrapper: :with_block_label, hint: safe_join([t('simple_form.hints.featured_tag.name'), safe_join(@recently_used_tags.map { |tag| link_to("##{tag.name}", settings_featured_tags_path(featured_tag: { name: tag.name }), method: :post) }, ', ')], ' ')
+    = f.input :name, wrapper: :with_block_label, hint: safe_join([t('simple_form.hints.featured_tag.name'), safe_join(@recently_used_tags.map { |tag| link_to("##{tag.display_name}", settings_featured_tags_path(featured_tag: { name: tag.name }), method: :post) }, ', ')], ' ')
 
   .actions
     = f.button :button, t('featured_tags.add_new'), type: :submit
diff --git a/app/views/settings/preferences/notifications/show.html.haml b/app/views/settings/preferences/notifications/show.html.haml
index 38e8b171e..943e21b50 100644
--- a/app/views/settings/preferences/notifications/show.html.haml
+++ b/app/views/settings/preferences/notifications/show.html.haml
@@ -18,14 +18,12 @@
       = ff.input :reblog, as: :boolean, wrapper: :with_label
       = ff.input :favourite, as: :boolean, wrapper: :with_label
       = ff.input :mention, as: :boolean, wrapper: :with_label
-
-      - if current_user.staff?
-        = ff.input :report, as: :boolean, wrapper: :with_label
-        = ff.input :appeal, as: :boolean, wrapper: :with_label
-        = ff.input :pending_account, as: :boolean, wrapper: :with_label
-        = ff.input :trending_tag, as: :boolean, wrapper: :with_label
-        = ff.input :trending_link, as: :boolean, wrapper: :with_label
-        = ff.input :trending_status, as: :boolean, wrapper: :with_label
+      = ff.input :report, as: :boolean, wrapper: :with_label if current_user.can?(:manage_reports)
+      = ff.input :appeal, as: :boolean, wrapper: :with_label if current_user.can?(:manage_appeals)
+      = ff.input :pending_account, as: :boolean, wrapper: :with_label if current_user.can?(:manage_users)
+      = ff.input :trending_tag, as: :boolean, wrapper: :with_label if current_user.can?(:manage_taxonomies)
+      = ff.input :trending_link, as: :boolean, wrapper: :with_label if current_user.can?(:manage_taxonomies)
+      = ff.input :trending_status, as: :boolean, wrapper: :with_label if current_user.can?(:manage_taxonomies)
 
   .fields-group
     = f.input :setting_always_send_emails, as: :boolean, wrapper: :with_label
diff --git a/app/views/tags/_og.html.haml b/app/views/tags/_og.html.haml
index a7c289bcb..37f644cf2 100644
--- a/app/views/tags/_og.html.haml
+++ b/app/views/tags/_og.html.haml
@@ -1,6 +1,6 @@
 = opengraph 'og:site_name', t('about.hosted_on', domain: site_hostname)
 = opengraph 'og:url', tag_url(@tag)
 = opengraph 'og:type', 'website'
-= opengraph 'og:title', "##{@tag.name}"
-= opengraph 'og:description', strip_tags(t('about.about_hashtag_html', hashtag: @tag.name))
+= opengraph 'og:title', "##{@tag.display_name}"
+= opengraph 'og:description', strip_tags(t('about.about_hashtag_html', hashtag: @tag.display_name))
 = opengraph 'twitter:card', 'summary'
diff --git a/app/views/tags/show.html.haml b/app/views/tags/show.html.haml
index 0e6d4c43d..608989a2b 100644
--- a/app/views/tags/show.html.haml
+++ b/app/views/tags/show.html.haml
@@ -1,5 +1,5 @@
 - content_for :page_title do
-  = "##{@tag.name}"
+  = "##{@tag.display_name}"
 
 - content_for :header_tags do
   %meta{ name: 'robots', content: 'noindex' }/
@@ -8,8 +8,8 @@
   = render 'og'
 
 .page-header
-  %h1= "##{@tag.name}"
-  %p= t('about.about_hashtag_html', hashtag: @tag.name)
+  %h1= "##{@tag.display_name}"
+  %p= t('about.about_hashtag_html', hashtag: @tag.display_name)
 
 #mastodon-timeline{ data: { props: Oj.dump(default_props.merge(hashtag: @tag.name, local: @local)) }}
 .notranslate#modal-container
diff --git a/app/views/tags/show.rss.ruby b/app/views/tags/show.rss.ruby
index 9ce71be74..8e0c2327b 100644
--- a/app/views/tags/show.rss.ruby
+++ b/app/views/tags/show.rss.ruby
@@ -1,6 +1,6 @@
 RSS::Builder.build do |doc|
-  doc.title("##{@tag.name}")
-  doc.description(I18n.t('rss.descriptions.tag', hashtag: @tag.name))
+  doc.title("##{@tag.display_name}")
+  doc.description(I18n.t('rss.descriptions.tag', hashtag: @tag.display_name))
   doc.link(tag_url(@tag))
   doc.last_build_date(@statuses.first.created_at) if @statuses.any?
   doc.generator("Mastodon v#{Mastodon::Version.to_s}")
@@ -26,7 +26,7 @@ RSS::Builder.build do |doc|
       end
 
       status.tags.each do |tag|
-        item.category(tag.name)
+        item.category(tag.display_name)
       end
     end
   end