Add limited timeline
This commit is contained in:
parent
2de85178bd
commit
64c363cb08
34 changed files with 453 additions and 54 deletions
|
@ -31,7 +31,8 @@ class Api::V1::Timelines::HomeController < Api::BaseController
|
|||
limit_param(DEFAULT_STATUSES_LIMIT),
|
||||
params[:max_id],
|
||||
params[:since_id],
|
||||
params[:min_id]
|
||||
params[:min_id],
|
||||
visibilities
|
||||
)
|
||||
end
|
||||
|
||||
|
@ -62,4 +63,10 @@ class Api::V1::Timelines::HomeController < Api::BaseController
|
|||
def pagination_since_id
|
||||
@statuses.first.id
|
||||
end
|
||||
|
||||
def visibilities
|
||||
val = params.permit(visibilities: [])[:visibilities] || []
|
||||
val = [val] unless val.is_a?(Enumerable)
|
||||
val
|
||||
end
|
||||
end
|
||||
|
|
|
@ -66,6 +66,7 @@ class Settings::PreferencesController < Settings::BaseController
|
|||
:setting_place_tab_bar_at_bottom,
|
||||
:setting_show_tab_bar_label,
|
||||
:setting_show_target,
|
||||
:setting_enable_limited_timeline,
|
||||
notification_emails: %i(follow follow_request reblog favourite mention digest report pending_account trending_tag),
|
||||
interactions: %i(must_be_follower must_be_following must_be_following_dm)
|
||||
)
|
||||
|
|
|
@ -7,6 +7,7 @@ import { useEmoji } from './emojis';
|
|||
import resizeImage from '../utils/resize_image';
|
||||
import { importFetchedAccounts } from './importer';
|
||||
import { updateTimeline } from './timelines';
|
||||
import { getHomeVisibilities, getLimitedVisibilities } from 'mastodon/selectors';
|
||||
import { showAlertForError } from './alerts';
|
||||
import { showAlert } from './alerts';
|
||||
import { openModal } from './modal';
|
||||
|
@ -158,6 +159,8 @@ export function submitCompose(routerHistory) {
|
|||
return function (dispatch, getState) {
|
||||
const status = getState().getIn(['compose', 'text'], '');
|
||||
const media = getState().getIn(['compose', 'media_attachments']);
|
||||
const homeVisibilities = getHomeVisibilities(getState());
|
||||
const limitedVisibilities = getLimitedVisibilities(getState());
|
||||
|
||||
if ((!status || !status.length) && media.size === 0) {
|
||||
return;
|
||||
|
@ -197,10 +200,14 @@ export function submitCompose(routerHistory) {
|
|||
}
|
||||
};
|
||||
|
||||
if (response.data.visibility !== 'direct') {
|
||||
if (homeVisibilities.length == 0 || homeVisibilities.includes(response.data.visibility)) {
|
||||
insertIfOnline('home');
|
||||
}
|
||||
|
||||
if (limitedVisibilities.includes(response.data.visibility)) {
|
||||
insertIfOnline('limited');
|
||||
}
|
||||
|
||||
if (response.data.in_reply_to_id === null && response.data.visibility === 'public') {
|
||||
insertIfOnline('public');
|
||||
insertIfOnline(`account:${response.data.account.id}`);
|
||||
|
|
|
@ -8,6 +8,7 @@ import {
|
|||
connectTimeline,
|
||||
disconnectTimeline,
|
||||
} from './timelines';
|
||||
import { getHomeVisibilities } from 'mastodon/selectors';
|
||||
import { updateNotifications, expandNotifications } from './notifications';
|
||||
import { updateConversations } from './conversations';
|
||||
import {
|
||||
|
@ -44,10 +45,10 @@ export const connectTimelineStream = (timelineId, channelName, params = {}, opti
|
|||
let pollingId;
|
||||
|
||||
/**
|
||||
* @param {function(Function, Function): void} fallback
|
||||
* @param {function(Function, Function, Function): void} fallback
|
||||
*/
|
||||
const useFallback = fallback => {
|
||||
fallback(dispatch, () => {
|
||||
fallback(dispatch, getState, () => {
|
||||
pollingId = setTimeout(() => useFallback(fallback), 20000 + randomUpTo(20000));
|
||||
});
|
||||
};
|
||||
|
@ -105,8 +106,10 @@ export const connectTimelineStream = (timelineId, channelName, params = {}, opti
|
|||
* @param {Function} dispatch
|
||||
* @param {function(): void} done
|
||||
*/
|
||||
const refreshHomeTimelineAndNotification = (dispatch, done) => {
|
||||
dispatch(expandHomeTimeline({}, () =>
|
||||
const refreshHomeTimelineAndNotification = (dispatch, getState, done) => {
|
||||
const visibilities = getHomeVisibilities(getState());
|
||||
|
||||
dispatch(expandHomeTimeline({ visibilities }, () =>
|
||||
dispatch(expandNotifications({}, () =>
|
||||
dispatch(fetchAnnouncements(done))))));
|
||||
};
|
||||
|
|
|
@ -5,6 +5,7 @@ import api, { getLinks } from 'mastodon/api';
|
|||
import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
|
||||
import compareId from 'mastodon/compare_id';
|
||||
import { usePendingItems as preferPendingItems } from 'mastodon/initial_state';
|
||||
import { getHomeVisibilities, getLimitedVisibilities } from 'mastodon/selectors';
|
||||
import { uniq } from '../utils/uniq';
|
||||
|
||||
export const TIMELINE_UPDATE = 'TIMELINE_UPDATE';
|
||||
|
@ -43,15 +44,30 @@ export function updateTimeline(timeline, status, accept) {
|
|||
dispatch(importFetchedStatus(status));
|
||||
dispatch(fetchRelationships([status.reblog ? status.reblog.account.id : status.account.id, status.quote ? status.quote.account.id : null].filter(function(e){return e})));
|
||||
|
||||
dispatch({
|
||||
type: TIMELINE_UPDATE,
|
||||
timeline,
|
||||
status,
|
||||
usePendingItems: preferPendingItems,
|
||||
});
|
||||
const insertTimeline = timeline => {
|
||||
dispatch({
|
||||
type: TIMELINE_UPDATE,
|
||||
timeline,
|
||||
status,
|
||||
usePendingItems: preferPendingItems,
|
||||
});
|
||||
};
|
||||
|
||||
const visibility = status.visibility_ex || status.visibility
|
||||
const homeVisibilities = getHomeVisibilities(getState());
|
||||
const limitedVisibilities = getLimitedVisibilities(getState());
|
||||
|
||||
if (timeline === 'home') {
|
||||
dispatch(submitMarkers());
|
||||
if (homeVisibilities.length == 0 || homeVisibilities.includes(visibility)) {
|
||||
insertTimeline('home');
|
||||
dispatch(submitMarkers());
|
||||
}
|
||||
|
||||
if (limitedVisibilities.includes(visibility)) {
|
||||
insertTimeline('limited');
|
||||
}
|
||||
} else {
|
||||
insertTimeline(timeline);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
@ -128,7 +144,8 @@ export function expandTimeline(timelineId, path, params = {}, done = noOp) {
|
|||
};
|
||||
};
|
||||
|
||||
export const expandHomeTimeline = ({ maxId } = {}, done = noOp) => expandTimeline('home', '/api/v1/timelines/home', { max_id: maxId }, done);
|
||||
export const expandHomeTimeline = ({ maxId, visibilities } = {}, done = noOp) => expandTimeline('home', '/api/v1/timelines/home', { max_id: maxId, visibilities: visibilities}, done);
|
||||
export const expandLimitedTimeline = ({ maxId, visibilities } = {}, done = noOp) => expandTimeline('limited', '/api/v1/timelines/home', { max_id: maxId, visibilities: visibilities}, done);
|
||||
export const expandPublicTimeline = ({ maxId, onlyMedia, onlyRemote } = {}, done = noOp) => expandTimeline(`public${onlyRemote ? ':remote' : ''}${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { remote: !!onlyRemote, max_id: maxId, only_media: !!onlyMedia }, done);
|
||||
export const expandDomainTimeline = (domain, { maxId, onlyMedia } = {}, done = noOp) => expandTimeline(`domain${onlyMedia ? ':media' : ''}:${domain}`, '/api/v1/timelines/public', { local: false, domain: domain, max_id: maxId, only_media: !!onlyMedia }, done);
|
||||
export const expandGroupTimeline = (id, { maxId, onlyMedia, tagged } = {}, done = noOp) => expandTimeline(`group:${id}${onlyMedia ? ':media' : ''}${tagged ? `:${tagged}` : ''}`, `/api/v1/timelines/group/${id}`, { max_id: maxId, only_media: !!onlyMedia, tagged: tagged }, done);
|
||||
|
|
|
@ -14,7 +14,7 @@ import SearchResultsContainer from './containers/search_results_container';
|
|||
import { changeComposing } from '../../actions/compose';
|
||||
import { openModal } from 'mastodon/actions/modal';
|
||||
import elephantUIPlane from '../../../images/elephant_ui_plane.svg';
|
||||
import { mascot, show_tab_bar_label } from '../../initial_state';
|
||||
import { mascot, show_tab_bar_label, enable_limited_timeline } from '../../initial_state';
|
||||
import Icon from 'mastodon/components/icon';
|
||||
import { logOut } from 'mastodon/utils/log_out';
|
||||
import NotificationsCounterIcon from '../ui/components/notifications_counter_icon';
|
||||
|
@ -23,6 +23,7 @@ import classNames from 'classnames';
|
|||
const messages = defineMessages({
|
||||
short_start: { id: 'navigation_bar.short.getting_started', defaultMessage: 'Started' },
|
||||
short_home_timeline: { id: 'navigation_bar.short.home', defaultMessage: 'Home' },
|
||||
short_limited_timeline: { id: 'navigation_bar.short.limited_timeline', defaultMessage: 'Ltd.' },
|
||||
short_notifications: { id: 'navigation_bar.short.notifications', defaultMessage: 'Notif.' },
|
||||
short_public: { id: 'navigation_bar.short.public_timeline', defaultMessage: 'FTL' },
|
||||
short_community: { id: 'navigation_bar.short.community_timeline', defaultMessage: 'LTL' },
|
||||
|
@ -31,6 +32,7 @@ const messages = defineMessages({
|
|||
short_logout: { id: 'navigation_bar.short.logout', defaultMessage: 'Logout' },
|
||||
start: { id: 'getting_started.heading', defaultMessage: 'Getting started' },
|
||||
home_timeline: { id: 'tabs_bar.home', defaultMessage: 'Home' },
|
||||
limited_timeline: { id: 'tabs_bar.limited_timeline', defaultMessage: 'Limited home' },
|
||||
notifications: { id: 'tabs_bar.notifications', defaultMessage: 'Notifications' },
|
||||
public: { id: 'navigation_bar.public_timeline', defaultMessage: 'Federated timeline' },
|
||||
lists: { id: 'navigation_bar.lists', defaultMessage: 'Lists' },
|
||||
|
@ -104,14 +106,15 @@ class Compose extends React.PureComponent {
|
|||
|
||||
if (!columns.some(column => column.get('id') === id)) {
|
||||
const tabParams = {
|
||||
'START': { to: '/getting-started', title: formatMessage(messages.start), label: formatMessage(messages.short_start), icon_id: 'bars' },
|
||||
'HOME': { to: '/timelines/home', title: formatMessage(messages.home_timeline), label: formatMessage(messages.short_home_timeline), icon_id: 'home' },
|
||||
'NOTIFICATIONS': { to: '/notifications', title: formatMessage(messages.notifications), label: formatMessage(messages.short_notifications), icon_id: 'bell' },
|
||||
'START': { to: '/getting-started', title: formatMessage(messages.start), label: formatMessage(messages.short_start), icon_id: 'bars' },
|
||||
'HOME': { to: '/timelines/home', title: formatMessage(messages.home_timeline), label: formatMessage(messages.short_home_timeline), icon_id: 'home' },
|
||||
'LIMITED': { to: '/timelines/limited', title: formatMessage(messages.limited_timeline), label: formatMessage(messages.short_limited_timeline), icon_id: 'lock' },
|
||||
'NOTIFICATIONS': { to: '/notifications', title: formatMessage(messages.notifications), label: formatMessage(messages.short_notifications), icon_id: 'bell' },
|
||||
// 'COMMUNITY': { to: '/timelines/public/local', title: formatMessage(messages.community), label: formatMessage(messages.short_community), icon_id: 'users' },
|
||||
'PUBLIC': { to: '/timelines/public', title: formatMessage(messages.public), label: formatMessage(messages.short_public), icon_id: 'globe' },
|
||||
'LIST': { to: '/lists', title: formatMessage(messages.lists), label: formatMessage(messages.short_lists), icon_id: 'list-ul' },
|
||||
'PREFERENCES': { href: '/settings/preferences', title: formatMessage(messages.preferences), label: formatMessage(messages.short_preferences), icon_id: 'cog' },
|
||||
'SIGN_OUT': { href: '/auth/sign_out', title: formatMessage(messages.logout), label: formatMessage(messages.short_logout), icon_id: 'sign-out', method: 'delete' },
|
||||
'PUBLIC': { to: '/timelines/public', title: formatMessage(messages.public), label: formatMessage(messages.short_public), icon_id: 'globe' },
|
||||
'LIST': { to: '/lists', title: formatMessage(messages.lists), label: formatMessage(messages.short_lists), icon_id: 'list-ul' },
|
||||
'PREFERENCES': { href: '/settings/preferences', title: formatMessage(messages.preferences), label: formatMessage(messages.short_preferences), icon_id: 'cog' },
|
||||
'SIGN_OUT': { href: '/auth/sign_out', title: formatMessage(messages.logout), label: formatMessage(messages.short_logout), icon_id: 'sign-out', method: 'delete' },
|
||||
};
|
||||
|
||||
const { href, to, title, label, icon_id, method } = tabParams[id];
|
||||
|
@ -138,7 +141,7 @@ class Compose extends React.PureComponent {
|
|||
|
||||
if (multiColumn) {
|
||||
// const defaultTabIds = ['START', 'HOME', 'NOTIFICATIONS', 'COMMUNITY', 'PUBLIC', 'LIST', 'PREFERENCES', 'SIGN_OUT'];
|
||||
const defaultTabIds = ['START', 'HOME', 'NOTIFICATIONS', 'PUBLIC', 'LIST', 'PREFERENCES', 'SIGN_OUT'];
|
||||
const defaultTabIds = enable_limited_timeline ? ['START', 'HOME', 'LIMITED', 'NOTIFICATIONS', 'PUBLIC', 'LIST', 'PREFERENCES', 'SIGN_OUT'] : ['START', 'HOME', 'NOTIFICATIONS', 'PUBLIC', 'LIST', 'PREFERENCES', 'SIGN_OUT'];
|
||||
|
||||
let tabs = defaultTabIds;
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@ import { connect } from 'react-redux';
|
|||
import PropTypes from 'prop-types';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { me, profile_directory, showTrends } from '../../initial_state';
|
||||
import { me, profile_directory, showTrends, enable_limited_timeline } from '../../initial_state';
|
||||
import { fetchFollowRequests } from 'mastodon/actions/accounts';
|
||||
import { List as ImmutableList } from 'immutable';
|
||||
import NavigationContainer from '../compose/containers/navigation_container';
|
||||
|
@ -29,6 +29,7 @@ const messages = defineMessages({
|
|||
domain_blocks: { id: 'navigation_bar.domain_blocks', defaultMessage: 'Hidden domains' },
|
||||
mutes: { id: 'navigation_bar.mutes', defaultMessage: 'Muted users' },
|
||||
pins: { id: 'navigation_bar.pins', defaultMessage: 'Pinned toots' },
|
||||
limited_timeline: { id: 'navigation_bar.limited_timeline', defaultMessage: 'Limited home' },
|
||||
lists: { id: 'navigation_bar.lists', defaultMessage: 'Lists' },
|
||||
circles: { id: 'navigation_bar.circles', defaultMessage: 'Circles' },
|
||||
discover: { id: 'navigation_bar.discover', defaultMessage: 'Discover' },
|
||||
|
@ -178,6 +179,13 @@ class GettingStarted extends ImmutablePureComponent {
|
|||
height += 48;
|
||||
}
|
||||
|
||||
if (enable_limited_timeline && multiColumn && !columns.find(item => item.get('id') === 'LIMITED')) {
|
||||
navItems.push(
|
||||
<ColumnLink key='limited_timeline' icon='lock' text={intl.formatMessage(messages.limited_timeline)} to='/timelines/limited' />,
|
||||
);
|
||||
height += 48;
|
||||
}
|
||||
|
||||
navItems.push(
|
||||
<ColumnLink key='direct' icon='envelope' text={intl.formatMessage(messages.direct)} to='/timelines/direct' />,
|
||||
<ColumnLink key='bookmark' icon='bookmark' text={intl.formatMessage(messages.bookmarks)} to='/bookmarks' />,
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
import React from 'react';
|
||||
import React, { Fragment } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import { injectIntl, FormattedMessage } from 'react-intl';
|
||||
import SettingToggle from '../../notifications/components/setting_toggle';
|
||||
import { enable_limited_timeline } from 'mastodon/initial_state';
|
||||
|
||||
export default @injectIntl
|
||||
class ColumnSettings extends React.PureComponent {
|
||||
|
@ -10,11 +11,12 @@ class ColumnSettings extends React.PureComponent {
|
|||
static propTypes = {
|
||||
settings: ImmutablePropTypes.map.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
onChangeClear: PropTypes.func.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
render () {
|
||||
const { settings, onChange } = this.props;
|
||||
const { settings, onChange, onChangeClear } = this.props;
|
||||
|
||||
return (
|
||||
<div>
|
||||
|
@ -27,6 +29,22 @@ class ColumnSettings extends React.PureComponent {
|
|||
<div className='column-settings__row'>
|
||||
<SettingToggle prefix='home_timeline' settings={settings} settingPath={['shows', 'reply']} onChange={onChange} label={<FormattedMessage id='home.column_settings.show_replies' defaultMessage='Show replies' />} />
|
||||
</div>
|
||||
|
||||
{enable_limited_timeline && <Fragment>
|
||||
<span className='column-settings__section'><FormattedMessage id='home.column_settings.visibility' defaultMessage='Visibility' /></span>
|
||||
|
||||
<div className='column-settings__row'>
|
||||
<SettingToggle prefix='home_timeline' settings={settings} settingPath={['shows', 'private']} onChange={onChangeClear} label={<FormattedMessage id='home.column_settings.show_private' defaultMessage='Show private' />} />
|
||||
</div>
|
||||
|
||||
<div className='column-settings__row'>
|
||||
<SettingToggle prefix='home_timeline' settings={settings} settingPath={['shows', 'limited']} onChange={onChangeClear} label={<FormattedMessage id='home.column_settings.show_limited' defaultMessage='Show limited' />} />
|
||||
</div>
|
||||
|
||||
<div className='column-settings__row'>
|
||||
<SettingToggle prefix='home_timeline' settings={settings} settingPath={['shows', 'direct']} onChange={onChangeClear} label={<FormattedMessage id='home.column_settings.show_direct' defaultMessage='Show direct' />} />
|
||||
</div>
|
||||
</Fragment>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { connect } from 'react-redux';
|
||||
import ColumnSettings from '../components/column_settings';
|
||||
import { changeSetting, saveSettings } from '../../../actions/settings';
|
||||
import { clearTimeline } from '../../../actions/timelines';
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
settings: state.getIn(['settings', 'home']),
|
||||
|
@ -12,6 +13,11 @@ const mapDispatchToProps = dispatch => ({
|
|||
dispatch(changeSetting(['home', ...key], checked));
|
||||
},
|
||||
|
||||
onChangeClear (key, checked) {
|
||||
dispatch(changeSetting(['home', ...key], checked));
|
||||
dispatch(clearTimeline('home'));
|
||||
},
|
||||
|
||||
onSave () {
|
||||
dispatch(saveSettings());
|
||||
},
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { expandHomeTimeline } from '../../actions/timelines';
|
||||
import { getHomeVisibilities } from 'mastodon/selectors';
|
||||
import PropTypes from 'prop-types';
|
||||
import StatusListContainer from '../ui/containers/status_list_container';
|
||||
import Column from '../../components/column';
|
||||
|
@ -26,6 +27,7 @@ const mapStateToProps = state => ({
|
|||
hasAnnouncements: !state.getIn(['announcements', 'items']).isEmpty(),
|
||||
unreadAnnouncements: state.getIn(['announcements', 'items']).count(item => !item.get('read')),
|
||||
showAnnouncements: state.getIn(['announcements', 'show']),
|
||||
visibilities: getHomeVisibilities(state),
|
||||
});
|
||||
|
||||
export default @connect(mapStateToProps)
|
||||
|
@ -42,6 +44,7 @@ class HomeTimeline extends React.PureComponent {
|
|||
hasAnnouncements: PropTypes.bool,
|
||||
unreadAnnouncements: PropTypes.number,
|
||||
showAnnouncements: PropTypes.bool,
|
||||
visibilities: PropTypes.arrayOf(PropTypes.string),
|
||||
};
|
||||
|
||||
handlePin = () => {
|
||||
|
@ -68,7 +71,9 @@ class HomeTimeline extends React.PureComponent {
|
|||
}
|
||||
|
||||
handleLoadMore = maxId => {
|
||||
this.props.dispatch(expandHomeTimeline({ maxId }));
|
||||
const { visibilities } = this.props;
|
||||
|
||||
this.props.dispatch(expandHomeTimeline({ maxId, visibilities }));
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
|
@ -77,6 +82,12 @@ class HomeTimeline extends React.PureComponent {
|
|||
}
|
||||
|
||||
componentDidUpdate (prevProps) {
|
||||
const { dispatch, visibilities } = this.props;
|
||||
|
||||
if (prevProps.visibilities.toString() !== visibilities.toString()) {
|
||||
dispatch(expandHomeTimeline({ visibilities }));
|
||||
}
|
||||
|
||||
this._checkIfReloadNeeded(prevProps.isPartial, this.props.isPartial);
|
||||
}
|
||||
|
||||
|
@ -85,13 +96,13 @@ class HomeTimeline extends React.PureComponent {
|
|||
}
|
||||
|
||||
_checkIfReloadNeeded (wasPartial, isPartial) {
|
||||
const { dispatch } = this.props;
|
||||
const { dispatch, visibilities } = this.props;
|
||||
|
||||
if (wasPartial === isPartial) {
|
||||
return;
|
||||
} else if (!wasPartial && isPartial) {
|
||||
this.polling = setInterval(() => {
|
||||
dispatch(expandHomeTimeline());
|
||||
dispatch(expandHomeTimeline({ visibilities }));
|
||||
}, 3000);
|
||||
} else if (wasPartial && !isPartial) {
|
||||
this._stopPolling();
|
||||
|
|
|
@ -0,0 +1,49 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import { injectIntl, FormattedMessage } from 'react-intl';
|
||||
import SettingToggle from '../../notifications/components/setting_toggle';
|
||||
|
||||
export default @injectIntl
|
||||
class ColumnSettings extends React.PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
settings: ImmutablePropTypes.map.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
onChangeClear: PropTypes.func.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
render () {
|
||||
const { settings, onChange, onChangeClear } = this.props;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<span className='column-settings__section'><FormattedMessage id='limited.column_settings.basic' defaultMessage='Basic' /></span>
|
||||
|
||||
<div className='column-settings__row'>
|
||||
<SettingToggle prefix='limited_timeline' settings={settings} settingPath={['shows', 'reblog']} onChange={onChange} label={<FormattedMessage id='limited.column_settings.show_reblogs' defaultMessage='Show boosts' />} />
|
||||
</div>
|
||||
|
||||
<div className='column-settings__row'>
|
||||
<SettingToggle prefix='limited_timeline' settings={settings} settingPath={['shows', 'reply']} onChange={onChange} label={<FormattedMessage id='limited.column_settings.show_replies' defaultMessage='Show replies' />} />
|
||||
</div>
|
||||
|
||||
<span className='column-settings__section'><FormattedMessage id='limited.column_settings.visibility' defaultMessage='Visibility' /></span>
|
||||
|
||||
<div className='column-settings__row'>
|
||||
<SettingToggle prefix='limited_timeline' settings={settings} settingPath={['shows', 'private']} onChange={onChangeClear} label={<FormattedMessage id='limited.column_settings.show_private' defaultMessage='Show private' />} />
|
||||
</div>
|
||||
|
||||
<div className='column-settings__row'>
|
||||
<SettingToggle prefix='limited_timeline' settings={settings} settingPath={['shows', 'limited']} onChange={onChangeClear} label={<FormattedMessage id='limited.column_settings.show_limited' defaultMessage='Show limited' />} />
|
||||
</div>
|
||||
|
||||
<div className='column-settings__row'>
|
||||
<SettingToggle prefix='limited_timeline' settings={settings} settingPath={['shows', 'direct']} onChange={onChangeClear} label={<FormattedMessage id='limited.column_settings.show_direct' defaultMessage='Show direct' />} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
import { connect } from 'react-redux';
|
||||
import ColumnSettings from '../components/column_settings';
|
||||
import { changeSetting, saveSettings } from '../../../actions/settings';
|
||||
import { clearTimeline } from '../../../actions/timelines';
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
settings: state.getIn(['settings', 'limited']),
|
||||
});
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
|
||||
onChange (key, checked) {
|
||||
dispatch(changeSetting(['limited', ...key], checked));
|
||||
},
|
||||
|
||||
onChangeClear (key, checked) {
|
||||
dispatch(changeSetting(['limited', ...key], checked));
|
||||
dispatch(clearTimeline('limited'));
|
||||
},
|
||||
|
||||
onSave () {
|
||||
dispatch(saveSettings());
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(ColumnSettings);
|
109
app/javascript/mastodon/features/limited_timeline/index.js
Normal file
109
app/javascript/mastodon/features/limited_timeline/index.js
Normal file
|
@ -0,0 +1,109 @@
|
|||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { expandLimitedTimeline } from '../../actions/timelines';
|
||||
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 { getLimitedVisibilities } from 'mastodon/selectors';
|
||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||
import ColumnSettingsContainer from './containers/column_settings_container';
|
||||
|
||||
const messages = defineMessages({
|
||||
title: { id: 'column.limited', defaultMessage: 'Limited' },
|
||||
});
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
hasUnread: state.getIn(['timelines', 'limited', 'unread']) > 0,
|
||||
visibilities: getLimitedVisibilities(state),
|
||||
});
|
||||
|
||||
export default @connect(mapStateToProps)
|
||||
@injectIntl
|
||||
class LimitedTimeline extends React.PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
hasUnread: PropTypes.bool,
|
||||
visibilities: PropTypes.arrayOf(PropTypes.string),
|
||||
columnId: PropTypes.string,
|
||||
multiColumn: PropTypes.bool,
|
||||
};
|
||||
|
||||
handlePin = () => {
|
||||
const { columnId, dispatch } = this.props;
|
||||
|
||||
if (columnId) {
|
||||
dispatch(removeColumn(columnId));
|
||||
} else {
|
||||
dispatch(addColumn('LIMITED', {}));
|
||||
}
|
||||
}
|
||||
|
||||
handleMove = (dir) => {
|
||||
const { columnId, dispatch } = this.props;
|
||||
dispatch(moveColumn(columnId, dir));
|
||||
}
|
||||
|
||||
handleHeaderClick = () => {
|
||||
this.column.scrollTop();
|
||||
}
|
||||
|
||||
setRef = c => {
|
||||
this.column = c;
|
||||
}
|
||||
|
||||
handleLoadMore = maxId => {
|
||||
const { dispatch, visibilities } = this.props;
|
||||
|
||||
dispatch(expandLimitedTimeline({ maxId, visibilities }));
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
const { dispatch, visibilities } = this.props;
|
||||
|
||||
dispatch(expandLimitedTimeline({ visibilities }));
|
||||
}
|
||||
|
||||
componentDidUpdate (prevProps) {
|
||||
const { dispatch, visibilities } = this.props;
|
||||
|
||||
if (prevProps.visibilities.toString() !== visibilities.toString()) {
|
||||
dispatch(expandLimitedTimeline({ visibilities }));
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
const { intl, hasUnread, columnId, multiColumn } = this.props;
|
||||
const pinned = !!columnId;
|
||||
|
||||
return (
|
||||
<Column bindToDocument={!multiColumn} ref={this.setRef} label={intl.formatMessage(messages.title)}>
|
||||
<ColumnHeader
|
||||
icon='lock'
|
||||
active={hasUnread}
|
||||
title={intl.formatMessage(messages.title)}
|
||||
onPin={this.handlePin}
|
||||
onMove={this.handleMove}
|
||||
onClick={this.handleHeaderClick}
|
||||
pinned={pinned}
|
||||
multiColumn={multiColumn}
|
||||
>
|
||||
<ColumnSettingsContainer />
|
||||
</ColumnHeader>
|
||||
|
||||
<StatusListContainer
|
||||
trackScroll={!pinned}
|
||||
scrollKey={`limited_timeline-${columnId}`}
|
||||
onLoadMore={this.handleLoadMore}
|
||||
timelineId='limited'
|
||||
emptyMessage={<FormattedMessage id='empty_column.limited' defaultMessage='Your limited timeline is empty.' />}
|
||||
bindToDocument={!multiColumn}
|
||||
/>
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
import React from 'react';
|
||||
import React, { Fragment } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
|
@ -8,7 +8,7 @@ import ReactSwipeableViews from 'react-swipeable-views';
|
|||
import TabsBar, { getSwipeableIndex, getSwipeableLink } from './tabs_bar';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import { disableSwiping, place_tab_bar_at_bottom } from 'mastodon/initial_state';
|
||||
import { disableSwiping, place_tab_bar_at_bottom, enable_limited_timeline } from 'mastodon/initial_state';
|
||||
|
||||
import BundleContainer from '../containers/bundle_container';
|
||||
import ColumnLoading from './column_loading';
|
||||
|
@ -23,6 +23,7 @@ import {
|
|||
DomainTimeline,
|
||||
HashtagTimeline,
|
||||
DirectTimeline,
|
||||
LimitedTimeline,
|
||||
FavouritedStatuses,
|
||||
BookmarkedStatuses,
|
||||
ListTimeline,
|
||||
|
@ -35,6 +36,7 @@ import Icon from 'mastodon/components/icon';
|
|||
import ComposePanel from './compose_panel';
|
||||
import NavigationPanel from './navigation_panel';
|
||||
import { show_navigation_panel } from 'mastodon/initial_state';
|
||||
import { removeColumn } from 'mastodon/actions/columns';
|
||||
|
||||
import { supportsPassiveEvents } from 'detect-passive-events';
|
||||
import { scrollRight } from '../../../scroll';
|
||||
|
@ -51,6 +53,7 @@ const componentMap = {
|
|||
'GROUP': GroupTimeline,
|
||||
'HASHTAG': HashtagTimeline,
|
||||
'DIRECT': DirectTimeline,
|
||||
'LIMITED': LimitedTimeline,
|
||||
'FAVOURITES': FavouritedStatuses,
|
||||
'BOOKMARKS': BookmarkedStatuses,
|
||||
'LIST': ListTimeline,
|
||||
|
@ -74,6 +77,7 @@ class ColumnsArea extends ImmutablePureComponent {
|
|||
};
|
||||
|
||||
static propTypes = {
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
columns: ImmutablePropTypes.list.isRequired,
|
||||
isModalOpen: PropTypes.bool.isRequired,
|
||||
|
@ -97,6 +101,8 @@ class ColumnsArea extends ImmutablePureComponent {
|
|||
}
|
||||
|
||||
componentDidMount() {
|
||||
const { dispatch, columns } = this.props;
|
||||
|
||||
if (!this.props.singleColumn) {
|
||||
this.node.addEventListener('wheel', this.handleWheel, supportsPassiveEvents ? { passive: true } : false);
|
||||
}
|
||||
|
@ -114,6 +120,14 @@ class ColumnsArea extends ImmutablePureComponent {
|
|||
this.isRtlLayout = document.getElementsByTagName('body')[0].classList.contains('rtl');
|
||||
|
||||
this.setState({ shouldAnimate: true });
|
||||
|
||||
if (!enable_limited_timeline) {
|
||||
const limitedColumn = columns.find(item => item.get('id') === 'LIMITED')
|
||||
|
||||
if (limitedColumn) {
|
||||
dispatch(removeColumn(limitedColumn.get('uuid')));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUpdate(nextProps) {
|
||||
|
|
|
@ -2,7 +2,7 @@ import React from 'react';
|
|||
import { NavLink, withRouter } from 'react-router-dom';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import Icon from 'mastodon/components/icon';
|
||||
import { profile_directory, showTrends } from 'mastodon/initial_state';
|
||||
import { profile_directory, showTrends, enable_limited_timeline } from 'mastodon/initial_state';
|
||||
import NotificationsCounterIcon from './notifications_counter_icon';
|
||||
import FollowRequestsNavLink from './follow_requests_nav_link';
|
||||
import ListPanel from './list_panel';
|
||||
|
@ -13,6 +13,7 @@ import TrendsContainer from 'mastodon/features/getting_started/containers/trends
|
|||
const NavigationPanel = () => (
|
||||
<div className='navigation-panel'>
|
||||
<NavLink className='column-link column-link--transparent' to='/timelines/home' data-preview-title-id='column.home' data-preview-icon='home' ><Icon className='column-link__icon' id='home' fixedWidth /><FormattedMessage id='tabs_bar.home' defaultMessage='Home' /></NavLink>
|
||||
{enable_limited_timeline && <NavLink className='column-link column-link--transparent' to='/timelines/limited' data-preview-title-id='column.limited' data-preview-icon='lock' ><Icon className='column-link__icon' id='lock' fixedWidth /><FormattedMessage id='navigation_bar.limited_timeline' defaultMessage='Limited home' /></NavLink>}
|
||||
<NavLink className='column-link column-link--transparent' to='/notifications' data-preview-title-id='column.notifications' data-preview-icon='bell' ><NotificationsCounterIcon className='column-link__icon' /><FormattedMessage id='tabs_bar.notifications' defaultMessage='Notifications' /></NavLink>
|
||||
<FollowRequestsNavLink />
|
||||
<NavLink className='column-link column-link--transparent' exact to='/timelines/public' data-preview-title-id='column.public' data-preview-icon='globe' ><Icon className='column-link__icon' id='globe' fixedWidth /><FormattedMessage id='tabs_bar.federated_timeline' defaultMessage='Federated' /></NavLink>
|
||||
|
|
|
@ -6,11 +6,12 @@ import { debounce, memoize } from 'lodash';
|
|||
import { isUserTouching } from '../../../is_mobile';
|
||||
import Icon from 'mastodon/components/icon';
|
||||
import NotificationsCounterIcon from './notifications_counter_icon';
|
||||
import { place_tab_bar_at_bottom, show_tab_bar_label } from 'mastodon/initial_state';
|
||||
import { place_tab_bar_at_bottom, show_tab_bar_label, enable_limited_timeline } from 'mastodon/initial_state';
|
||||
import classNames from 'classnames';
|
||||
|
||||
export const links = [
|
||||
const links = [
|
||||
<NavLink className='tabs-bar__link' to='/timelines/home' data-preview-title-id='column.home' data-preview-icon='home' ><Icon id='home' fixedWidth /><span className='tabs-bar__link__full-label'><FormattedMessage id='tabs_bar.home' defaultMessage='Home' /></span><span className='tabs-bar__link__short-label'><FormattedMessage id='navigation_bar.short.home' defaultMessage='Home' /></span></NavLink>,
|
||||
<NavLink className='tabs-bar__link tabs-bar__limited' to='/timelines/limited' data-preview-title-id='column.limited' data-preview-icon='lock' ><Icon id='lock' fixedWidth /><span className='tabs-bar__link__full-label'><FormattedMessage id='tabs_bar.limited_timeline' defaultMessage='Limited' /></span><span className='tabs-bar__link__short-label'><FormattedMessage id='navigation_bar.short.limited_timeline' defaultMessage='Ltd.' /></span></NavLink>,
|
||||
<NavLink className='tabs-bar__link' to='/notifications' data-preview-title-id='column.notifications' data-preview-icon='bell' ><NotificationsCounterIcon /><span className='tabs-bar__link__full-label'><FormattedMessage id='tabs_bar.notifications' defaultMessage='Notifications' /></span><span className='tabs-bar__link__short-label'><FormattedMessage id='navigation_bar.short.notifications' defaultMessage='Notif.' /></span></NavLink>,
|
||||
<NavLink className='tabs-bar__link' exact to='/timelines/public' data-preview-title-id='column.public' data-preview-icon='globe' ><Icon id='globe' fixedWidth /><span className='tabs-bar__link__full-label'><FormattedMessage id='tabs_bar.federated_timeline' defaultMessage='Federated' /></span><span className='tabs-bar__link__short-label'><FormattedMessage id='navigation_bar.short.public_timeline' defaultMessage='FTL' /></span></NavLink>,
|
||||
<NavLink className='tabs-bar__link' exact to='/lists' data-preview-title-id='column.lists' data-preview-icon='list-ul' ><Icon id='list-ul' fixedWidth /><span className='tabs-bar__link__full-label'><FormattedMessage id='tabs_bar.lists' defaultMessage='Lists' /></span><span className='tabs-bar__link__short-label'><FormattedMessage id='navigation_bar.short.lists' defaultMessage='List' /></span></NavLink>,
|
||||
|
@ -20,8 +21,18 @@ export const links = [
|
|||
<NavLink className='tabs-bar__link hamburger' to='/getting-started' data-preview-title-id='getting_started.heading' data-preview-icon='bars' ><Icon id='bars' fixedWidth /></NavLink>,
|
||||
];
|
||||
|
||||
export const getLinks = memoize(() => {
|
||||
return links.filter(link => {
|
||||
const classes = link.props.className.split(/\s+/);
|
||||
return !(!enable_limited_timeline && classes.includes('tabs-bar__limited'));
|
||||
});
|
||||
});
|
||||
|
||||
export const getSwipeableLinks = memoize(() => {
|
||||
return links.filter(link => link.props.className.split(/\s+/).includes('tabs-bar__link'));
|
||||
return getLinks().filter(link => {
|
||||
const classes = link.props.className.split(/\s+/);
|
||||
return classes.includes('tabs-bar__link');
|
||||
});
|
||||
});
|
||||
|
||||
export function getSwipeableIndex (path) {
|
||||
|
@ -33,11 +44,11 @@ export function getSwipeableLink (index) {
|
|||
}
|
||||
|
||||
export function getIndex (path) {
|
||||
return links.findIndex(link => link.props.to === path);
|
||||
return getLinks().findIndex(link => link.props.to === path);
|
||||
}
|
||||
|
||||
export function getLink (index) {
|
||||
return links[index].props.to;
|
||||
return getLinks()[index].props.to;
|
||||
}
|
||||
|
||||
export default @injectIntl
|
||||
|
@ -64,7 +75,7 @@ class TabsBar extends React.PureComponent {
|
|||
const tabs = Array(...this.node.querySelectorAll('.tabs-bar__link'));
|
||||
const currentTab = tabs.find(tab => tab.classList.contains('active'));
|
||||
const nextTab = tabs.find(tab => tab.contains(e.target));
|
||||
const { props: { to } } = links[Array(...this.node.childNodes).indexOf(nextTab)];
|
||||
const { props: { to } } = getLinks()[Array(...this.node.childNodes).indexOf(nextTab)];
|
||||
|
||||
|
||||
if (currentTab !== nextTab) {
|
||||
|
@ -91,7 +102,7 @@ class TabsBar extends React.PureComponent {
|
|||
return (
|
||||
<div className='tabs-bar__wrapper'>
|
||||
<nav className={classNames('tabs-bar', { 'bottom-bar': place_tab_bar_at_bottom })} ref={this.setRef}>
|
||||
{links.map(link => React.cloneElement(link, { key: link.props.to, className: classNames(link.props.className, { 'short-label': show_tab_bar_label }), onClick: link.props.href ? null : this.handleClick, 'aria-label': formatMessage({ id: link.props['data-preview-title-id'] }) }))}
|
||||
{getLinks().map(link => React.cloneElement(link, { key: link.props.to, className: classNames(link.props.className, { 'short-label': show_tab_bar_label }), onClick: link.props.href ? null : this.handleClick, 'aria-label': formatMessage({ id: link.props['data-preview-title-id'] }) }))}
|
||||
</nav>
|
||||
|
||||
<div id='tabs-bar__portal' />
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { connect } from 'react-redux';
|
||||
import StatusList from '../../../components/status_list';
|
||||
import { scrollTopTimeline, loadPending } from '../../../actions/timelines';
|
||||
import { getHomeVisibilities, getLimitedVisibilities } from 'mastodon/selectors';
|
||||
import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
|
||||
import { createSelector } from 'reselect';
|
||||
import { debounce } from 'lodash';
|
||||
|
@ -8,16 +9,21 @@ import { me } from '../../../initial_state';
|
|||
|
||||
const makeGetStatusIds = (pending = false) => createSelector([
|
||||
(state, { type }) => state.getIn(['settings', type], ImmutableMap()),
|
||||
(state, { type }) => type === 'home' ? getHomeVisibilities(state) : type === 'limited' ? getLimitedVisibilities(state) : [],
|
||||
(state, { type }) => state.getIn(['timelines', type, pending ? 'pendingItems' : 'items'], ImmutableList()),
|
||||
(state) => state.get('statuses'),
|
||||
], (columnSettings, statusIds, statuses) => {
|
||||
], (columnSettings, visibilities, statusIds, statuses) => {
|
||||
return statusIds.filter(id => {
|
||||
if (id === null) return true;
|
||||
|
||||
const statusForId = statuses.get(id);
|
||||
let showStatus = true;
|
||||
|
||||
if (statusForId.get('account') === me) return true;
|
||||
if (visibilities.length) {
|
||||
showStatus = showStatus && visibilities.includes(statusForId.get('visibility'));
|
||||
}
|
||||
|
||||
if (statusForId.get('account') === me) return showStatus;
|
||||
|
||||
if (columnSettings.getIn(['shows', 'reblog']) === false) {
|
||||
showStatus = showStatus && statusForId.get('reblog') === null;
|
||||
|
|
|
@ -17,6 +17,7 @@ import { fetchFilters } from '../../actions/filters';
|
|||
import { clearHeight } from '../../actions/height_cache';
|
||||
import { focusApp, unfocusApp, changeLayout } from 'mastodon/actions/app';
|
||||
import { synchronouslySubmitMarkers, submitMarkers, fetchMarkers } from 'mastodon/actions/markers';
|
||||
import { getHomeVisibilities } from 'mastodon/selectors';
|
||||
import { WrappedSwitch, WrappedRoute } from './util/react_router_helpers';
|
||||
import UploadArea from './components/upload_area';
|
||||
import ColumnsAreaContainer from './containers/columns_area_container';
|
||||
|
@ -40,6 +41,7 @@ import {
|
|||
Favourites,
|
||||
Mentions,
|
||||
DirectTimeline,
|
||||
LimitedTimeline,
|
||||
HashtagTimeline,
|
||||
Notifications,
|
||||
FollowRequests,
|
||||
|
@ -79,6 +81,7 @@ const mapStateToProps = state => ({
|
|||
canUploadMore: !state.getIn(['compose', 'media_attachments']).some(x => ['audio', 'video'].includes(x.get('type'))) && state.getIn(['compose', 'media_attachments']).size < 4,
|
||||
dropdownMenuIsOpen: state.getIn(['dropdown_menu', 'openId']) !== null,
|
||||
firstLaunch: state.getIn(['settings', 'introductionVersion'], 0) < INTRODUCTION_VERSION,
|
||||
visibilities: getHomeVisibilities(state),
|
||||
});
|
||||
|
||||
const keyMap = {
|
||||
|
@ -163,6 +166,7 @@ class SwitchingColumnsArea extends React.PureComponent {
|
|||
<WrappedRoute path='/timelines/public/domain/:domain' exact component={DomainTimeline} content={children} />
|
||||
<WrappedRoute path='/timelines/groups/:id/:tagged?' exact component={GroupTimeline} content={children} />
|
||||
<WrappedRoute path='/timelines/direct' component={DirectTimeline} content={children} />
|
||||
<WrappedRoute path='/timelines/limited' component={LimitedTimeline} content={children} />
|
||||
<WrappedRoute path='/timelines/tag/:id' component={HashtagTimeline} content={children} />
|
||||
<WrappedRoute path='/timelines/list/:id' component={ListTimeline} content={children} />
|
||||
|
||||
|
@ -227,6 +231,7 @@ class UI extends React.PureComponent {
|
|||
dropdownMenuIsOpen: PropTypes.bool,
|
||||
layout: PropTypes.string.isRequired,
|
||||
firstLaunch: PropTypes.bool,
|
||||
visibilities: PropTypes.arrayOf(PropTypes.string),
|
||||
};
|
||||
|
||||
state = {
|
||||
|
@ -347,6 +352,8 @@ class UI extends React.PureComponent {
|
|||
}
|
||||
|
||||
componentDidMount () {
|
||||
const { dispatch, visibilities } = this.props;
|
||||
|
||||
window.addEventListener('focus', this.handleWindowFocus, false);
|
||||
window.addEventListener('blur', this.handleWindowBlur, false);
|
||||
window.addEventListener('beforeunload', this.handleBeforeUnload, false);
|
||||
|
@ -365,12 +372,12 @@ class UI extends React.PureComponent {
|
|||
// On first launch, redirect to the follow recommendations page
|
||||
if (this.props.firstLaunch) {
|
||||
this.context.router.history.replace('/start');
|
||||
this.props.dispatch(closeOnboarding());
|
||||
dispatch(closeOnboarding());
|
||||
}
|
||||
|
||||
this.props.dispatch(fetchMarkers());
|
||||
this.props.dispatch(expandHomeTimeline());
|
||||
this.props.dispatch(expandNotifications());
|
||||
dispatch(fetchMarkers());
|
||||
dispatch(expandHomeTimeline({ visibilities }));
|
||||
dispatch(expandNotifications());
|
||||
setTimeout(() => this.props.dispatch(fetchFilters()), 500);
|
||||
|
||||
this.hotkeys.__mousetrap__.stopCallback = (e, element) => {
|
||||
|
|
|
@ -34,6 +34,10 @@ export function DirectTimeline() {
|
|||
return import(/* webpackChunkName: "features/direct_timeline" */'../../direct_timeline');
|
||||
}
|
||||
|
||||
export function LimitedTimeline() {
|
||||
return import(/* webpackChunkName: "features/limited_timeline" */'../../limited_timeline');
|
||||
}
|
||||
|
||||
export function ListTimeline () {
|
||||
return import(/* webpackChunkName: "features/list_timeline" */'../../list_timeline');
|
||||
}
|
||||
|
|
|
@ -38,5 +38,6 @@ export const show_bookmark_button = getMeta('show_bookmark_button');
|
|||
export const show_target = getMeta('show_target');
|
||||
export const place_tab_bar_at_bottom = getMeta('place_tab_bar_at_bottom');
|
||||
export const show_tab_bar_label = getMeta('show_tab_bar_label');
|
||||
export const enable_limited_timeline = getMeta('enable_limited_timeline');
|
||||
|
||||
export default initialState;
|
||||
|
|
|
@ -94,6 +94,7 @@
|
|||
"column.follow_requests": "Follow requests",
|
||||
"column.group": "Group timeline",
|
||||
"column.home": "Home",
|
||||
"column.limited": "Limited home",
|
||||
"column.lists": "Lists",
|
||||
"column.mutes": "Muted users",
|
||||
"column.notifications": "Notifications",
|
||||
|
@ -197,6 +198,7 @@
|
|||
"empty_column.group": "The group timeline is empty. When members of this group post new toots, they will appear here.",
|
||||
"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. {suggestions}",
|
||||
"empty_column.home.public_timeline": "the public timeline",
|
||||
"empty_column.home.suggestions": "See some suggestions",
|
||||
"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.",
|
||||
|
@ -242,8 +244,12 @@
|
|||
"home.account.add": "Add to home",
|
||||
"home.account.remove": "Remove from home",
|
||||
"home.column_settings.basic": "Basic",
|
||||
"home.column_settings.show_direct": "Show direct",
|
||||
"home.column_settings.show_limited": "Show limited",
|
||||
"home.column_settings.show_private": "Show private",
|
||||
"home.column_settings.show_reblogs": "Show boosts",
|
||||
"home.column_settings.show_replies": "Show replies",
|
||||
"home.column_settings.visibility": "Visibility",
|
||||
"home.hide_announcements": "Hide announcements",
|
||||
"home.hide_group_detail": "Hide group detail",
|
||||
"home.show_announcements": "Show announcements",
|
||||
|
@ -290,6 +296,13 @@
|
|||
"lightbox.expand": "Expand image view box",
|
||||
"lightbox.next": "Next",
|
||||
"lightbox.previous": "Previous",
|
||||
"limited.column_settings.basic": "Basic",
|
||||
"limited.column_settings.show_direct": "Show direct",
|
||||
"limited.column_settings.show_limited": "Show limited",
|
||||
"limited.column_settings.show_private": "Show private",
|
||||
"limited.column_settings.show_reblogs": "Show boosts",
|
||||
"limited.column_settings.show_replies": "Show replies",
|
||||
"limited.column_settings.visibility": "Visibility",
|
||||
"lists.account.add": "Add to list",
|
||||
"lists.account.remove": "Remove from list",
|
||||
"lists.delete": "Delete list",
|
||||
|
@ -330,6 +343,7 @@
|
|||
"navigation_bar.information": "Information",
|
||||
"navigation_bar.information_acct": "Fedibird info",
|
||||
"navigation_bar.keyboard_shortcuts": "Hotkeys",
|
||||
"navigation_bar.limited_timeline": "Limited home",
|
||||
"navigation_bar.lists": "Lists",
|
||||
"navigation_bar.logout": "Logout",
|
||||
"navigation_bar.mutes": "Muted users",
|
||||
|
@ -341,6 +355,7 @@
|
|||
"navigation_bar.short.community_timeline": "LTL",
|
||||
"navigation_bar.short.getting_started": "Started",
|
||||
"navigation_bar.short.home": "Home",
|
||||
"navigation_bar.short.limited_timeline": "Ltd.",
|
||||
"navigation_bar.short.lists": "Lists",
|
||||
"navigation_bar.short.logout": "Logout",
|
||||
"navigation_bar.short.notifications": "Notif.",
|
||||
|
@ -494,6 +509,7 @@
|
|||
"suggestions.heading": "Suggestions",
|
||||
"tabs_bar.federated_timeline": "Federated",
|
||||
"tabs_bar.home": "Home",
|
||||
"tabs_bar.limited_timeline": "Limited",
|
||||
"tabs_bar.lists": "List",
|
||||
"tabs_bar.local_timeline": "Local",
|
||||
"tabs_bar.notifications": "Notifications",
|
||||
|
|
|
@ -94,6 +94,7 @@
|
|||
"column.group": "グループタイムライン",
|
||||
"column.group_directory": "グループディレクトリ",
|
||||
"column.home": "ホーム",
|
||||
"column.limited": "限定ホーム",
|
||||
"column.lists": "リスト",
|
||||
"column.mutes": "ミュートしたユーザー",
|
||||
"column.notifications": "通知",
|
||||
|
@ -199,6 +200,8 @@
|
|||
"empty_column.home": "ホームタイムラインはまだ空っぽです。誰かフォローして埋めてみましょう。 {suggestions}",
|
||||
"empty_column.home.suggestions": "おすすめを見る",
|
||||
"empty_column.list": "このリストにはまだなにもありません。このリストのメンバーが新しい投稿をするとここに表示されます。",
|
||||
"empty_column.limited": "まだ誰からも公開範囲が限定された投稿を受け取っていません。",
|
||||
"empty_column.list": "このリストにはまだなにもありません。このリストのメンバーが新しい投稿をするとここに表示されます。",
|
||||
"empty_column.lists": "まだリストがありません。リストを作るとここに表示されます。",
|
||||
"empty_column.mutes": "まだ誰もミュートしていません。",
|
||||
"empty_column.notifications": "まだ通知がありません。他の人とふれ合って会話を始めましょう。",
|
||||
|
@ -242,8 +245,12 @@
|
|||
"home.account.add": "ホームに追加",
|
||||
"home.account.remove": "ホームから外す",
|
||||
"home.column_settings.basic": "基本設定",
|
||||
"home.column_settings.show_direct": "ダイレクトメッセージを表示",
|
||||
"home.column_settings.show_limited": "サークルを表示",
|
||||
"home.column_settings.show_private": "フォロワー限定を表示",
|
||||
"home.column_settings.show_reblogs": "ブースト表示",
|
||||
"home.column_settings.show_replies": "返信表示",
|
||||
"home.column_settings.visibility": "公開範囲",
|
||||
"home.hide_announcements": "お知らせを隠す",
|
||||
"home.hide_group_detail": "グループ詳細を隠す",
|
||||
"home.show_announcements": "お知らせを表示",
|
||||
|
@ -290,6 +297,13 @@
|
|||
"lightbox.expand": "画像ビューボックスを開く",
|
||||
"lightbox.next": "次",
|
||||
"lightbox.previous": "前",
|
||||
"limited.column_settings.basic": "基本設定",
|
||||
"limited.column_settings.show_direct": "ダイレクトメッセージを表示",
|
||||
"limited.column_settings.show_limited": "サークルを表示",
|
||||
"limited.column_settings.show_private": "フォロワー限定を表示",
|
||||
"limited.column_settings.show_reblogs": "ブースト表示",
|
||||
"limited.column_settings.show_replies": "返信表示",
|
||||
"limited.column_settings.visibility": "公開範囲",
|
||||
"lists.account.add": "リストに追加",
|
||||
"lists.account.remove": "リストから外す",
|
||||
"lists.delete": "リストを削除",
|
||||
|
@ -330,6 +344,7 @@
|
|||
"navigation_bar.information": "お知らせと情報",
|
||||
"navigation_bar.information_acct": "Fedibirdインフォメーション",
|
||||
"navigation_bar.keyboard_shortcuts": "ホットキー",
|
||||
"navigation_bar.limited_timeline": "限定ホーム",
|
||||
"navigation_bar.lists": "リスト",
|
||||
"navigation_bar.logout": "ログアウト",
|
||||
"navigation_bar.mutes": "ミュートしたユーザー",
|
||||
|
@ -341,6 +356,7 @@
|
|||
"navigation_bar.short.community_timeline": "ローカル",
|
||||
"navigation_bar.short.getting_started": "スタート",
|
||||
"navigation_bar.short.home": "ホーム",
|
||||
"navigation_bar.short.limited_timeline": "限定",
|
||||
"navigation_bar.short.lists": "リスト",
|
||||
"navigation_bar.short.logout": "ログアウト",
|
||||
"navigation_bar.short.notifications": "通知",
|
||||
|
@ -494,6 +510,7 @@
|
|||
"suggestions.heading": "おすすめユーザー",
|
||||
"tabs_bar.federated_timeline": "連合",
|
||||
"tabs_bar.home": "ホーム",
|
||||
"tabs_bar.limited_timeline": "限定ホーム",
|
||||
"tabs_bar.lists": "リスト",
|
||||
"tabs_bar.local_timeline": "ローカル",
|
||||
"tabs_bar.notifications": "通知",
|
||||
|
|
|
@ -20,6 +20,23 @@ const initialState = ImmutableMap({
|
|||
shows: ImmutableMap({
|
||||
reblog: true,
|
||||
reply: true,
|
||||
private: false,
|
||||
limited: false,
|
||||
direct: false,
|
||||
}),
|
||||
|
||||
regex: ImmutableMap({
|
||||
body: '',
|
||||
}),
|
||||
}),
|
||||
|
||||
limited: ImmutableMap({
|
||||
shows: ImmutableMap({
|
||||
reblog: true,
|
||||
reply: true,
|
||||
private: true,
|
||||
limited: true,
|
||||
direct: true,
|
||||
}),
|
||||
|
||||
regex: ImmutableMap({
|
||||
|
|
|
@ -160,7 +160,7 @@ export default function timelines(state = initialState, action) {
|
|||
return filterTimelines(state, action.relationship, action.statuses);
|
||||
case ACCOUNT_UNFOLLOW_SUCCESS:
|
||||
case ACCOUNT_UNSUBSCRIBE_SUCCESS:
|
||||
return filterTimeline('home', state, action.relationship, action.statuses);
|
||||
return filterTimeline('home', state, action.relationship, action.statuses).filterTimeline('limited', state, action.relationship, action.statuses);
|
||||
case TIMELINE_SCROLL_TOP:
|
||||
return updateTop(state, action.timeline, action.top);
|
||||
case TIMELINE_CONNECT:
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { createSelector } from 'reselect';
|
||||
import { List as ImmutableList, Map as ImmutableMap, is } from 'immutable';
|
||||
import { me } from '../initial_state';
|
||||
import { me, enable_limited_timeline } from '../initial_state';
|
||||
|
||||
const getAccountBase = (state, id) => state.getIn(['accounts', id], null);
|
||||
const getAccountCounters = (state, id) => state.getIn(['accounts_counters', id], null);
|
||||
|
@ -214,3 +214,25 @@ export const getAccountGallery = createSelector([
|
|||
|
||||
return medias;
|
||||
});
|
||||
|
||||
export const getHomeVisibilities = createSelector(
|
||||
state => state.getIn(['settings', 'home', 'shows']),
|
||||
shows => {
|
||||
return enable_limited_timeline ? (
|
||||
['public', 'unlisted']
|
||||
.concat(shows.get('private') ? ['private'] : [])
|
||||
.concat(shows.get('limited') ? ['limited'] : [])
|
||||
.concat(shows.get('direct') ? ['direct'] : [])
|
||||
) : [];
|
||||
});
|
||||
|
||||
export const getLimitedVisibilities = createSelector(
|
||||
state => state.getIn(['settings', 'limited', 'shows']),
|
||||
shows => {
|
||||
return enable_limited_timeline ? (
|
||||
[]
|
||||
.concat(shows.get('private') ? ['private'] : [])
|
||||
.concat(shows.get('limited') ? ['limited'] : [])
|
||||
.concat(shows.get('direct') ? ['direct'] : [])
|
||||
) : [];
|
||||
});
|
||||
|
|
|
@ -50,6 +50,7 @@ class UserSettingsDecorator
|
|||
user.settings['show_target'] = show_target_preference if change?('setting_show_target')
|
||||
user.settings['place_tab_bar_at_bottom'] = place_tab_bar_at_bottom_preference if change?('setting_place_tab_bar_at_bottom')
|
||||
user.settings['show_tab_bar_label'] = show_tab_bar_label_preference if change?('setting_show_tab_bar_label')
|
||||
user.settings['enable_limited_timeline'] = enable_limited_timeline_preference if change?('setting_enable_limited_timeline')
|
||||
end
|
||||
|
||||
def merged_notification_emails
|
||||
|
@ -192,6 +193,10 @@ class UserSettingsDecorator
|
|||
boolean_cast_setting 'setting_show_tab_bar_label'
|
||||
end
|
||||
|
||||
def enable_limited_timeline_preference
|
||||
boolean_cast_setting 'setting_enable_limited_timeline'
|
||||
end
|
||||
|
||||
def boolean_cast_setting(key)
|
||||
ActiveModel::Type::Boolean.new.cast(settings[key])
|
||||
end
|
||||
|
|
|
@ -8,27 +8,29 @@ class Feed
|
|||
@id = id
|
||||
end
|
||||
|
||||
def get(limit, max_id = nil, since_id = nil, min_id = nil)
|
||||
def get(limit, max_id = nil, since_id = nil, min_id = nil, visibilities = [])
|
||||
limit = limit.to_i
|
||||
max_id = max_id.to_i if max_id.present?
|
||||
since_id = since_id.to_i if since_id.present?
|
||||
min_id = min_id.to_i if min_id.present?
|
||||
|
||||
from_redis(limit, max_id, since_id, min_id)
|
||||
from_redis(limit, max_id, since_id, min_id, visibilities)
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def from_redis(limit, max_id, since_id, min_id)
|
||||
def from_redis(limit, max_id, since_id, min_id, visibilities)
|
||||
max_id = '+inf' if max_id.blank?
|
||||
if min_id.blank?
|
||||
since_id = '-inf' if since_id.blank?
|
||||
unhydrated = redis.zrevrangebyscore(key, "(#{max_id}", "(#{since_id}", limit: [0, limit], with_scores: true).map(&:first).map(&:to_i)
|
||||
unhydrated = redis.zrevrangebyscore(key, "(#{max_id}", "(#{since_id}", limit: [0, visibilities.empty? ? limit : FeedManager::MAX_ITEMS], with_scores: true).map(&:first).map(&:to_i)
|
||||
else
|
||||
unhydrated = redis.zrangebyscore(key, "(#{min_id}", "(#{max_id}", limit: [0, limit], with_scores: true).map(&:first).map(&:to_i)
|
||||
unhydrated = redis.zrangebyscore(key, "(#{min_id}", "(#{max_id}", limit: [0, visibilities.empty? ? limit : FeedManager::MAX_ITEMS], with_scores: true).map(&:first).map(&:to_i)
|
||||
end
|
||||
|
||||
Status.where(id: unhydrated).cache_ids
|
||||
statuses = Status.where(id: unhydrated)
|
||||
statuses = statuses.where(visibility: visibilities).limit(limit) unless visibilities.empty?
|
||||
statuses.cache_ids
|
||||
end
|
||||
|
||||
def key
|
||||
|
|
|
@ -129,7 +129,7 @@ class User < ApplicationRecord
|
|||
:show_follow_button_on_timeline, :show_subscribe_button_on_timeline, :show_target,
|
||||
:show_follow_button_on_timeline, :show_subscribe_button_on_timeline, :show_followed_by, :show_target,
|
||||
:follow_button_to_list_adder, :show_navigation_panel, :show_quote_button, :show_bookmark_button,
|
||||
:place_tab_bar_at_bottom,:show_tab_bar_label,
|
||||
:place_tab_bar_at_bottom,:show_tab_bar_label, :enable_limited_timeline,
|
||||
|
||||
to: :settings, prefix: :setting, allow_nil: false
|
||||
|
||||
|
|
|
@ -52,6 +52,7 @@ class InitialStateSerializer < ActiveModel::Serializer
|
|||
store[:show_target] = object.current_account.user.setting_show_target
|
||||
store[:place_tab_bar_at_bottom] = object.current_account.user.setting_place_tab_bar_at_bottom
|
||||
store[:show_tab_bar_label] = object.current_account.user.setting_show_tab_bar_label
|
||||
store[:enable_limited_timeline] = object.current_account.user.setting_enable_limited_timeline
|
||||
else
|
||||
store[:auto_play_gif] = Setting.auto_play_gif
|
||||
store[:display_media] = Setting.display_media
|
||||
|
|
|
@ -111,6 +111,7 @@ class REST::InstanceSerializer < ActiveModel::Serializer
|
|||
:subscribe_account,
|
||||
:subscribe_domain,
|
||||
:subscribe_keyword,
|
||||
:timeline_home_visibility,
|
||||
:timeline_no_local,
|
||||
:timeline_domain,
|
||||
:timeline_group,
|
||||
|
|
|
@ -60,6 +60,9 @@
|
|||
.fields-group
|
||||
= f.input :setting_show_tab_bar_label, as: :boolean, wrapper: :with_label
|
||||
|
||||
.fields-group
|
||||
= f.input :setting_enable_limited_timeline, as: :boolean, wrapper: :with_label
|
||||
|
||||
-# .fields-group
|
||||
-# = f.input :setting_show_target, as: :boolean, wrapper: :with_label
|
||||
|
||||
|
|
|
@ -51,6 +51,7 @@ en:
|
|||
setting_display_media_default: Hide media marked as sensitive
|
||||
setting_display_media_hide_all: Always hide media
|
||||
setting_display_media_show_all: Always show media
|
||||
setting_enable_limited_timeline: Enable a limited home to display private and circle and direct message
|
||||
setting_follow_button_to_list_adder: Change the behavior of the Follow / Subscribe button, open a dialog where you can select a list to follow / subscribe, or opt out of receiving at home
|
||||
setting_hide_network: Who you follow and who follows you will be hidden on your profile
|
||||
setting_noindex: Affects your public profile and post pages
|
||||
|
@ -186,6 +187,7 @@ en:
|
|||
setting_display_media_default: Default
|
||||
setting_display_media_hide_all: Hide all
|
||||
setting_display_media_show_all: Show all
|
||||
setting_enable_limited_timeline: Enable limited timeline
|
||||
setting_expand_spoilers: Always expand posts marked with content warnings
|
||||
setting_follow_button_to_list_adder: Open list add dialog with follow button
|
||||
setting_hide_network: Hide your social graph
|
||||
|
|
|
@ -51,6 +51,7 @@ ja:
|
|||
setting_display_media_default: 閲覧注意としてマークされたメディアは隠す
|
||||
setting_display_media_hide_all: メディアを常に隠す
|
||||
setting_display_media_show_all: メディアを常に表示する
|
||||
setting_enable_limited_timeline: フォロワー限定・サークル・ダイレクトメッセージを表示する限定ホームを有効にします
|
||||
setting_follow_button_to_list_adder: フォロー・購読ボタンの動作を変更し、フォロー・購読するリストを選択したり、ホームで受け取らないよう設定するダイアログを開きます
|
||||
setting_hide_network: フォローとフォロワーの情報がプロフィールページで見られないようにします
|
||||
setting_noindex: 公開プロフィールおよび各投稿ページに影響します
|
||||
|
@ -186,6 +187,7 @@ ja:
|
|||
setting_display_media_default: 標準
|
||||
setting_display_media_hide_all: 非表示
|
||||
setting_display_media_show_all: 表示
|
||||
setting_enable_limited_timeline: 限定ホームを有効にする
|
||||
setting_expand_spoilers: 閲覧注意としてマークされた投稿を常に展開する
|
||||
setting_follow_button_to_list_adder: フォローボタンでリスト追加ダイアログを開く
|
||||
setting_hide_network: 繋がりを隠す
|
||||
|
|
|
@ -49,6 +49,7 @@ defaults: &defaults
|
|||
show_target: false
|
||||
place_tab_bar_at_bottom: false
|
||||
show_tab_bar_label: false
|
||||
enable_limited_timeline: false
|
||||
notification_emails:
|
||||
follow: false
|
||||
reblog: false
|
||||
|
|
Loading…
Reference in a new issue