Add limited timeline

This commit is contained in:
noellabo 2021-04-12 21:43:25 +09:00
parent 2de85178bd
commit 64c363cb08
34 changed files with 453 additions and 54 deletions

View file

@ -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

View file

@ -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)
)

View file

@ -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}`);

View file

@ -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))))));
};

View file

@ -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);

View file

@ -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;

View file

@ -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' />,

View file

@ -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>
);
}

View file

@ -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());
},

View file

@ -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();

View file

@ -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>
);
}
}

View file

@ -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);

View 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>
);
}
}

View file

@ -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) {

View file

@ -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>

View file

@ -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' />

View file

@ -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;

View file

@ -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) => {

View file

@ -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');
}

View file

@ -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;

View file

@ -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",

View file

@ -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": "通知",

View file

@ -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({

View file

@ -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:

View file

@ -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'] : [])
) : [];
});

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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,

View file

@ -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

View file

@ -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

View file

@ -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: 繋がりを隠す

View file

@ -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