Merge pull request #198 from glitch-soc/gs-direct-timeline

Direct messages timeline from tootsuite/mastodon#4514
This commit is contained in:
beatrix 2017-10-25 17:37:06 -04:00 committed by GitHub
commit 31814ddda0
21 changed files with 341 additions and 11 deletions

View File

@ -0,0 +1,60 @@
# frozen_string_literal: true
class Api::V1::Timelines::DirectController < Api::BaseController
before_action -> { doorkeeper_authorize! :read }, only: [:show]
before_action :require_user!, only: [:show]
after_action :insert_pagination_headers, unless: -> { @statuses.empty? }
respond_to :json
def show
@statuses = load_statuses
render json: @statuses, each_serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id)
end
private
def load_statuses
cached_direct_statuses
end
def cached_direct_statuses
cache_collection direct_statuses, Status
end
def direct_statuses
direct_timeline_statuses.paginate_by_max_id(
limit_param(DEFAULT_STATUSES_LIMIT),
params[:max_id],
params[:since_id]
)
end
def direct_timeline_statuses
Status.as_direct_timeline(current_account)
end
def insert_pagination_headers
set_pagination_headers(next_path, prev_path)
end
def pagination_params(core_params)
params.permit(:local, :limit).merge(core_params)
end
def next_path
api_v1_timelines_direct_url pagination_params(max_id: pagination_max_id)
end
def prev_path
api_v1_timelines_direct_url pagination_params(since_id: pagination_since_id)
end
def pagination_max_id
@statuses.last.id
end
def pagination_since_id
@statuses.first.id
end
end

View File

@ -8,6 +8,7 @@ import {
refreshHomeTimeline,
refreshCommunityTimeline,
refreshPublicTimeline,
refreshDirectTimeline,
} from './timelines';
export const COMPOSE_CHANGE = 'COMPOSE_CHANGE';
@ -133,6 +134,8 @@ export function submitCompose() {
if (response.data.in_reply_to_id === null && response.data.visibility === 'public') {
insertOrRefresh('community', refreshCommunityTimeline);
insertOrRefresh('public', refreshPublicTimeline);
} else if (response.data.visibility === 'direct') {
insertOrRefresh('direct', refreshDirectTimeline);
}
}).catch(function (error) {
dispatch(submitComposeFail(error));

View File

@ -92,3 +92,4 @@ export const connectCommunityStream = () => connectTimelineStream('community', '
export const connectMediaStream = () => connectTimelineStream('community', 'public:local');
export const connectPublicStream = () => connectTimelineStream('public', 'public');
export const connectHashtagStream = (tag) => connectTimelineStream(`hashtag:${tag}`, `hashtag&tag=${tag}`);
export const connectDirectStream = () => connectTimelineStream('direct', 'direct');

View File

@ -115,6 +115,7 @@ export function refreshTimeline(timelineId, path, params = {}) {
export const refreshHomeTimeline = () => refreshTimeline('home', '/api/v1/timelines/home');
export const refreshPublicTimeline = () => refreshTimeline('public', '/api/v1/timelines/public');
export const refreshCommunityTimeline = () => refreshTimeline('community', '/api/v1/timelines/public', { local: true });
export const refreshDirectTimeline = () => refreshTimeline('direct', '/api/v1/timelines/direct');
export const refreshAccountTimeline = accountId => refreshTimeline(`account:${accountId}`, `/api/v1/accounts/${accountId}/statuses`);
export const refreshAccountMediaTimeline = accountId => refreshTimeline(`account:${accountId}:media`, `/api/v1/accounts/${accountId}/statuses`, { only_media: true });
export const refreshHashtagTimeline = hashtag => refreshTimeline(`hashtag:${hashtag}`, `/api/v1/timelines/tag/${hashtag}`);
@ -155,6 +156,7 @@ export function expandTimeline(timelineId, path, params = {}) {
export const expandHomeTimeline = () => expandTimeline('home', '/api/v1/timelines/home');
export const expandPublicTimeline = () => expandTimeline('public', '/api/v1/timelines/public');
export const expandCommunityTimeline = () => expandTimeline('community', '/api/v1/timelines/public', { local: true });
export const expandDirectTimeline = () => expandTimeline('direct', '/api/v1/timelines/direct');
export const expandAccountTimeline = accountId => expandTimeline(`account:${accountId}`, `/api/v1/accounts/${accountId}/statuses`);
export const expandAccountMediaTimeline = accountId => expandTimeline(`account:${accountId}:media`, `/api/v1/accounts/${accountId}/statuses`, { only_media: true });
export const expandHashtagTimeline = hashtag => expandTimeline(`hashtag:${hashtag}`, `/api/v1/timelines/tag/${hashtag}`);

View File

@ -0,0 +1,17 @@
import { connect } from 'react-redux';
import ColumnSettings from '../../community_timeline/components/column_settings';
import { changeSetting } from '../../../actions/settings';
const mapStateToProps = state => ({
settings: state.getIn(['settings', 'direct']),
});
const mapDispatchToProps = dispatch => ({
onChange (key, checked) {
dispatch(changeSetting(['direct', ...key], checked));
},
});
export default connect(mapStateToProps, mapDispatchToProps)(ColumnSettings);

View File

@ -0,0 +1,107 @@
import React from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import StatusListContainer from '../ui/containers/status_list_container';
import Column from '../../components/column';
import ColumnHeader from '../../components/column_header';
import {
refreshDirectTimeline,
expandDirectTimeline,
} from '../../actions/timelines';
import { addColumn, removeColumn, moveColumn } from '../../actions/columns';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import ColumnSettingsContainer from './containers/column_settings_container';
import { connectDirectStream } from '../../actions/streaming';
const messages = defineMessages({
title: { id: 'column.direct', defaultMessage: 'Direct messages' },
});
const mapStateToProps = state => ({
hasUnread: state.getIn(['timelines', 'direct', 'unread']) > 0,
});
@connect(mapStateToProps)
@injectIntl
export default class DirectTimeline extends React.PureComponent {
static propTypes = {
dispatch: PropTypes.func.isRequired,
columnId: PropTypes.string,
intl: PropTypes.object.isRequired,
hasUnread: PropTypes.bool,
multiColumn: PropTypes.bool,
};
handlePin = () => {
const { columnId, dispatch } = this.props;
if (columnId) {
dispatch(removeColumn(columnId));
} else {
dispatch(addColumn('DIRECT', {}));
}
}
handleMove = (dir) => {
const { columnId, dispatch } = this.props;
dispatch(moveColumn(columnId, dir));
}
handleHeaderClick = () => {
this.column.scrollTop();
}
componentDidMount () {
const { dispatch } = this.props;
dispatch(refreshDirectTimeline());
this.disconnect = dispatch(connectDirectStream());
}
componentWillUnmount () {
if (this.disconnect) {
this.disconnect();
this.disconnect = null;
}
}
setRef = c => {
this.column = c;
}
handleLoadMore = () => {
this.props.dispatch(expandDirectTimeline());
}
render () {
const { intl, hasUnread, columnId, multiColumn } = this.props;
const pinned = !!columnId;
return (
<Column ref={this.setRef}>
<ColumnHeader
icon='envelope'
active={hasUnread}
title={intl.formatMessage(messages.title)}
onPin={this.handlePin}
onMove={this.handleMove}
onClick={this.handleHeaderClick}
pinned={pinned}
multiColumn={multiColumn}
>
<ColumnSettingsContainer />
</ColumnHeader>
<StatusListContainer
trackScroll={!pinned}
scrollKey={`direct_timeline-${columnId}`}
timelineId='direct'
loadMore={this.handleLoadMore}
emptyMessage={<FormattedMessage id='empty_column.direct' defaultMessage="You don't have any direct messages yet. When you send or receive one, it will show up here." />}
/>
</Column>
);
}
}

View File

<
@ -17,6 +17,7 @@ const messages = defineMessages({
navigation_subheading: { id: 'column_subheading.navigation', defaultMessage: 'Navigation' },
settings_subheading: { id: 'column_subheading.settings', defaultMessage: 'Settings' },
community_timeline: { id: 'navigation_bar.community_timeline', defaultMessage: 'Local timeline' },
direct: { id: 'navigation_bar.direct', defaultMessage: 'Direct messages' },
preferences: { id: 'navigation_bar.preferences', defaultMessage: 'Preferences' },
settings: { id: 'navigation_bar.app_settings', defaultMessage: 'App settings' },
follow_requests: { id: 'navigation_bar.follow_requests', defaultMessage: 'Follow requests' },
@ -78,18 +79,22 @@ export default class GettingStarted extends ImmutablePureComponent {
}
}
navItems = navItems.concat([
<ColumnLink key='4' icon='star' text={intl.formatMessage(messages.favourites)} to='/favourites' />,
<ColumnLink key='5' icon='thumb-tack' text={intl.formatMessage(messages.pins)} to='/pinned' />,
]);
if (me.get('locked')) {
navItems.push(<ColumnLink key='6' icon='users' text={intl.formatMessage(messages.follow_requests)} to='/follow_requests' />);
if (!multiColumn || !columns.find(item => item.get('id') === 'DIRECT')) {
navItems.push(<ColumnLink key='4' icon='envelope' text={intl.formatMessage(messages.direct)} to='/timelines/direct' />);
}
navItems = navItems.concat([
<ColumnLink key='7' icon='volume-off' text={intl.formatMessage(messages.mutes)} to='/mutes' />,
<ColumnLink key='8' icon='ban' text={intl.formatMessage(messages.blocks)} to='/blocks' />,
<ColumnLink key='5' icon='star' text={intl.formatMessage(messages.favourites)} to='/favourites' />,
<ColumnLink key='6' icon='thumb-tack' text={intl.formatMessage(messages.pins)} to='/pinned' />,
]);
if (me.get('locked')) {
navItems.push(<ColumnLink key='7' icon='users' text={intl.formatMessage(messages.follow_requests)} to='/follow_requests' />);
}
navItems = navItems.concat([
<ColumnLink key='8' icon='volume-off' text={intl.formatMessage(messages.mutes)} to='/mutes' />,
<ColumnLink key='9' icon='ban' text={intl.formatMessage(messages.blocks)} to='/blocks' />,
]);