add local only timeline
This commit is contained in:
parent
96a814d31e
commit
25cc000a97
@ -0,0 +1,44 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-unsafe-call,
|
||||||
|
@typescript-eslint/no-unsafe-return,
|
||||||
|
@typescript-eslint/no-unsafe-assignment,
|
||||||
|
@typescript-eslint/no-unsafe-member-access
|
||||||
|
-- the settings store is not yet typed */
|
||||||
|
import { useCallback } from 'react';
|
||||||
|
|
||||||
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
|
import { useAppSelector, useAppDispatch } from 'mastodon/store';
|
||||||
|
|
||||||
|
import { changeSetting } from '../../../actions/settings';
|
||||||
|
import SettingToggle from '../../notifications/components/setting_toggle';
|
||||||
|
|
||||||
|
export const ColumnSettings: React.FC = () => {
|
||||||
|
const settings = useAppSelector((state) => state.settings.get('local'));
|
||||||
|
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const onChange = useCallback(
|
||||||
|
(key: string, checked: boolean) => {
|
||||||
|
dispatch(changeSetting(['local', ...key], checked));
|
||||||
|
},
|
||||||
|
[dispatch],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className='column-settings__row'>
|
||||||
|
<SettingToggle
|
||||||
|
prefix='local_timeline'
|
||||||
|
settings={settings}
|
||||||
|
settingPath={['shows', 'media']}
|
||||||
|
onChange={onChange}
|
||||||
|
label={
|
||||||
|
<FormattedMessage
|
||||||
|
id='local.column_settings.show_media'
|
||||||
|
defaultMessage='Show Media'
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,14 @@
|
|||||||
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
import { DismissableBanner } from 'mastodon/components/dismissable_banner';
|
||||||
|
import { title } from 'mastodon/initial_state';
|
||||||
|
|
||||||
|
export const ExplorePrompt = () => (
|
||||||
|
<DismissableBanner id='local.explore_prompt'>
|
||||||
|
<p>
|
||||||
|
<FormattedMessage
|
||||||
|
id='local.explore_prompt.body'
|
||||||
|
defaultMessage="Your home feed will have a mix of posts from the hashtags you've chosen to follow, the people you've chosen to follow, and the posts they boost. If that feels too quiet, you may want to:" values={{ title }}
|
||||||
|
/>
|
||||||
|
</p>
|
||||||
|
</DismissableBanner>
|
||||||
|
);
|
195
app/javascript/mastodon/features/local_timeline/index.jsx
Normal file
195
app/javascript/mastodon/features/local_timeline/index.jsx
Normal file
@ -0,0 +1,195 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { PureComponent } from 'react';
|
||||||
|
|
||||||
|
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
|
import { Helmet } from 'react-helmet';
|
||||||
|
|
||||||
|
import { List as ImmutableList } from 'immutable';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
|
||||||
|
import { NotSignedInIndicator } from 'mastodon/components/not_signed_in_indicator';
|
||||||
|
import { me } from 'mastodon/initial_state';
|
||||||
|
|
||||||
|
import { addColumn, removeColumn, moveColumn } from '../../actions/columns';
|
||||||
|
import { expandHomeTimeline } from '../../actions/timelines';
|
||||||
|
import Column from '../../components/column';
|
||||||
|
import ColumnHeader from '../../components/column_header';
|
||||||
|
import LocalStatusListContainer from '../ui/containers/local_status_list_container';
|
||||||
|
|
||||||
|
import { ColumnSettings } from './components/column_settings';
|
||||||
|
import { ExplorePrompt } from './components/explore_prompt';
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
title: { id: 'column.local', defaultMessage: 'local' },
|
||||||
|
});
|
||||||
|
|
||||||
|
const getHomeFeedSpeed = createSelector([
|
||||||
|
state => state.getIn(['timelines', 'home', 'items'], ImmutableList()),
|
||||||
|
state => state.getIn(['timelines', 'home', 'pendingItems'], ImmutableList()),
|
||||||
|
state => state.get('statuses'),
|
||||||
|
], (statusIds, pendingStatusIds, statusMap) => {
|
||||||
|
const recentStatusIds = pendingStatusIds.size > 0 ? pendingStatusIds : statusIds;
|
||||||
|
const statuses = recentStatusIds.filter(id => id !== null).map(id => statusMap.get(id)).filter(status => status?.get('account') !== me).take(20);
|
||||||
|
|
||||||
|
if (statuses.isEmpty()) {
|
||||||
|
return {
|
||||||
|
gap: 0,
|
||||||
|
newest: new Date(0),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const datetimes = statuses.map(status => status.get('created_at', 0));
|
||||||
|
const oldest = new Date(datetimes.min());
|
||||||
|
const newest = new Date(datetimes.max());
|
||||||
|
const averageGap = (newest - oldest) / (1000 * (statuses.size + 1)); // Average gap between posts on first page in seconds
|
||||||
|
|
||||||
|
return {
|
||||||
|
gap: averageGap,
|
||||||
|
newest,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const homeTooSlow = createSelector([
|
||||||
|
state => state.getIn(['timelines', 'home', 'isLoading']),
|
||||||
|
state => state.getIn(['timelines', 'home', 'isPartial']),
|
||||||
|
getHomeFeedSpeed,
|
||||||
|
], (isLoading, isPartial, speed) =>
|
||||||
|
!isLoading && !isPartial // Only if the home feed has finished loading
|
||||||
|
&& (
|
||||||
|
(speed.gap > (30 * 60) // If the average gap between posts is more than 30 minutes
|
||||||
|
|| (Date.now() - speed.newest) > (1000 * 3600)) // If the most recent post is from over an hour ago
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
const mapStateToProps = state => ({
|
||||||
|
hasUnread: state.getIn(['timelines', 'home', 'unread']) > 0,
|
||||||
|
isPartial: state.getIn(['timelines', 'home', 'isPartial']),
|
||||||
|
tooSlow: homeTooSlow(state),
|
||||||
|
});
|
||||||
|
|
||||||
|
class HomeTimeline extends PureComponent {
|
||||||
|
|
||||||
|
static contextTypes = {
|
||||||
|
identity: PropTypes.object,
|
||||||
|
};
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
dispatch: PropTypes.func.isRequired,
|
||||||
|
intl: PropTypes.object.isRequired,
|
||||||
|
hasUnread: PropTypes.bool,
|
||||||
|
isPartial: PropTypes.bool,
|
||||||
|
columnId: PropTypes.string,
|
||||||
|
multiColumn: PropTypes.bool,
|
||||||
|
tooSlow: 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 = maxId => {
|
||||||
|
this.props.dispatch(expandHomeTimeline({ maxId }));
|
||||||
|
};
|
||||||
|
|
||||||
|
componentDidUpdate (prevProps) {
|
||||||
|
this._checkIfReloadNeeded(prevProps.isPartial, this.props.isPartial);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount () {
|
||||||
|
this._stopPolling();
|
||||||
|
}
|
||||||
|
|
||||||
|
_checkIfReloadNeeded (wasPartial, isPartial) {
|
||||||
|
const { dispatch } = this.props;
|
||||||
|
|
||||||
|
if (wasPartial === isPartial) {
|
||||||
|
return;
|
||||||
|
} else if (!wasPartial && isPartial) {
|
||||||
|
this.polling = setInterval(() => {
|
||||||
|
dispatch(expandHomeTimeline());
|
||||||
|
}, 3000);
|
||||||
|
} else if (wasPartial && !isPartial) {
|
||||||
|
this._stopPolling();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_stopPolling () {
|
||||||
|
if (this.polling) {
|
||||||
|
clearInterval(this.polling);
|
||||||
|
this.polling = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const { intl, hasUnread, columnId, multiColumn, tooSlow } = this.props;
|
||||||
|
const pinned = !!columnId;
|
||||||
|
const { signedIn } = this.context.identity;
|
||||||
|
const banners = [];
|
||||||
|
|
||||||
|
|
||||||
|
if (tooSlow) {
|
||||||
|
banners.push(<ExplorePrompt key='explore-prompt' />);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Column bindToDocument={!multiColumn} ref={this.setRef} label={intl.formatMessage(messages.title)}>
|
||||||
|
<ColumnHeader
|
||||||
|
icon='home'
|
||||||
|
active={hasUnread}
|
||||||
|
title={intl.formatMessage(messages.title)}
|
||||||
|
onPin={this.handlePin}
|
||||||
|
onMove={this.handleMove}
|
||||||
|
onClick={this.handleHeaderClick}
|
||||||
|
pinned={pinned}
|
||||||
|
multiColumn={multiColumn}
|
||||||
|
>
|
||||||
|
<ColumnSettings />
|
||||||
|
</ColumnHeader>
|
||||||
|
|
||||||
|
{signedIn ? (
|
||||||
|
<LocalStatusListContainer
|
||||||
|
prepend={banners}
|
||||||
|
alwaysPrepend
|
||||||
|
trackScroll={!pinned}
|
||||||
|
scrollKey={`home_timeline-${columnId}`}
|
||||||
|
onLoadMore={this.handleLoadMore}
|
||||||
|
timelineId='home'
|
||||||
|
emptyMessage={<FormattedMessage id='empty_column.home' defaultMessage='Your home timeline is empty! Follow more people to fill it up.' />}
|
||||||
|
bindToDocument={!multiColumn}
|
||||||
|
/>
|
||||||
|
) : <NotSignedInIndicator />}
|
||||||
|
|
||||||
|
<Helmet>
|
||||||
|
<title>{intl.formatMessage(messages.title)}</title>
|
||||||
|
<meta name='robots' content='noindex' />
|
||||||
|
</Helmet>
|
||||||
|
</Column>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(mapStateToProps)(injectIntl(HomeTimeline));
|
@ -1,74 +0,0 @@
|
|||||||
import { connect } from 'react-redux';
|
|
||||||
|
|
||||||
import { initBoostModal } from '../../../actions/boosts';
|
|
||||||
import { mentionCompose } from '../../../actions/compose';
|
|
||||||
import {
|
|
||||||
reblog,
|
|
||||||
favourite,
|
|
||||||
unreblog,
|
|
||||||
unfavourite,
|
|
||||||
} from '../../../actions/interactions';
|
|
||||||
import {
|
|
||||||
hideStatus,
|
|
||||||
revealStatus,
|
|
||||||
} from '../../../actions/statuses';
|
|
||||||
import { boostModal } from '../../../initial_state';
|
|
||||||
import { makeGetNotification, makeGetStatus, makeGetReport } from '../../../selectors';
|
|
||||||
import Notification from '../components/notification';
|
|
||||||
|
|
||||||
const makeMapStateToProps = () => {
|
|
||||||
const getNotification = makeGetNotification();
|
|
||||||
const getStatus = makeGetStatus();
|
|
||||||
const getReport = makeGetReport();
|
|
||||||
|
|
||||||
const mapStateToProps = (state, props) => {
|
|
||||||
const notification = getNotification(state, props.notification, props.accountId);
|
|
||||||
return {
|
|
||||||
notification: notification ,
|
|
||||||
status: notification.get('status') ? getStatus(state, { id: notification.get('status'), contextType: 'notifications' }) : null,
|
|
||||||
report: notification.get('report') ? getReport(state, notification.get('report'), notification.getIn(['report', 'target_account', 'id'])) : null,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
return mapStateToProps;
|
|
||||||
};
|
|
||||||
|
|
||||||
const mapDispatchToProps = dispatch => ({
|
|
||||||
onMention: (account, router) => {
|
|
||||||
dispatch(mentionCompose(account, router));
|
|
||||||
},
|
|
||||||
|
|
||||||
onModalReblog (status, privacy) {
|
|
||||||
dispatch(reblog(status, privacy));
|
|
||||||
},
|
|
||||||
|
|
||||||
onReblog (status, e) {
|
|
||||||
if (status.get('reblogged')) {
|
|
||||||
dispatch(unreblog(status));
|
|
||||||
} else {
|
|
||||||
if (e.shiftKey || !boostModal) {
|
|
||||||
this.onModalReblog(status);
|
|
||||||
} else {
|
|
||||||
dispatch(initBoostModal({ status, onReblog: this.onModalReblog }));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
onFavourite (status) {
|
|
||||||
if (status.get('favourited')) {
|
|
||||||
dispatch(unfavourite(status));
|
|
||||||
} else {
|
|
||||||
dispatch(favourite(status));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
onToggleHidden (status) {
|
|
||||||
if (status.get('hidden')) {
|
|
||||||
dispatch(revealStatus(status.get('id')));
|
|
||||||
} else {
|
|
||||||
dispatch(hideStatus(status.get('id')));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export default connect(makeMapStateToProps, mapDispatchToProps)(Notification);
|
|
@ -1,74 +0,0 @@
|
|||||||
import { connect } from 'react-redux';
|
|
||||||
|
|
||||||
import { initBoostModal } from '../../../actions/boosts';
|
|
||||||
import { mentionCompose } from '../../../actions/compose';
|
|
||||||
import {
|
|
||||||
reblog,
|
|
||||||
favourite,
|
|
||||||
unreblog,
|
|
||||||
unfavourite,
|
|
||||||
} from '../../../actions/interactions';
|
|
||||||
import {
|
|
||||||
hideStatus,
|
|
||||||
revealStatus,
|
|
||||||
} from '../../../actions/statuses';
|
|
||||||
import { boostModal } from '../../../initial_state';
|
|
||||||
import { makeGetNotification, makeGetStatus, makeGetReport } from '../../../selectors';
|
|
||||||
import Notification from '../components/notification';
|
|
||||||
|
|
||||||
const makeMapStateToProps = () => {
|
|
||||||
const getNotification = makeGetNotification();
|
|
||||||
const getStatus = makeGetStatus();
|
|
||||||
const getReport = makeGetReport();
|
|
||||||
|
|
||||||
const mapStateToProps = (state, props) => {
|
|
||||||
const notification = getNotification(state, props.notification, props.accountId);
|
|
||||||
return {
|
|
||||||
notification: (notification.get('status') && getStatus(state, { id: notification.get('status'), contextType: 'notifications' }).get('visibility')!=='direct') || notification.get('report') ? notification : null,
|
|
||||||
status: notification.get('status') ? getStatus(state, { id: notification.get('status'), contextType: 'notifications' }) : null,
|
|
||||||
report: notification.get('report') ? getReport(state, notification.get('report'), notification.getIn(['report', 'target_account', 'id'])) : null,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
return mapStateToProps;
|
|
||||||
};
|
|
||||||
|
|
||||||
const mapDispatchToProps = dispatch => ({
|
|
||||||
onMention: (account, router) => {
|
|
||||||
dispatch(mentionCompose(account, router));
|
|
||||||
},
|
|
||||||
|
|
||||||
onModalReblog (status, privacy) {
|
|
||||||
dispatch(reblog(status, privacy));
|
|
||||||
},
|
|
||||||
|
|
||||||
onReblog (status, e) {
|
|
||||||
if (status.get('reblogged')) {
|
|
||||||
dispatch(unreblog(status));
|
|
||||||
} else {
|
|
||||||
if (e.shiftKey || !boostModal) {
|
|
||||||
this.onModalReblog(status);
|
|
||||||
} else {
|
|
||||||
dispatch(initBoostModal({ status, onReblog: this.onModalReblog }));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
onFavourite (status) {
|
|
||||||
if (status.get('favourited')) {
|
|
||||||
dispatch(unfavourite(status));
|
|
||||||
} else {
|
|
||||||
dispatch(favourite(status));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
onToggleHidden (status) {
|
|
||||||
if (status.get('hidden')) {
|
|
||||||
dispatch(revealStatus(status.get('id')));
|
|
||||||
} else {
|
|
||||||
dispatch(hideStatus(status.get('id')));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export default connect(makeMapStateToProps, mapDispatchToProps)(Notification);
|
|
@ -45,9 +45,9 @@ class StatusCheckBox extends PureComponent {
|
|||||||
|
|
||||||
const visibilityIconInfo = {
|
const visibilityIconInfo = {
|
||||||
'public': { icon: 'globe', text: intl.formatMessage(messages.public_short) },
|
'public': { icon: 'globe', text: intl.formatMessage(messages.public_short) },
|
||||||
'unlisted': { icon: 'unlock', text: intl.formatMessage(messages.unlisted_short) },
|
'unlisted': { icon: 'cloud', text: intl.formatMessage(messages.unlisted_short) },
|
||||||
'private': { icon: 'lock', text: intl.formatMessage(messages.private_short) },
|
'private': { icon: 'lock', text: intl.formatMessage(messages.private_short) },
|
||||||
'direct': { icon: 'at', text: intl.formatMessage(messages.direct_short) },
|
'direct': { icon: 'explore', text: intl.formatMessage(messages.direct_short) },
|
||||||
};
|
};
|
||||||
|
|
||||||
const visibilityIcon = visibilityIconInfo[status.get('visibility')];
|
const visibilityIcon = visibilityIconInfo[status.get('visibility')];
|
||||||
|
@ -76,7 +76,7 @@ class NavigationPanel extends Component {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{(signedIn) && (
|
{(signedIn) && (
|
||||||
<ColumnLink transparent to='/public/local' isActive={this.isFirehoseActive} icon='hashtag' text={intl.formatMessage(messages.firehose)} />
|
<ColumnLink transparent to='/local' isActive={this.isFirehoseActive} icon='hashtag' text={intl.formatMessage(messages.firehose)} />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!signedIn && (
|
{!signedIn && (
|
||||||
|
@ -0,0 +1,66 @@
|
|||||||
|
import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
|
||||||
|
import { debounce } from 'lodash';
|
||||||
|
|
||||||
|
import { scrollTopTimeline, loadPending } from '../../../actions/timelines';
|
||||||
|
import StatusList from '../../../components/status_list';
|
||||||
|
|
||||||
|
const makeGetStatusIds = (pending = false) => createSelector([
|
||||||
|
(state) => state.getIn(['settings', 'local'], ImmutableMap()),
|
||||||
|
(state, { type }) => state.getIn(['timelines', type, pending ? 'pendingItems' : 'items'], ImmutableList()),
|
||||||
|
(state) => state.get('statuses'),
|
||||||
|
(type) => type
|
||||||
|
], (columnSettings, statusIds, statuses,type) => {
|
||||||
|
return statusIds.filter(id => {
|
||||||
|
if (id === null) return true;
|
||||||
|
|
||||||
|
const statusForId = statuses.get(id);
|
||||||
|
|
||||||
|
let showStatus = true;
|
||||||
|
|
||||||
|
if (statusForId.get('visibility') === 'direct')
|
||||||
|
return false;
|
||||||
|
if (statusForId.get('in_reply_to_id')) {
|
||||||
|
showStatus = showStatus && statusForId.get('in_reply_to_account_id') === statusForId.get('account')
|
||||||
|
}
|
||||||
|
if (columnSettings.getIn(['shows', 'media']) === true) {
|
||||||
|
showStatus = showStatus && statusForId.get('media_attachments').size>0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return showStatus;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const makeMapStateToProps = () => {
|
||||||
|
const getStatusIds = makeGetStatusIds();
|
||||||
|
const getPendingStatusIds = makeGetStatusIds(true);
|
||||||
|
|
||||||
|
const mapStateToProps = (state, { timelineId }) => ({
|
||||||
|
statusIds: getStatusIds(state, { type: timelineId }),
|
||||||
|
lastId: state.getIn(['timelines', timelineId, 'items'])?.last(),
|
||||||
|
isLoading: state.getIn(['timelines', timelineId, 'isLoading'], true),
|
||||||
|
isPartial: state.getIn(['timelines', timelineId, 'isPartial'], false),
|
||||||
|
hasMore: state.getIn(['timelines', timelineId, 'hasMore']),
|
||||||
|
numPending: getPendingStatusIds(state, { type: timelineId }).size,
|
||||||
|
});
|
||||||
|
|
||||||
|
return mapStateToProps;
|
||||||
|
};
|
||||||
|
|
||||||
|
const mapDispatchToProps = (dispatch, { timelineId }) => ({
|
||||||
|
|
||||||
|
onScrollToTop: debounce(() => {
|
||||||
|
dispatch(scrollTopTimeline(timelineId, true));
|
||||||
|
}, 100),
|
||||||
|
|
||||||
|
onScroll: debounce(() => {
|
||||||
|
dispatch(scrollTopTimeline(timelineId, false));
|
||||||
|
}, 100),
|
||||||
|
|
||||||
|
onLoadPending: () => dispatch(loadPending(timelineId)),
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
export default connect(makeMapStateToProps, mapDispatchToProps)(StatusList);
|
@ -12,7 +12,8 @@ const makeGetStatusIds = (pending = false) => createSelector([
|
|||||||
(state, { type }) => state.getIn(['settings', type], ImmutableMap()),
|
(state, { type }) => state.getIn(['settings', type], ImmutableMap()),
|
||||||
(state, { type }) => state.getIn(['timelines', type, pending ? 'pendingItems' : 'items'], ImmutableList()),
|
(state, { type }) => state.getIn(['timelines', type, pending ? 'pendingItems' : 'items'], ImmutableList()),
|
||||||
(state) => state.get('statuses'),
|
(state) => state.get('statuses'),
|
||||||
], (columnSettings, statusIds, statuses) => {
|
(type) => type
|
||||||
|
], (columnSettings, statusIds, statuses,type) => {
|
||||||
return statusIds.filter(id => {
|
return statusIds.filter(id => {
|
||||||
if (id === null) return true;
|
if (id === null) return true;
|
||||||
|
|
||||||
|
@ -40,6 +40,7 @@ import {
|
|||||||
AccountTimeline,
|
AccountTimeline,
|
||||||
AccountGallery,
|
AccountGallery,
|
||||||
HomeTimeline,
|
HomeTimeline,
|
||||||
|
LocalTimeline,
|
||||||
Followers,
|
Followers,
|
||||||
Following,
|
Following,
|
||||||
Reblogs,
|
Reblogs,
|
||||||
@ -199,7 +200,12 @@ class SwitchingColumnsArea extends PureComponent {
|
|||||||
<Redirect from='/timelines/public/local' to='/public/local' exact />
|
<Redirect from='/timelines/public/local' to='/public/local' exact />
|
||||||
<WrappedRoute path='/public' exact component={Firehose} componentParams={{ feedType: 'public' }} content={children} />
|
<WrappedRoute path='/public' exact component={Firehose} componentParams={{ feedType: 'public' }} content={children} />
|
||||||
<WrappedRoute path='/public/local' exact component={Firehose} componentParams={{ feedType: 'community' }} content={children} />
|
<WrappedRoute path='/public/local' exact component={Firehose} componentParams={{ feedType: 'community' }} content={children} />
|
||||||
<WrappedRoute path='/public/remote' exact component={Firehose} componentParams={{ feedType: 'public:remote' }} content={children} />
|
|
||||||
|
|
||||||
|
<Redirect from='/timelines/local' to='/local' exact />
|
||||||
|
<WrappedRoute path='/local' exact component={LocalTimeline} content={children} />
|
||||||
|
|
||||||
|
|
||||||
<WrappedRoute path={['/conversations', '/timelines/direct']} component={DirectTimeline} content={children} />
|
<WrappedRoute path={['/conversations', '/timelines/direct']} component={DirectTimeline} content={children} />
|
||||||
<WrappedRoute path='/tags/:id' component={HashtagTimeline} content={children} />
|
<WrappedRoute path='/tags/:id' component={HashtagTimeline} content={children} />
|
||||||
<WrappedRoute path='/lists/:id' component={ListTimeline} content={children} />
|
<WrappedRoute path='/lists/:id' component={ListTimeline} content={children} />
|
||||||
|
@ -14,6 +14,9 @@ export function HomeTimeline () {
|
|||||||
return import(/* webpackChunkName: "features/home_timeline" */'../../home_timeline');
|
return import(/* webpackChunkName: "features/home_timeline" */'../../home_timeline');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function LocalTimeline () {
|
||||||
|
return import(/* webpackChunkName: "features/home_timeline" */'../../local_timeline');
|
||||||
|
}
|
||||||
export function PublicTimeline () {
|
export function PublicTimeline () {
|
||||||
return import(/* webpackChunkName: "features/public_timeline" */'../../public_timeline');
|
return import(/* webpackChunkName: "features/public_timeline" */'../../public_timeline');
|
||||||
}
|
}
|
||||||
|
@ -117,6 +117,7 @@
|
|||||||
"column.firehose": "Live feeds",
|
"column.firehose": "Live feeds",
|
||||||
"column.follow_requests": "Follow requests",
|
"column.follow_requests": "Follow requests",
|
||||||
"column.home": "Home",
|
"column.home": "Home",
|
||||||
|
"column.local": "feed",
|
||||||
"column.lists": "Lists",
|
"column.lists": "Lists",
|
||||||
"column.mutes": "Muted users",
|
"column.mutes": "Muted users",
|
||||||
"column.notifications": "Notifications",
|
"column.notifications": "Notifications",
|
||||||
@ -239,6 +240,7 @@
|
|||||||
"empty_column.followed_tags": "You have not followed any hashtags yet. When you do, they will show up here.",
|
"empty_column.followed_tags": "You have not followed any hashtags yet. When you do, they will show up here.",
|
||||||
"empty_column.hashtag": "There is nothing in this hashtag yet.",
|
"empty_column.hashtag": "There is nothing in this hashtag yet.",
|
||||||
"empty_column.home": "Your home timeline is empty! Follow more people to fill it up.",
|
"empty_column.home": "Your home timeline is empty! Follow more people to fill it up.",
|
||||||
|
"empty_column.local": "Your home timeline is empty! Follow more people to fill it up.",
|
||||||
"empty_column.list": "There is nothing in this list yet. When members of this list publish new posts, they will appear here.",
|
"empty_column.list": "There is nothing in this list yet. When members of this list publish new posts, they will appear here.",
|
||||||
"empty_column.lists": "You don't have any lists yet. When you create one, it will show up here.",
|
"empty_column.lists": "You don't have any lists yet. When you create one, it will show up here.",
|
||||||
"empty_column.mutes": "You haven't muted any users yet.",
|
"empty_column.mutes": "You haven't muted any users yet.",
|
||||||
@ -309,8 +311,14 @@
|
|||||||
"home.column_settings.basic": "Basic",
|
"home.column_settings.basic": "Basic",
|
||||||
"home.column_settings.show_reblogs": "Show boosts",
|
"home.column_settings.show_reblogs": "Show boosts",
|
||||||
"home.column_settings.show_replies": "Show replies",
|
"home.column_settings.show_replies": "Show replies",
|
||||||
|
"local.column_settings.basic": "local",
|
||||||
|
"local.column_settings.show_media": "Show Media",
|
||||||
"home.explore_prompt.body": "Your home feed will have a mix of posts from the hashtags you've chosen to follow, the people you've chosen to follow, and the posts they boost. If that feels too quiet, you may want to:",
|
"home.explore_prompt.body": "Your home feed will have a mix of posts from the hashtags you've chosen to follow, the people you've chosen to follow, and the posts they boost. If that feels too quiet, you may want to:",
|
||||||
"home.explore_prompt.title": "This is your home base within Mastodon.",
|
"home.explore_prompt.title": "This is your home base within Mastodon.",
|
||||||
|
|
||||||
|
"local.explore_prompt.body": "Your home feed will have a mix of posts from the hashtags you've chosen to follow, the people you've chosen to follow, and the posts they boost. If that feels too quiet, you may want to:",
|
||||||
|
"local.explore_prompt.title": "This is your home base within Mastodon.",
|
||||||
|
|
||||||
"home.hide_announcements": "Hide announcements",
|
"home.hide_announcements": "Hide announcements",
|
||||||
"home.pending_critical_update.body": "Please update your Mastodon server as soon as possible!",
|
"home.pending_critical_update.body": "Please update your Mastodon server as soon as possible!",
|
||||||
"home.pending_critical_update.link": "See updates",
|
"home.pending_critical_update.link": "See updates",
|
||||||
|
@ -117,6 +117,7 @@
|
|||||||
"column.firehose": "실시간 피드",
|
"column.firehose": "실시간 피드",
|
||||||
"column.follow_requests": "팔로우 요청",
|
"column.follow_requests": "팔로우 요청",
|
||||||
"column.home": "홈",
|
"column.home": "홈",
|
||||||
|
"column.local": "실시간 피드",
|
||||||
"column.lists": "리스트",
|
"column.lists": "리스트",
|
||||||
"column.mutes": "뮤트한 사용자",
|
"column.mutes": "뮤트한 사용자",
|
||||||
"column.notifications": "알림",
|
"column.notifications": "알림",
|
||||||
@ -239,6 +240,7 @@
|
|||||||
"empty_column.followed_tags": "아직 아무 해시태그도 팔로우하고 있지 않습니다. 해시태그를 팔로우하면, 여기에 표시됩니다.",
|
"empty_column.followed_tags": "아직 아무 해시태그도 팔로우하고 있지 않습니다. 해시태그를 팔로우하면, 여기에 표시됩니다.",
|
||||||
"empty_column.hashtag": "이 해시태그는 아직 사용되지 않았습니다.",
|
"empty_column.hashtag": "이 해시태그는 아직 사용되지 않았습니다.",
|
||||||
"empty_column.home": "당신의 홈 타임라인은 비어있습니다! 더 많은 사람들을 팔로우 하여 채워보세요.",
|
"empty_column.home": "당신의 홈 타임라인은 비어있습니다! 더 많은 사람들을 팔로우 하여 채워보세요.",
|
||||||
|
"empty_column.local": "당신의 실시간 피드는 비어있습니다! 더 많은 사람들을 팔로우 하여 채워보세요.",
|
||||||
"empty_column.list": "리스트에 아직 아무것도 없습니다. 리스트의 누군가가 게시물을 올리면 여기에 나타납니다.",
|
"empty_column.list": "리스트에 아직 아무것도 없습니다. 리스트의 누군가가 게시물을 올리면 여기에 나타납니다.",
|
||||||
"empty_column.lists": "아직 리스트가 없습니다. 리스트를 만들면 여기에 나타납니다.",
|
"empty_column.lists": "아직 리스트가 없습니다. 리스트를 만들면 여기에 나타납니다.",
|
||||||
"empty_column.mutes": "아직 아무도 뮤트하지 않았습니다.",
|
"empty_column.mutes": "아직 아무도 뮤트하지 않았습니다.",
|
||||||
@ -309,8 +311,14 @@
|
|||||||
"home.column_settings.basic": "기본",
|
"home.column_settings.basic": "기본",
|
||||||
"home.column_settings.show_reblogs": "부스트 표시",
|
"home.column_settings.show_reblogs": "부스트 표시",
|
||||||
"home.column_settings.show_replies": "답글 표시",
|
"home.column_settings.show_replies": "답글 표시",
|
||||||
|
"local.column_settings.basic": "실시간 피드",
|
||||||
|
"local.column_settings.show_media": "미디어만",
|
||||||
"home.explore_prompt.body": "홈 피드에는 내가 팔로우한 해시태그 그리고 팔로우한 사람과 부스트가 함께 나타납니다. 너무 고요하게 느껴진다면, 다음 것들을 살펴볼 수 있습니다.",
|
"home.explore_prompt.body": "홈 피드에는 내가 팔로우한 해시태그 그리고 팔로우한 사람과 부스트가 함께 나타납니다. 너무 고요하게 느껴진다면, 다음 것들을 살펴볼 수 있습니다.",
|
||||||
"home.explore_prompt.title": "이곳은 마스토돈의 내 본거지입니다.",
|
"home.explore_prompt.title": "이곳은 마스토돈의 내 본거지입니다.",
|
||||||
|
|
||||||
|
"local.explore_prompt.body": "실시간 피드에는 {title}에 있는 사람들의 답장이 아닌 최근 게시글을 살펴볼 수 있습니다.",
|
||||||
|
"local.explore_prompt.title": "이곳은 마스토돈의 내 본거지입니다.",
|
||||||
|
|
||||||
"home.hide_announcements": "공지사항 숨기기",
|
"home.hide_announcements": "공지사항 숨기기",
|
||||||
"home.pending_critical_update.body": "서둘러 마스토돈 서버를 업데이트 하세요!",
|
"home.pending_critical_update.body": "서둘러 마스토돈 서버를 업데이트 하세요!",
|
||||||
"home.pending_critical_update.link": "업데이트 보기",
|
"home.pending_critical_update.link": "업데이트 보기",
|
||||||
|
@ -20,7 +20,7 @@ const initialState = ImmutableMap({
|
|||||||
|
|
||||||
home: ImmutableMap({
|
home: ImmutableMap({
|
||||||
shows: ImmutableMap({
|
shows: ImmutableMap({
|
||||||
reblog: true,
|
media: true,
|
||||||
reply: true,
|
reply: true,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
@ -29,6 +29,17 @@ const initialState = ImmutableMap({
|
|||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
local: ImmutableMap({
|
||||||
|
shows: ImmutableMap({
|
||||||
|
media: false
|
||||||
|
}),
|
||||||
|
|
||||||
|
regex: ImmutableMap({
|
||||||
|
body: '',
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
|
||||||
|
|
||||||
notifications: ImmutableMap({
|
notifications: ImmutableMap({
|
||||||
alerts: ImmutableMap({
|
alerts: ImmutableMap({
|
||||||
follow: false,
|
follow: false,
|
||||||
|
@ -1551,21 +1551,21 @@ body.embed .detailed-status__favorites {
|
|||||||
body.embed .detailed-status__link > .fa-reply + span::after,
|
body.embed .detailed-status__link > .fa-reply + span::after,
|
||||||
.layout-multiple-columns .detailed-status__link > .fa-reply + span::after {
|
.layout-multiple-columns .detailed-status__link > .fa-reply + span::after {
|
||||||
color: var(--color-dim);
|
color: var(--color-dim);
|
||||||
content: 'Replies';
|
content: '답장';
|
||||||
font-weight: var(--font-weight-semibold);
|
font-weight: var(--font-weight-semibold);
|
||||||
}
|
}
|
||||||
|
|
||||||
body.embed .detailed-status__link > .fa-retweet + span::after,
|
body.embed .detailed-status__link > .fa-retweet + span::after,
|
||||||
.layout-multiple-columns .detailed-status__link > .fa-retweet + span::after {
|
.layout-multiple-columns .detailed-status__link > .fa-retweet + span::after {
|
||||||
color: var(--color-dim);
|
color: var(--color-dim);
|
||||||
content: 'Boosts';
|
content: '부스트';
|
||||||
font-weight: var(--font-weight-semibold);
|
font-weight: var(--font-weight-semibold);
|
||||||
}
|
}
|
||||||
|
|
||||||
body.embed .detailed-status__link > .fa-star + span::after,
|
body.embed .detailed-status__link > .fa-star + span::after,
|
||||||
.layout-multiple-columns .detailed-status__link > .fa-star + span::after {
|
.layout-multiple-columns .detailed-status__link > .fa-star + span::after {
|
||||||
color: var(--color-dim);
|
color: var(--color-dim);
|
||||||
content: 'Favourites';
|
content: '좋아요';
|
||||||
font-weight: var(--font-weight-semibold);
|
font-weight: var(--font-weight-semibold);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2961,6 +2961,11 @@ body.embed .button.logo-button:hover,
|
|||||||
order: 5;
|
order: 5;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.layout-multiple-columns .column-link[href="/local"] {
|
||||||
|
order: 5;
|
||||||
|
}
|
||||||
|
|
||||||
.layout-multiple-columns .column-link[href="/public"] {
|
.layout-multiple-columns .column-link[href="/public"] {
|
||||||
order: 6;
|
order: 6;
|
||||||
}
|
}
|
||||||
|
@ -1584,21 +1584,21 @@ body.embed > .activity-stream {
|
|||||||
body.embed .detailed-status__link > .fa-reply + span::after,
|
body.embed .detailed-status__link > .fa-reply + span::after,
|
||||||
.layout-single-column .detailed-status__link > .fa-reply + span::after {
|
.layout-single-column .detailed-status__link > .fa-reply + span::after {
|
||||||
color: var(--color-dim);
|
color: var(--color-dim);
|
||||||
content: 'Replies';
|
content: '답장';
|
||||||
font-weight: var(--font-weight-semibold);
|
font-weight: var(--font-weight-semibold);
|
||||||
}
|
}
|
||||||
|
|
||||||
body.embed .detailed-status__link > .fa-retweet + span::after,
|
body.embed .detailed-status__link > .fa-retweet + span::after,
|
||||||
.layout-single-column .detailed-status__link > .fa-retweet + span::after {
|
.layout-single-column .detailed-status__link > .fa-retweet + span::after {
|
||||||
color: var(--color-dim);
|
color: var(--color-dim);
|
||||||
content: 'Boosts';
|
content: '부스트';
|
||||||
font-weight: var(--font-weight-semibold);
|
font-weight: var(--font-weight-semibold);
|
||||||
}
|
}
|
||||||
|
|
||||||
body.embed .detailed-status__link > .fa-star + span::after,
|
body.embed .detailed-status__link > .fa-star + span::after,
|
||||||
.layout-single-column .detailed-status__link > .fa-star + span::after {
|
.layout-single-column .detailed-status__link > .fa-star + span::after {
|
||||||
color: var(--color-dim);
|
color: var(--color-dim);
|
||||||
content: 'Favourites';
|
content: '좋아요';
|
||||||
font-weight: var(--font-weight-semibold);
|
font-weight: var(--font-weight-semibold);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2997,6 +2997,10 @@ body.embed .button.logo-button:hover,
|
|||||||
order: 5;
|
order: 5;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.layout-single-column .column-link[href="/local"] {
|
||||||
|
order: 5;
|
||||||
|
}
|
||||||
|
|
||||||
.layout-single-column .column-link[href="/public"] {
|
.layout-single-column .column-link[href="/public"] {
|
||||||
order: 6;
|
order: 6;
|
||||||
}
|
}
|
||||||
|
@ -1551,21 +1551,21 @@ body.embed .detailed-status__favorites {
|
|||||||
body.embed .detailed-status__link > .fa-reply + span::after,
|
body.embed .detailed-status__link > .fa-reply + span::after,
|
||||||
.layout-multiple-columns .detailed-status__link > .fa-reply + span::after {
|
.layout-multiple-columns .detailed-status__link > .fa-reply + span::after {
|
||||||
color: var(--color-dim);
|
color: var(--color-dim);
|
||||||
content: 'Replies';
|
content: '답장';
|
||||||
font-weight: var(--font-weight-semibold);
|
font-weight: var(--font-weight-semibold);
|
||||||
}
|
}
|
||||||
|
|
||||||
body.embed .detailed-status__link > .fa-retweet + span::after,
|
body.embed .detailed-status__link > .fa-retweet + span::after,
|
||||||
.layout-multiple-columns .detailed-status__link > .fa-retweet + span::after {
|
.layout-multiple-columns .detailed-status__link > .fa-retweet + span::after {
|
||||||
color: var(--color-dim);
|
color: var(--color-dim);
|
||||||
content: 'Boosts';
|
content: '부스트';
|
||||||
font-weight: var(--font-weight-semibold);
|
font-weight: var(--font-weight-semibold);
|
||||||
}
|
}
|
||||||
|
|
||||||
body.embed .detailed-status__link > .fa-star + span::after,
|
body.embed .detailed-status__link > .fa-star + span::after,
|
||||||
.layout-multiple-columns .detailed-status__link > .fa-star + span::after {
|
.layout-multiple-columns .detailed-status__link > .fa-star + span::after {
|
||||||
color: var(--color-dim);
|
color: var(--color-dim);
|
||||||
content: 'Favourites';
|
content: '좋아요';
|
||||||
font-weight: var(--font-weight-semibold);
|
font-weight: var(--font-weight-semibold);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2961,6 +2961,12 @@ body.embed .button.logo-button:hover,
|
|||||||
order: 5;
|
order: 5;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.layout-multiple-columns .column-link[href="/local"] {
|
||||||
|
order: 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
.layout-multiple-columns .column-link[href="/public"] {
|
.layout-multiple-columns .column-link[href="/public"] {
|
||||||
order: 6;
|
order: 6;
|
||||||
}
|
}
|
||||||
|
@ -1590,21 +1590,21 @@ body.embed > .activity-stream {
|
|||||||
body.embed .detailed-status__link > .fa-reply + span::after,
|
body.embed .detailed-status__link > .fa-reply + span::after,
|
||||||
.layout-single-column .detailed-status__link > .fa-reply + span::after {
|
.layout-single-column .detailed-status__link > .fa-reply + span::after {
|
||||||
color: var(--color-dim);
|
color: var(--color-dim);
|
||||||
content: 'Replies';
|
content: '답장';
|
||||||
font-weight: var(--font-weight-semibold);
|
font-weight: var(--font-weight-semibold);
|
||||||
}
|
}
|
||||||
|
|
||||||
body.embed .detailed-status__link > .fa-retweet + span::after,
|
body.embed .detailed-status__link > .fa-retweet + span::after,
|
||||||
.layout-single-column .detailed-status__link > .fa-retweet + span::after {
|
.layout-single-column .detailed-status__link > .fa-retweet + span::after {
|
||||||
color: var(--color-dim);
|
color: var(--color-dim);
|
||||||
content: 'Boosts';
|
content: '부스트';
|
||||||
font-weight: var(--font-weight-semibold);
|
font-weight: var(--font-weight-semibold);
|
||||||
}
|
}
|
||||||
|
|
||||||
body.embed .detailed-status__link > .fa-star + span::after,
|
body.embed .detailed-status__link > .fa-star + span::after,
|
||||||
.layout-single-column .detailed-status__link > .fa-star + span::after {
|
.layout-single-column .detailed-status__link > .fa-star + span::after {
|
||||||
color: var(--color-dim);
|
color: var(--color-dim);
|
||||||
content: 'Favourites';
|
content: '좋아요';
|
||||||
font-weight: var(--font-weight-semibold);
|
font-weight: var(--font-weight-semibold);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -3002,6 +3002,10 @@ body.embed .button.logo-button:hover,
|
|||||||
order: 5;
|
order: 5;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.layout-single-column .column-link[href="/local"] {
|
||||||
|
order: 5;
|
||||||
|
}
|
||||||
|
|
||||||
.layout-single-column .column-link[href="/public"] {
|
.layout-single-column .column-link[href="/public"] {
|
||||||
order: 6;
|
order: 6;
|
||||||
}
|
}
|
||||||
|
@ -25,6 +25,7 @@
|
|||||||
%p.hint= t 'appearance.advanced_web_interface_hint'
|
%p.hint= t 'appearance.advanced_web_interface_hint'
|
||||||
|
|
||||||
.fields-group
|
.fields-group
|
||||||
|
= ff.input :'web.advanced_layout', wrapper: :with_label, hint: false, label: I18n.t('simple_form.labels.defaults.setting_advanced_layout')
|
||||||
%h4= t 'appearance.animations_and_accessibility'
|
%h4= t 'appearance.animations_and_accessibility'
|
||||||
|
|
||||||
.fields-group
|
.fields-group
|
||||||
|
@ -23,6 +23,7 @@ Rails.application.routes.draw do
|
|||||||
/publish
|
/publish
|
||||||
/follow_requests
|
/follow_requests
|
||||||
/direct_messages
|
/direct_messages
|
||||||
|
/local
|
||||||
/blocks
|
/blocks
|
||||||
/domain_blocks
|
/domain_blocks
|
||||||
/mutes
|
/mutes
|
||||||
|
Loading…
Reference in New Issue
Block a user