dispatch(updateTimeline('home', { ...response.data }));
if (response.data.in_reply_to_id === null && response.data.visibility === 'public') {
+ dispatch(updateTimeline('community', { ...response.data }));
dispatch(updateTimeline('public', { ...response.data }));
}
}).catch(function (error) {
-import api from '../api'
+import api, { getLinks } from '../api'
import Immutable from 'immutable';
export const TIMELINE_UPDATE = 'TIMELINE_UPDATE';
export const TIMELINE_SCROLL_TOP = 'TIMELINE_SCROLL_TOP';
-export function refreshTimelineSuccess(timeline, statuses, skipLoading) {
+export function refreshTimelineSuccess(timeline, statuses, skipLoading, next) {
return {
type: TIMELINE_REFRESH_SUCCESS,
timeline,
statuses,
- skipLoading
+ skipLoading,
+ next
};
};
const ids = getState().getIn(['timelines', timeline, 'items'], Immutable.List());
const newestId = ids.size > 0 ? ids.first() : null;
+ const params = getState().getIn(['timelines', timeline, 'params'], {});
+ const path = getState().getIn(['timelines', timeline, 'path'])(id);
- let params = '';
- let path = timeline;
let skipLoading = false;
if (newestId !== null && getState().getIn(['timelines', timeline, 'loaded']) && (id === null || getState().getIn(['timelines', timeline, 'id']) === id)) {
- params = `?since_id=${newestId}`;
- skipLoading = true;
- }
-
- if (id) {
- path = `${path}/${id}`
+ params.since_id = newestId;
+ skipLoading = true;
}
dispatch(refreshTimelineRequest(timeline, id, skipLoading));
- api(getState).get(`/api/v1/timelines/${path}${params}`).then(function (response) {
- dispatch(refreshTimelineSuccess(timeline, response.data, skipLoading));
- }).catch(function (error) {
+ api(getState).get(path, { params }).then(response => {
+ const next = getLinks(response).refs.find(link => link.rel === 'next');
+ dispatch(refreshTimelineSuccess(timeline, response.data, skipLoading, next ? next.uri : null));
+ }).catch(error => {
dispatch(refreshTimelineFail(timeline, error, skipLoading));
});
};
};
};
-export function expandTimeline(timeline, id = null) {
+export function expandTimeline(timeline) {
return (dispatch, getState) => {
- const lastId = getState().getIn(['timelines', timeline, 'items'], Immutable.List()).last();
-
- if (!lastId || getState().getIn(['timelines', timeline, 'isLoading'])) {
- // If timeline is empty, don't try to load older posts since there are none
- // Also if already loading
+ if (getState().getIn(['timelines', timeline, 'isLoading'])) {
return;
}
- dispatch(expandTimelineRequest(timeline, id));
+ const next = getState().getIn(['timelines', timeline, 'next']);
+ const params = getState().getIn(['timelines', timeline, 'params'], {});
- let path = timeline;
-
- if (id) {
- path = `${path}/${id}`
+ if (next === null) {
+ return;
}
- api(getState).get(`/api/v1/timelines/${path}`, {
+ dispatch(expandTimelineRequest(timeline));
+
+ api(getState).get(next, {
params: {
- limit: 10,
- max_id: lastId
+ ...params,
+ limit: 10
}
}).then(response => {
- dispatch(expandTimelineSuccess(timeline, response.data));
+ const next = getLinks(response).refs.find(link => link.rel === 'next');
+ dispatch(expandTimelineSuccess(timeline, response.data, next ? next.uri : null));
}).catch(error => {
dispatch(expandTimelineFail(timeline, error));
});
};
};
-export function expandTimelineRequest(timeline, id) {
+export function expandTimelineRequest(timeline) {
return {
type: TIMELINE_EXPAND_REQUEST,
- timeline,
- id
+ timeline
};
};
-export function expandTimelineSuccess(timeline, statuses) {
+export function expandTimelineSuccess(timeline, statuses, next) {
return {
type: TIMELINE_EXPAND_SUCCESS,
timeline,
- statuses
+ statuses,
+ next
};
};
import Status from '../features/status';
import GettingStarted from '../features/getting_started';
import PublicTimeline from '../features/public_timeline';
+import CommunityTimeline from '../features/community_timeline';
import AccountTimeline from '../features/account_timeline';
import HomeTimeline from '../features/home_timeline';
import Compose from '../features/compose';
<Route path='getting-started' component={GettingStarted} />
<Route path='timelines/home' component={HomeTimeline} />
<Route path='timelines/public' component={PublicTimeline} />
+ <Route path='timelines/community' component={CommunityTimeline} />
<Route path='timelines/tag/:id' component={HashtagTimeline} />
<Route path='notifications' component={Notifications} />
--- /dev/null
+import { connect } from 'react-redux';
+import PureRenderMixin from 'react-addons-pure-render-mixin';
+import StatusListContainer from '../ui/containers/status_list_container';
+import Column from '../ui/components/column';
+import {
+ refreshTimeline,
+ updateTimeline,
+ deleteFromTimelines
+} from '../../actions/timelines';
+import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
+import ColumnBackButtonSlim from '../../components/column_back_button_slim';
+import createStream from '../../stream';
+
+const messages = defineMessages({
+ title: { id: 'column.community', defaultMessage: 'Public' }
+});
+
+const mapStateToProps = state => ({
+ accessToken: state.getIn(['meta', 'access_token'])
+});
+
+const CommunityTimeline = React.createClass({
+
+ propTypes: {
+ dispatch: React.PropTypes.func.isRequired,
+ intl: React.PropTypes.object.isRequired,
+ accessToken: React.PropTypes.string.isRequired
+ },
+
+ mixins: [PureRenderMixin],
+
+ componentDidMount () {
+ const { dispatch, accessToken } = this.props;
+
+ dispatch(refreshTimeline('community'));
+
+ this.subscription = createStream(accessToken, 'public:local', {
+
+ 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;
+ }
+ },
+
+ render () {
+ const { intl } = this.props;
+
+ return (
+ <Column icon='users' heading={intl.formatMessage(messages.title)}>
+ <ColumnBackButtonSlim />
+ <StatusListContainer type='community' emptyMessage={<FormattedMessage id='empty_column.community' defaultMessage='The community timeline is empty. Write something publicly to get the ball rolling!' />} />
+ </Column>
+ );
+ },
+
+});
+
+export default connect(mapStateToProps)(injectIntl(CommunityTimeline));
const messages = defineMessages({
start: { id: 'getting_started.heading', defaultMessage: 'Getting started' },
public: { id: 'navigation_bar.public_timeline', defaultMessage: 'Public timeline' },
+ community: { id: 'navigation_bar.community_timeline', defaultMessage: 'Community timeline' },
preferences: { id: 'navigation_bar.preferences', defaultMessage: 'Preferences' },
logout: { id: 'navigation_bar.logout', defaultMessage: 'Logout' }
});
header = (
<div className='drawer__header'>
<Link title={intl.formatMessage(messages.start)} className='drawer__tab' to='/getting-started'><i className='fa fa-fw fa-asterisk' /></Link>
+ <Link title={intl.formatMessage(messages.community)} className='drawer__tab' to='/timelines/community'><i className='fa fa-fw fa-users' /></Link>
<Link title={intl.formatMessage(messages.public)} className='drawer__tab' to='/timelines/public'><i className='fa fa-fw fa-globe' /></Link>
<a title={intl.formatMessage(messages.preferences)} className='drawer__tab' href='/settings/preferences'><i className='fa fa-fw fa-cog' /></a>
<a title={intl.formatMessage(messages.logout)} className='drawer__tab' href='/auth/sign_out' data-method='delete'><i className='fa fa-fw fa-sign-out' /></a>
const messages = defineMessages({
heading: { id: 'getting_started.heading', defaultMessage: 'Getting started' },
- public_timeline: { id: 'navigation_bar.public_timeline', defaultMessage: 'Public timeline' },
+ public_timeline: { id: 'navigation_bar.public_timeline', defaultMessage: 'Whole Known Network' },
+ community_timeline: { id: 'navigation_bar.community_timeline', defaultMessage: 'Public timeline' },
preferences: { id: 'navigation_bar.preferences', defaultMessage: 'Preferences' },
follow_requests: { id: 'navigation_bar.follow_requests', defaultMessage: 'Follow requests' },
sign_out: { id: 'navigation_bar.logout', defaultMessage: 'Sign out' },
return (
<Column icon='asterisk' heading={intl.formatMessage(messages.heading)}>
<div style={{ position: 'relative' }}>
+ <ColumnLink icon='users' text={intl.formatMessage(messages.community_timeline)} to='/timelines/community' />
<ColumnLink icon='globe' text={intl.formatMessage(messages.public_timeline)} to='/timelines/public' />
<ColumnLink icon='cog' text={intl.formatMessage(messages.preferences)} href='/settings/preferences' />
<ColumnLink icon='star' text={intl.formatMessage(messages.favourites)} to='/favourites' />
updateTimeline,
deleteFromTimelines
} from '../../actions/timelines';
-import { defineMessages, injectIntl } from 'react-intl';
+import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import ColumnBackButtonSlim from '../../components/column_back_button_slim';
import createStream from '../../stream';
const messages = defineMessages({
- title: { id: 'column.public', defaultMessage: 'Public' }
+ title: { id: 'column.public', defaultMessage: 'Whole Known Network' }
});
const mapStateToProps = state => ({
return (
<Column icon='globe' heading={intl.formatMessage(messages.title)}>
<ColumnBackButtonSlim />
- <StatusListContainer type='public' />
+ <StatusListContainer type='public' 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>
);
},
import { expandTimeline, scrollTopTimeline } from '../../../actions/timelines';
import Immutable from 'immutable';
import { createSelector } from 'reselect';
+import { debounce } from 'react-decoration';
const getStatusIds = createSelector([
(state, { type }) => state.getIn(['settings', type], Immutable.Map()),
const mapDispatchToProps = (dispatch, { type, id }) => ({
+ @debounce(300, true)
onScrollToBottom () {
dispatch(scrollTopTimeline(type, false));
dispatch(expandTimeline(type, id));
},
+ @debounce(300, true)
onScrollToTop () {
dispatch(scrollTopTimeline(type, true));
},
+ @debounce(500)
onScroll () {
dispatch(scrollTopTimeline(type, false));
}
"getting_started.about_developer": "The developer of this project can be followed as Gargron@mastodon.social",
"getting_started.open_source_notice": "Mastodon is open source software. You can contribute or report issues on github at {github}.",
"column.home": "Home",
- "column.mentions": "Mentions",
- "column.public": "Public",
+ "column.community": "Public",
+ "column.public": "Whole Known Network",
"column.notifications": "Notifications",
"tabs_bar.compose": "Compose",
"tabs_bar.home": "Home",
"compose_form.unlisted": "Do not display in public timeline",
"navigation_bar.edit_profile": "Edit profile",
"navigation_bar.preferences": "Preferences",
- "navigation_bar.public_timeline": "Public timeline",
+ "navigation_bar.community_timeline": "Public timeline",
+ "navigation_bar.public_timeline": "Whole Known Network",
"navigation_bar.logout": "Logout",
"reply_indicator.cancel": "Cancel",
"search.placeholder": "Search",
const initialState = Immutable.Map({
home: Immutable.Map({
+ path: () => '/api/v1/timelines/home',
+ next: null,
isLoading: false,
loaded: false,
top: true,
items: Immutable.List()
}),
- mentions: Immutable.Map({
+ public: Immutable.Map({
+ path: () => '/api/v1/timelines/public',
+ next: null,
isLoading: false,
loaded: false,
top: true,
items: Immutable.List()
}),
- public: Immutable.Map({
+ community: Immutable.Map({
+ path: () => '/api/v1/timelines/public',
+ next: null,
+ params: { local: true },
isLoading: false,
loaded: false,
top: true,
}),
tag: Immutable.Map({
+ path: (id) => `/api/v1/timelines/tag/${id}`,
+ next: null,
isLoading: false,
id: null,
loaded: false,
return state;
};
-const normalizeTimeline = (state, timeline, statuses, replace = false) => {
+const normalizeTimeline = (state, timeline, statuses, next) => {
let ids = Immutable.List();
const loaded = state.getIn([timeline, 'loaded']);
state = state.setIn([timeline, 'loaded'], true);
state = state.setIn([timeline, 'isLoading'], false);
+ state = state.setIn([timeline, 'next'], next);
return state.updateIn([timeline, 'items'], Immutable.List(), list => (loaded ? list.unshift(...ids) : ids));
};
-const appendNormalizedTimeline = (state, timeline, statuses) => {
+const appendNormalizedTimeline = (state, timeline, statuses, next) => {
let moreIds = Immutable.List();
statuses.forEach((status, i) => {
});
state = state.setIn([timeline, 'isLoading'], false);
+ state = state.setIn([timeline, 'next'], next);
return state.updateIn([timeline, 'items'], Immutable.List(), list => list.push(...moreIds));
};
}
// Remove references from timelines
- ['home', 'mentions', 'public', 'tag'].forEach(function (timeline) {
+ ['home', 'public', 'community', 'tag'].forEach(function (timeline) {
state = state.updateIn([timeline, 'items'], list => list.filterNot(item => item === id));
});
};
const resetTimeline = (state, timeline, id) => {
- if (timeline === 'tag' && state.getIn([timeline, 'id']) !== id) {
+ if (timeline === 'tag' && typeof id !== 'undefined' && state.getIn([timeline, 'id']) !== id) {
state = state.update(timeline, map => map
.set('id', id)
.set('isLoading', true)
case TIMELINE_EXPAND_FAIL:
return state.setIn([action.timeline, 'isLoading'], false);
case TIMELINE_REFRESH_SUCCESS:
- return normalizeTimeline(state, action.timeline, Immutable.fromJS(action.statuses));
+ return normalizeTimeline(state, action.timeline, Immutable.fromJS(action.statuses), action.next);
case TIMELINE_EXPAND_SUCCESS:
- return appendNormalizedTimeline(state, action.timeline, Immutable.fromJS(action.statuses));
+ return appendNormalizedTimeline(state, action.timeline, Immutable.fromJS(action.statuses), action.next);
case TIMELINE_UPDATE:
return updateTimeline(state, action.timeline, Immutable.fromJS(action.status), action.references);
case TIMELINE_DELETE:
validates :domain, presence: true, uniqueness: true
def self.blocked?(domain)
- where(domain: domain).exists?
+ where(domain: domain, severity: :suspend).exists?
end
end
private
def filter_from_context?(status, account)
- account&.blocking?(status.account_id) || !status.permitted?(account)
+ account&.blocking?(status.account_id) || (status.account.silenced? && !account&.following?(status.account_id)) || !status.permitted?(account)
end
end