Add exclusive lists (#22048)
Co-authored-by: Liam Cooke <liam@liamcooke.com> Co-authored-by: John Holdun <john@johnholdun.com> Co-authored-by: Effy Elden <effy@effy.space> Co-authored-by: Lina Reyne <git@lina.pizza> Co-authored-by: Lina <20880695+necropolina@users.noreply.github.com> Co-authored-by: Claire <claire.github-309c@sitedethib.com>pull/62/head
parent
0daf78f903
commit
bacb674921
|
@ -42,6 +42,6 @@ class Api::V1::ListsController < Api::BaseController
|
||||||
end
|
end
|
||||||
|
|
||||||
def list_params
|
def list_params
|
||||||
params.permit(:title, :replies_policy)
|
params.permit(:title, :replies_policy, :exclusive)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -151,10 +151,10 @@ export const createListFail = error => ({
|
||||||
error,
|
error,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const updateList = (id, title, shouldReset, replies_policy) => (dispatch, getState) => {
|
export const updateList = (id, title, shouldReset, isExclusive, replies_policy) => (dispatch, getState) => {
|
||||||
dispatch(updateListRequest(id));
|
dispatch(updateListRequest(id));
|
||||||
|
|
||||||
api(getState).put(`/api/v1/lists/${id}`, { title, replies_policy }).then(({ data }) => {
|
api(getState).put(`/api/v1/lists/${id}`, { title, replies_policy, exclusive: typeof isExclusive === 'undefined' ? undefined : !!isExclusive }).then(({ data }) => {
|
||||||
dispatch(updateListSuccess(data));
|
dispatch(updateListSuccess(data));
|
||||||
|
|
||||||
if (shouldReset) {
|
if (shouldReset) {
|
||||||
|
|
|
@ -8,6 +8,8 @@ import { Helmet } from 'react-helmet';
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
|
|
||||||
|
import Toggle from 'react-toggle';
|
||||||
|
|
||||||
import { addColumn, removeColumn, moveColumn } from 'mastodon/actions/columns';
|
import { addColumn, removeColumn, moveColumn } from 'mastodon/actions/columns';
|
||||||
import { fetchList, deleteList, updateList } from 'mastodon/actions/lists';
|
import { fetchList, deleteList, updateList } from 'mastodon/actions/lists';
|
||||||
import { openModal } from 'mastodon/actions/modal';
|
import { openModal } from 'mastodon/actions/modal';
|
||||||
|
@ -145,7 +147,13 @@ class ListTimeline extends PureComponent {
|
||||||
handleRepliesPolicyChange = ({ target }) => {
|
handleRepliesPolicyChange = ({ target }) => {
|
||||||
const { dispatch } = this.props;
|
const { dispatch } = this.props;
|
||||||
const { id } = this.props.params;
|
const { id } = this.props.params;
|
||||||
dispatch(updateList(id, undefined, false, target.value));
|
dispatch(updateList(id, undefined, false, undefined, target.value));
|
||||||
|
};
|
||||||
|
|
||||||
|
onExclusiveToggle = ({ target }) => {
|
||||||
|
const { dispatch } = this.props;
|
||||||
|
const { id } = this.props.params;
|
||||||
|
dispatch(updateList(id, undefined, false, target.checked, undefined));
|
||||||
};
|
};
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
|
@ -154,6 +162,7 @@ class ListTimeline extends PureComponent {
|
||||||
const pinned = !!columnId;
|
const pinned = !!columnId;
|
||||||
const title = list ? list.get('title') : id;
|
const title = list ? list.get('title') : id;
|
||||||
const replies_policy = list ? list.get('replies_policy') : undefined;
|
const replies_policy = list ? list.get('replies_policy') : undefined;
|
||||||
|
const isExclusive = list ? list.get('exclusive') : undefined;
|
||||||
|
|
||||||
if (typeof list === 'undefined') {
|
if (typeof list === 'undefined') {
|
||||||
return (
|
return (
|
||||||
|
@ -191,6 +200,13 @@ class ListTimeline extends PureComponent {
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className='setting-toggle'>
|
||||||
|
<Toggle id={`list-${id}-exclusive`} defaultChecked={isExclusive} onChange={this.onExclusiveToggle} />
|
||||||
|
<label htmlFor={`list-${id}-exclusive`} className='setting-toggle__label'>
|
||||||
|
<FormattedMessage id='lists.exclusive' defaultMessage='Hide these posts from home' />
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
{ replies_policy !== undefined && (
|
{ replies_policy !== undefined && (
|
||||||
<div role='group' aria-labelledby={`list-${id}-replies-policy`}>
|
<div role='group' aria-labelledby={`list-${id}-replies-policy`}>
|
||||||
<span id={`list-${id}-replies-policy`} className='column-settings__section'>
|
<span id={`list-${id}-replies-policy`} className='column-settings__section'>
|
||||||
|
|
|
@ -356,6 +356,7 @@
|
||||||
"lists.delete": "Delete list",
|
"lists.delete": "Delete list",
|
||||||
"lists.edit": "Edit list",
|
"lists.edit": "Edit list",
|
||||||
"lists.edit.submit": "Change title",
|
"lists.edit.submit": "Change title",
|
||||||
|
"lists.exclusive": "Hide these posts from home",
|
||||||
"lists.new.create": "Add list",
|
"lists.new.create": "Add list",
|
||||||
"lists.new.title_placeholder": "New list title",
|
"lists.new.title_placeholder": "New list title",
|
||||||
"lists.replies_policy.followed": "Any followed user",
|
"lists.replies_policy.followed": "Any followed user",
|
||||||
|
|
|
@ -25,6 +25,7 @@ const initialState = ImmutableMap({
|
||||||
isSubmitting: false,
|
isSubmitting: false,
|
||||||
isChanged: false,
|
isChanged: false,
|
||||||
title: '',
|
title: '',
|
||||||
|
isExclusive: false,
|
||||||
|
|
||||||
accounts: ImmutableMap({
|
accounts: ImmutableMap({
|
||||||
items: ImmutableList(),
|
items: ImmutableList(),
|
||||||
|
@ -46,6 +47,7 @@ export default function listEditorReducer(state = initialState, action) {
|
||||||
return state.withMutations(map => {
|
return state.withMutations(map => {
|
||||||
map.set('listId', action.list.get('id'));
|
map.set('listId', action.list.get('id'));
|
||||||
map.set('title', action.list.get('title'));
|
map.set('title', action.list.get('title'));
|
||||||
|
map.set('isExclusive', action.list.get('is_exclusive'));
|
||||||
map.set('isSubmitting', false);
|
map.set('isSubmitting', false);
|
||||||
});
|
});
|
||||||
case LIST_EDITOR_TITLE_CHANGE:
|
case LIST_EDITOR_TITLE_CHANGE:
|
||||||
|
|
|
@ -40,9 +40,9 @@ class FeedManager
|
||||||
def filter?(timeline_type, status, receiver)
|
def filter?(timeline_type, status, receiver)
|
||||||
case timeline_type
|
case timeline_type
|
||||||
when :home
|
when :home
|
||||||
filter_from_home?(status, receiver.id, build_crutches(receiver.id, [status]))
|
filter_from_home?(status, receiver.id, build_crutches(receiver.id, [status]), :home)
|
||||||
when :list
|
when :list
|
||||||
filter_from_list?(status, receiver) || filter_from_home?(status, receiver.account_id, build_crutches(receiver.account_id, [status]))
|
filter_from_list?(status, receiver) || filter_from_home?(status, receiver.account_id, build_crutches(receiver.account_id, [status]), :list)
|
||||||
when :mentions
|
when :mentions
|
||||||
filter_from_mentions?(status, receiver.id)
|
filter_from_mentions?(status, receiver.id)
|
||||||
when :tags
|
when :tags
|
||||||
|
@ -351,9 +351,10 @@ class FeedManager
|
||||||
# @param [Integer] receiver_id
|
# @param [Integer] receiver_id
|
||||||
# @param [Hash] crutches
|
# @param [Hash] crutches
|
||||||
# @return [Boolean]
|
# @return [Boolean]
|
||||||
def filter_from_home?(status, receiver_id, crutches)
|
def filter_from_home?(status, receiver_id, crutches, timeline_type = :home)
|
||||||
return false if receiver_id == status.account_id
|
return false if receiver_id == status.account_id
|
||||||
return true if status.reply? && (status.in_reply_to_id.nil? || status.in_reply_to_account_id.nil?)
|
return true if status.reply? && (status.in_reply_to_id.nil? || status.in_reply_to_account_id.nil?)
|
||||||
|
return true if timeline_type != :list && crutches[:exclusive_list_users][status.account_id].present?
|
||||||
return true if crutches[:languages][status.account_id].present? && status.language.present? && !crutches[:languages][status.account_id].include?(status.language)
|
return true if crutches[:languages][status.account_id].present? && status.language.present? && !crutches[:languages][status.account_id].include?(status.language)
|
||||||
|
|
||||||
check_for_blocks = crutches[:active_mentions][status.id] || []
|
check_for_blocks = crutches[:active_mentions][status.id] || []
|
||||||
|
@ -543,6 +544,8 @@ class FeedManager
|
||||||
arr
|
arr
|
||||||
end
|
end
|
||||||
|
|
||||||
|
lists = List.where(account_id: receiver_id, exclusive: true)
|
||||||
|
|
||||||
crutches[:following] = Follow.where(account_id: receiver_id, target_account_id: statuses.filter_map(&:in_reply_to_account_id)).pluck(:target_account_id).index_with(true)
|
crutches[:following] = Follow.where(account_id: receiver_id, target_account_id: statuses.filter_map(&:in_reply_to_account_id)).pluck(:target_account_id).index_with(true)
|
||||||
crutches[:languages] = Follow.where(account_id: receiver_id, target_account_id: statuses.map(&:account_id)).pluck(:target_account_id, :languages).to_h
|
crutches[:languages] = Follow.where(account_id: receiver_id, target_account_id: statuses.map(&:account_id)).pluck(:target_account_id, :languages).to_h
|
||||||
crutches[:hiding_reblogs] = Follow.where(account_id: receiver_id, target_account_id: statuses.filter_map { |s| s.account_id if s.reblog? }, show_reblogs: false).pluck(:target_account_id).index_with(true)
|
crutches[:hiding_reblogs] = Follow.where(account_id: receiver_id, target_account_id: statuses.filter_map { |s| s.account_id if s.reblog? }, show_reblogs: false).pluck(:target_account_id).index_with(true)
|
||||||
|
@ -550,6 +553,7 @@ class FeedManager
|
||||||
crutches[:muting] = Mute.where(account_id: receiver_id, target_account_id: check_for_blocks).pluck(:target_account_id).index_with(true)
|
crutches[:muting] = Mute.where(account_id: receiver_id, target_account_id: check_for_blocks).pluck(:target_account_id).index_with(true)
|
||||||
crutches[:domain_blocking] = AccountDomainBlock.where(account_id: receiver_id, domain: statuses.flat_map { |s| [s.account.domain, s.reblog&.account&.domain] }.compact).pluck(:domain).index_with(true)
|
crutches[:domain_blocking] = AccountDomainBlock.where(account_id: receiver_id, domain: statuses.flat_map { |s| [s.account.domain, s.reblog&.account&.domain] }.compact).pluck(:domain).index_with(true)
|
||||||
crutches[:blocked_by] = Block.where(target_account_id: receiver_id, account_id: statuses.map { |s| [s.account_id, s.reblog&.account_id] }.flatten.compact).pluck(:account_id).index_with(true)
|
crutches[:blocked_by] = Block.where(target_account_id: receiver_id, account_id: statuses.map { |s| [s.account_id, s.reblog&.account_id] }.flatten.compact).pluck(:account_id).index_with(true)
|
||||||
|
crutches[:exclusive_list_users] = ListAccount.where(list: lists, account_id: statuses.map(&:account_id)).pluck(:account_id).index_with(true)
|
||||||
|
|
||||||
crutches
|
crutches
|
||||||
end
|
end
|
||||||
|
|
|
@ -10,6 +10,7 @@
|
||||||
# created_at :datetime not null
|
# created_at :datetime not null
|
||||||
# updated_at :datetime not null
|
# updated_at :datetime not null
|
||||||
# replies_policy :integer default("list"), not null
|
# replies_policy :integer default("list"), not null
|
||||||
|
# exclusive :boolean default(FALSE)
|
||||||
#
|
#
|
||||||
|
|
||||||
class List < ApplicationRecord
|
class List < ApplicationRecord
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class REST::ListSerializer < ActiveModel::Serializer
|
class REST::ListSerializer < ActiveModel::Serializer
|
||||||
attributes :id, :title, :replies_policy
|
attributes :id, :title, :replies_policy, :exclusive
|
||||||
|
|
||||||
def id
|
def id
|
||||||
object.id.to_s
|
object.id.to_s
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class AddExclusiveToLists < ActiveRecord::Migration[6.1]
|
||||||
|
def change
|
||||||
|
add_column :lists, :exclusive, :boolean, null: false, default: false
|
||||||
|
end
|
||||||
|
end
|
|
@ -10,7 +10,7 @@
|
||||||
#
|
#
|
||||||
# It's strongly recommended that you check this file into your version control system.
|
# It's strongly recommended that you check this file into your version control system.
|
||||||
|
|
||||||
ActiveRecord::Schema.define(version: 2023_05_31_154811) do
|
ActiveRecord::Schema.define(version: 2023_06_05_085710) do
|
||||||
|
|
||||||
# These are extensions that must be enabled in order to support this database
|
# These are extensions that must be enabled in order to support this database
|
||||||
enable_extension "plpgsql"
|
enable_extension "plpgsql"
|
||||||
|
@ -567,6 +567,7 @@ ActiveRecord::Schema.define(version: 2023_05_31_154811) do
|
||||||
t.datetime "created_at", null: false
|
t.datetime "created_at", null: false
|
||||||
t.datetime "updated_at", null: false
|
t.datetime "updated_at", null: false
|
||||||
t.integer "replies_policy", default: 0, null: false
|
t.integer "replies_policy", default: 0, null: false
|
||||||
|
t.boolean "exclusive", default: false
|
||||||
t.index ["account_id"], name: "index_lists_on_account_id"
|
t.index ["account_id"], name: "index_lists_on_account_id"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -26,6 +26,7 @@ RSpec.describe FeedManager do
|
||||||
let(:alice) { Fabricate(:account, username: 'alice') }
|
let(:alice) { Fabricate(:account, username: 'alice') }
|
||||||
let(:bob) { Fabricate(:account, username: 'bob', domain: 'example.com') }
|
let(:bob) { Fabricate(:account, username: 'bob', domain: 'example.com') }
|
||||||
let(:jeff) { Fabricate(:account, username: 'jeff') }
|
let(:jeff) { Fabricate(:account, username: 'jeff') }
|
||||||
|
let(:list) { Fabricate(:list, account: alice) }
|
||||||
|
|
||||||
context 'with home feed' do
|
context 'with home feed' do
|
||||||
it 'returns false for followee\'s status' do
|
it 'returns false for followee\'s status' do
|
||||||
|
@ -153,6 +154,42 @@ RSpec.describe FeedManager do
|
||||||
status = Fabricate(:status, text: 'Hallo Welt', account: bob, language: 'de')
|
status = Fabricate(:status, text: 'Hallo Welt', account: bob, language: 'de')
|
||||||
expect(FeedManager.instance.filter?(:home, status, alice)).to be false
|
expect(FeedManager.instance.filter?(:home, status, alice)).to be false
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'returns true for post from followee on exclusive list' do
|
||||||
|
list.exclusive = true
|
||||||
|
alice.follow!(bob)
|
||||||
|
list.accounts << bob
|
||||||
|
allow(List).to receive(:where).and_return(list)
|
||||||
|
status = Fabricate(:status, text: 'I post a lot', account: bob)
|
||||||
|
expect(FeedManager.instance.filter?(:home, status, alice)).to be true
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns true for reblog from followee on exclusive list' do
|
||||||
|
list.exclusive = true
|
||||||
|
alice.follow!(jeff)
|
||||||
|
list.accounts << jeff
|
||||||
|
allow(List).to receive(:where).and_return(list)
|
||||||
|
status = Fabricate(:status, text: 'I post a lot', account: bob)
|
||||||
|
reblog = Fabricate(:status, reblog: status, account: jeff)
|
||||||
|
expect(FeedManager.instance.filter?(:home, reblog, alice)).to be true
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns false for post from followee on non-exclusive list' do
|
||||||
|
list.exclusive = false
|
||||||
|
alice.follow!(bob)
|
||||||
|
list.accounts << bob
|
||||||
|
status = Fabricate(:status, text: 'I post a lot', account: bob)
|
||||||
|
expect(FeedManager.instance.filter?(:home, status, alice)).to be false
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns false for reblog from followee on non-exclusive list' do
|
||||||
|
list.exclusive = false
|
||||||
|
alice.follow!(jeff)
|
||||||
|
list.accounts << jeff
|
||||||
|
status = Fabricate(:status, text: 'I post a lot', account: bob)
|
||||||
|
reblog = Fabricate(:status, reblog: status, account: jeff)
|
||||||
|
expect(FeedManager.instance.filter?(:home, reblog, alice)).to be false
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'with mentions feed' do
|
context 'with mentions feed' do
|
||||||
|
|
Loading…
Reference in New Issue