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
pull/2690/head
Matt Jankowski 2017-05-01 11:42:13 -04:00 committed by Eugen Rochko
parent 3988f2dade
commit f025cc6782
10 changed files with 66 additions and 4 deletions

View File

@ -326,3 +326,10 @@ code {
flex: 0 0 auto; flex: 0 0 auto;
} }
} }
.user_allowed_languages {
li {
float: left;
width: 50%;
}
}

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)