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:
		
				
					committed by
					
						 Eugen Rochko
						Eugen Rochko
					
				
			
			
				
	
			
			
			
						parent
						
							3988f2dade
						
					
				
				
					commit
					f025cc6782
				
			| @@ -326,3 +326,10 @@ code { | ||||
|     flex: 0 0 auto; | ||||
|   } | ||||
| } | ||||
|  | ||||
| .user_allowed_languages { | ||||
|   li { | ||||
|     float: left; | ||||
|     width: 50%; | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -25,7 +25,8 @@ class Settings::PreferencesController < ApplicationController | ||||
|  | ||||
|   def user_params | ||||
|     params.require(:user).permit( | ||||
|       :locale | ||||
|       :locale, | ||||
|       allowed_languages: [] | ||||
|     ) | ||||
|   end | ||||
|  | ||||
|   | ||||
| @@ -82,6 +82,8 @@ class Account < ApplicationRecord | ||||
|            prefix: true, | ||||
|            allow_nil: true | ||||
|  | ||||
|   delegate :allowed_languages, to: :user, prefix: false, allow_nil: true | ||||
|  | ||||
|   def follow!(other_account) | ||||
|     active_relationships.where(target_account: other_account).first_or_create!(target_account: other_account) | ||||
|   end | ||||
|   | ||||
| @@ -119,6 +119,10 @@ class Status < ApplicationRecord | ||||
|   end | ||||
|  | ||||
|   class << self | ||||
|     def in_allowed_languages(account) | ||||
|       where(language: account.allowed_languages) | ||||
|     end | ||||
|  | ||||
|     def as_home_timeline(account) | ||||
|       where(account: [account] + account.following) | ||||
|     end | ||||
| @@ -198,6 +202,7 @@ class Status < ApplicationRecord | ||||
|  | ||||
|     def filter_timeline_for_account(query, 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)) | ||||
|     end | ||||
|  | ||||
|   | ||||
| @@ -7,6 +7,16 @@ | ||||
|   .fields-group | ||||
|     = 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' | ||||
|  | ||||
|   .fields-group | ||||
|   | ||||
| @@ -12,6 +12,8 @@ en: | ||||
|         data: CSV file exported from another Mastodon instance | ||||
|       sessions: | ||||
|         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: | ||||
|       defaults: | ||||
|         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.datetime "last_emailed_at" | ||||
|     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 ["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 ["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 | ||||
|   | ||||
| @@ -3,7 +3,7 @@ require 'rails_helper' | ||||
| describe Settings::PreferencesController do | ||||
|   render_views | ||||
|  | ||||
|   let(:user) { Fabricate(:user) } | ||||
|   let(:user) { Fabricate(:user, allowed_languages: []) } | ||||
|  | ||||
|   before do | ||||
|     sign_in user, scope: :user | ||||
| @@ -18,10 +18,12 @@ describe Settings::PreferencesController do | ||||
|  | ||||
|   describe 'PUT #update' 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(user.reload.locale).to eq 'en' | ||||
|       user.reload | ||||
|       expect(user.locale).to eq 'en' | ||||
|       expect(user.allowed_languages).to eq ['es', 'fr'] | ||||
|     end | ||||
|  | ||||
|     it 'updates user settings' do | ||||
|   | ||||
| @@ -251,6 +251,31 @@ RSpec.describe Status, type: :model do | ||||
|         expect(results).not_to include(muted_status) | ||||
|       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 | ||||
|         it 'includes statuses from other accounts that are silenced' do | ||||
|           @account.update(silenced: true) | ||||
|   | ||||
		Reference in New Issue
	
	Block a user