Compare commits

...
This repository has been archived on 2023-07-01. You can view files and clone it, but cannot push or open issues or pull requests.

4 Commits

Author SHA1 Message Date
kibigo! 4d0fde3d93 Timeline should be a feature, not component 2017-06-24 17:10:55 -07:00
kibigo! ff0ecca84c Removing vestigial hasUnread, pt. II 2017-06-24 16:37:02 -07:00
kibigo! cdd7f792cb Removed vestigial `hasUnread` 2017-06-24 16:15:43 -07:00
kibigo! 77908849bb Unify timeline code 2017-06-24 15:55:55 -07:00
5 changed files with 248 additions and 398 deletions

View File

@ -1,144 +1,44 @@
import React from 'react'; import React from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types'; 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 { import {
refreshCommunityTimeline, refreshCommunityTimeline,
expandCommunityTimeline, expandCommunityTimeline,
updateTimeline,
deleteFromTimelines,
connectTimeline,
disconnectTimeline,
} from '../../actions/timelines'; } from '../../actions/timelines';
import { addColumn, removeColumn, moveColumn } from '../../actions/columns';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import ColumnSettingsContainer from './containers/column_settings_container'; import ColumnSettingsContainer from './containers/column_settings_container';
import createStream from '../../stream'; import Timeline from '../timeline';
const messages = defineMessages({ const messages = defineMessages({
title: { id: 'column.community', defaultMessage: 'Local timeline' }, title: { id: 'column.community', defaultMessage: 'Local timeline' },
}); });
const mapStateToProps = state => ({
hasUnread: state.getIn(['timelines', 'community', 'unread']) > 0,
streamingAPIBaseURL: state.getIn(['meta', 'streaming_api_base_url']),
accessToken: state.getIn(['meta', 'access_token']),
});
@connect(mapStateToProps)
@injectIntl @injectIntl
export default class CommunityTimeline extends React.PureComponent { export default class CommunityTimeline extends React.PureComponent {
static propTypes = { static propTypes = {
dispatch: PropTypes.func.isRequired,
columnId: PropTypes.string, columnId: PropTypes.string,
intl: PropTypes.object.isRequired, intl: PropTypes.object.isRequired,
streamingAPIBaseURL: PropTypes.string.isRequired,
accessToken: PropTypes.string.isRequired,
hasUnread: PropTypes.bool,
multiColumn: PropTypes.bool, multiColumn: PropTypes.bool,
}; };
handlePin = () => {
const { columnId, dispatch } = this.props;
if (columnId) {
dispatch(removeColumn(columnId));
} else {
dispatch(addColumn('COMMUNITY', {}));
}
}
handleMove = (dir) => {
const { columnId, dispatch } = this.props;
dispatch(moveColumn(columnId, dir));
}
handleHeaderClick = () => {
this.column.scrollTop();
}
componentDidMount () {
const { dispatch, streamingAPIBaseURL, accessToken } = this.props;
dispatch(refreshCommunityTimeline());
if (typeof this._subscription !== 'undefined') {
return;
}
this._subscription = createStream(streamingAPIBaseURL, accessToken, 'public:local', {
connected () {
dispatch(connectTimeline('community'));
},
reconnected () {
dispatch(connectTimeline('community'));
},
disconnected () {
dispatch(disconnectTimeline('community'));
},
received (data) {
switch(data.event) {
case 'update':
dispatch(updateTimeline('community', JSON.parse(data.payload)));
break;
case 'delete':
dispatch(deleteFromTimelines(data.payload));
break;
}
},
});
}
componentWillUnmount () {
if (typeof this._subscription !== 'undefined') {
this._subscription.close();
this._subscription = null;
}
}
setRef = c => {
this.column = c;
}
handleLoadMore = () => {
this.props.dispatch(expandCommunityTimeline());
}
render () { render () {
const { intl, hasUnread, columnId, multiColumn } = this.props; const { intl, columnId, multiColumn } = this.props;
const pinned = !!columnId;
return ( return (
<Column ref={this.setRef}> <Timeline
<ColumnHeader expand={expandCommunityTimeline}
icon='users' refresh={refreshCommunityTimeline}
active={hasUnread} streamId='public:local'
title={intl.formatMessage(messages.title)} columnName='COMMUNITY'
onPin={this.handlePin} columnId={columnId}
onMove={this.handleMove} mulitColumn={multiColumn}
onClick={this.handleHeaderClick} emptyMessage={<FormattedMessage id='empty_column.community' defaultMessage='The local timeline is empty. Write something publicly to get the ball rolling!' />}
pinned={pinned} icon='users'
multiColumn={multiColumn} title={intl.formatMessage(messages.title)}
> settings={<ColumnSettingsContainer />}
<ColumnSettingsContainer /> scrollName='community_timeline'
</ColumnHeader> timelineId='community'
/>
<StatusListContainer
trackScroll={!pinned}
scrollKey={`community_timeline-${columnId}`}
timelineId='community'
loadMore={this.handleLoadMore}
emptyMessage={<FormattedMessage id='empty_column.community' defaultMessage='The local timeline is empty. Write something publicly to get the ball rolling!' />}
/>
</Column>
); );
} }

View File

@ -1,138 +1,53 @@
import React from 'react'; import React from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types'; 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 { import {
refreshHashtagTimeline, refreshHashtagTimeline,
expandHashtagTimeline, expandHashtagTimeline,
updateTimeline,
deleteFromTimelines,
} from '../../actions/timelines'; } from '../../actions/timelines';
import { addColumn, removeColumn, moveColumn } from '../../actions/columns';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
import createStream from '../../stream'; import Timeline from '../timeline';
const mapStateToProps = state => ({
hasUnread: state.getIn(['timelines', 'tag', 'unread']) > 0,
streamingAPIBaseURL: state.getIn(['meta', 'streaming_api_base_url']),
accessToken: state.getIn(['meta', 'access_token']),
});
@connect(mapStateToProps)
export default class HashtagTimeline extends React.PureComponent { export default class HashtagTimeline extends React.PureComponent {
static propTypes = { static propTypes = {
params: PropTypes.object.isRequired, params: PropTypes.object.isRequired,
columnId: PropTypes.string, columnId: PropTypes.string,
dispatch: PropTypes.func.isRequired,
streamingAPIBaseURL: PropTypes.string.isRequired,
accessToken: PropTypes.string.isRequired,
hasUnread: PropTypes.bool,
multiColumn: PropTypes.bool, multiColumn: PropTypes.bool,
}; };
handlePin = () => { componentWillMount () {
const { columnId, dispatch } = this.props; const id = this.props.params.id;
this.expand = () => expandHashtagTimeline(id);
if (columnId) { this.refresh = () => refreshHashtagTimeline(id);
dispatch(removeColumn(columnId));
} else {
dispatch(addColumn('HASHTAG', { id: this.props.params.id }));
}
}
handleMove = (dir) => {
const { columnId, dispatch } = this.props;
dispatch(moveColumn(columnId, dir));
}
handleHeaderClick = () => {
this.column.scrollTop();
}
_subscribe (dispatch, id) {
const { streamingAPIBaseURL, accessToken } = this.props;
this.subscription = createStream(streamingAPIBaseURL, accessToken, `hashtag&tag=${id}`, {
received (data) {
switch(data.event) {
case 'update':
dispatch(updateTimeline(`hashtag:${id}`, JSON.parse(data.payload)));
break;
case 'delete':
dispatch(deleteFromTimelines(data.payload));
break;
}
},
});
}
_unsubscribe () {
if (typeof this.subscription !== 'undefined') {
this.subscription.close();
this.subscription = null;
}
}
componentDidMount () {
const { dispatch } = this.props;
const { id } = this.props.params;
dispatch(refreshHashtagTimeline(id));
this._subscribe(dispatch, id);
} }
componentWillReceiveProps (nextProps) { componentWillReceiveProps (nextProps) {
if (nextProps.params.id !== this.props.params.id) { if (nextProps.params.id !== this.props.params.id) {
this.props.dispatch(refreshHashtagTimeline(nextProps.params.id)); const id = nextProps.params.id;
this._unsubscribe(); this.expand = () => expandHashtagTimeline(id);
this._subscribe(this.props.dispatch, nextProps.params.id); this.refresh = () => refreshHashtagTimeline(id);
} }
} }
componentWillUnmount () {
this._unsubscribe();
}
setRef = c => {
this.column = c;
}
handleLoadMore = () => {
this.props.dispatch(expandHashtagTimeline(this.props.params.id));
}
render () { render () {
const { hasUnread, columnId, multiColumn } = this.props; const { columnId, multiColumn } = this.props;
const { id } = this.props.params; const { id } = this.props.params;
const pinned = !!columnId;
return ( return (
<Column ref={this.setRef}> <Timeline
<ColumnHeader expand={this.expand}
icon='hashtag' refresh={this.refresh}
active={hasUnread} streamId={`hashtag&tag=${id}`}
title={id} columnName='HASHTAG'
onPin={this.handlePin} columnProps={{ id }}
onMove={this.handleMove} columnId={columnId}
onClick={this.handleHeaderClick} mulitColumn={multiColumn}
pinned={pinned} emptyMessage={<FormattedMessage id='empty_column.hashtag' defaultMessage='There is nothing in this hashtag yet.' />}
multiColumn={multiColumn} icon='hashtag'
showBackButton title={id}
/> scrollName='hashtag_timeline'
timelineId={`hashtag:${id}`}
<StatusListContainer />
trackScroll={!pinned}
scrollKey={`hashtag_timeline-${columnId}`}
timelineId={`hashtag:${id}`}
loadMore={this.handleLoadMore}
emptyMessage={<FormattedMessage id='empty_column.hashtag' defaultMessage='There is nothing in this hashtag yet.' />}
/>
</Column>
); );
} }

View File

@ -2,12 +2,9 @@ import React from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { expandHomeTimeline } from '../../actions/timelines'; import { expandHomeTimeline } from '../../actions/timelines';
import PropTypes from 'prop-types'; 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 { addColumn, removeColumn, moveColumn } from '../../actions/columns';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import ColumnSettingsContainer from './containers/column_settings_container'; import ColumnSettingsContainer from './containers/column_settings_container';
import Timeline from '../timeline';
import Link from 'react-router-dom/Link'; import Link from 'react-router-dom/Link';
const messages = defineMessages({ const messages = defineMessages({
@ -15,7 +12,6 @@ const messages = defineMessages({
}); });
const mapStateToProps = state => ({ const mapStateToProps = state => ({
hasUnread: state.getIn(['timelines', 'home', 'unread']) > 0,
hasFollows: state.getIn(['accounts_counters', state.getIn(['meta', 'me']), 'following_count']) > 0, hasFollows: state.getIn(['accounts_counters', state.getIn(['meta', 'me']), 'following_count']) > 0,
}); });
@ -24,44 +20,14 @@ const mapStateToProps = state => ({
export default class HomeTimeline extends React.PureComponent { export default class HomeTimeline extends React.PureComponent {
static propTypes = { static propTypes = {
dispatch: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired, intl: PropTypes.object.isRequired,
hasUnread: PropTypes.bool,
hasFollows: PropTypes.bool, hasFollows: PropTypes.bool,
columnId: PropTypes.string, columnId: PropTypes.string,
multiColumn: PropTypes.bool, multiColumn: PropTypes.bool,
}; };
handlePin = () => {
const { columnId, dispatch } = this.props;
if (columnId) {
dispatch(removeColumn(columnId));
} else {
dispatch(addColumn('HOME', {}));
}
}
handleMove = (dir) => {
const { columnId, dispatch } = this.props;
dispatch(moveColumn(columnId, dir));
}
handleHeaderClick = () => {
this.column.scrollTop();
}
setRef = c => {
this.column = c;
}
handleLoadMore = () => {
this.props.dispatch(expandHomeTimeline());
}
render () { render () {
const { intl, hasUnread, hasFollows, columnId, multiColumn } = this.props; const { intl, hasFollows, columnId, multiColumn } = this.props;
const pinned = !!columnId;
let emptyMessage; let emptyMessage;
@ -72,28 +38,18 @@ export default class HomeTimeline extends React.PureComponent {
} }
return ( return (
<Column ref={this.setRef}> <Timeline
<ColumnHeader expand={expandHomeTimeline}
icon='home' columnName='HOME'
active={hasUnread} columnId={columnId}
title={intl.formatMessage(messages.title)} mulitColumn={multiColumn}
onPin={this.handlePin} emptyMessage={emptyMessage}
onMove={this.handleMove} icon='home'
onClick={this.handleHeaderClick} title={intl.formatMessage(messages.title)}
pinned={pinned} settings={<ColumnSettingsContainer />}
multiColumn={multiColumn} scrollName='home_timeline'
> timelineId='home'
<ColumnSettingsContainer /> />
</ColumnHeader>
<StatusListContainer
trackScroll={!pinned}
scrollKey={`home_timeline-${columnId}`}
loadMore={this.handleLoadMore}
timelineId='home'
emptyMessage={emptyMessage}
/>
</Column>
); );
} }

View File

@ -1,144 +1,44 @@
import React from 'react'; import React from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types'; 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 { import {
refreshPublicTimeline, refreshPublicTimeline,
expandPublicTimeline, expandPublicTimeline,
updateTimeline,
deleteFromTimelines,
connectTimeline,
disconnectTimeline,
} from '../../actions/timelines'; } from '../../actions/timelines';
import { addColumn, removeColumn, moveColumn } from '../../actions/columns';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import ColumnSettingsContainer from './containers/column_settings_container'; import ColumnSettingsContainer from './containers/column_settings_container';
import createStream from '../../stream'; import Timeline from '../timeline';
const messages = defineMessages({ const messages = defineMessages({
title: { id: 'column.public', defaultMessage: 'Federated timeline' }, title: { id: 'column.public', defaultMessage: 'Federated timeline' },
}); });
const mapStateToProps = state => ({
hasUnread: state.getIn(['timelines', 'public', 'unread']) > 0,
streamingAPIBaseURL: state.getIn(['meta', 'streaming_api_base_url']),
accessToken: state.getIn(['meta', 'access_token']),
});
@connect(mapStateToProps)
@injectIntl @injectIntl
export default class PublicTimeline extends React.PureComponent { export default class PublicTimeline extends React.PureComponent {
static propTypes = { static propTypes = {
dispatch: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired, intl: PropTypes.object.isRequired,
columnId: PropTypes.string, columnId: PropTypes.string,
multiColumn: PropTypes.bool, multiColumn: PropTypes.bool,
streamingAPIBaseURL: PropTypes.string.isRequired,
accessToken: PropTypes.string.isRequired,
hasUnread: PropTypes.bool,
}; };
handlePin = () => {
const { columnId, dispatch } = this.props;
if (columnId) {
dispatch(removeColumn(columnId));
} else {
dispatch(addColumn('PUBLIC', {}));
}
}
handleMove = (dir) => {
const { columnId, dispatch } = this.props;
dispatch(moveColumn(columnId, dir));
}
handleHeaderClick = () => {
this.column.scrollTop();
}
componentDidMount () {
const { dispatch, streamingAPIBaseURL, accessToken } = this.props;
dispatch(refreshPublicTimeline());
if (typeof this._subscription !== 'undefined') {
return;
}
this._subscription = createStream(streamingAPIBaseURL, accessToken, 'public', {
connected () {
dispatch(connectTimeline('public'));
},
reconnected () {
dispatch(connectTimeline('public'));
},
disconnected () {
dispatch(disconnectTimeline('public'));
},
received (data) {
switch(data.event) {
case 'update':
dispatch(updateTimeline('public', JSON.parse(data.payload)));
break;
case 'delete':
dispatch(deleteFromTimelines(data.payload));
break;
}
},
});
}
componentWillUnmount () {
if (typeof this._subscription !== 'undefined') {
this._subscription.close();
this._subscription = null;
}
}
setRef = c => {
this.column = c;
}
handleLoadMore = () => {
this.props.dispatch(expandPublicTimeline());
}
render () { render () {
const { intl, columnId, hasUnread, multiColumn } = this.props; const { intl, columnId, multiColumn } = this.props;
const pinned = !!columnId;
return ( return (
<Column ref={this.setRef}> <Timeline
<ColumnHeader expand={expandPublicTimeline}
icon='globe' refresh={refreshPublicTimeline}
active={hasUnread} streamId='public'
title={intl.formatMessage(messages.title)} columnName='PUBLIC'
onPin={this.handlePin} columnId={columnId}
onMove={this.handleMove} mulitColumn={multiColumn}
onClick={this.handleHeaderClick} emptyMessage={<FormattedMessage id='empty_column.public' defaultMessage='There is nothing here! Write something publicly, or manually follow users from other instances to fill it up' />}
pinned={pinned} icon='globe'
multiColumn={multiColumn} title={intl.formatMessage(messages.title)}
> settings={<ColumnSettingsContainer />}
<ColumnSettingsContainer /> scrollName='public_timeline'
</ColumnHeader> timelineId='public'
/>
<StatusListContainer
timelineId='public'
loadMore={this.handleLoadMore}
trackScroll={!pinned}
scrollKey={`public_timeline-${columnId}`}
emptyMessage={<FormattedMessage id='empty_column.public' defaultMessage='There is nothing here! Write something publicly, or manually follow users from other instances to fill it up' />}
/>
</Column>
); );
} }

View File

@ -0,0 +1,179 @@
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 {
updateTimeline,
deleteFromTimelines,
connectTimeline,
disconnectTimeline,
} from '../../actions/timelines';
import { addColumn, removeColumn, moveColumn } from '../../actions/columns';
import createStream from '../../stream';
const mapStateToProps = (state, ownprops) => ({
streamingAPIBaseURL: state.getIn(['meta', 'streaming_api_base_url']),
accessToken: state.getIn(['meta', 'access_token']),
hasUnread: state.getIn(['timelines', ownprops.timelineId, 'unread']) > 0,
});
@connect(mapStateToProps)
export default class Timeline extends React.PureComponent {
static propTypes = {
dispatch: PropTypes.func.isRequired,
streamingAPIBaseURL: PropTypes.string.isRequired,
accessToken: PropTypes.string.isRequired,
expand: PropTypes.func.isRequired,
refresh: PropTypes.func,
streamId: PropTypes.string,
hasUnread: PropTypes.bool,
columnName: PropTypes.string.isRequired,
columnProps: PropTypes.object,
columnId: PropTypes.string,
multiColumn: PropTypes.bool,
emptyMessage: PropTypes.oneOfType([
PropTypes.element,
PropTypes.string,
]),
icon: PropTypes.string.isRequired,
title: PropTypes.string.isRequired,
settings: PropTypes.element,
scrollName: PropTypes.string.isRequired,
timelineId: PropTypes.string.isRequired,
};
handlePin = () => {
const { columnName, columnProps, columnId, dispatch } = this.props;
if (columnId) {
dispatch(removeColumn(columnId));
} else {
dispatch(addColumn(columnName, columnProps || {}));
}
}
handleMove = (dir) => {
const { columnId, dispatch } = this.props;
dispatch(moveColumn(columnId, dir));
}
handleHeaderClick = () => {
this.column.scrollTop();
}
setRef = c => {
this.column = c;
}
handleLoadMore = () => {
this.props.dispatch(this.props.expand());
}
_subscribe (dispatch, streamId, timelineId) {
const { streamingAPIBaseURL, accessToken } = this.props;
if (!streamId || !timelineId) return;
this.subscription = createStream(streamingAPIBaseURL, accessToken, streamId, {
connected () {
dispatch(connectTimeline(timelineId));
},
reconnected () {
dispatch(connectTimeline(timelineId));
},
disconnected () {
dispatch(disconnectTimeline(timelineId));
},
received (data) {
switch(data.event) {
case 'update':
dispatch(updateTimeline(timelineId, JSON.parse(data.payload)));
break;
case 'delete':
dispatch(deleteFromTimelines(data.payload));
break;
}
},
});
}
_unsubscribe () {
if (typeof this.subscription !== 'undefined') {
this.subscription.close();
this.subscription = null;
}
}
componentDidMount () {
const { dispatch, refresh, streamId, timelineId } = this.props;
if (typeof refresh !== 'function') return;
dispatch(refresh());
this._subscribe(dispatch, streamId, timelineId);
}
componentWillReceiveProps (nextProps) {
if (nextProps.streamId !== this.props.streamId || nextProps.timelineId !== this.props.timelineId) {
if (typeof refresh !== 'function') return;
this.props.dispatch(this.props.refresh());
this._unsubscribe();
this._subscribe(this.props.dispatch, nextProps.streamId, nextProps.timelineId);
}
}
componentWillUnmount () {
this._unsubscribe();
}
render () {
const {
hasUnread,
columnId,
multiColumn,
emptyMessage,
icon,
title,
settings,
scrollName,
timelineId,
} = this.props;
const pinned = !!columnId;
return (
<Column ref={this.setRef}>
<ColumnHeader
icon={icon}
active={hasUnread}
title={title}
onPin={this.handlePin}
onMove={this.handleMove}
onClick={this.handleHeaderClick}
pinned={pinned}
multiColumn={multiColumn}
>
{settings}
</ColumnHeader>
<StatusListContainer
trackScroll={!pinned}
scrollKey={`${scrollName}-${columnId}`}
loadMore={this.handleLoadMore}
timelineId={timelineId}
emptyMessage={emptyMessage}
/>
</Column>
);
}
}