From 64c363cb083b3b9482a9bf058f3495e882348021 Mon Sep 17 00:00:00 2001 From: noellabo Date: Mon, 12 Apr 2021 21:43:25 +0900 Subject: [PATCH] Add limited timeline --- .../api/v1/timelines/home_controller.rb | 9 +- .../settings/preferences_controller.rb | 1 + app/javascript/mastodon/actions/compose.js | 9 +- app/javascript/mastodon/actions/streaming.js | 11 +- app/javascript/mastodon/actions/timelines.js | 33 ++++-- .../mastodon/features/compose/index.js | 21 ++-- .../features/getting_started/index.js | 10 +- .../components/column_settings.js | 22 +++- .../containers/column_settings_container.js | 6 + .../mastodon/features/home_timeline/index.js | 17 ++- .../components/column_settings.js | 49 ++++++++ .../containers/column_settings_container.js | 27 +++++ .../features/limited_timeline/index.js | 109 ++++++++++++++++++ .../features/ui/components/columns_area.js | 18 ++- .../ui/components/navigation_panel.js | 3 +- .../features/ui/components/tabs_bar.js | 25 ++-- .../ui/containers/status_list_container.js | 10 +- app/javascript/mastodon/features/ui/index.js | 15 ++- .../features/ui/util/async-components.js | 4 + app/javascript/mastodon/initial_state.js | 1 + app/javascript/mastodon/locales/en.json | 16 +++ app/javascript/mastodon/locales/ja.json | 17 +++ app/javascript/mastodon/reducers/settings.js | 17 +++ app/javascript/mastodon/reducers/timelines.js | 2 +- app/javascript/mastodon/selectors/index.js | 24 +++- app/lib/user_settings_decorator.rb | 5 + app/models/feed.rb | 14 ++- app/models/user.rb | 2 +- app/serializers/initial_state_serializer.rb | 1 + app/serializers/rest/instance_serializer.rb | 1 + .../settings/preferences/other/show.html.haml | 3 + config/locales/simple_form.en.yml | 2 + config/locales/simple_form.ja.yml | 2 + config/settings.yml | 1 + 34 files changed, 453 insertions(+), 54 deletions(-) create mode 100644 app/javascript/mastodon/features/limited_timeline/components/column_settings.js create mode 100644 app/javascript/mastodon/features/limited_timeline/containers/column_settings_container.js create mode 100644 app/javascript/mastodon/features/limited_timeline/index.js diff --git a/app/controllers/api/v1/timelines/home_controller.rb b/app/controllers/api/v1/timelines/home_controller.rb index c07e1a820..6003dc192 100644 --- a/app/controllers/api/v1/timelines/home_controller.rb +++ b/app/controllers/api/v1/timelines/home_controller.rb @@ -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 diff --git a/app/controllers/settings/preferences_controller.rb b/app/controllers/settings/preferences_controller.rb index 5f58721e3..8d88091b2 100644 --- a/app/controllers/settings/preferences_controller.rb +++ b/app/controllers/settings/preferences_controller.rb @@ -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) ) diff --git a/app/javascript/mastodon/actions/compose.js b/app/javascript/mastodon/actions/compose.js index 25aea0fe2..f20f2a3b9 100644 --- a/app/javascript/mastodon/actions/compose.js +++ b/app/javascript/mastodon/actions/compose.js @@ -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}`); diff --git a/app/javascript/mastodon/actions/streaming.js b/app/javascript/mastodon/actions/streaming.js index 10f5be5e8..2c74ce7d0 100644 --- a/app/javascript/mastodon/actions/streaming.js +++ b/app/javascript/mastodon/actions/streaming.js @@ -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)))))); }; diff --git a/app/javascript/mastodon/actions/timelines.js b/app/javascript/mastodon/actions/timelines.js index 227a08835..895dddc88 100644 --- a/app/javascript/mastodon/actions/timelines.js +++ b/app/javascript/mastodon/actions/timelines.js @@ -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); diff --git a/app/javascript/mastodon/features/compose/index.js b/app/javascript/mastodon/features/compose/index.js index 26eb5d2fc..16201835e 100644 --- a/app/javascript/mastodon/features/compose/index.js +++ b/app/javascript/mastodon/features/compose/index.js @@ -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; diff --git a/app/javascript/mastodon/features/getting_started/index.js b/app/javascript/mastodon/features/getting_started/index.js index 13b2122fc..f0586d8fc 100644 --- a/app/javascript/mastodon/features/getting_started/index.js +++ b/app/javascript/mastodon/features/getting_started/index.js @@ -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( + , + ); + height += 48; + } + navItems.push( , , diff --git a/app/javascript/mastodon/features/home_timeline/components/column_settings.js b/app/javascript/mastodon/features/home_timeline/components/column_settings.js index 455e21881..0247f60e2 100644 --- a/app/javascript/mastodon/features/home_timeline/components/column_settings.js +++ b/app/javascript/mastodon/features/home_timeline/components/column_settings.js @@ -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 (
@@ -27,6 +29,22 @@ class ColumnSettings extends React.PureComponent {
} />
+ + {enable_limited_timeline && + + +
+ } /> +
+ +
+ } /> +
+ +
+ } /> +
+
}
); } diff --git a/app/javascript/mastodon/features/home_timeline/containers/column_settings_container.js b/app/javascript/mastodon/features/home_timeline/containers/column_settings_container.js index fd8a39298..ed9d61e8e 100644 --- a/app/javascript/mastodon/features/home_timeline/containers/column_settings_container.js +++ b/app/javascript/mastodon/features/home_timeline/containers/column_settings_container.js @@ -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()); }, diff --git a/app/javascript/mastodon/features/home_timeline/index.js b/app/javascript/mastodon/features/home_timeline/index.js index dc440f2fe..a3fea194b 100644 --- a/app/javascript/mastodon/features/home_timeline/index.js +++ b/app/javascript/mastodon/features/home_timeline/index.js @@ -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(); diff --git a/app/javascript/mastodon/features/limited_timeline/components/column_settings.js b/app/javascript/mastodon/features/limited_timeline/components/column_settings.js new file mode 100644 index 000000000..e93b1a55d --- /dev/null +++ b/app/javascript/mastodon/features/limited_timeline/components/column_settings.js @@ -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 ( +
+ + +
+ } /> +
+ +
+ } /> +
+ + + +
+ } /> +
+ +
+ } /> +
+ +
+ } /> +
+
+ ); + } + +} diff --git a/app/javascript/mastodon/features/limited_timeline/containers/column_settings_container.js b/app/javascript/mastodon/features/limited_timeline/containers/column_settings_container.js new file mode 100644 index 000000000..1bdb8183c --- /dev/null +++ b/app/javascript/mastodon/features/limited_timeline/containers/column_settings_container.js @@ -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); diff --git a/app/javascript/mastodon/features/limited_timeline/index.js b/app/javascript/mastodon/features/limited_timeline/index.js new file mode 100644 index 000000000..9ba828936 --- /dev/null +++ b/app/javascript/mastodon/features/limited_timeline/index.js @@ -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 ( + + + + + + } + bindToDocument={!multiColumn} + /> + + ); + } + +} diff --git a/app/javascript/mastodon/features/ui/components/columns_area.js b/app/javascript/mastodon/features/ui/components/columns_area.js index c7f30aab1..50e33b90d 100644 --- a/app/javascript/mastodon/features/ui/components/columns_area.js +++ b/app/javascript/mastodon/features/ui/components/columns_area.js @@ -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) { diff --git a/app/javascript/mastodon/features/ui/components/navigation_panel.js b/app/javascript/mastodon/features/ui/components/navigation_panel.js index 71999ddda..70353d797 100644 --- a/app/javascript/mastodon/features/ui/components/navigation_panel.js +++ b/app/javascript/mastodon/features/ui/components/navigation_panel.js @@ -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 = () => (
+ {enable_limited_timeline && } diff --git a/app/javascript/mastodon/features/ui/components/tabs_bar.js b/app/javascript/mastodon/features/ui/components/tabs_bar.js index 607611cdc..306bcb09f 100644 --- a/app/javascript/mastodon/features/ui/components/tabs_bar.js +++ b/app/javascript/mastodon/features/ui/components/tabs_bar.js @@ -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 = [ , + , , , , @@ -20,8 +21,18 @@ export const links = [ , ]; +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 (
diff --git a/app/javascript/mastodon/features/ui/containers/status_list_container.js b/app/javascript/mastodon/features/ui/containers/status_list_container.js index 4ce4ac6c8..cc90423f4 100644 --- a/app/javascript/mastodon/features/ui/containers/status_list_container.js +++ b/app/javascript/mastodon/features/ui/containers/status_list_container.js @@ -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; diff --git a/app/javascript/mastodon/features/ui/index.js b/app/javascript/mastodon/features/ui/index.js index 625e8d41d..d37d491d9 100644 --- a/app/javascript/mastodon/features/ui/index.js +++ b/app/javascript/mastodon/features/ui/index.js @@ -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 { + @@ -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) => { diff --git a/app/javascript/mastodon/features/ui/util/async-components.js b/app/javascript/mastodon/features/ui/util/async-components.js index f4fd13383..5bac77a1d 100644 --- a/app/javascript/mastodon/features/ui/util/async-components.js +++ b/app/javascript/mastodon/features/ui/util/async-components.js @@ -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'); } diff --git a/app/javascript/mastodon/initial_state.js b/app/javascript/mastodon/initial_state.js index 83f4c4dc4..fc8b2da4c 100644 --- a/app/javascript/mastodon/initial_state.js +++ b/app/javascript/mastodon/initial_state.js @@ -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; diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json index 6f0903aea..5fd016af4 100644 --- a/app/javascript/mastodon/locales/en.json +++ b/app/javascript/mastodon/locales/en.json @@ -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", diff --git a/app/javascript/mastodon/locales/ja.json b/app/javascript/mastodon/locales/ja.json index bd2f0eab6..ed5be52ee 100644 --- a/app/javascript/mastodon/locales/ja.json +++ b/app/javascript/mastodon/locales/ja.json @@ -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": "通知", diff --git a/app/javascript/mastodon/reducers/settings.js b/app/javascript/mastodon/reducers/settings.js index 5ecc33344..72d4015f0 100644 --- a/app/javascript/mastodon/reducers/settings.js +++ b/app/javascript/mastodon/reducers/settings.js @@ -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({ diff --git a/app/javascript/mastodon/reducers/timelines.js b/app/javascript/mastodon/reducers/timelines.js index ac77bc5a1..993bc7199 100644 --- a/app/javascript/mastodon/reducers/timelines.js +++ b/app/javascript/mastodon/reducers/timelines.js @@ -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: diff --git a/app/javascript/mastodon/selectors/index.js b/app/javascript/mastodon/selectors/index.js index a274fbfe9..ed88ce743 100644 --- a/app/javascript/mastodon/selectors/index.js +++ b/app/javascript/mastodon/selectors/index.js @@ -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'] : []) + ) : []; +}); diff --git a/app/lib/user_settings_decorator.rb b/app/lib/user_settings_decorator.rb index 3d5db8f3e..08babe856 100644 --- a/app/lib/user_settings_decorator.rb +++ b/app/lib/user_settings_decorator.rb @@ -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 diff --git a/app/models/feed.rb b/app/models/feed.rb index f51dcfab1..28947cae5 100644 --- a/app/models/feed.rb +++ b/app/models/feed.rb @@ -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 diff --git a/app/models/user.rb b/app/models/user.rb index 12efce102..fbe3b0f85 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -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 diff --git a/app/serializers/initial_state_serializer.rb b/app/serializers/initial_state_serializer.rb index a918497a7..638a27aae 100644 --- a/app/serializers/initial_state_serializer.rb +++ b/app/serializers/initial_state_serializer.rb @@ -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 diff --git a/app/serializers/rest/instance_serializer.rb b/app/serializers/rest/instance_serializer.rb index e3d7d445e..8ddb0cb06 100644 --- a/app/serializers/rest/instance_serializer.rb +++ b/app/serializers/rest/instance_serializer.rb @@ -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, diff --git a/app/views/settings/preferences/other/show.html.haml b/app/views/settings/preferences/other/show.html.haml index 15f8ea618..eddad5484 100644 --- a/app/views/settings/preferences/other/show.html.haml +++ b/app/views/settings/preferences/other/show.html.haml @@ -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 diff --git a/config/locales/simple_form.en.yml b/config/locales/simple_form.en.yml index d36700397..2ba2cc128 100644 --- a/config/locales/simple_form.en.yml +++ b/config/locales/simple_form.en.yml @@ -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 diff --git a/config/locales/simple_form.ja.yml b/config/locales/simple_form.ja.yml index 8a66c3ee3..a52d482d0 100644 --- a/config/locales/simple_form.ja.yml +++ b/config/locales/simple_form.ja.yml @@ -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: 繋がりを隠す diff --git a/config/settings.yml b/config/settings.yml index b944445a1..f5b15be49 100644 --- a/config/settings.yml +++ b/config/settings.yml @@ -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