commit
86f4f8e158
@ -0,0 +1,28 @@
|
|||||||
|
import api from '../api';
|
||||||
|
|
||||||
|
export const LIST_FETCH_REQUEST = 'LIST_FETCH_REQUEST';
|
||||||
|
export const LIST_FETCH_SUCCESS = 'LIST_FETCH_SUCCESS';
|
||||||
|
export const LIST_FETCH_FAIL = 'LIST_FETCH_FAIL';
|
||||||
|
|
||||||
|
export const fetchList = id => (dispatch, getState) => {
|
||||||
|
dispatch(fetchListRequest(id));
|
||||||
|
|
||||||
|
api(getState).get(`/api/v1/lists/${id}`)
|
||||||
|
.then(({ data }) => dispatch(fetchListSuccess(data)))
|
||||||
|
.catch(err => dispatch(fetchListFail(err)));
|
||||||
|
};
|
||||||
|
|
||||||
|
export const fetchListRequest = id => ({
|
||||||
|
type: LIST_FETCH_REQUEST,
|
||||||
|
id,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const fetchListSuccess = list => ({
|
||||||
|
type: LIST_FETCH_SUCCESS,
|
||||||
|
list,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const fetchListFail = error => ({
|
||||||
|
type: LIST_FETCH_FAIL,
|
||||||
|
error,
|
||||||
|
});
|
@ -0,0 +1,106 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
|
import StatusListContainer from '../ui/containers/status_list_container';
|
||||||
|
import Column from '../../components/column';
|
||||||
|
import ColumnHeader from '../../components/column_header';
|
||||||
|
import { addColumn, removeColumn, moveColumn } from '../../actions/columns';
|
||||||
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
import { connectListStream } from '../../actions/streaming';
|
||||||
|
import { refreshListTimeline, expandListTimeline } from '../../actions/timelines';
|
||||||
|
import { fetchList } from '../../actions/lists';
|
||||||
|
|
||||||
|
const mapStateToProps = (state, props) => ({
|
||||||
|
list: state.getIn(['lists', props.params.id]),
|
||||||
|
hasUnread: state.getIn(['timelines', `list:${props.params.id}`, 'unread']) > 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
@connect(mapStateToProps)
|
||||||
|
export default class ListTimeline extends React.PureComponent {
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
params: PropTypes.object.isRequired,
|
||||||
|
dispatch: PropTypes.func.isRequired,
|
||||||
|
columnId: PropTypes.string,
|
||||||
|
hasUnread: PropTypes.bool,
|
||||||
|
multiColumn: PropTypes.bool,
|
||||||
|
list: ImmutablePropTypes.map,
|
||||||
|
};
|
||||||
|
|
||||||
|
handlePin = () => {
|
||||||
|
const { columnId, dispatch } = this.props;
|
||||||
|
|
||||||
|
if (columnId) {
|
||||||
|
dispatch(removeColumn(columnId));
|
||||||
|
} else {
|
||||||
|
dispatch(addColumn('LIST', { id: this.props.params.id }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleMove = (dir) => {
|
||||||
|
const { columnId, dispatch } = this.props;
|
||||||
|
dispatch(moveColumn(columnId, dir));
|
||||||
|
}
|
||||||
|
|
||||||
|
handleHeaderClick = () => {
|
||||||
|
this.column.scrollTop();
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount () {
|
||||||
|
const { dispatch } = this.props;
|
||||||
|
const { id } = this.props.params;
|
||||||
|
|
||||||
|
dispatch(fetchList(id));
|
||||||
|
dispatch(refreshListTimeline(id));
|
||||||
|
|
||||||
|
this.disconnect = dispatch(connectListStream(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount () {
|
||||||
|
if (this.disconnect) {
|
||||||
|
this.disconnect();
|
||||||
|
this.disconnect = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setRef = c => {
|
||||||
|
this.column = c;
|
||||||
|
}
|
||||||
|
|
||||||
|
handleLoadMore = () => {
|
||||||
|
const { id } = this.props.params;
|
||||||
|
this.props.dispatch(expandListTimeline(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const { hasUnread, columnId, multiColumn, list } = this.props;
|
||||||
|
const { id } = this.props.params;
|
||||||
|
const pinned = !!columnId;
|
||||||
|
const title = list ? list.get('title') : id;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Column ref={this.setRef}>
|
||||||
|
<ColumnHeader
|
||||||
|
icon='bars'
|
||||||
|
active={hasUnread}
|
||||||
|
title={title}
|
||||||
|
onPin={this.handlePin}
|
||||||
|
onMove={this.handleMove}
|
||||||
|
onClick={this.handleHeaderClick}
|
||||||
|
pinned={pinned}
|
||||||
|
multiColumn={multiColumn}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<StatusListContainer
|
||||||
|
trackScroll={!pinned}
|
||||||
|
scrollKey={`list_timeline-${columnId}`}
|
||||||
|
timelineId={`list:${id}`}
|
||||||
|
loadMore={this.handleLoadMore}
|
||||||
|
emptyMessage={<FormattedMessage id='empty_column.list' defaultMessage='There is nothing in this list yet.' />}
|
||||||
|
/>
|
||||||
|
</Column>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,2 @@
|
|||||||
|
[
|
||||||
|
]
|
@ -0,0 +1,15 @@
|
|||||||
|
import { LIST_FETCH_SUCCESS } from '../actions/lists';
|
||||||
|
import { Map as ImmutableMap, fromJS } from 'immutable';
|
||||||
|
|
||||||
|
const initialState = ImmutableMap();
|
||||||
|
|
||||||
|
const normalizeList = (state, list) => state.set(list.id, fromJS(list));
|
||||||
|
|
||||||
|
export default function lists(state = initialState, action) {
|
||||||
|
switch(action.type) {
|
||||||
|
case LIST_FETCH_SUCCESS:
|
||||||
|
return normalizeList(state, action.list);
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
};
|
@ -0,0 +1,6 @@
|
|||||||
|
class AddIndexAccountAndReblogOfIdToStatuses < ActiveRecord::Migration[5.1]
|
||||||
|
def change
|
||||||
|
commit_db_transaction
|
||||||
|
add_index :statuses, [:account_id, :reblog_of_id], algorithm: :concurrently
|
||||||
|
end
|
||||||
|
end
|
@ -0,0 +1,63 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe Streamable do
|
||||||
|
class Parent
|
||||||
|
def title; end
|
||||||
|
|
||||||
|
def target; end
|
||||||
|
|
||||||
|
def thread; end
|
||||||
|
|
||||||
|
def self.has_one(*); end
|
||||||
|
|
||||||
|
def self.after_create; end
|
||||||
|
end
|
||||||
|
|
||||||
|
class Child < Parent
|
||||||
|
include Streamable
|
||||||
|
end
|
||||||
|
|
||||||
|
child = Child.new
|
||||||
|
|
||||||
|
describe '#title' do
|
||||||
|
it 'calls Parent#title' do
|
||||||
|
expect_any_instance_of(Parent).to receive(:title)
|
||||||
|
child.title
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#content' do
|
||||||
|
it 'calls #title' do
|
||||||
|
expect_any_instance_of(Parent).to receive(:title)
|
||||||
|
child.content
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#target' do
|
||||||
|
it 'calls Parent#target' do
|
||||||
|
expect_any_instance_of(Parent).to receive(:target)
|
||||||
|
child.target
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#object_type' do
|
||||||
|
it 'returns :activity' do
|
||||||
|
expect(child.object_type).to eq :activity
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#thread' do
|
||||||
|
it 'calls Parent#thread' do
|
||||||
|
expect_any_instance_of(Parent).to receive(:thread)
|
||||||
|
child.thread
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#hidden?' do
|
||||||
|
it 'returns false' do
|
||||||
|
expect(child.hidden?).to be false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
@ -0,0 +1,82 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe AccountRelationshipsPresenter do
|
||||||
|
describe '.initialize' do
|
||||||
|
before do
|
||||||
|
allow(Account).to receive(:following_map).with(account_ids, current_account_id).and_return(default_map)
|
||||||
|
allow(Account).to receive(:followed_by_map).with(account_ids, current_account_id).and_return(default_map)
|
||||||
|
allow(Account).to receive(:blocking_map).with(account_ids, current_account_id).and_return(default_map)
|
||||||
|
allow(Account).to receive(:muting_map).with(account_ids, current_account_id).and_return(default_map)
|
||||||
|
allow(Account).to receive(:requested_map).with(account_ids, current_account_id).and_return(default_map)
|
||||||
|
allow(Account).to receive(:domain_blocking_map).with(account_ids, current_account_id).and_return(default_map)
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:presenter) { AccountRelationshipsPresenter.new(account_ids, current_account_id, options) }
|
||||||
|
let(:current_account_id) { Fabricate(:account).id }
|
||||||
|
let(:account_ids) { [Fabricate(:account).id] }
|
||||||
|
let(:default_map) { { 1 => true } }
|
||||||
|
|
||||||
|
context 'options are not set' do
|
||||||
|
let(:options) { {} }
|
||||||
|
|
||||||
|
it 'sets default maps' do
|
||||||
|
expect(presenter.following).to eq default_map
|
||||||
|
expect(presenter.followed_by).to eq default_map
|
||||||
|
expect(presenter.blocking).to eq default_map
|
||||||
|
expect(presenter.muting).to eq default_map
|
||||||
|
expect(presenter.requested).to eq default_map
|
||||||
|
expect(presenter.domain_blocking).to eq default_map
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'options[:following_map] is set' do
|
||||||
|
let(:options) { { following_map: { 2 => true } } }
|
||||||
|
|
||||||
|
it 'sets @following merged with default_map and options[:following_map]' do
|
||||||
|
expect(presenter.following).to eq default_map.merge(options[:following_map])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'options[:followed_by_map] is set' do
|
||||||
|
let(:options) { { followed_by_map: { 3 => true } } }
|
||||||
|
|
||||||
|
it 'sets @followed_by merged with default_map and options[:followed_by_map]' do
|
||||||
|
expect(presenter.followed_by).to eq default_map.merge(options[:followed_by_map])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'options[:blocking_map] is set' do
|
||||||
|
let(:options) { { blocking_map: { 4 => true } } }
|
||||||
|
|
||||||
|
it 'sets @blocking merged with default_map and options[:blocking_map]' do
|
||||||
|
expect(presenter.blocking).to eq default_map.merge(options[:blocking_map])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'options[:muting_map] is set' do
|
||||||
|
let(:options) { { muting_map: { 5 => true } } }
|
||||||
|
|
||||||
|
it 'sets @muting merged with default_map and options[:muting_map]' do
|
||||||
|
expect(presenter.muting).to eq default_map.merge(options[:muting_map])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'options[:requested_map] is set' do
|
||||||
|
let(:options) { { requested_map: { 6 => true } } }
|
||||||
|
|
||||||
|
it 'sets @requested merged with default_map and options[:requested_map]' do
|
||||||
|
expect(presenter.requested).to eq default_map.merge(options[:requested_map])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'options[:domain_blocking_map] is set' do
|
||||||
|
let(:options) { { domain_blocking_map: { 7 => true } } }
|
||||||
|
|
||||||
|
it 'sets @domain_blocking merged with default_map and options[:domain_blocking_map]' do
|
||||||
|
expect(presenter.domain_blocking).to eq default_map.merge(options[:domain_blocking_map])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
Loading…
Reference in new issue