Merge branch 'feature/adding_queued_timeline' of https://code.gab.com/gab/social/gab-social into develop

This commit is contained in:
mgabdev
2019-07-11 12:18:37 -04:00
31 changed files with 361 additions and 309 deletions

View File

@@ -11,7 +11,6 @@ class ColumnSettings extends React.PureComponent {
settings: ImmutablePropTypes.map.isRequired,
onChange: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired,
columnId: PropTypes.string,
};
render () {

View File

@@ -1,26 +1,15 @@
import { connect } from 'react-redux';
import ColumnSettings from '../components/column_settings';
import { changeSetting } from '../../../actions/settings';
import { changeColumnParams } from '../../../actions/columns';
const mapStateToProps = (state, { columnId }) => {
const uuid = columnId;
const columns = state.getIn(['settings', 'columns']);
const index = columns.findIndex(c => c.get('uuid') === uuid);
const mapStateToProps = state => ({
settings: state.getIn(['settings', 'community']),
});
return {
settings: (uuid && index >= 0) ? columns.get(index).get('params') : state.getIn(['settings', 'community']),
};
};
const mapDispatchToProps = (dispatch, { columnId }) => {
const mapDispatchToProps = (dispatch) => {
return {
onChange (key, checked) {
if (columnId) {
dispatch(changeColumnParams(columnId, key, checked));
} else {
dispatch(changeSetting(['community', ...key], checked));
}
dispatch(changeSetting(['community', ...key], checked));
},
};
};

View File

@@ -13,16 +13,10 @@ const messages = defineMessages({
title: { id: 'column.community', defaultMessage: 'Local timeline' },
});
const mapStateToProps = (state, { onlyMedia, columnId }) => {
const uuid = columnId;
const columns = state.getIn(['settings', 'columns']);
const index = columns.findIndex(c => c.get('uuid') === uuid);
return {
hasUnread: state.getIn(['timelines', `community${onlyMedia ? ':media' : ''}`, 'unread']) > 0,
onlyMedia: (columnId && index >= 0) ? columns.get(index).getIn(['params', 'other', 'onlyMedia']) : state.getIn(['settings', 'community', 'other', 'onlyMedia']),
};
};
const mapStateToProps = (state, { onlyMedia }) => ({
hasUnread: state.getIn(['timelines', `community${onlyMedia ? ':media' : ''}`, 'unread']) > 0,
onlyMedia: state.getIn(['settings', 'community', 'other', 'onlyMedia']),
});
export default @connect(mapStateToProps)
@injectIntl
@@ -38,7 +32,6 @@ class CommunityTimeline extends React.PureComponent {
static propTypes = {
dispatch: PropTypes.func.isRequired,
columnId: PropTypes.string,
intl: PropTypes.object.isRequired,
hasUnread: PropTypes.bool,
onlyMedia: PropTypes.bool,
@@ -75,7 +68,7 @@ class CommunityTimeline extends React.PureComponent {
}
render () {
const { intl, hasUnread, columnId, onlyMedia } = this.props;
const { intl, hasUnread, onlyMedia } = this.props;
return (
<Column label={intl.formatMessage(messages.title)}>
@@ -86,7 +79,7 @@ class CommunityTimeline extends React.PureComponent {
<ColumnSettingsContainer />
</HomeColumnHeader>
<StatusListContainer
scrollKey={`community_timeline-${columnId}`}
scrollKey='community_timeline'
timelineId={`community${onlyMedia ? ':media' : ''}`}
onLoadMore={this.handleLoadMore}
emptyMessage={<FormattedMessage id='empty_column.community' defaultMessage='The local timeline is empty. Write something publicly to get the ball rolling!' />}

View File

@@ -18,7 +18,6 @@ class DirectTimeline extends React.PureComponent {
static propTypes = {
dispatch: PropTypes.func.isRequired,
columnId: PropTypes.string,
intl: PropTypes.object.isRequired,
hasUnread: PropTypes.bool,
};
@@ -45,14 +44,14 @@ class DirectTimeline extends React.PureComponent {
}
render () {
const { intl, hasUnread, columnId } = this.props;
const { intl, hasUnread } = this.props;
return (
<Column label={intl.formatMessage(messages.title)}>
<ColumnHeader icon='envelope' active={hasUnread} title={intl.formatMessage(messages.title)} />
<ConversationsListContainer
scrollKey={`direct_timeline-${columnId}`}
scrollKey='direct_timeline'
timelineId='direct'
onLoadMore={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." />}

View File

@@ -29,7 +29,6 @@ class Favourites extends ImmutablePureComponent {
dispatch: PropTypes.func.isRequired,
statusIds: ImmutablePropTypes.list.isRequired,
intl: PropTypes.object.isRequired,
columnId: PropTypes.string,
hasMore: PropTypes.bool,
isLoading: PropTypes.bool,
isMyAccount: PropTypes.bool.isRequired,
@@ -44,7 +43,7 @@ class Favourites extends ImmutablePureComponent {
}, 300, { leading: true })
render () {
const { intl, statusIds, columnId, hasMore, isLoading, isMyAccount } = this.props;
const { intl, statusIds, hasMore, isLoading, isMyAccount } = this.props;
if (!isMyAccount) {
return (
@@ -60,7 +59,7 @@ class Favourites extends ImmutablePureComponent {
<Column>
<StatusList
statusIds={statusIds}
scrollKey={`favourited_statuses-${columnId}`}
scrollKey='favourited_statuses'
hasMore={hasMore}
isLoading={isLoading}
onLoadMore={this.handleLoadMore}

View File

@@ -30,7 +30,6 @@ class GroupTimeline extends React.PureComponent {
static propTypes = {
params: PropTypes.object.isRequired,
dispatch: PropTypes.func.isRequired,
columnId: PropTypes.string,
hasUnread: PropTypes.bool,
group: PropTypes.oneOfType([ImmutablePropTypes.map, PropTypes.bool]),
intl: PropTypes.object.isRequired,
@@ -59,7 +58,7 @@ class GroupTimeline extends React.PureComponent {
}
render () {
const { hasUnread, columnId, group } = this.props;
const { hasUnread, group } = this.props;
const { id } = this.props.params;
const title = group ? group.get('title') : id;
@@ -93,7 +92,7 @@ class GroupTimeline extends React.PureComponent {
<StatusListContainer
prepend={<HeaderContainer groupId={id} />}
alwaysPrepend
scrollKey={`group_timeline-${columnId}`}
scrollKey='group_timeline'
timelineId={`group:${id}`}
onLoadMore={this.handleLoadMore}
emptyMessage={<FormattedMessage id='empty_column.group' defaultMessage='There is nothing in this group yet. When members of this group post new statuses, they will appear here.' />}

View File

@@ -1,113 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import Toggle from 'react-toggle';
import AsyncSelect from 'react-select/lib/Async';
const messages = defineMessages({
placeholder: { id: 'hashtag.column_settings.select.placeholder', defaultMessage: 'Enter hashtags…' },
noOptions: { id: 'hashtag.column_settings.select.no_options_message', defaultMessage: 'No suggestions found' },
});
export default @injectIntl
class ColumnSettings extends React.PureComponent {
static propTypes = {
settings: ImmutablePropTypes.map.isRequired,
onChange: PropTypes.func.isRequired,
onLoad: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired,
};
state = {
open: this.hasTags(),
};
hasTags () {
return ['all', 'any', 'none'].map(mode => this.tags(mode).length > 0).includes(true);
}
tags (mode) {
let tags = this.props.settings.getIn(['tags', mode]) || [];
if (tags.toJSON) {
return tags.toJSON();
} else {
return tags;
}
};
onSelect = mode => value => this.props.onChange(['tags', mode], value);
onToggle = () => {
if (this.state.open && this.hasTags()) {
this.props.onChange('tags', {});
}
this.setState({ open: !this.state.open });
};
noOptionsMessage = () => this.props.intl.formatMessage(messages.noOptions);
modeSelect (mode) {
return (
<div className='column-settings__row'>
<span className='column-settings__section'>
{this.modeLabel(mode)}
</span>
<AsyncSelect
isMulti
autoFocus
value={this.tags(mode)}
onChange={this.onSelect(mode)}
loadOptions={this.props.onLoad}
className='column-select__container'
classNamePrefix='column-select'
name='tags'
placeholder={this.props.intl.formatMessage(messages.placeholder)}
noOptionsMessage={this.noOptionsMessage}
/>
</div>
);
}
modeLabel (mode) {
switch(mode) {
case 'any':
return <FormattedMessage id='hashtag.column_settings.tag_mode.any' defaultMessage='Any of these' />;
case 'all':
return <FormattedMessage id='hashtag.column_settings.tag_mode.all' defaultMessage='All of these' />;
case 'none':
return <FormattedMessage id='hashtag.column_settings.tag_mode.none' defaultMessage='None of these' />;
default:
return '';
}
};
render () {
return (
<div>
<div className='column-settings__row'>
<div className='setting-toggle'>
<Toggle id='hashtag.column_settings.tag_toggle' onChange={this.onToggle} checked={this.state.open} />
<span className='setting-toggle__label'>
<FormattedMessage id='hashtag.column_settings.tag_toggle' defaultMessage='Include additional tags in this column' />
</span>
</div>
</div>
{this.state.open && (
<div className='column-settings__hashtags'>
{this.modeSelect('any')}
{this.modeSelect('all')}
{this.modeSelect('none')}
</div>
)}
</div>
);
}
}

View File

@@ -1,31 +0,0 @@
import { connect } from 'react-redux';
import ColumnSettings from '../components/column_settings';
import { changeColumnParams } from '../../../actions/columns';
import api from '../../../api';
const mapStateToProps = (state, { columnId }) => {
const columns = state.getIn(['settings', 'columns']);
const index = columns.findIndex(c => c.get('uuid') === columnId);
if (!(columnId && index >= 0)) {
return {};
}
return { settings: columns.get(index).get('params') };
};
const mapDispatchToProps = (dispatch, { columnId }) => ({
onChange (key, value) {
dispatch(changeColumnParams(columnId, key, value));
},
onLoad (value) {
return api().get('/api/v2/search', { params: { q: value } }).then(response => {
return (response.data.hashtags || []).map((tag) => {
return { value: tag.name, label: `#${tag.name}` };
});
});
},
});
export default connect(mapStateToProps, mapDispatchToProps)(ColumnSettings);

View File

@@ -4,7 +4,6 @@ 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 ColumnSettingsContainer from './containers/column_settings_container';
import { expandHashtagTimeline, clearTimeline } from '../../actions/timelines';
import { FormattedMessage } from 'react-intl';
import { connectHashtagStream } from '../../actions/streaming';
@@ -21,7 +20,6 @@ class HashtagTimeline extends React.PureComponent {
static propTypes = {
params: PropTypes.object.isRequired,
columnId: PropTypes.string,
dispatch: PropTypes.func.isRequired,
hasUnread: PropTypes.bool,
};
@@ -104,17 +102,14 @@ class HashtagTimeline extends React.PureComponent {
}
render () {
const { hasUnread, columnId } = this.props;
const { hasUnread } = this.props;
const { id } = this.props.params;
return (
<Column label={`#${id}`}>
<ColumnHeader icon='hashtag' active={hasUnread} title={this.title()}>
{columnId && <ColumnSettingsContainer columnId={columnId} />}
</ColumnHeader>
<ColumnHeader icon='hashtag' active={hasUnread} title={this.title()} />
<StatusListContainer
scrollKey={`hashtag_timeline-${columnId}`}
scrollKey='hashtag_timeline'
timelineId={`hashtag:${id}`}
onLoadMore={this.handleLoadMore}
emptyMessage={<FormattedMessage id='empty_column.hashtag' defaultMessage='There is nothing in this hashtag yet.' />}

View File

@@ -26,7 +26,6 @@ class HomeTimeline extends React.PureComponent {
intl: PropTypes.object.isRequired,
hasUnread: PropTypes.bool,
isPartial: PropTypes.bool,
columnId: PropTypes.string,
};
handleLoadMore = maxId => {
@@ -67,7 +66,7 @@ class HomeTimeline extends React.PureComponent {
}
render () {
const { intl, hasUnread, columnId } = this.props;
const { intl, hasUnread } = this.props;
return (
<Column label={intl.formatMessage(messages.title)}>
@@ -78,7 +77,7 @@ class HomeTimeline extends React.PureComponent {
<ColumnSettingsContainer />
</HomeColumnHeader>
<StatusListContainer
scrollKey={`home_timeline-${columnId}`}
scrollKey='home_timeline'
onLoadMore={this.handleLoadMore}
timelineId='home'
emptyMessage={<FormattedMessage id='empty_column.home' defaultMessage='Your home timeline is empty. Start following other users to recieve their content here.'/>}

View File

@@ -36,7 +36,6 @@ class ListTimeline extends React.PureComponent {
static propTypes = {
params: PropTypes.object.isRequired,
dispatch: PropTypes.func.isRequired,
columnId: PropTypes.string,
hasUnread: PropTypes.bool,
list: PropTypes.oneOfType([ImmutablePropTypes.map, PropTypes.bool]),
intl: PropTypes.object.isRequired,
@@ -69,7 +68,7 @@ class ListTimeline extends React.PureComponent {
}
handleDeleteClick = () => {
const { dispatch, columnId, intl } = this.props;
const { dispatch, intl } = this.props;
const { id } = this.props.params;
dispatch(openModal('CONFIRM', {
@@ -77,18 +76,13 @@ class ListTimeline extends React.PureComponent {
confirm: intl.formatMessage(messages.deleteConfirm),
onConfirm: () => {
dispatch(deleteList(id));
if (!!columnId) {
//
} else {
this.context.router.history.push('/lists');
}
this.context.router.history.push('/lists');
},
}));
}
render () {
const { hasUnread, columnId, list } = this.props;
const { hasUnread, list } = this.props;
const { id } = this.props.params;
const title = list ? list.get('title') : id;
@@ -126,7 +120,7 @@ class ListTimeline extends React.PureComponent {
</ColumnHeader>
<StatusListContainer
scrollKey={`list_timeline-${columnId}`}
scrollKey='list_timeline'
timelineId={`list:${id}`}
onLoadMore={this.handleLoadMore}
emptyMessage={<FormattedMessage id='empty_column.list' defaultMessage='There is nothing in this list yet. When members of this list post new statuses, they will appear here.' />}

View File

@@ -4,7 +4,11 @@ import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import Column from '../../components/column';
import ColumnHeader from '../../components/column_header';
import { expandNotifications, scrollTopNotifications } from '../../actions/notifications';
import {
expandNotifications,
scrollTopNotifications,
dequeueNotifications,
} from '../../actions/notifications';
import NotificationContainer from './containers/notification_container';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import ColumnSettingsContainer from './containers/column_settings_container';
@@ -14,6 +18,7 @@ import { List as ImmutableList } from 'immutable';
import { debounce } from 'lodash';
import ScrollableList from '../../components/scrollable_list';
import LoadGap from '../../components/load_gap';
import TimelineQueueButtonHeader from '../../components/timeline_queue_button_header';
const messages = defineMessages({
title: { id: 'column.notifications', defaultMessage: 'Notifications' },
@@ -40,6 +45,7 @@ const mapStateToProps = state => ({
isLoading: state.getIn(['notifications', 'isLoading'], true),
isUnread: state.getIn(['notifications', 'unread']) > 0,
hasMore: state.getIn(['notifications', 'hasMore']),
totalQueuedNotificationsCount: state.getIn(['notifications', 'totalQueuedNotificationsCount'], 0),
});
export default @connect(mapStateToProps)
@@ -47,7 +53,6 @@ export default @connect(mapStateToProps)
class Notifications extends React.PureComponent {
static propTypes = {
columnId: PropTypes.string,
notifications: ImmutablePropTypes.list.isRequired,
showFilterBar: PropTypes.bool.isRequired,
dispatch: PropTypes.func.isRequired,
@@ -55,6 +60,8 @@ class Notifications extends React.PureComponent {
isLoading: PropTypes.bool,
isUnread: PropTypes.bool,
hasMore: PropTypes.bool,
dequeueNotifications: PropTypes.func,
totalQueuedNotificationsCount: PropTypes.number,
};
componentWillUnmount () {
@@ -62,6 +69,7 @@ class Notifications extends React.PureComponent {
this.handleScrollToTop.cancel();
this.handleScroll.cancel();
this.props.dispatch(scrollTopNotifications(false));
this.handleDequeueNotifications();
}
componentDidMount() {
@@ -113,8 +121,12 @@ class Notifications extends React.PureComponent {
}
}
handleDequeueNotifications = () => {
this.props.dispatch(dequeueNotifications());
};
render () {
const { intl, notifications, isLoading, isUnread, columnId, hasMore, showFilterBar } = this.props;
const { intl, notifications, isLoading, isUnread, hasMore, showFilterBar, totalQueuedNotificationsCount } = this.props;
const emptyMessage = <FormattedMessage id='empty_column.notifications' defaultMessage="You don't have any notifications yet. Interact with others to start the conversation." />;
let scrollableContent = null;
@@ -150,7 +162,7 @@ class Notifications extends React.PureComponent {
const scrollContainer = (
<ScrollableList
scrollKey={`notifications-${columnId}`}
scrollKey='notifications'
isLoading={isLoading}
showLoading={isLoading && notifications.size === 0}
hasMore={hasMore}
@@ -169,6 +181,7 @@ class Notifications extends React.PureComponent {
<ColumnSettingsContainer />
</ColumnHeader>
{filterBarContainer}
<TimelineQueueButtonHeader onClick={this.handleDequeueNotifications} count={totalQueuedNotificationsCount} itemType='notification' />
{scrollContainer}
</Column>
);

View File

@@ -1,26 +1,15 @@
import { connect } from 'react-redux';
import ColumnSettings from '../../community_timeline/components/column_settings';
import { changeSetting } from '../../../actions/settings';
import { changeColumnParams } from '../../../actions/columns';
const mapStateToProps = (state, { columnId }) => {
const uuid = columnId;
const columns = state.getIn(['settings', 'columns']);
const index = columns.findIndex(c => c.get('uuid') === uuid);
const mapStateToProps = state => ({
settings: state.getIn(['settings', 'public']),
});
return {
settings: (uuid && index >= 0) ? columns.get(index).get('params') : state.getIn(['settings', 'public']),
};
};
const mapDispatchToProps = (dispatch, { columnId }) => {
const mapDispatchToProps = dispatch => {
return {
onChange (key, checked) {
if (columnId) {
dispatch(changeColumnParams(columnId, key, checked));
} else {
dispatch(changeSetting(['public', ...key], checked));
}
dispatch(changeSetting(['public', ...key], checked));
},
};
};

View File

@@ -13,14 +13,12 @@ const messages = defineMessages({
title: { id: 'column.public', defaultMessage: 'Federated timeline' },
});
const mapStateToProps = (state, { onlyMedia, columnId }) => {
const uuid = columnId;
const mapStateToProps = (state, { onlyMedia }) => {
const columns = state.getIn(['settings', 'columns']);
const index = columns.findIndex(c => c.get('uuid') === uuid);
return {
hasUnread: state.getIn(['timelines', `public${onlyMedia ? ':media' : ''}`, 'unread']) > 0,
onlyMedia: (columnId && index >= 0) ? columns.get(index).getIn(['params', 'other', 'onlyMedia']) : state.getIn(['settings', 'public', 'other', 'onlyMedia']),
onlyMedia: state.getIn(['settings', 'public', 'other', 'onlyMedia']),
};
};
@@ -39,7 +37,6 @@ class PublicTimeline extends React.PureComponent {
static propTypes = {
dispatch: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired,
columnId: PropTypes.string,
hasUnread: PropTypes.bool,
onlyMedia: PropTypes.bool,
};
@@ -80,13 +77,13 @@ class PublicTimeline extends React.PureComponent {
return (
<Column label={intl.formatMessage(messages.title)}>
<ColumnHeader icon='globe' active={hasUnread} title={intl.formatMessage(messages.title)}>
<ColumnSettingsContainer columnId={columnId} />
<ColumnSettingsContainer/>
</ColumnHeader>
<StatusListContainer
timelineId={`public${onlyMedia ? ':media' : ''}`}
onLoadMore={this.handleLoadMore}
scrollKey={`public_timeline-${columnId}`}
scrollKey='public_timeline'
emptyMessage={<FormattedMessage id='empty_column.public' defaultMessage='There is nothing here! Write something publicly, or manually follow users from other servers to fill it up' />}
/>
</Column>

View File

@@ -4,6 +4,7 @@ import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
import { createSelector } from 'reselect';
import { debounce } from 'lodash';
import { me } from '../../../initial_state';
import { dequeueTimeline } from 'gabsocial/actions/timelines';
const makeGetStatusIds = () => createSelector([
(state, { type }) => state.getIn(['settings', type], ImmutableMap()),
@@ -28,17 +29,22 @@ const makeGetStatusIds = () => createSelector([
});
});
const makeMapStateToProps = () => {
const mapStateToProps = (state, {timelineId}) => {
const getStatusIds = makeGetStatusIds();
const mapStateToProps = (state, { timelineId }) => ({
return {
statusIds: getStatusIds(state, { type: timelineId }),
isLoading: state.getIn(['timelines', timelineId, 'isLoading'], true),
isPartial: state.getIn(['timelines', timelineId, 'isPartial'], false),
hasMore: state.getIn(['timelines', timelineId, 'hasMore']),
});
return mapStateToProps;
totalQueuedItemsCount: state.getIn(['timelines', timelineId, 'totalQueuedItemsCount']),
};
};
export default connect(makeMapStateToProps)(StatusList);
const mapDispatchToProps = (dispatch, ownProps) => ({
onDequeueTimeline(timelineId) {
dispatch(dequeueTimeline(timelineId, ownProps.onLoadMore));
},
});
export default connect(mapStateToProps, mapDispatchToProps)(StatusList);