"whole known network" which is what public timeline used to be Only domain blocks with suspend severity will block PuSH subscriptions Silenced accounts should not appear in conversations unless followedmaster
@@ -85,6 +85,7 @@ export function submitCompose() { | |||
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) { | |||
@@ -1,4 +1,4 @@ | |||
import api from '../api' | |||
import api, { getLinks } from '../api' | |||
import Immutable from 'immutable'; | |||
export const TIMELINE_UPDATE = 'TIMELINE_UPDATE'; | |||
@@ -14,12 +14,13 @@ export const TIMELINE_EXPAND_FAIL = 'TIMELINE_EXPAND_FAIL'; | |||
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 | |||
}; | |||
}; | |||
@@ -69,25 +70,22 @@ export function refreshTimeline(timeline, id = null) { | |||
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)); | |||
}); | |||
}; | |||
@@ -102,50 +100,48 @@ export function 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 | |||
}; | |||
}; | |||
@@ -21,6 +21,7 @@ import UI from '../features/ui'; | |||
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'; | |||
@@ -116,6 +117,7 @@ const Mastodon = React.createClass({ | |||
<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} /> | |||
@@ -0,0 +1,73 @@ | |||
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)); |
@@ -4,6 +4,7 @@ import { injectIntl, defineMessages } from 'react-intl'; | |||
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' } | |||
}); | |||
@@ -15,6 +16,7 @@ const Drawer = ({ children, withHeader, intl }) => { | |||
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> | |||
@@ -7,7 +7,8 @@ import ImmutablePropTypes from 'react-immutable-proptypes'; | |||
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' }, | |||
@@ -30,6 +31,7 @@ const GettingStarted = ({ intl, me }) => { | |||
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' /> | |||
@@ -7,12 +7,12 @@ import { | |||
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 => ({ | |||
@@ -63,7 +63,7 @@ const PublicTimeline = React.createClass({ | |||
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> | |||
); | |||
}, | |||
@@ -3,6 +3,7 @@ import StatusList from '../../../components/status_list'; | |||
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()), | |||
@@ -40,15 +41,18 @@ const mapStateToProps = (state, props) => ({ | |||
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)); | |||
} | |||
@@ -28,8 +28,8 @@ const en = { | |||
"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", | |||
@@ -45,7 +45,8 @@ const en = { | |||
"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", | |||
@@ -31,20 +31,27 @@ import Immutable from 'immutable'; | |||
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, | |||
@@ -52,6 +59,8 @@ const initialState = Immutable.Map({ | |||
}), | |||
tag: Immutable.Map({ | |||
path: (id) => `/api/v1/timelines/tag/${id}`, | |||
next: null, | |||
isLoading: false, | |||
id: null, | |||
loaded: false, | |||
@@ -81,7 +90,7 @@ const normalizeStatus = (state, status) => { | |||
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']); | |||
@@ -92,11 +101,12 @@ const normalizeTimeline = (state, timeline, statuses, replace = false) => { | |||
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) => { | |||
@@ -105,6 +115,7 @@ const appendNormalizedTimeline = (state, timeline, statuses) => { | |||
}); | |||
state = state.setIn([timeline, 'isLoading'], false); | |||
state = state.setIn([timeline, 'next'], next); | |||
return state.updateIn([timeline, 'items'], Immutable.List(), list => list.push(...moreIds)); | |||
}; | |||
@@ -169,7 +180,7 @@ const deleteStatus = (state, id, accountId, references, reblogOf) => { | |||
} | |||
// 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)); | |||
}); | |||
@@ -221,7 +232,7 @@ const normalizeContext = (state, id, ancestors, descendants) => { | |||
}; | |||
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) | |||
@@ -243,9 +254,9 @@ export default function timelines(state = initialState, action) { | |||
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: | |||
@@ -6,6 +6,6 @@ class DomainBlock < ApplicationRecord | |||
validates :domain, presence: true, uniqueness: true | |||
def self.blocked?(domain) | |||
where(domain: domain).exists? | |||
where(domain: domain, severity: :suspend).exists? | |||
end | |||
end |
@@ -192,6 +192,6 @@ class Status < ApplicationRecord | |||
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 |