Add circle posts history support (#18)
* Wip: make web backend * Wip: keep statuses if edit circle * Wip: Add circle history page and record circle posts * Add circle post to history in web ui when post * Add test
This commit is contained in:
parent
0a42f4b7e2
commit
df3b3f4185
19 changed files with 544 additions and 15 deletions
66
app/controllers/api/v1/circles/statuses_controller.rb
Normal file
66
app/controllers/api/v1/circles/statuses_controller.rb
Normal file
|
@ -0,0 +1,66 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Api::V1::Circles::StatusesController < Api::BaseController
|
||||
before_action -> { doorkeeper_authorize! :read, :'read:lists' }, only: [:show]
|
||||
before_action -> { doorkeeper_authorize! :write, :'write:lists' }, except: [:show]
|
||||
|
||||
before_action :require_user!
|
||||
before_action :set_circle
|
||||
|
||||
after_action :insert_pagination_headers, only: :show
|
||||
|
||||
def show
|
||||
@statuses = load_statuses
|
||||
render json: @statuses, each_serializer: REST::StatusSerializer
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_circle
|
||||
@circle = current_account.circles.find(params[:circle_id])
|
||||
end
|
||||
|
||||
def load_statuses
|
||||
if unlimited?
|
||||
@circle.statuses.includes(:status_stat).all
|
||||
else
|
||||
@circle.statuses.includes(:status_stat).paginate_by_max_id(limit_param(DEFAULT_STATUSES_LIMIT), params[:max_id], params[:since_id])
|
||||
end
|
||||
end
|
||||
|
||||
def insert_pagination_headers
|
||||
set_pagination_headers(next_path, prev_path)
|
||||
end
|
||||
|
||||
def next_path
|
||||
return if unlimited?
|
||||
|
||||
api_v1_circle_statuses_url pagination_params(max_id: pagination_max_id) if records_continue?
|
||||
end
|
||||
|
||||
def prev_path
|
||||
return if unlimited?
|
||||
|
||||
api_v1_circle_statuses_url pagination_params(since_id: pagination_since_id) unless @statuses.empty?
|
||||
end
|
||||
|
||||
def pagination_max_id
|
||||
@statuses.last.id
|
||||
end
|
||||
|
||||
def pagination_since_id
|
||||
@statuses.first.id
|
||||
end
|
||||
|
||||
def records_continue?
|
||||
@statuses.size == limit_param(DEFAULT_STATUSES_LIMIT)
|
||||
end
|
||||
|
||||
def pagination_params(core_params)
|
||||
params.slice(:limit).permit(:limit).merge(core_params)
|
||||
end
|
||||
|
||||
def unlimited?
|
||||
params[:limit] == '0'
|
||||
end
|
||||
end
|
|
@ -1,7 +1,7 @@
|
|||
import api from '../api';
|
||||
import api, { getLinks } from '../api';
|
||||
|
||||
import { showAlertForError } from './alerts';
|
||||
import { importFetchedAccounts } from './importer';
|
||||
import { importFetchedAccounts, importFetchedStatuses } from './importer';
|
||||
|
||||
export const CIRCLE_FETCH_REQUEST = 'CIRCLE_FETCH_REQUEST';
|
||||
export const CIRCLE_FETCH_SUCCESS = 'CIRCLE_FETCH_SUCCESS';
|
||||
|
@ -50,6 +50,14 @@ export const CIRCLE_ADDER_CIRCLES_FETCH_REQUEST = 'CIRCLE_ADDER_CIRCLES_FETCH_RE
|
|||
export const CIRCLE_ADDER_CIRCLES_FETCH_SUCCESS = 'CIRCLE_ADDER_CIRCLES_FETCH_SUCCESS';
|
||||
export const CIRCLE_ADDER_CIRCLES_FETCH_FAIL = 'CIRCLE_ADDER_CIRCLES_FETCH_FAIL';
|
||||
|
||||
export const CIRCLE_STATUSES_FETCH_REQUEST = 'CIRCLE_STATUSES_FETCH_REQUEST';
|
||||
export const CIRCLE_STATUSES_FETCH_SUCCESS = 'CIRCLE_STATUSES_FETCH_SUCCESS';
|
||||
export const CIRCLE_STATUSES_FETCH_FAIL = 'CIRCLE_STATUSES_FETCH_FAIL';
|
||||
|
||||
export const CIRCLE_STATUSES_EXPAND_REQUEST = 'CIRCLE_STATUSES_EXPAND_REQUEST';
|
||||
export const CIRCLE_STATUSES_EXPAND_SUCCESS = 'CIRCLE_STATUSES_EXPAND_SUCCESS';
|
||||
export const CIRCLE_STATUSES_EXPAND_FAIL = 'CIRCLE_STATUSES_EXPAND_FAIL';
|
||||
|
||||
export const fetchCircle = id => (dispatch, getState) => {
|
||||
if (getState().getIn(['circles', id])) {
|
||||
return;
|
||||
|
@ -370,3 +378,89 @@ export const removeFromCircleAdder = circleId => (dispatch, getState) => {
|
|||
dispatch(removeFromCircle(circleId, getState().getIn(['circleAdder', 'accountId'])));
|
||||
};
|
||||
|
||||
export function fetchCircleStatuses(circleId) {
|
||||
return (dispatch, getState) => {
|
||||
if (getState().getIn(['circles', circleId, 'statuses', 'isLoading'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(fetchCircleStatusesRequest(circleId));
|
||||
|
||||
api(getState).get(`/api/v1/circles/${circleId}/statuses`).then(response => {
|
||||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||
dispatch(importFetchedStatuses(response.data));
|
||||
dispatch(fetchCircleStatusesSuccess(circleId, response.data, next ? next.uri : null));
|
||||
}).catch(error => {
|
||||
dispatch(fetchCircleStatusesFail(circleId, error));
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function fetchCircleStatusesRequest(id) {
|
||||
return {
|
||||
type: CIRCLE_STATUSES_FETCH_REQUEST,
|
||||
id,
|
||||
};
|
||||
}
|
||||
|
||||
export function fetchCircleStatusesSuccess(id, statuses, next) {
|
||||
return {
|
||||
type: CIRCLE_STATUSES_FETCH_SUCCESS,
|
||||
id,
|
||||
statuses,
|
||||
next,
|
||||
};
|
||||
}
|
||||
|
||||
export function fetchCircleStatusesFail(id, error) {
|
||||
return {
|
||||
type: CIRCLE_STATUSES_FETCH_FAIL,
|
||||
id,
|
||||
error,
|
||||
};
|
||||
}
|
||||
|
||||
export function expandCircleStatuses(circleId) {
|
||||
return (dispatch, getState) => {
|
||||
const url = getState().getIn(['circles', circleId, 'statuses', 'next'], null);
|
||||
|
||||
if (url === null || getState().getIn(['circles', circleId, 'statuses', 'isLoading'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(expandCircleStatusesRequest(circleId));
|
||||
|
||||
api(getState).get(url).then(response => {
|
||||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||
dispatch(importFetchedStatuses(response.data));
|
||||
dispatch(expandCircleStatusesSuccess(circleId, response.data, next ? next.uri : null));
|
||||
}).catch(error => {
|
||||
dispatch(expandCircleStatusesFail(circleId, error));
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function expandCircleStatusesRequest(id) {
|
||||
return {
|
||||
type: CIRCLE_STATUSES_EXPAND_REQUEST,
|
||||
id,
|
||||
};
|
||||
}
|
||||
|
||||
export function expandCircleStatusesSuccess(id, statuses, next) {
|
||||
return {
|
||||
type: CIRCLE_STATUSES_EXPAND_SUCCESS,
|
||||
id,
|
||||
statuses,
|
||||
next,
|
||||
};
|
||||
}
|
||||
|
||||
export function expandCircleStatusesFail(id, error) {
|
||||
return {
|
||||
type: CIRCLE_STATUSES_EXPAND_FAIL,
|
||||
id,
|
||||
error,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -28,6 +28,8 @@ export const COMPOSE_DIRECT = 'COMPOSE_DIRECT';
|
|||
export const COMPOSE_MENTION = 'COMPOSE_MENTION';
|
||||
export const COMPOSE_RESET = 'COMPOSE_RESET';
|
||||
|
||||
export const COMPOSE_WITH_CIRCLE_SUCCESS = 'COMPOSE_WITH_CIRCLE_SUCCESS';
|
||||
|
||||
export const COMPOSE_UPLOAD_REQUEST = 'COMPOSE_UPLOAD_REQUEST';
|
||||
export const COMPOSE_UPLOAD_SUCCESS = 'COMPOSE_UPLOAD_SUCCESS';
|
||||
export const COMPOSE_UPLOAD_FAIL = 'COMPOSE_UPLOAD_FAIL';
|
||||
|
@ -174,6 +176,7 @@ export function submitCompose(routerHistory) {
|
|||
const status = getState().getIn(['compose', 'text'], '');
|
||||
const media = getState().getIn(['compose', 'media_attachments']);
|
||||
const statusId = getState().getIn(['compose', 'id'], null);
|
||||
const circleId = getState().getIn(['compose', 'circle_id'], null);
|
||||
|
||||
if ((!status || !status.length) && media.size === 0) {
|
||||
return;
|
||||
|
@ -253,6 +256,10 @@ export function submitCompose(routerHistory) {
|
|||
insertIfOnline(`account:${response.data.account.id}`);
|
||||
}
|
||||
|
||||
if (statusId === null && circleId !== null && circleId !== 0) {
|
||||
dispatch(submitComposeWithCircleSuccess({ ...response.data }, circleId));
|
||||
}
|
||||
|
||||
dispatch(showAlert({
|
||||
message: statusId === null ? messages.published : messages.saved,
|
||||
action: messages.open,
|
||||
|
@ -278,6 +285,14 @@ export function submitComposeSuccess(status) {
|
|||
};
|
||||
}
|
||||
|
||||
export function submitComposeWithCircleSuccess(status, circleId) {
|
||||
return {
|
||||
type: COMPOSE_WITH_CIRCLE_SUCCESS,
|
||||
status,
|
||||
circleId,
|
||||
}
|
||||
}
|
||||
|
||||
export function submitComposeFail(error) {
|
||||
return {
|
||||
type: COMPOSE_SUBMIT_FAIL,
|
||||
|
|
182
app/javascript/mastodon/features/circle_statuses/index.jsx
Normal file
182
app/javascript/mastodon/features/circle_statuses/index.jsx
Normal file
|
@ -0,0 +1,182 @@
|
|||
import PropTypes from 'prop-types';
|
||||
|
||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||
|
||||
import { Helmet } from 'react-helmet';
|
||||
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { debounce } from 'lodash';
|
||||
|
||||
import { deleteCircle, expandCircleStatuses, fetchCircle, fetchCircleStatuses } from 'mastodon/actions/circles';
|
||||
import { addColumn, removeColumn, moveColumn } from 'mastodon/actions/columns';
|
||||
import { openModal } from 'mastodon/actions/modal';
|
||||
import ColumnHeader from 'mastodon/components/column_header';
|
||||
import { Icon } from 'mastodon/components/icon';
|
||||
import { LoadingIndicator } from 'mastodon/components/loading_indicator';
|
||||
import StatusList from 'mastodon/components/status_list';
|
||||
import BundleColumnError from 'mastodon/features/ui/components/bundle_column_error';
|
||||
import Column from 'mastodon/features/ui/components/column';
|
||||
import { getCircleStatusList } from 'mastodon/selectors';
|
||||
|
||||
|
||||
const messages = defineMessages({
|
||||
deleteMessage: { id: 'confirmations.delete_circle.message', defaultMessage: 'Are you sure you want to permanently delete this circle?' },
|
||||
deleteConfirm: { id: 'confirmations.delete_circle.confirm', defaultMessage: 'Delete' },
|
||||
heading: { id: 'column.circles', defaultMessage: 'Circles' },
|
||||
});
|
||||
|
||||
const mapStateToProps = (state, { params }) => ({
|
||||
circle: state.getIn(['circles', params.id]),
|
||||
statusIds: getCircleStatusList(state, params.id),
|
||||
isLoading: state.getIn(['circles', params.id, 'isLoading'], true),
|
||||
isEditing: state.getIn(['circleEditor', 'circleId']) === params.id,
|
||||
hasMore: !!state.getIn(['circles', params.id, 'next']),
|
||||
});
|
||||
|
||||
class CircleStatuses extends ImmutablePureComponent {
|
||||
|
||||
static contextTypes = {
|
||||
router: PropTypes.object,
|
||||
};
|
||||
|
||||
static propTypes = {
|
||||
params: PropTypes.object.isRequired,
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
statusIds: ImmutablePropTypes.list.isRequired,
|
||||
circle: PropTypes.oneOfType([ImmutablePropTypes.map, PropTypes.bool]),
|
||||
intl: PropTypes.object.isRequired,
|
||||
columnId: PropTypes.string,
|
||||
multiColumn: PropTypes.bool,
|
||||
hasMore: PropTypes.bool,
|
||||
isLoading: PropTypes.bool,
|
||||
};
|
||||
|
||||
UNSAFE_componentWillMount () {
|
||||
this.props.dispatch(fetchCircle(this.props.params.id));
|
||||
this.props.dispatch(fetchCircleStatuses(this.props.params.id));
|
||||
}
|
||||
|
||||
handlePin = () => {
|
||||
const { columnId, dispatch } = this.props;
|
||||
|
||||
if (columnId) {
|
||||
dispatch(removeColumn(columnId));
|
||||
} else {
|
||||
dispatch(addColumn('CIRCLE_STATUSES', { id: this.props.params.id }));
|
||||
this.context.router.history.push('/');
|
||||
}
|
||||
};
|
||||
|
||||
handleMove = (dir) => {
|
||||
const { columnId, dispatch } = this.props;
|
||||
dispatch(moveColumn(columnId, dir));
|
||||
};
|
||||
|
||||
handleHeaderClick = () => {
|
||||
this.column.scrollTop();
|
||||
};
|
||||
|
||||
handleEditClick = () => {
|
||||
this.props.dispatch(openModal({
|
||||
modalType: 'CIRCLE_EDITOR',
|
||||
modalProps: { circleId: this.props.params.id },
|
||||
}));
|
||||
};
|
||||
|
||||
handleDeleteClick = () => {
|
||||
const { dispatch, columnId, intl } = this.props;
|
||||
const { id } = this.props.params;
|
||||
|
||||
dispatch(openModal({
|
||||
modalType: 'CONFIRM',
|
||||
modalProps: {
|
||||
message: intl.formatMessage(messages.deleteMessage),
|
||||
confirm: intl.formatMessage(messages.deleteConfirm),
|
||||
onConfirm: () => {
|
||||
dispatch(deleteCircle(id));
|
||||
|
||||
if (columnId) {
|
||||
dispatch(removeColumn(columnId));
|
||||
} else {
|
||||
this.context.router.history.push('/circles');
|
||||
}
|
||||
},
|
||||
},
|
||||
}));
|
||||
};
|
||||
|
||||
setRef = c => {
|
||||
this.column = c;
|
||||
};
|
||||
|
||||
handleLoadMore = debounce(() => {
|
||||
this.props.dispatch(expandCircleStatuses());
|
||||
}, 300, { leading: true });
|
||||
|
||||
render () {
|
||||
const { intl, circle, statusIds, columnId, multiColumn, hasMore, isLoading } = this.props;
|
||||
const pinned = !!columnId;
|
||||
|
||||
if (typeof circle === 'undefined') {
|
||||
return (
|
||||
<Column>
|
||||
<div className='scrollable'>
|
||||
<LoadingIndicator />
|
||||
</div>
|
||||
</Column>
|
||||
);
|
||||
} else if (circle === false) {
|
||||
return (
|
||||
<BundleColumnError multiColumn={multiColumn} errorType='routing' />
|
||||
);
|
||||
}
|
||||
|
||||
const emptyMessage = <FormattedMessage id='empty_column.circle_statuses' defaultMessage="You don't have any circle posts yet. When you post one as circle, it will show up here." />;
|
||||
|
||||
return (
|
||||
<Column bindToDocument={!multiColumn} ref={this.setRef} label={intl.formatMessage(messages.heading)}>
|
||||
<ColumnHeader
|
||||
icon='user-circle'
|
||||
title={circle.get('title')}
|
||||
onPin={this.handlePin}
|
||||
onMove={this.handleMove}
|
||||
onClick={this.handleHeaderClick}
|
||||
pinned={pinned}
|
||||
multiColumn={multiColumn}
|
||||
>
|
||||
<div className='column-settings__row column-header__links'>
|
||||
<button type='button' className='text-btn column-header__setting-btn' tabIndex={0} onClick={this.handleEditClick}>
|
||||
<Icon id='pencil' /> <FormattedMessage id='circles.edit' defaultMessage='Edit circle' />
|
||||
</button>
|
||||
|
||||
<button type='button' className='text-btn column-header__setting-btn' tabIndex={0} onClick={this.handleDeleteClick}>
|
||||
<Icon id='trash' /> <FormattedMessage id='circles.delete' defaultMessage='Delete circle' />
|
||||
</button>
|
||||
</div>
|
||||
</ColumnHeader>
|
||||
|
||||
<StatusList
|
||||
trackScroll={!pinned}
|
||||
statusIds={statusIds}
|
||||
scrollKey={`circle_statuses-${columnId}`}
|
||||
hasMore={hasMore}
|
||||
isLoading={isLoading}
|
||||
onLoadMore={this.handleLoadMore}
|
||||
emptyMessage={emptyMessage}
|
||||
bindToDocument={!multiColumn}
|
||||
/>
|
||||
|
||||
<Helmet>
|
||||
<title>{intl.formatMessage(messages.heading)}</title>
|
||||
<meta name='robots' content='noindex' />
|
||||
</Helmet>
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps)(injectIntl(CircleStatuses));
|
|
@ -13,7 +13,6 @@ import { fetchCircles, deleteCircle } from 'mastodon/actions/circles';
|
|||
import { openModal } from 'mastodon/actions/modal';
|
||||
import Column from 'mastodon/components/column';
|
||||
import ColumnHeader from 'mastodon/components/column_header';
|
||||
import { IconButton } from 'mastodon/components/icon_button';
|
||||
import { LoadingIndicator } from 'mastodon/components/loading_indicator';
|
||||
import ScrollableList from 'mastodon/components/scrollable_list';
|
||||
import ColumnLink from 'mastodon/features/ui/components/column_link';
|
||||
|
@ -106,10 +105,7 @@ class Circles extends ImmutablePureComponent {
|
|||
bindToDocument={!multiColumn}
|
||||
>
|
||||
{circles.map(circle =>
|
||||
(<div key={circle.get('id')} className='circle-item'>
|
||||
<ColumnLink to={`#`} data-id={circle.get('id')} onClick={this.handleEditClick} icon='user-circle' text={circle.get('title')} />
|
||||
<IconButton icon='trash' data_id={circle.get('id')} onClick={this.handleRemoveClick} />
|
||||
</div>)
|
||||
<ColumnLink key={circle.get('id')} to={`/circles/${circle.get('id')}`} icon='user-circle' text={circle.get('title')} />,
|
||||
)}
|
||||
</ScrollableList>
|
||||
|
||||
|
|
|
@ -24,6 +24,7 @@ import {
|
|||
BookmarkCategoryStatuses,
|
||||
AntennaSetting,
|
||||
AntennaTimeline,
|
||||
CircleStatuses,
|
||||
} from '../util/async-components';
|
||||
|
||||
import BundleColumnError from './bundle_column_error';
|
||||
|
@ -45,6 +46,7 @@ const componentMap = {
|
|||
'EMOJI_REACTIONS': EmojiReactedStatuses,
|
||||
'BOOKMARKS': BookmarkedStatuses,
|
||||
'BOOKMARKS_EX': BookmarkCategoryStatuses,
|
||||
'CIRCLE_STATUSES': CircleStatuses,
|
||||
'ANTENNA': AntennaSetting,
|
||||
'ANTENNA_TIMELINE': AntennaTimeline,
|
||||
'LIST': ListTimeline,
|
||||
|
|
|
@ -65,6 +65,7 @@ import {
|
|||
Lists,
|
||||
Antennas,
|
||||
Circles,
|
||||
CircleStatuses,
|
||||
AntennaSetting,
|
||||
Directory,
|
||||
Explore,
|
||||
|
@ -259,6 +260,7 @@ class SwitchingColumnsArea extends PureComponent {
|
|||
<WrappedRoute path='/mutes' component={Mutes} content={children} />
|
||||
<WrappedRoute path='/lists' component={Lists} content={children} />
|
||||
<WrappedRoute path='/antennasw' component={Antennas} content={children} />
|
||||
<WrappedRoute path='/circles/:id' component={CircleStatuses} content={children} />
|
||||
<WrappedRoute path='/circles' component={Circles} content={children} />
|
||||
|
||||
<Route component={BundleColumnError} />
|
||||
|
|
|
@ -54,6 +54,10 @@ export function Circles () {
|
|||
return import(/* webpackChunkName: "features/circles" */'../../circles');
|
||||
}
|
||||
|
||||
export function CircleStatuses () {
|
||||
return import(/* webpackChunkName: "features/circle_statuses" */'../../circle_statuses');
|
||||
}
|
||||
|
||||
export function Status () {
|
||||
return import(/* webpackChunkName: "features/status" */'../../status');
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { List as ImmutableList, fromJS } from 'immutable';
|
||||
import { List as ImmutableList, Map as ImmutableMap, OrderedSet as ImmutableOrderedSet, fromJS } from 'immutable';
|
||||
|
||||
import {
|
||||
CIRCLE_FETCH_SUCCESS,
|
||||
|
@ -7,31 +7,106 @@ import {
|
|||
CIRCLE_CREATE_SUCCESS,
|
||||
CIRCLE_UPDATE_SUCCESS,
|
||||
CIRCLE_DELETE_SUCCESS,
|
||||
CIRCLE_STATUSES_FETCH_REQUEST,
|
||||
CIRCLE_STATUSES_FETCH_SUCCESS,
|
||||
CIRCLE_STATUSES_FETCH_FAIL,
|
||||
CIRCLE_STATUSES_EXPAND_REQUEST,
|
||||
CIRCLE_STATUSES_EXPAND_SUCCESS,
|
||||
CIRCLE_STATUSES_EXPAND_FAIL,
|
||||
} from '../actions/circles';
|
||||
import {
|
||||
COMPOSE_WITH_CIRCLE_SUCCESS,
|
||||
} from '../actions/compose';
|
||||
|
||||
const initialState = ImmutableList();
|
||||
|
||||
const normalizeList = (state, circle) => state.set(circle.id, fromJS(circle));
|
||||
const initialStatusesState = ImmutableMap({
|
||||
items: ImmutableList(),
|
||||
isLoading: false,
|
||||
next: null,
|
||||
});
|
||||
|
||||
const normalizeLists = (state, circles) => {
|
||||
const normalizeCircle = (state, circle) => {
|
||||
const old = state.get(circle.id);
|
||||
if (old === false) {
|
||||
return state;
|
||||
}
|
||||
|
||||
let s = state.set(circle.id, fromJS(circle));
|
||||
if (old) {
|
||||
s = s.setIn([circle.id, 'statuses'], old.get('statuses'));
|
||||
} else {
|
||||
s = s.setIn([circle.id, 'statuses'], initialStatusesState);
|
||||
}
|
||||
return s;
|
||||
};
|
||||
|
||||
const normalizeCircles = (state, circles) => {
|
||||
circles.forEach(circle => {
|
||||
state = normalizeList(state, circle);
|
||||
state = normalizeCircle(state, circle);
|
||||
});
|
||||
|
||||
return state;
|
||||
};
|
||||
|
||||
const normalizeCircleStatuses = (state, circleId, statuses, next) => {
|
||||
return state.updateIn([circleId, 'statuses'], listMap => listMap.withMutations(map => {
|
||||
map.set('next', next);
|
||||
map.set('loaded', true);
|
||||
map.set('isLoading', false);
|
||||
map.set('items', ImmutableOrderedSet(statuses.map(item => item.id)));
|
||||
}));
|
||||
};
|
||||
|
||||
const appendToCircleStatuses = (state, circleId, statuses, next) => {
|
||||
return appendToCircleStatusesById(state, circleId, statuses.map(item => item.id), next);
|
||||
};
|
||||
|
||||
const appendToCircleStatusesById = (state, circleId, statuses, next) => {
|
||||
return state.updateIn([circleId, 'statuses'], listMap => listMap.withMutations(map => {
|
||||
if (typeof next !== 'undefined') {
|
||||
map.set('next', next);
|
||||
}
|
||||
map.set('isLoading', false);
|
||||
if (map.get('items')) {
|
||||
map.set('items', map.get('items').union(statuses));
|
||||
}
|
||||
}));
|
||||
};
|
||||
|
||||
const prependToCircleStatusById = (state, circleId, statusId) => {
|
||||
if (!state.get(circleId)) return state;
|
||||
|
||||
return state.updateIn([circleId], circle => circle.withMutations(map => {
|
||||
if (map.getIn(['statuses', 'items'])) {
|
||||
map.updateIn(['statuses', 'items'], list => ImmutableOrderedSet([statusId]).union(list));
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
export default function circles(state = initialState, action) {
|
||||
switch(action.type) {
|
||||
case CIRCLE_FETCH_SUCCESS:
|
||||
case CIRCLE_CREATE_SUCCESS:
|
||||
case CIRCLE_UPDATE_SUCCESS:
|
||||
return normalizeList(state, action.circle);
|
||||
return normalizeCircle(state, action.circle);
|
||||
case CIRCLES_FETCH_SUCCESS:
|
||||
return normalizeLists(state, action.circles);
|
||||
return normalizeCircles(state, action.circles);
|
||||
case CIRCLE_DELETE_SUCCESS:
|
||||
case CIRCLE_FETCH_FAIL:
|
||||
return state.set(action.id, false);
|
||||
case CIRCLE_STATUSES_FETCH_REQUEST:
|
||||
case CIRCLE_STATUSES_EXPAND_REQUEST:
|
||||
return state.setIn([action.id, 'statuses', 'isLoading'], true);
|
||||
case CIRCLE_STATUSES_FETCH_FAIL:
|
||||
case CIRCLE_STATUSES_EXPAND_FAIL:
|
||||
return state.setIn([action.id, 'statuses', 'isLoading'], false);
|
||||
case CIRCLE_STATUSES_FETCH_SUCCESS:
|
||||
return normalizeCircleStatuses(state, action.id, action.statuses, action.next);
|
||||
case CIRCLE_STATUSES_EXPAND_SUCCESS:
|
||||
return appendToCircleStatuses(state, action.id, action.statuses, action.next);
|
||||
case COMPOSE_WITH_CIRCLE_SUCCESS:
|
||||
return prependToCircleStatusById(state, action.circleId, action.status.id);
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
|
|
|
@ -135,3 +135,7 @@ export const getStatusList = createSelector([
|
|||
export const getBookmarkCategoryStatusList = createSelector([
|
||||
(state, bookmarkCategoryId) => state.getIn(['bookmark_categories', bookmarkCategoryId, 'items']),
|
||||
], (items) => items ? items.toList() : ImmutableList());
|
||||
|
||||
export const getCircleStatusList = createSelector([
|
||||
(state, circleId) => state.getIn(['circles', circleId, 'statuses', 'items']),
|
||||
], (items) => items ? items.toList() : ImmutableList());
|
||||
|
|
|
@ -20,6 +20,8 @@ class Circle < ApplicationRecord
|
|||
|
||||
has_many :circle_accounts, inverse_of: :circle, dependent: :destroy
|
||||
has_many :accounts, through: :circle_accounts
|
||||
has_many :circle_statuses, inverse_of: :circle, dependent: :destroy
|
||||
has_many :statuses, through: :circle_statuses
|
||||
|
||||
validates :title, presence: true
|
||||
|
||||
|
|
26
app/models/circle_status.rb
Normal file
26
app/models/circle_status.rb
Normal file
|
@ -0,0 +1,26 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# == Schema Information
|
||||
#
|
||||
# Table name: circle_statuses
|
||||
#
|
||||
# id :bigint(8) not null, primary key
|
||||
# circle_id :bigint(8)
|
||||
# status_id :bigint(8) not null
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
#
|
||||
|
||||
class CircleStatus < ApplicationRecord
|
||||
belongs_to :circle
|
||||
belongs_to :status
|
||||
|
||||
validates :status, uniqueness: { scope: :circle }
|
||||
validate :account_own_status
|
||||
|
||||
private
|
||||
|
||||
def account_own_status
|
||||
errors.add(:status_id, :invalid) unless status.account_id == circle.account_id
|
||||
end
|
||||
end
|
|
@ -106,6 +106,7 @@ class Status < ApplicationRecord
|
|||
has_one :poll, inverse_of: :status, dependent: :destroy
|
||||
has_one :trend, class_name: 'StatusTrend', inverse_of: :status
|
||||
has_one :scheduled_expiration_status, inverse_of: :status, dependent: :destroy
|
||||
has_one :circle_status, inverse_of: :status, dependent: :destroy
|
||||
|
||||
validates :uri, uniqueness: true, presence: true, unless: :local?
|
||||
validates :text, presence: true, unless: -> { with_media? || reblog? }
|
||||
|
|
|
@ -112,5 +112,7 @@ class ProcessMentionsService < BaseService
|
|||
@circle.accounts.find_each do |target_account|
|
||||
@current_mentions << @status.mentions.new(silent: true, account: target_account) unless mentioned_account_ids.include?(target_account.id)
|
||||
end
|
||||
|
||||
@circle.statuses << @status
|
||||
end
|
||||
end
|
||||
|
|
|
@ -18,7 +18,7 @@ Rails.application.routes.draw do
|
|||
/lists/(*any)
|
||||
/antennasw/(*any)
|
||||
/antennast/(*any)
|
||||
/circles
|
||||
/circles/(*any)
|
||||
/notifications
|
||||
/favourites
|
||||
/emoji_reactions
|
||||
|
|
|
@ -226,6 +226,7 @@ namespace :api, format: false do
|
|||
|
||||
resources :circles, only: [:index, :create, :show, :update, :destroy] do
|
||||
resource :accounts, only: [:show, :create, :destroy], controller: 'circles/accounts'
|
||||
resource :statuses, only: [:show], controller: 'circles/statuses'
|
||||
end
|
||||
|
||||
resources :bookmark_categories, only: [:index, :create, :show, :update, :destroy] do
|
||||
|
|
22
db/migrate/20230923103430_create_circle_statuses.rb
Normal file
22
db/migrate/20230923103430_create_circle_statuses.rb
Normal file
|
@ -0,0 +1,22 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require Rails.root.join('lib', 'mastodon', 'migration_helpers')
|
||||
|
||||
class CreateCircleStatuses < ActiveRecord::Migration[7.0]
|
||||
include Mastodon::MigrationHelpers
|
||||
|
||||
disable_ddl_transaction!
|
||||
|
||||
def change
|
||||
safety_assured do
|
||||
create_table :circle_statuses do |t|
|
||||
t.belongs_to :circle, null: true, foreign_key: { on_delete: :cascade }
|
||||
t.belongs_to :status, null: false, foreign_key: { on_delete: :cascade }
|
||||
t.datetime :created_at, null: false
|
||||
t.datetime :updated_at, null: false
|
||||
end
|
||||
|
||||
add_index :circle_statuses, [:circle_id, :status_id], unique: true
|
||||
end
|
||||
end
|
||||
end
|
14
db/schema.rb
14
db/schema.rb
|
@ -10,7 +10,7 @@
|
|||
#
|
||||
# It's strongly recommended that you check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema[7.0].define(version: 2023_09_19_232836) do
|
||||
ActiveRecord::Schema[7.0].define(version: 2023_09_23_103430) do
|
||||
# These are extensions that must be enabled in order to support this database
|
||||
enable_extension "plpgsql"
|
||||
|
||||
|
@ -447,6 +447,16 @@ ActiveRecord::Schema[7.0].define(version: 2023_09_19_232836) do
|
|||
t.index ["follow_id"], name: "index_circle_accounts_on_follow_id"
|
||||
end
|
||||
|
||||
create_table "circle_statuses", force: :cascade do |t|
|
||||
t.bigint "circle_id"
|
||||
t.bigint "status_id", null: false
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.index ["circle_id", "status_id"], name: "index_circle_statuses_on_circle_id_and_status_id", unique: true
|
||||
t.index ["circle_id"], name: "index_circle_statuses_on_circle_id"
|
||||
t.index ["status_id"], name: "index_circle_statuses_on_status_id"
|
||||
end
|
||||
|
||||
create_table "circles", force: :cascade do |t|
|
||||
t.bigint "account_id", null: false
|
||||
t.string "title", default: "", null: false
|
||||
|
@ -1414,6 +1424,8 @@ ActiveRecord::Schema[7.0].define(version: 2023_09_19_232836) do
|
|||
add_foreign_key "circle_accounts", "accounts", on_delete: :cascade
|
||||
add_foreign_key "circle_accounts", "circles", on_delete: :cascade
|
||||
add_foreign_key "circle_accounts", "follows", on_delete: :cascade
|
||||
add_foreign_key "circle_statuses", "circles", on_delete: :cascade
|
||||
add_foreign_key "circle_statuses", "statuses", on_delete: :cascade
|
||||
add_foreign_key "circles", "accounts", on_delete: :cascade
|
||||
add_foreign_key "conversation_mutes", "accounts", name: "fk_225b4212bb", on_delete: :cascade
|
||||
add_foreign_key "conversation_mutes", "conversations", on_delete: :cascade
|
||||
|
|
|
@ -103,4 +103,27 @@ RSpec.describe ProcessMentionsService, type: :service do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'with circle post' do
|
||||
let(:status) { Fabricate(:status, account: account) }
|
||||
let(:circle) { Fabricate(:circle, account: account) }
|
||||
let(:follower) { Fabricate(:account) }
|
||||
let(:other) { Fabricate(:account) }
|
||||
|
||||
before do
|
||||
follower.follow!(account)
|
||||
other.follow!(account)
|
||||
circle.accounts << follower
|
||||
described_class.new.call(status, limited_type: :circle, circle: circle)
|
||||
end
|
||||
|
||||
it 'remains circle post on history' do
|
||||
expect(CircleStatus.exists?(circle_id: circle.id, status_id: status.id)).to be true
|
||||
end
|
||||
|
||||
it 'post is delivered to circle members' do
|
||||
expect(status.mentioned_accounts.count).to eq 1
|
||||
expect(status.mentioned_accounts.first.id).to eq follower.id
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue