Filter on allowed user language preferences (#2361)
* Naive approached to timeline filtering * Convert allowed_languages into a db column * Allow users to choose languages to see statuses in * Style list items as two columns * Add a hint to explain language filtering preference
This commit is contained in:
parent
3988f2dade
commit
f025cc6782
|
@ -326,3 +326,10 @@ code {
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.user_allowed_languages {
|
||||||
|
li {
|
||||||
|
float: left;
|
||||||
|
width: 50%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -25,7 +25,8 @@ class Settings::PreferencesController < ApplicationController
|
||||||
|
|
||||||
def user_params
|
def user_params
|
||||||
params.require(:user).permit(
|
params.require(:user).permit(
|
||||||
:locale
|
:locale,
|
||||||
|
allowed_languages: []
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -82,6 +82,8 @@ class Account < ApplicationRecord
|
||||||
prefix: true,
|
prefix: true,
|
||||||
allow_nil: true
|
allow_nil: true
|
||||||
|
|
||||||
|
delegate :allowed_languages, to: :user, prefix: false, allow_nil: true
|
||||||
|
|
||||||
def follow!(other_account)
|
def follow!(other_account)
|
||||||
active_relationships.where(target_account: other_account).first_or_create!(target_account: other_account)
|
active_relationships.where(target_account: other_account).first_or_create!(target_account: other_account)
|
||||||
end
|
end
|
||||||
|
|
|
@ -119,6 +119,10 @@ class Status < ApplicationRecord
|
||||||
end
|
end
|
||||||
|
|
||||||
class << self
|
class << self
|
||||||
|
def in_allowed_languages(account)
|
||||||
|
where(language: account.allowed_languages)
|
||||||
|
end
|
||||||
|
|
||||||
def as_home_timeline(account)
|
def as_home_timeline(account)
|
||||||
where(account: [account] + account.following)
|
where(account: [account] + account.following)
|
||||||
end
|
end
|
||||||
|
@ -198,6 +202,7 @@ class Status < ApplicationRecord
|
||||||
|
|
||||||
def filter_timeline_for_account(query, account)
|
def filter_timeline_for_account(query, account)
|
||||||
query = query.not_excluded_by_account(account)
|
query = query.not_excluded_by_account(account)
|
||||||
|
query = query.in_allowed_languages(account) if account.allowed_languages.present?
|
||||||
query.merge(account_silencing_filter(account))
|
query.merge(account_silencing_filter(account))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -7,6 +7,16 @@
|
||||||
.fields-group
|
.fields-group
|
||||||
= f.input :locale, collection: I18n.available_locales, wrapper: :with_label, include_blank: false, label_method: lambda { |locale| human_locale(locale) }
|
= f.input :locale, collection: I18n.available_locales, wrapper: :with_label, include_blank: false, label_method: lambda { |locale| human_locale(locale) }
|
||||||
|
|
||||||
|
= f.input :allowed_languages,
|
||||||
|
collection: I18n.available_locales,
|
||||||
|
wrapper: :with_label,
|
||||||
|
include_blank: false,
|
||||||
|
label_method: lambda { |locale| human_locale(locale) },
|
||||||
|
required: false,
|
||||||
|
as: :check_boxes,
|
||||||
|
collection_wrapper_tag: 'ul',
|
||||||
|
item_wrapper_tag: 'li'
|
||||||
|
|
||||||
= f.input :setting_default_privacy, collection: Status.visibilities.keys - ['direct'], wrapper: :with_label, include_blank: false, label_method: lambda { |visibility| safe_join([I18n.t("statuses.visibilities.#{visibility}"), content_tag(:span, I18n.t("statuses.visibilities.#{visibility}_long"), class: 'hint')]) }, required: false, as: :radio_buttons, collection_wrapper_tag: 'ul', item_wrapper_tag: 'li'
|
= f.input :setting_default_privacy, collection: Status.visibilities.keys - ['direct'], wrapper: :with_label, include_blank: false, label_method: lambda { |visibility| safe_join([I18n.t("statuses.visibilities.#{visibility}"), content_tag(:span, I18n.t("statuses.visibilities.#{visibility}_long"), class: 'hint')]) }, required: false, as: :radio_buttons, collection_wrapper_tag: 'ul', item_wrapper_tag: 'li'
|
||||||
|
|
||||||
.fields-group
|
.fields-group
|
||||||
|
|
|
@ -12,6 +12,8 @@ en:
|
||||||
data: CSV file exported from another Mastodon instance
|
data: CSV file exported from another Mastodon instance
|
||||||
sessions:
|
sessions:
|
||||||
otp: Enter the Two-factor code from your phone or use one of your recovery codes.
|
otp: Enter the Two-factor code from your phone or use one of your recovery codes.
|
||||||
|
user:
|
||||||
|
allowed_languages: These languages will be allowed in your public timelines. Languages that are not selected will be filtered out.
|
||||||
labels:
|
labels:
|
||||||
defaults:
|
defaults:
|
||||||
avatar: Avatar
|
avatar: Avatar
|
||||||
|
|
|
@ -0,0 +1,6 @@
|
||||||
|
class AddAllowedLanguagesToUser < ActiveRecord::Migration[5.0]
|
||||||
|
def change
|
||||||
|
add_column :users, :allowed_languages, :string, array: true, default: [], null: false
|
||||||
|
add_index :users, :allowed_languages, using: :gin
|
||||||
|
end
|
||||||
|
end
|
|
@ -326,7 +326,9 @@ ActiveRecord::Schema.define(version: 20170425202925) do
|
||||||
t.boolean "otp_required_for_login"
|
t.boolean "otp_required_for_login"
|
||||||
t.datetime "last_emailed_at"
|
t.datetime "last_emailed_at"
|
||||||
t.string "otp_backup_codes", array: true
|
t.string "otp_backup_codes", array: true
|
||||||
|
t.string "allowed_languages", default: [], null: false, array: true
|
||||||
t.index ["account_id"], name: "index_users_on_account_id", using: :btree
|
t.index ["account_id"], name: "index_users_on_account_id", using: :btree
|
||||||
|
t.index ["allowed_languages"], name: "index_users_on_allowed_languages", using: :gin
|
||||||
t.index ["confirmation_token"], name: "index_users_on_confirmation_token", unique: true, using: :btree
|
t.index ["confirmation_token"], name: "index_users_on_confirmation_token", unique: true, using: :btree
|
||||||
t.index ["email"], name: "index_users_on_email", unique: true, using: :btree
|
t.index ["email"], name: "index_users_on_email", unique: true, using: :btree
|
||||||
t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true, using: :btree
|
t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true, using: :btree
|
||||||
|
|
|
@ -3,7 +3,7 @@ require 'rails_helper'
|
||||||
describe Settings::PreferencesController do
|
describe Settings::PreferencesController do
|
||||||
render_views
|
render_views
|
||||||
|
|
||||||
let(:user) { Fabricate(:user) }
|
let(:user) { Fabricate(:user, allowed_languages: []) }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
sign_in user, scope: :user
|
sign_in user, scope: :user
|
||||||
|
@ -18,10 +18,12 @@ describe Settings::PreferencesController do
|
||||||
|
|
||||||
describe 'PUT #update' do
|
describe 'PUT #update' do
|
||||||
it 'updates the user record' do
|
it 'updates the user record' do
|
||||||
put :update, params: { user: { locale: 'en' } }
|
put :update, params: { user: { locale: 'en', allowed_languages: ['es', 'fr'] } }
|
||||||
|
|
||||||
expect(response).to redirect_to(settings_preferences_path)
|
expect(response).to redirect_to(settings_preferences_path)
|
||||||
expect(user.reload.locale).to eq 'en'
|
user.reload
|
||||||
|
expect(user.locale).to eq 'en'
|
||||||
|
expect(user.allowed_languages).to eq ['es', 'fr']
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'updates user settings' do
|
it 'updates user settings' do
|
||||||
|
|
|
@ -251,6 +251,31 @@ RSpec.describe Status, type: :model do
|
||||||
expect(results).not_to include(muted_status)
|
expect(results).not_to include(muted_status)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'with language preferences' do
|
||||||
|
it 'excludes statuses in languages not allowed by the account user' do
|
||||||
|
user = Fabricate(:user, allowed_languages: [:en, :es])
|
||||||
|
@account.update(user: user)
|
||||||
|
en_status = Fabricate(:status, language: 'en')
|
||||||
|
es_status = Fabricate(:status, language: 'es')
|
||||||
|
fr_status = Fabricate(:status, language: 'fr')
|
||||||
|
|
||||||
|
results = Status.as_public_timeline(@account)
|
||||||
|
expect(results).to include(en_status)
|
||||||
|
expect(results).to include(es_status)
|
||||||
|
expect(results).not_to include(fr_status)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'includes all languages when account does not have a user' do
|
||||||
|
expect(@account.user).to be_nil
|
||||||
|
en_status = Fabricate(:status, language: 'en')
|
||||||
|
es_status = Fabricate(:status, language: 'es')
|
||||||
|
|
||||||
|
results = Status.as_public_timeline(@account)
|
||||||
|
expect(results).to include(en_status)
|
||||||
|
expect(results).to include(es_status)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
context 'where that account is silenced' do
|
context 'where that account is silenced' do
|
||||||
it 'includes statuses from other accounts that are silenced' do
|
it 'includes statuses from other accounts that are silenced' do
|
||||||
@account.update(silenced: true)
|
@account.update(silenced: true)
|
||||||
|
|
Reference in a new issue