From ec075c3ccb361877ebda3618f581d25b1117b267 Mon Sep 17 00:00:00 2001 From: noellabo Date: Thu, 29 Dec 2022 17:47:23 +0900 Subject: [PATCH] Add personal visibility feature --- .../api/v1/accounts/statuses_controller.rb | 13 +- .../api/v1/timelines/personal_controller.rb | 70 ++++++++++ .../settings/preferences_controller.rb | 4 + app/helpers/application_helper.rb | 4 +- app/javascript/mastodon/actions/compose.js | 10 +- app/javascript/mastodon/actions/timelines.js | 11 +- app/javascript/mastodon/components/status.js | 2 + .../mastodon/components/status_action_bar.js | 4 +- .../compose/components/privacy_dropdown.js | 5 +- .../features/getting_started/index.js | 14 +- .../components/column_settings.js | 4 + .../components/column_settings.js | 4 + .../components/column_settings.js | 31 +++++ .../containers/column_settings_container.js | 21 +++ .../features/personal_timeline/index.js | 120 ++++++++++++++++++ .../picture_in_picture/components/footer.js | 2 +- .../features/status/components/action_bar.js | 2 +- .../status/components/detailed_status.js | 2 + .../features/ui/components/columns_area.js | 4 +- .../ui/components/navigation_panel.js | 3 +- .../features/ui/components/tabs_bar.js | 4 +- .../ui/containers/status_list_container.js | 12 +- app/javascript/mastodon/features/ui/index.js | 2 + .../features/ui/util/async-components.js | 4 + app/javascript/mastodon/initial_state.js | 4 + app/javascript/mastodon/locales/en.json | 11 ++ app/javascript/mastodon/locales/ja.json | 11 ++ app/javascript/mastodon/reducers/compose.js | 8 +- app/javascript/mastodon/reducers/settings.js | 18 ++- app/javascript/mastodon/reducers/timelines.js | 2 +- app/javascript/mastodon/selectors/index.js | 21 ++- app/lib/user_settings_decorator.rb | 4 + app/models/account.rb | 2 +- app/models/concerns/account_interactions.rb | 4 + app/models/personal_feed.rb | 30 +++++ app/models/status.rb | 35 ++++- app/models/user.rb | 4 +- app/policies/status_policy.rb | 8 +- app/serializers/initial_state_serializer.rb | 4 + app/serializers/rest/instance_serializer.rb | 1 + app/serializers/rest/status_serializer.rb | 4 +- app/services/fan_out_on_write_service.rb | 13 +- app/services/post_status_service.rb | 4 +- app/services/reblog_service.rb | 2 +- .../settings/preferences/other/show.html.haml | 12 ++ config/locales/en.yml | 2 + config/locales/ja.yml | 2 + config/locales/simple_form.en.yml | 8 ++ config/locales/simple_form.ja.yml | 8 ++ config/routes.rb | 1 + config/settings.yml | 4 + ...21230091953_add_personal_timeline_index.rb | 11 ++ db/schema.rb | 1 + 53 files changed, 540 insertions(+), 51 deletions(-) create mode 100644 app/controllers/api/v1/timelines/personal_controller.rb create mode 100644 app/javascript/mastodon/features/personal_timeline/components/column_settings.js create mode 100644 app/javascript/mastodon/features/personal_timeline/containers/column_settings_container.js create mode 100644 app/javascript/mastodon/features/personal_timeline/index.js create mode 100644 app/models/personal_feed.rb create mode 100644 db/migrate/20221230091953_add_personal_timeline_index.rb diff --git a/app/controllers/api/v1/accounts/statuses_controller.rb b/app/controllers/api/v1/accounts/statuses_controller.rb index 3c78170c7..261e31a4b 100644 --- a/app/controllers/api/v1/accounts/statuses_controller.rb +++ b/app/controllers/api/v1/accounts/statuses_controller.rb @@ -31,10 +31,11 @@ class Api::V1::Accounts::StatusesController < Api::BaseController def cached_account_statuses statuses = truthy_param?(:pinned) ? pinned_scope : permitted_account_statuses - statuses.merge!(only_media_scope) if truthy_param?(:only_media) - statuses.merge!(no_replies_scope) if truthy_param?(:exclude_replies) - statuses.merge!(no_reblogs_scope) if truthy_param?(:exclude_reblogs) - statuses.merge!(hashtag_scope) if params[:tagged].present? + statuses.merge!(only_media_scope) if truthy_param?(:only_media) + statuses.merge!(no_replies_scope) if truthy_param?(:exclude_replies) + statuses.merge!(no_reblogs_scope) if truthy_param?(:exclude_reblogs) + statuses.merge!(hashtag_scope) if params[:tagged].present? + statuses.merge!(no_personal_scope) if current_user&.setting_hide_personal_from_account cache_collection_paginated_by_id( statuses, @@ -78,6 +79,10 @@ class Api::V1::Accounts::StatusesController < Api::BaseController end end + def no_personal_scope + Status.without_personal_visibility + end + def pagination_params(core_params) params.slice(:limit, :only_media, :exclude_replies, :compact).permit(:limit, :only_media, :exclude_replies, :compact).merge(core_params) end diff --git a/app/controllers/api/v1/timelines/personal_controller.rb b/app/controllers/api/v1/timelines/personal_controller.rb new file mode 100644 index 000000000..9fc1e641b --- /dev/null +++ b/app/controllers/api/v1/timelines/personal_controller.rb @@ -0,0 +1,70 @@ +# frozen_string_literal: true + +class Api::V1::Timelines::PersonalController < Api::BaseController + before_action -> { doorkeeper_authorize! :read, :'read:statuses' }, only: [:show] + before_action :require_user!, only: [:show] + after_action :insert_pagination_headers, unless: -> { @statuses.empty? } + + def show + @statuses = load_statuses + + if compact? + render json: CompactStatusesPresenter.new(statuses: @statuses), serializer: REST::CompactStatusesSerializer + else + account_ids = @statuses.filter(&:quote?).map { |status| status.quote.account_id }.uniq + + render json: @statuses, each_serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id), account_relationships: AccountRelationshipsPresenter.new(account_ids, current_user&.account_id), status: account_home_feed.regenerating? ? 206 : 200 + end + end + + private + + def load_statuses + cached_personal_statuses + end + + def cached_personal_statuses + cache_collection personal_statuses, Status + end + + def personal_statuses + personal_feed.get( + limit_param(DEFAULT_STATUSES_LIMIT), + params[:max_id], + params[:since_id], + params[:min_id], + ) + end + + def personal_feed + PersonalFeed.new(current_account) + end + + def compact? + truthy_param?(:compact) + end + + def insert_pagination_headers + set_pagination_headers(next_path, prev_path) + end + + def pagination_params(core_params) + params.slice(:local, :limit).permit(:local, :limit).merge(core_params) + end + + def next_path + api_v1_timelines_home_url pagination_params(max_id: pagination_max_id) + end + + def prev_path + api_v1_timelines_home_url pagination_params(min_id: pagination_since_id) + end + + def pagination_max_id + @statuses.last.id + end + + def pagination_since_id + @statuses.first.id + end +end diff --git a/app/controllers/settings/preferences_controller.rb b/app/controllers/settings/preferences_controller.rb index 5b961bfd0..8a08ab35c 100644 --- a/app/controllers/settings/preferences_controller.rb +++ b/app/controllers/settings/preferences_controller.rb @@ -72,6 +72,7 @@ class Settings::PreferencesController < Settings::BaseController :setting_show_target, :setting_enable_federated_timeline, :setting_enable_limited_timeline, + :setting_enable_personal_timeline, :setting_enable_local_timeline, :setting_enable_reaction, :setting_compact_reaction, @@ -113,6 +114,9 @@ class Settings::PreferencesController < Settings::BaseController :setting_disable_account_delete, :setting_prohibited_words, :setting_disable_relative_time, + :setting_hide_direct_from_timeline, + :setting_hide_personal_from_timeline, + :setting_hide_personal_from_account, setting_prohibited_visibilities: [], notification_emails: %i(follow follow_request reblog favourite emoji_reaction status_reference mention digest report pending_account trending_tag), interactions: %i(must_be_follower must_be_following must_be_following_dm must_be_dm_to_send_email must_be_following_reference) diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 76a2f101c..a479864cd 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -106,6 +106,8 @@ module ApplicationHelper fa_icon('user-circle', title: I18n.t('statuses.visibilities.limited')) elsif status.direct_visibility? fa_icon('envelope', title: I18n.t('statuses.visibilities.direct')) + elsif status.personal_visibility? + fa_icon('book', title: I18n.t('statuses.visibilities.personal')) end end @@ -205,7 +207,7 @@ module ApplicationHelper text: [params[:title], params[:text], params[:url]].compact.join(' '), } - permit_visibilities = %w(public unlisted private mutual direct) + permit_visibilities = %w(public unlisted private mutual direct personal) default_privacy = current_account&.user&.setting_default_privacy permit_visibilities.shift(permit_visibilities.index(default_privacy) + 1) if default_privacy.present? state_params[:visibility] = params[:visibility] if permit_visibilities.include? params[:visibility] diff --git a/app/javascript/mastodon/actions/compose.js b/app/javascript/mastodon/actions/compose.js index 3c144eed9..382552eeb 100644 --- a/app/javascript/mastodon/actions/compose.js +++ b/app/javascript/mastodon/actions/compose.js @@ -207,14 +207,14 @@ export const getDateTimeFromText = (value, origin = new Date()) => { if (value.length >= 7) { const isoDateTime = parseISO(value); - if (isoDateTime.toString() === "Invalid Date") { + if (isoDateTime.toString() === 'Invalid Date') { return null; } else { return isoDateTime; } } - return null + return null; })(); return { @@ -304,7 +304,7 @@ export function submitCompose(routerHistory) { } }; - if (homeVisibilities.length == 0 || homeVisibilities.includes(response.data.visibility)) { + if (homeVisibilities.length || homeVisibilities.includes(response.data.visibility)) { insertIfOnline('home'); } @@ -312,6 +312,10 @@ export function submitCompose(routerHistory) { insertIfOnline('limited'); } + if (['personal'].includes(response.data.visibility)) { + insertIfOnline('personal'); + } + if (response.data.in_reply_to_id === null && response.data.visibility === 'public') { if (enableFederatedTimeline) { insertIfOnline('public'); diff --git a/app/javascript/mastodon/actions/timelines.js b/app/javascript/mastodon/actions/timelines.js index af1f1f31b..7288bb206 100644 --- a/app/javascript/mastodon/actions/timelines.js +++ b/app/javascript/mastodon/actions/timelines.js @@ -63,7 +63,7 @@ export function updateTimeline(timeline, status, accept) { const limitedVisibilities = getLimitedVisibilities(getState()); if (timeline === 'home') { - if (homeVisibilities.length == 0 || homeVisibilities.includes(visibility)) { + if (homeVisibilities.length || homeVisibilities.includes(visibility)) { insertTimeline('home'); dispatch(submitMarkers()); } @@ -71,6 +71,10 @@ export function updateTimeline(timeline, status, accept) { if (limitedVisibilities.includes(visibility)) { insertTimeline('limited'); } + + if (visibility === 'personal') { + insertTimeline('personal'); + } } else { insertTimeline(timeline); } @@ -177,8 +181,9 @@ export function expandTimeline(timelineId, path, params = {}, done = noOp) { }; }; -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 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 expandPersonalTimeline = ({ maxId } = {}, done = noOp) => expandTimeline('personal', '/api/v1/timelines/personal', { max_id: maxId}, done); export const expandPublicTimeline = ({ maxId, onlyMedia, withoutMedia, withoutBot, onlyRemote } = {}, done = noOp) => expandTimeline(`public${onlyRemote ? ':remote' : ''}${withoutBot ? ':nobot' : ':bot'}${withoutMedia ? ':nomedia' : ''}${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { remote: !!onlyRemote, max_id: maxId, only_media: !!onlyMedia, without_media: !!withoutMedia, without_bot: !!withoutBot }, done); export const expandDomainTimeline = (domain, { maxId, onlyMedia, withoutMedia, withoutBot } = {}, done = noOp) => expandTimeline(`domain${withoutBot ? ':nobot' : ':bot'}${withoutMedia ? ':nomedia' : ''}${onlyMedia ? ':media' : ''}:${domain}`, '/api/v1/timelines/public', { local: false, domain: domain, max_id: maxId, only_media: !!onlyMedia, without_media: !!withoutMedia, without_bot: !!withoutBot }, done); export const expandGroupTimeline = (id, { maxId, onlyMedia, withoutMedia, tagged } = {}, done = noOp) => expandTimeline(`group:${id}${withoutMedia ? ':nomedia' : ''}${onlyMedia ? ':media' : ''}${tagged ? `:${tagged}` : ''}`, `/api/v1/timelines/group/${id}`, { max_id: maxId, only_media: !!onlyMedia, without_media: !!withoutMedia, tagged: tagged }, done); diff --git a/app/javascript/mastodon/components/status.js b/app/javascript/mastodon/components/status.js index 746526309..a8bac7a34 100644 --- a/app/javascript/mastodon/components/status.js +++ b/app/javascript/mastodon/components/status.js @@ -84,6 +84,7 @@ const messages = defineMessages({ unlisted_short: { id: 'privacy.unlisted.short', defaultMessage: 'Unlisted' }, private_short: { id: 'privacy.private.short', defaultMessage: 'Followers-only' }, mutual_short: { id: 'privacy.mutual.short', defaultMessage: 'Mutual-followers-only' }, + personal_short: { id: 'privacy.personal.short', defaultMessage: 'Personal' }, limited_short: { id: 'privacy.limited.short', defaultMessage: 'Circle' }, direct_short: { id: 'privacy.direct.short', defaultMessage: 'Direct' }, mark_ancestor: { id: 'thread_mark.ancestor', defaultMessage: 'Has reference' }, @@ -420,6 +421,7 @@ class Status extends ImmutablePureComponent { 'mutual': { icon: 'exchange', text: intl.formatMessage(messages.mutual_short) }, 'limited': { icon: 'user-circle', text: intl.formatMessage(messages.limited_short) }, 'direct': { icon: 'envelope', text: intl.formatMessage(messages.direct_short) }, + 'personal': { icon: 'book', text: intl.formatMessage(messages.personal_short) }, }; if (hidden) { diff --git a/app/javascript/mastodon/components/status_action_bar.js b/app/javascript/mastodon/components/status_action_bar.js index 281ca9bc6..7dbae7aed 100644 --- a/app/javascript/mastodon/components/status_action_bar.js +++ b/app/javascript/mastodon/components/status_action_bar.js @@ -473,9 +473,9 @@ class StatusActionBar extends ImmutablePureComponent { ); - const referenceDisabled = expired || !referenced && referenceCountLimit || ['limited', 'direct'].includes(status.get('visibility')); + const referenceDisabled = expired || !referenced && referenceCountLimit || ['limited', 'direct', 'personal'].includes(status.get('visibility')); - const reactionsCounter = compactReaction && contextType != 'thread' && status.get('emoji_reactions_count') > 0 ? status.get('emoji_reactions_count') : undefined; + const reactionsCounter = compactReaction && contextType !== 'thread' && status.get('emoji_reactions_count') > 0 ? status.get('emoji_reactions_count') : undefined; return (
diff --git a/app/javascript/mastodon/features/compose/components/privacy_dropdown.js b/app/javascript/mastodon/features/compose/components/privacy_dropdown.js index b91ace184..b4f401a81 100644 --- a/app/javascript/mastodon/features/compose/components/privacy_dropdown.js +++ b/app/javascript/mastodon/features/compose/components/privacy_dropdown.js @@ -19,6 +19,8 @@ const messages = defineMessages({ private_long: { id: 'privacy.private.long', defaultMessage: 'Visible for followers only' }, mutual_short: { id: 'privacy.mutual.short', defaultMessage: 'Mutuals-followers-only' }, mutual_long: { id: 'privacy.mutual.long', defaultMessage: 'Visible for mutual followers only (Supported servers only)' }, + personal_short: { id: 'privacy.personal.short', defaultMessage: 'Personal' }, + personal_long: { id: 'privacy.personal.long', defaultMessage: 'Visible for personal only' }, direct_short: { id: 'privacy.direct.short', defaultMessage: 'Direct' }, direct_long: { id: 'privacy.direct.long', defaultMessage: 'Visible for mentioned users only' }, limited_short: { id: 'privacy.limited.short', defaultMessage: 'Circle' }, @@ -249,8 +251,9 @@ class PrivacyDropdown extends React.PureComponent { ...!this.props.noDirect && [ { icon: 'user-circle', value: 'limited', text: formatMessage(messages.limited_short), meta: formatMessage(messages.limited_long) }, { icon: 'envelope', value: 'direct', text: formatMessage(messages.direct_short), meta: formatMessage(messages.direct_long) }, + { icon: 'book', value: 'personal', text: formatMessage(messages.personal_short), meta: formatMessage(messages.personal_long) }, ], - ].filter(option => !prohibitedVisibilities?.includes(option.value)); + ].filter(option => option && !prohibitedVisibilities?.includes(option.value)); } render () { diff --git a/app/javascript/mastodon/features/getting_started/index.js b/app/javascript/mastodon/features/getting_started/index.js index f524bb060..2fffffc8a 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, enableLimitedTimeline, enableFederatedTimeline, enableLocalTimeline, enableEmptyColumn, defaultColumnWidth } from '../../initial_state'; +import { me, profile_directory, showTrends, enableLimitedTimeline, enablePersonalTimeline, enableFederatedTimeline, enableLocalTimeline, enableEmptyColumn, defaultColumnWidth } from '../../initial_state'; import { fetchFollowRequests } from 'mastodon/actions/accounts'; import { fetchFavouriteDomains } from 'mastodon/actions/favourite_domains'; import { fetchFavouriteTags } from 'mastodon/actions/favourite_tags'; @@ -38,6 +38,7 @@ const messages = defineMessages({ 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' }, + personal_timeline: { id: 'navigation_bar.personal_timeline', defaultMessage: 'Personal' }, lists: { id: 'navigation_bar.lists', defaultMessage: 'Lists' }, circles: { id: 'navigation_bar.circles', defaultMessage: 'Circles' }, discover: { id: 'navigation_bar.discover', defaultMessage: 'Discover' }, @@ -122,7 +123,7 @@ class GettingStarted extends ImmutablePureComponent { render () { const { intl, myAccount, columns, multiColumn, unreadFollowRequests, lists, favourite_domains, favourite_tags, columnWidth } = this.props; - + const navItems = []; let height = (multiColumn) ? 0 : 60; @@ -178,7 +179,7 @@ class GettingStarted extends ImmutablePureComponent { height += 34 + 48*2; navItems.push( - + , ); height += 34; @@ -226,6 +227,13 @@ class GettingStarted extends ImmutablePureComponent { height += 48; } + if (enablePersonalTimeline && multiColumn && !columns.find(item => item.get('id') === 'PERSONAL')) { + 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 166689649..c338bb15b 100644 --- a/app/javascript/mastodon/features/home_timeline/components/column_settings.js +++ b/app/javascript/mastodon/features/home_timeline/components/column_settings.js @@ -44,6 +44,10 @@ class ColumnSettings extends React.PureComponent {
} />
+ +
+ } /> +
}
); diff --git a/app/javascript/mastodon/features/limited_timeline/components/column_settings.js b/app/javascript/mastodon/features/limited_timeline/components/column_settings.js index e93b1a55d..99b1a9fef 100644 --- a/app/javascript/mastodon/features/limited_timeline/components/column_settings.js +++ b/app/javascript/mastodon/features/limited_timeline/components/column_settings.js @@ -42,6 +42,10 @@ class ColumnSettings extends React.PureComponent {
} />
+ +
+ } /> +
); } diff --git a/app/javascript/mastodon/features/personal_timeline/components/column_settings.js b/app/javascript/mastodon/features/personal_timeline/components/column_settings.js new file mode 100644 index 000000000..c84a34e9a --- /dev/null +++ b/app/javascript/mastodon/features/personal_timeline/components/column_settings.js @@ -0,0 +1,31 @@ +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 } = this.props; + + return ( +
+ + +
+ } /> +
+
+ ); + } + +} diff --git a/app/javascript/mastodon/features/personal_timeline/containers/column_settings_container.js b/app/javascript/mastodon/features/personal_timeline/containers/column_settings_container.js new file mode 100644 index 000000000..92e6c995b --- /dev/null +++ b/app/javascript/mastodon/features/personal_timeline/containers/column_settings_container.js @@ -0,0 +1,21 @@ +import { connect } from 'react-redux'; +import ColumnSettings from '../components/column_settings'; +import { changeSetting, saveSettings } from '../../../actions/settings'; + +const mapStateToProps = state => ({ + settings: state.getIn(['settings', 'personal']), +}); + +const mapDispatchToProps = dispatch => ({ + + onChange (key, checked) { + dispatch(changeSetting(['personal', ...key], checked)); + }, + + onSave () { + dispatch(saveSettings()); + }, + +}); + +export default connect(mapStateToProps, mapDispatchToProps)(ColumnSettings); diff --git a/app/javascript/mastodon/features/personal_timeline/index.js b/app/javascript/mastodon/features/personal_timeline/index.js new file mode 100644 index 000000000..49b1e7aa2 --- /dev/null +++ b/app/javascript/mastodon/features/personal_timeline/index.js @@ -0,0 +1,120 @@ +import React from 'react'; +import { connect } from 'react-redux'; +import { expandPersonalTimeline } 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 { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; +import ColumnSettingsContainer from './containers/column_settings_container'; +import { defaultColumnWidth } from 'mastodon/initial_state'; +import { changeSetting } from '../../actions/settings'; +import { changeColumnParams } from '../../actions/columns'; + +const messages = defineMessages({ + title: { id: 'column.personal', defaultMessage: 'Personal' }, +}); + +const mapStateToProps = (state, { columnId }) => { + const uuid = columnId; + const columns = state.getIn(['settings', 'columns']); + const index = columns.findIndex(c => c.get('uuid') === uuid); + const columnWidth = (columnId && index >= 0) ? columns.get(index).getIn(['params', 'columnWidth']) : state.getIn(['settings', 'personal', 'columnWidth']); + + return { + hasUnread: state.getIn(['timelines', 'personal', 'unread']) > 0, + columnWidth: columnWidth ?? defaultColumnWidth, + }; +}; + +export default @connect(mapStateToProps) +@injectIntl +class PersonalTimeline extends React.PureComponent { + + static propTypes = { + dispatch: PropTypes.func.isRequired, + intl: PropTypes.object.isRequired, + hasUnread: PropTypes.bool, + columnId: PropTypes.string, + multiColumn: PropTypes.bool, + columnWidth: PropTypes.string, + }; + + handlePin = () => { + const { columnId, dispatch } = this.props; + + if (columnId) { + dispatch(removeColumn(columnId)); + } else { + dispatch(addColumn('PERSONAL', {})); + } + } + + handleMove = (dir) => { + const { columnId, dispatch } = this.props; + dispatch(moveColumn(columnId, dir)); + } + + handleHeaderClick = () => { + this.column.scrollTop(); + } + + setRef = c => { + this.column = c; + } + + handleLoadMore = maxId => { + const { dispatch } = this.props; + + dispatch(expandPersonalTimeline({ maxId })); + } + + componentDidMount () { + const { dispatch } = this.props; + + dispatch(expandPersonalTimeline({})); + } + + handleWidthChange = (value) => { + const { columnId, dispatch } = this.props; + + if (columnId) { + dispatch(changeColumnParams(columnId, 'columnWidth', value)); + } else { + dispatch(changeSetting(['personal', 'columnWidth'], value)); + } + } + + render () { + const { intl, hasUnread, columnId, multiColumn, columnWidth } = this.props; + const pinned = !!columnId; + + return ( + + + + } + bindToDocument={!multiColumn} + /> + + ); + } + +} diff --git a/app/javascript/mastodon/features/picture_in_picture/components/footer.js b/app/javascript/mastodon/features/picture_in_picture/components/footer.js index c22e5e279..9ae80b62d 100644 --- a/app/javascript/mastodon/features/picture_in_picture/components/footer.js +++ b/app/javascript/mastodon/features/picture_in_picture/components/footer.js @@ -247,7 +247,7 @@ class Footer extends ImmutablePureComponent { reblogTitle = intl.formatMessage(messages.cannot_reblog); } - const referenceDisabled = expired || !referenced && referenceCountLimit || ['limited', 'direct'].includes(status.get('visibility')); + const referenceDisabled = expired || !referenced && referenceCountLimit || ['limited', 'direct', 'personal'].includes(status.get('visibility')); return (
diff --git a/app/javascript/mastodon/features/status/components/action_bar.js b/app/javascript/mastodon/features/status/components/action_bar.js index 89fdc5561..053da6b42 100644 --- a/app/javascript/mastodon/features/status/components/action_bar.js +++ b/app/javascript/mastodon/features/status/components/action_bar.js @@ -419,7 +419,7 @@ class ActionBar extends React.PureComponent { reblogTitle = intl.formatMessage(messages.cannot_reblog); } - const referenceDisabled = expired || !referenced && referenceCountLimit || ['limited', 'direct'].includes(status.get('visibility')); + const referenceDisabled = expired || !referenced && referenceCountLimit || ['limited', 'direct', 'personal'].includes(status.get('visibility')); return (
diff --git a/app/javascript/mastodon/features/status/components/detailed_status.js b/app/javascript/mastodon/features/status/components/detailed_status.js index 2a069f960..6e8bfea64 100644 --- a/app/javascript/mastodon/features/status/components/detailed_status.js +++ b/app/javascript/mastodon/features/status/components/detailed_status.js @@ -25,6 +25,7 @@ const messages = defineMessages({ unlisted_short: { id: 'privacy.unlisted.short', defaultMessage: 'Unlisted' }, private_short: { id: 'privacy.private.short', defaultMessage: 'Followers-only' }, mutual_short: { id: 'privacy.mutual.short', defaultMessage: 'Mutual-followers-only' }, + personal_short: { id: 'privacy.personal.short', defaultMessage: 'Personal' }, limited_short: { id: 'privacy.limited.short', defaultMessage: 'Circle' }, direct_short: { id: 'privacy.direct.short', defaultMessage: 'Direct' }, }); @@ -357,6 +358,7 @@ class DetailedStatus extends ImmutablePureComponent { 'mutual': { icon: 'exchange', text: intl.formatMessage(messages.mutual_short) }, 'limited': { icon: 'user-circle', text: intl.formatMessage(messages.limited_short) }, 'direct': { icon: 'envelope', text: intl.formatMessage(messages.direct_short) }, + 'personal': { icon: 'book', text: intl.formatMessage(messages.personal_short) }, }; const visibilityIcon = visibilityIconInfo[status.get('visibility')]; diff --git a/app/javascript/mastodon/features/ui/components/columns_area.js b/app/javascript/mastodon/features/ui/components/columns_area.js index 8b32c5431..d662f8917 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, { Fragment } from 'react'; +import React from 'react'; import PropTypes from 'prop-types'; import { defineMessages, injectIntl } from 'react-intl'; import ImmutablePropTypes from 'react-immutable-proptypes'; @@ -25,6 +25,7 @@ import { HashtagTimeline, DirectTimeline, LimitedTimeline, + PersonalTimeline, FavouritedStatuses, BookmarkedStatuses, ListTimeline, @@ -56,6 +57,7 @@ const componentMap = { 'HASHTAG': HashtagTimeline, 'DIRECT': DirectTimeline, 'LIMITED': LimitedTimeline, + 'PERSONAL': PersonalTimeline, 'FAVOURITES': FavouritedStatuses, 'BOOKMARKS': BookmarkedStatuses, 'LIST': ListTimeline, diff --git a/app/javascript/mastodon/features/ui/components/navigation_panel.js b/app/javascript/mastodon/features/ui/components/navigation_panel.js index a4a33b410..d5eb641cc 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, enableLimitedTimeline, enableFederatedTimeline, enableLocalTimeline } from 'mastodon/initial_state'; +import { profile_directory, showTrends, enableLimitedTimeline, enableFederatedTimeline, enableLocalTimeline, enablePersonalTimeline } from 'mastodon/initial_state'; import NotificationsCounterIcon from './notifications_counter_icon'; import FollowRequestsNavLink from './follow_requests_nav_link'; import ListPanel from './list_panel'; @@ -14,6 +14,7 @@ const NavigationPanel = () => (
{enableLimitedTimeline && } + {enablePersonalTimeline && } {enableLocalTimeline && } diff --git a/app/javascript/mastodon/features/ui/components/tabs_bar.js b/app/javascript/mastodon/features/ui/components/tabs_bar.js index f41bbaadd..ab414dc3e 100644 --- a/app/javascript/mastodon/features/ui/components/tabs_bar.js +++ b/app/javascript/mastodon/features/ui/components/tabs_bar.js @@ -6,7 +6,7 @@ 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, enableLimitedTimeline, enableFederatedTimeline, enableLocalTimeline } from 'mastodon/initial_state'; +import { place_tab_bar_at_bottom, show_tab_bar_label, enableLimitedTimeline, enableFederatedTimeline, enableLocalTimeline, enablePersonalTimeline } from 'mastodon/initial_state'; import ImmutablePropTypes from 'react-immutable-proptypes'; import { connect } from 'react-redux'; import { createSelector } from 'reselect'; @@ -14,6 +14,7 @@ import classNames from 'classnames'; const link_home = <>{msg}} /> <>{msg}} />; const link_limited = <>{msg}} /> <>{msg}} />; +const link_personal = <>{msg}} /> <>{msg}} />; const link_notifications = <>{msg}} /> <>{msg}} />; const link_local = <>{msg}} /> <>{msg}} />; const link_public = <>{msg}} /> <>{msg}} />; @@ -40,6 +41,7 @@ export const getLinks = memoize((favouriteLists = null) => { return [ link_home, enableLimitedTimeline ? link_limited : null, + enablePersonalTimeline ? link_personal : null, link_favourite_lists, link_notifications, enableLocalTimeline ? link_local : null, 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 cc90423f4..a9df07aa0 100644 --- a/app/javascript/mastodon/features/ui/containers/status_list_container.js +++ b/app/javascript/mastodon/features/ui/containers/status_list_container.js @@ -7,9 +7,19 @@ import { createSelector } from 'reselect'; import { debounce } from 'lodash'; import { me } from '../../../initial_state'; +const visibilitiesByType = (state, type) => { + if (type === 'home') { + return getHomeVisibilities(state); + } else if (type === 'limited') { + return getLimitedVisibilities(state); + } else { + return []; + } +}; + const makeGetStatusIds = (pending = false) => createSelector([ (state, { type }) => state.getIn(['settings', type], ImmutableMap()), - (state, { type }) => type === 'home' ? getHomeVisibilities(state) : type === 'limited' ? getLimitedVisibilities(state) : [], + (state, { type }) => visibilitiesByType(state, type), (state, { type }) => state.getIn(['timelines', type, pending ? 'pendingItems' : 'items'], ImmutableList()), (state) => state.get('statuses'), ], (columnSettings, visibilities, statusIds, statuses) => { diff --git a/app/javascript/mastodon/features/ui/index.js b/app/javascript/mastodon/features/ui/index.js index 3a2d6786d..0dcdc9bc1 100644 --- a/app/javascript/mastodon/features/ui/index.js +++ b/app/javascript/mastodon/features/ui/index.js @@ -46,6 +46,7 @@ import { Mentions, DirectTimeline, LimitedTimeline, + PersonalTimeline, HashtagTimeline, Notifications, FollowRequests, @@ -182,6 +183,7 @@ class SwitchingColumnsArea extends React.PureComponent { + diff --git a/app/javascript/mastodon/features/ui/util/async-components.js b/app/javascript/mastodon/features/ui/util/async-components.js index 8281a1a66..4e1397d52 100644 --- a/app/javascript/mastodon/features/ui/util/async-components.js +++ b/app/javascript/mastodon/features/ui/util/async-components.js @@ -42,6 +42,10 @@ export function LimitedTimeline() { return import(/* webpackChunkName: "features/limited_timeline" */'../../limited_timeline'); } +export function PersonalTimeline() { + return import(/* webpackChunkName: "features/personal_timeline" */'../../personal_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 e7b0aaad2..a168ef990 100644 --- a/app/javascript/mastodon/initial_state.js +++ b/app/javascript/mastodon/initial_state.js @@ -43,6 +43,7 @@ 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 enableLimitedTimeline = getMeta('enable_limited_timeline'); +export const enablePersonalTimeline = getMeta('enable_personal_timeline'); export const enableFederatedTimeline = getMeta('enable_federated_timeline') ?? true; export const enableLocalTimeline = getMeta('enable_local_timeline') ?? true; export const enableReaction = getMeta('enable_reaction'); @@ -66,6 +67,9 @@ export const disableDomainBlock = getMeta('disable_domain_block'); export const disableClearAllNotifications = getMeta('disable_clear_all_notifications'); export const disableAccountDelete = getMeta('disable_account_delete'); export const disableRelativeTime = getMeta('disable_relative_time'); +export const hideDirectFromTimeline = getMeta('hide_direct_from_timeline'); +export const hidePersonalFromTimeline = getMeta('hide_personal_from_timeline'); +export const hidePersonalFromAccount = getMeta('hide_personal_from_account'); export const maxChars = initialState?.max_toot_chars ?? 500; diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json index 1b0878344..d1c6c4bf3 100644 --- a/app/javascript/mastodon/locales/en.json +++ b/app/javascript/mastodon/locales/en.json @@ -125,6 +125,7 @@ "column.lists": "Lists", "column.mutes": "Muted users", "column.notifications": "Notifications", + "column.personal": "Personal home", "column.pins": "Pinned posts", "column.public": "Federated timeline", "column_back_button.label": "Back", @@ -265,6 +266,7 @@ "empty_column.lists": "You don't have any lists yet. When you create one, it will show up here.", "empty_column.mutes": "You haven't muted any users yet.", "empty_column.notifications": "You don't have any notifications yet. When other people interact with you, you will see it here.", + "empty_column.personal": "Personal posts unavailable", "empty_column.pinned_unavailable": "Pinned posts unavailable", "empty_column.public": "There is nothing here! Write something publicly, or manually follow users from other servers to fill it up", "empty_column.referred_by_statuses": "There are no referred by posts yet. When someone refers a post, it will appear here.", @@ -310,6 +312,7 @@ "home.column_settings.basic": "Basic", "home.column_settings.show_direct": "Show direct", "home.column_settings.show_limited": "Show limited", + "home.column_settings.show_personal": "Show personal", "home.column_settings.show_private": "Show private", "home.column_settings.show_reblogs": "Show boosts", "home.column_settings.show_replies": "Show replies", @@ -367,6 +370,7 @@ "limited.column_settings.basic": "Basic", "limited.column_settings.show_direct": "Show direct", "limited.column_settings.show_limited": "Show limited", + "limited.column_settings.show_personal": "Show personal", "limited.column_settings.show_private": "Show private", "limited.column_settings.show_reblogs": "Show boosts", "limited.column_settings.show_replies": "Show replies", @@ -418,6 +422,7 @@ "navigation_bar.logout": "Logout", "navigation_bar.mutes": "Muted users", "navigation_bar.personal": "Personal", + "navigation_bar.personal_timeline": "Personal home", "navigation_bar.pins": "Pinned posts", "navigation_bar.preferences": "Preferences", "navigation_bar.public_timeline": "Federated timeline", @@ -429,6 +434,7 @@ "navigation_bar.short.lists": "Lists", "navigation_bar.short.logout": "Logout", "navigation_bar.short.notifications": "Notif.", + "navigation_bar.short.personal_timeline": "Per.", "navigation_bar.short.preferences": "Pref.", "navigation_bar.short.public_timeline": "FTL", "navigation_bar.short.search": "Search", @@ -481,6 +487,8 @@ "notifications_permission_banner.enable": "Enable desktop notifications", "notifications_permission_banner.how_to_control": "To receive notifications when Mastodon isn't open, enable desktop notifications. You can control precisely which types of interactions generate desktop notifications through the {icon} button above once they're enabled.", "notifications_permission_banner.title": "Never miss a thing", + "personal.column_settings.basic": "Basic", + "personal.column_settings.show_replies": "Show replies", "picture_in_picture.restore": "Put it back", "poll.closed": "Closed", "poll.refresh": "Refresh", @@ -499,6 +507,8 @@ "privacy.mutual.short": "Mutual-followers-only", "privacy.none.long": "No visibility allowed", "privacy.none.short": "None", + "privacy.personal.long": "Visible for personal only", + "privacy.personal.short": "Personal", "privacy.private.long": "Visible for followers only", "privacy.private.short": "Followers-only", "privacy.public.long": "Visible for all, shown in public timelines", @@ -614,6 +624,7 @@ "tabs_bar.lists": "List", "tabs_bar.local_timeline": "Local", "tabs_bar.notifications": "Notifications", + "tabs_bar.personal_timeline": "Personal", "tabs_bar.search": "Search", "thread_mark.ancestor": "Has reference", "thread_mark.both": "Has reference and reply", diff --git a/app/javascript/mastodon/locales/ja.json b/app/javascript/mastodon/locales/ja.json index 7b0564c3c..2d2ed7c76 100644 --- a/app/javascript/mastodon/locales/ja.json +++ b/app/javascript/mastodon/locales/ja.json @@ -125,6 +125,7 @@ "column.lists": "リスト", "column.mutes": "ミュートしたユーザー", "column.notifications": "通知", + "column.personal": "自分限定ホーム", "column.pins": "固定された投稿", "column.public": "連合タイムライン", "column_back_button.label": "戻る", @@ -265,6 +266,7 @@ "empty_column.lists": "まだリストがありません。リストを作るとここに表示されます。", "empty_column.mutes": "まだ誰もミュートしていません。", "empty_column.notifications": "まだ通知がありません。他の人とふれ合って会話を始めましょう。", + "empty_column.personal": "自分限定投稿はありません。", "empty_column.pinned_unavailable": "固定された投稿はありません。", "empty_column.referred_by_statuses": "まだ、参照している投稿はありません。誰かが投稿を参照すると、ここに表示されます。", "empty_column.suggestions": "まだおすすめできるユーザーがいません。", @@ -310,6 +312,7 @@ "home.column_settings.basic": "基本設定", "home.column_settings.show_direct": "ダイレクトメッセージを表示", "home.column_settings.show_limited": "サークルを表示", + "home.column_settings.show_personal": "自分限定を表示", "home.column_settings.show_private": "フォロワー限定を表示", "home.column_settings.show_reblogs": "ブースト表示", "home.column_settings.show_replies": "返信表示", @@ -367,6 +370,7 @@ "limited.column_settings.basic": "基本設定", "limited.column_settings.show_direct": "ダイレクトメッセージを表示", "limited.column_settings.show_limited": "サークルを表示", + "limited.column_settings.show_personal": "自分限定を表示", "limited.column_settings.show_private": "フォロワー限定を表示", "limited.column_settings.show_reblogs": "ブースト表示", "limited.column_settings.show_replies": "返信表示", @@ -418,6 +422,7 @@ "navigation_bar.logout": "ログアウト", "navigation_bar.mutes": "ミュートしたユーザー", "navigation_bar.personal": "個人用", + "navigation_bar.personal_timeline": "自分限定ホーム", "navigation_bar.pins": "固定した投稿", "navigation_bar.preferences": "ユーザー設定", "navigation_bar.public_timeline": "連合タイムライン", @@ -429,6 +434,7 @@ "navigation_bar.short.lists": "リスト", "navigation_bar.short.logout": "ログアウト", "navigation_bar.short.notifications": "通知", + "navigation_bar.short.personal_timeline": "自分", "navigation_bar.short.preferences": "設定", "navigation_bar.short.public_timeline": "連合", "navigation_bar.short.search": "検索", @@ -481,6 +487,8 @@ "notifications_permission_banner.enable": "デスクトップ通知を有効にする", "notifications_permission_banner.how_to_control": "Mastodon を閉じている間でも通知を受信するにはデスクトップ通知を有効にしてください。有効にすると上の {icon} ボタンから通知の内容を細かくカスタマイズできます。", "notifications_permission_banner.title": "お見逃しなく", + "personal.column_settings.basic": "基本設定", + "personal.column_settings.show_replies": "返信表示", "picture_in_picture.restore": "元に戻す", "poll.closed": "終了", "poll.refresh": "更新", @@ -499,6 +507,8 @@ "privacy.mutual.short": "相互フォロー限定", "privacy.none.long": "許可された公開範囲なし", "privacy.none.short": "なし", + "privacy.personal.long": "自分のみ閲覧可", + "privacy.personal.short": "自分限定", "privacy.private.long": "フォロワーのみ閲覧可", "privacy.private.short": "フォロワー限定", "privacy.public.long": "誰でも閲覧可、公開TLに表示", @@ -614,6 +624,7 @@ "tabs_bar.lists": "リスト", "tabs_bar.local_timeline": "ローカル", "tabs_bar.notifications": "通知", + "tabs_bar.personal_timeline": "自分限定ホーム", "tabs_bar.search": "検索", "thread_mark.ancestor": "参照あり", "thread_mark.both": "参照・返信あり", diff --git a/app/javascript/mastodon/reducers/compose.js b/app/javascript/mastodon/reducers/compose.js index 38e9ca3f7..a0dbce25e 100644 --- a/app/javascript/mastodon/reducers/compose.js +++ b/app/javascript/mastodon/reducers/compose.js @@ -248,18 +248,18 @@ const insertEmoji = (state, position, emojiData, needsSpace) => { }; const privacyExpand = (a, b) => { - const order = ['public', 'unlisted', 'private', 'mutual', 'limited', 'direct']; + const order = ['public', 'unlisted', 'private', 'mutual', 'limited', 'direct', 'personal']; return order[Math.min(order.indexOf(a), order.indexOf(b), order.length - 1)]; }; const privacyCap = (a, b) => { - const order = ['public', 'unlisted', 'private', 'mutual', 'limited', 'direct']; + const order = ['public', 'unlisted', 'private', 'mutual', 'limited', 'direct', 'personal']; return order[Math.max(order.indexOf(a), order.indexOf(b), 0)]; }; const searchabilityCap = (a, b) => { - const order = ['public', 'unlisted', 'private', 'mutual', 'limited', 'direct']; - const to = ['public', 'private', 'private', 'direct', 'direct', 'direct']; + const order = ['public', 'unlisted', 'private', 'mutual', 'limited', 'direct', 'personal']; + const to = ['public', 'private', 'private', 'direct', 'direct', 'direct', 'direct']; return to[Math.max(order.indexOf(a), order.indexOf(b), 0)]; }; diff --git a/app/javascript/mastodon/reducers/settings.js b/app/javascript/mastodon/reducers/settings.js index b4a316ecd..d0d3c99fc 100644 --- a/app/javascript/mastodon/reducers/settings.js +++ b/app/javascript/mastodon/reducers/settings.js @@ -31,9 +31,10 @@ const initialState = ImmutableMap({ shows: ImmutableMap({ reblog: true, reply: true, - private: false, - limited: false, - direct: false, + private: true, + limited: true, + direct: true, + personal: true, }), regex: ImmutableMap({ @@ -48,6 +49,17 @@ const initialState = ImmutableMap({ private: true, limited: true, direct: true, + personal: true, + }), + + regex: ImmutableMap({ + body: '', + }), + }), + + personal: ImmutableMap({ + shows: ImmutableMap({ + reply: true, }), regex: ImmutableMap({ diff --git a/app/javascript/mastodon/reducers/timelines.js b/app/javascript/mastodon/reducers/timelines.js index 4dd1236a6..c08020f16 100644 --- a/app/javascript/mastodon/reducers/timelines.js +++ b/app/javascript/mastodon/reducers/timelines.js @@ -69,7 +69,7 @@ const expandNormalizedTimeline = (state, timeline, statuses, next, isPartial, is const updateTimeline = (state, timeline, status, usePendingItems) => { const top = state.getIn([timeline, 'top']); - if (usePendingItems || !state.getIn([timeline, 'pendingItems']).isEmpty()) { + if (usePendingItems || !state.getIn([timeline, 'pendingItems'], ImmutableList()).isEmpty()) { if (state.getIn([timeline, 'pendingItems'], ImmutableList()).includes(status.get('id')) || state.getIn([timeline, 'items'], ImmutableList()).includes(status.get('id'))) { return state; } diff --git a/app/javascript/mastodon/selectors/index.js b/app/javascript/mastodon/selectors/index.js index 76a2d63fa..0da4c84ee 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, enableLimitedTimeline } from '../initial_state'; +import { me, enableLimitedTimeline, hideDirectFromTimeline, hidePersonalFromTimeline, enablePersonalTimeline } from '../initial_state'; const getAccountBase = (state, id) => state.getIn(['accounts', id], null); const getAccountCounters = (state, id) => state.getIn(['accounts_counters', id], null); @@ -249,13 +249,21 @@ export const getAccountGallery = createSelector([ export const getHomeVisibilities = createSelector( state => state.getIn(['settings', 'home', 'shows']), - shows => !enableLimitedTimeline ? [] : [ + shows => (!enableLimitedTimeline ? [ + 'public', + 'unlisted', + 'private', + 'limited', + !hideDirectFromTimeline ? 'direct' : null, + !hidePersonalFromTimeline ? 'personal' : null, + ] : [ 'public', 'unlisted', shows.get('private') ? 'private' : null, shows.get('limited') ? 'limited' : null, - shows.get('direct') ? 'direct' : null, - ].filter(x => !!x), + shows.get('direct') && !hideDirectFromTimeline ? 'direct' : null, + shows.get('personal') && !hidePersonalFromTimeline ? 'personal' : null, + ]).filter(x => !!x), ); export const getLimitedVisibilities = createSelector( @@ -264,5 +272,10 @@ export const getLimitedVisibilities = createSelector( shows.get('private') ? 'private' : null, shows.get('limited') ? 'limited' : null, shows.get('direct') ? 'direct' : null, + shows.get('personal') ? 'personal' : null, ].filter(x => !!x), ); + +export const getPersonalVisibilities = createSelector( + state => state.getIn(['settings', 'personal', 'shows']), +); diff --git a/app/lib/user_settings_decorator.rb b/app/lib/user_settings_decorator.rb index b45eb8b92..6b1ad26ce 100644 --- a/app/lib/user_settings_decorator.rb +++ b/app/lib/user_settings_decorator.rb @@ -65,6 +65,7 @@ class UserSettingsDecorator show_tab_bar_label enable_federated_timeline enable_limited_timeline + enable_personal_timeline enable_local_timeline enable_reaction compact_reaction @@ -90,6 +91,9 @@ class UserSettingsDecorator disable_clear_all_notifications disable_account_delete disable_relative_time + hide_direct_from_timeline + hide_personal_from_timeline + hide_personal_from_account ).freeze STRING_KEYS = %w( diff --git a/app/models/account.rb b/app/models/account.rb index f937d9a07..16fcd4df4 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -89,7 +89,7 @@ class Account < ApplicationRecord enum protocol: [:ostatus, :activitypub] enum suspension_origin: [:local, :remote], _prefix: true enum silence_mode: { soft: 0, hard: 1 }, _suffix: :silence_mode - enum searchability: [:public, :unlisted, :private, :direct, :limited, :mutual], _suffix: :searchability + enum searchability: { public: 0, unlisted: 1, private: 2, direct: 3, limited: 4, mutual: 100, personal: 200 }, _suffix: :searchability validates :username, presence: true validates_with UniqueUsernameValidator, if: -> { will_save_change_to_username? } diff --git a/app/models/concerns/account_interactions.rb b/app/models/concerns/account_interactions.rb index dd4ab4504..293817429 100644 --- a/app/models/concerns/account_interactions.rb +++ b/app/models/concerns/account_interactions.rb @@ -243,6 +243,10 @@ module AccountInteractions .where('users.current_sign_in_at > ?', User::ACTIVE_DURATION.ago) end + def self_included_lists + owned_lists.joins(:list_accounts).where(list_accounts: {account_id: id}) + end + def remote_followers_hash(url) url_prefix = url[Account::URL_PREFIX_RE] return if url_prefix.blank? diff --git a/app/models/personal_feed.rb b/app/models/personal_feed.rb new file mode 100644 index 000000000..f06a69ca3 --- /dev/null +++ b/app/models/personal_feed.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +class PersonalFeed + # @param [Account] account + # @param [Hash] options + # @option [Boolean] :with_replies + def initialize(account, options = {}) + @account = account + @options = options + end + + # @param [Integer] limit + # @param [Integer] max_id + # @param [Integer] since_id + # @param [Integer] min_id + # @return [Array] + def get(limit, max_id = nil, since_id = nil, min_id = nil) + scope = personal_scope + + scope.cache_ids.to_a_paginated_by_id(limit, max_id: max_id, since_id: since_id, min_id: min_id) + end + + private + + attr_reader :account, :options + + def personal_scope + Status.include_expired.where(account_id: account.id).without_reblogs.with_personal_visibility + end +end diff --git a/app/models/status.rb b/app/models/status.rb index 9b500bbcf..953b60889 100644 --- a/app/models/status.rb +++ b/app/models/status.rb @@ -48,8 +48,13 @@ class Status < ApplicationRecord update_index('statuses', :proper) - enum visibility: [:public, :unlisted, :private, :direct, :limited, :mutual], _suffix: :visibility - enum searchability: [:public, :unlisted, :private, :direct, :limited, :mutual], _suffix: :searchability + enum visibility: { public: 0, unlisted: 1, private: 2, direct: 3, limited: 4, mutual: 100, personal: 200 }, _suffix: :visibility + enum searchability: { public: 0, unlisted: 1, private: 2, direct: 3, limited: 4, mutual: 100, personal: 200 }, _suffix: :searchability + + STANDARD_VISIBILITY = %w(public unlisted private direct) + EXTRA_VISIBILITY = %w(limited personal) + PSEUDO_VISIBILITY = %w(mutual) + UNCOUNT_VISIBILITY = %w(direct personal) belongs_to :application, class_name: 'Doorkeeper::Application', optional: true @@ -111,6 +116,8 @@ class Status < ApplicationRecord scope :without_replies, -> { where('statuses.reply = FALSE OR statuses.in_reply_to_account_id = statuses.account_id') } scope :without_reblogs, -> { where('statuses.reblog_of_id IS NULL') } scope :with_public_visibility, -> { where(visibility: :public) } + scope :with_personal_visibility, -> { where(visibility: :personal) } + scope :without_personal_visibility, -> { where.not(visibility: :personal) } scope :tagged_with, ->(tag_ids) { joins(:statuses_tags).where(statuses_tags: { tag_id: tag_ids }) } scope :in_chosen_languages, ->(account) { where(language: nil).or where(language: account.chosen_languages) } scope :mentioned_with, ->(account) { joins(:mentions).where(mentions: { account_id: account }) } @@ -189,6 +196,22 @@ class Status < ApplicationRecord searchability || Status.searchabilities.invert.fetch([Account.searchabilities[account.searchability], Status.visibilities[visibility] || 0].max, nil) || 'direct' end + def standard_visibility? + STANDARD_VISIBILITY.include?(visibility) + end + + def extra_visibility? + EXTRA_VISIBILITY.include?(visibility) + end + + def pseudo_visibility? + PSEUDO_VISIBILITY.include?(visibility) + end + + def uncount_visibility? + UNCOUNT_VISIBILITY.include?(visibility) + end + def reply? !in_reply_to_id.nil? || attributes['reply'] end @@ -412,7 +435,7 @@ class Status < ApplicationRecord end def selectable_searchabilities - searchabilities.keys - %w(unlisted limited mutual) + searchabilities.keys - %w(unlisted limited mutual personal) end def favourites_map(status_ids, account_id) @@ -628,7 +651,7 @@ class Status < ApplicationRecord end def increment_counter_caches - return if direct_visibility? + return if uncount_visibility? account&.increment_count!(:statuses_count) reblog&.increment_count!(:reblogs_count) if reblog? @@ -636,7 +659,7 @@ class Status < ApplicationRecord end def decrement_counter_caches - return if direct_visibility? + return if uncount_visibility? account&.decrement_count!(:statuses_count) reblog&.decrement_count!(:reblogs_count) if reblog? @@ -644,7 +667,7 @@ class Status < ApplicationRecord end def unlink_from_conversations - return unless direct_visibility? + return if uncount_visibility? mentioned_accounts = (association(:mentions).loaded? ? mentions : mentions.includes(:account)).map(&:account) inbox_owners = mentioned_accounts.select(&:local?) + (account.local? ? [account] : []) diff --git a/app/models/user.rb b/app/models/user.rb index eeedd05ab..a94a5f718 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -130,7 +130,7 @@ class User < ApplicationRecord :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, - :enable_local_timeline, :enable_federated_timeline, :enable_limited_timeline, + :enable_local_timeline, :enable_federated_timeline, :enable_limited_timeline, :enable_personal_timeline, :enable_reaction, :compact_reaction, :show_reply_tree_button, :hide_statuses_count, :hide_following_count, :hide_followers_count, :disable_joke_appearance, @@ -145,7 +145,7 @@ class User < ApplicationRecord :show_reload_button, :default_column_width, :disable_post, :disable_reactions, :disable_follow, :disable_unfollow, :disable_block, :disable_domain_block, :disable_clear_all_notifications, :disable_account_delete, :prohibited_visibilities, :prohibited_words, - :disable_relative_time, + :disable_relative_time, :hide_direct_from_timeline, :hide_personal_from_timeline, :hide_personal_from_account, to: :settings, prefix: :setting, allow_nil: false diff --git a/app/policies/status_policy.rb b/app/policies/status_policy.rb index 344a6721b..dbd54ac53 100644 --- a/app/policies/status_policy.rb +++ b/app/policies/status_policy.rb @@ -18,7 +18,9 @@ class StatusPolicy < ApplicationPolicy return false if author.suspended? return false unless expired_show? - if requires_mention? + if personal? + owned? + elsif requires_mention? owned? || mention_exists? elsif private? owned? || following_author? || mention_exists? @@ -85,6 +87,10 @@ class StatusPolicy < ApplicationPolicy record.limited_visibility? end + def personal? + record.personal_visibility? + end + def mention_exists? return false if current_account.nil? diff --git a/app/serializers/initial_state_serializer.rb b/app/serializers/initial_state_serializer.rb index 002560fa2..fe33bf025 100644 --- a/app/serializers/initial_state_serializer.rb +++ b/app/serializers/initial_state_serializer.rb @@ -55,6 +55,7 @@ class InitialStateSerializer < ActiveModel::Serializer store[:show_tab_bar_label] = object.current_account.user.setting_show_tab_bar_label store[:enable_federated_timeline] = object.current_account.user.setting_enable_federated_timeline store[:enable_limited_timeline] = object.current_account.user.setting_enable_limited_timeline + store[:enable_personal_timeline] = object.current_account.user.setting_enable_personal_timeline store[:enable_local_timeline] = false #object.current_account.user.setting_enable_local_timeline store[:enable_reaction] = object.current_account.user.setting_enable_reaction store[:compact_reaction] = object.current_account.user.setting_compact_reaction @@ -87,6 +88,9 @@ class InitialStateSerializer < ActiveModel::Serializer store[:disable_clear_all_notifications] = object.current_account.user.setting_disable_clear_all_notifications store[:disable_account_delete] = object.current_account.user.setting_disable_account_delete store[:disable_relative_time] = object.current_account.user.setting_disable_relative_time + store[:hide_direct_from_timeline] = object.current_account.user.setting_hide_direct_from_timeline + store[:hide_personal_from_timeline] = object.current_account.user.setting_hide_personal_from_timeline + store[:hide_personal_from_account] = object.current_account.user.setting_hide_personal_from_account 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 cf910156a..1df7380e3 100644 --- a/app/serializers/rest/instance_serializer.rb +++ b/app/serializers/rest/instance_serializer.rb @@ -139,6 +139,7 @@ class REST::InstanceSerializer < ActiveModel::Serializer :timeline_group_directory, :visibility_mutual, :visibility_limited, + :visibility_personal, :emoji_reaction, :misskey_birthday, :misskey_location, diff --git a/app/serializers/rest/status_serializer.rb b/app/serializers/rest/status_serializer.rb index 20ca7820d..1f6167413 100644 --- a/app/serializers/rest/status_serializer.rb +++ b/app/serializers/rest/status_serializer.rb @@ -102,7 +102,7 @@ class REST::StatusSerializer < ActiveModel::Serializer end def visibility_ex? - object.limited_visibility? + !object.standard_visibility? end def visibility @@ -111,6 +111,8 @@ class REST::StatusSerializer < ActiveModel::Serializer # UX differences if object.limited_visibility? 'private' + elsif object.personal_visibility? + 'direct' else object.visibility end diff --git a/app/services/fan_out_on_write_service.rb b/app/services/fan_out_on_write_service.rb index 225fd700a..48feb851e 100644 --- a/app/services/fan_out_on_write_service.rb +++ b/app/services/fan_out_on_write_service.rb @@ -6,9 +6,12 @@ class FanOutOnWriteService < BaseService def call(status) raise Mastodon::RaceConditionError if status.visibility.nil? - deliver_to_self(status) if status.account.local? + deliver_to_self(status) if status.account.local? && !(status.direct_visibility? && status.account.user.setting_hide_direct_from_timeline) - if status.direct_visibility? + if status.personal_visibility? + deliver_to_self_included_lists(status) if status.account.local? && !status.account.user.setting_hide_personal_from_timeline + return + elsif status.direct_visibility? deliver_to_mentioned_followers(status) deliver_to_own_conversation(status) elsif status.limited_visibility? @@ -283,4 +286,10 @@ class FanOutOnWriteService < BaseService def deliver_to_own_conversation(status) AccountConversation.add_status(status.account, status) end + + def deliver_to_self_included_lists(status) + FeedInsertWorker.push_bulk(status.account.self_included_lists.pluck(:id)) do |list_id| + [status.id, list_id, :list] + end + end end diff --git a/app/services/post_status_service.rb b/app/services/post_status_service.rb index e6857f977..26cbab2dd 100644 --- a/app/services/post_status_service.rb +++ b/app/services/post_status_service.rb @@ -120,7 +120,7 @@ class PostStatusService < BaseService end ProcessHashtagsService.new.call(@status) - ProcessMentionsService.new.call(@status, @circle) + ProcessMentionsService.new.call(@status, @circle) unless @status.personal_visibility? ProcessStatusReferenceService.new.call(@status, status_reference_ids: (@options[:status_reference_ids] || []) + [@quote_id], urls: @options[:status_reference_urls]) end @@ -144,7 +144,7 @@ class PostStatusService < BaseService def postprocess_status! LinkCrawlWorker.perform_async(@status.id) unless @status.spoiler_text? DistributionWorker.perform_async(@status.id) - ActivityPub::DistributionWorker.perform_async(@status.id) + ActivityPub::DistributionWorker.perform_async(@status.id) unless @status.personal_visibility? PollExpirationNotifyWorker.perform_at(@status.poll.expires_at, @status.poll.id) if @status.poll @status.status_expire.queue_action if expires_soon? end diff --git a/app/services/reblog_service.rb b/app/services/reblog_service.rb index 264832c3d..9515fc8da 100644 --- a/app/services/reblog_service.rb +++ b/app/services/reblog_service.rb @@ -37,7 +37,7 @@ class ReblogService < BaseService end end - raise Mastodon::ValidationError, I18n.t('status_prohibit.validations.prohibited_visibilities') if account.user&.setting_prohibited_visibilities&.filter(&:present?)&.include?(visibility) + raise Mastodon::ValidationError, I18n.t('status_prohibit.validations.prohibited_visibilities') if account.user&.setting_prohibited_visibilities&.filter(&:present?)&.include?(visibility) || visibility == 'personal' ApplicationRecord.transaction do reblog = account.statuses.create!(reblog: reblogged_status, text: '', visibility: visibility, rate_limit: options[:with_rate_limit]) diff --git a/app/views/settings/preferences/other/show.html.haml b/app/views/settings/preferences/other/show.html.haml index 055adc701..2d6a93cf0 100644 --- a/app/views/settings/preferences/other/show.html.haml +++ b/app/views/settings/preferences/other/show.html.haml @@ -95,6 +95,9 @@ .fields-group = f.input :setting_enable_limited_timeline, as: :boolean, wrapper: :with_label, fedibird_features: true + .fields-group + = f.input :setting_enable_personal_timeline, as: :boolean, wrapper: :with_label, fedibird_features: true + .fields-group = f.input :setting_enable_reaction, as: :boolean, wrapper: :with_label, fedibird_features: true @@ -119,6 +122,15 @@ .fields-group = f.input :setting_disable_relative_time, as: :boolean, wrapper: :with_label, fedibird_features: true + .fields-group + = f.input :setting_hide_direct_from_timeline, as: :boolean, wrapper: :with_label, fedibird_features: true + + .fields-group + = f.input :setting_hide_personal_from_timeline, as: :boolean, wrapper: :with_label, fedibird_features: true + + .fields-group + = f.input :setting_hide_personal_from_account, as: :boolean, wrapper: :with_label, fedibird_features: true + %h4= t 'preferences.public_timelines' .fields-group diff --git a/config/locales/en.yml b/config/locales/en.yml index e718c8c2c..189ef16a6 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -1464,6 +1464,8 @@ en: limited_long: Only show to circle users mutual: Mutual mutual_long: Only show to mutual followers + personal: Personal + personal_long: Only show to personal private: Followers-only private_long: Only show to followers public: Public diff --git a/config/locales/ja.yml b/config/locales/ja.yml index b35b6ca85..20b49faba 100644 --- a/config/locales/ja.yml +++ b/config/locales/ja.yml @@ -1401,6 +1401,8 @@ ja: limited_long: サークルで指定したユーザーのみ閲覧可 mutual: 相互フォロー限定 mutual_long: 相互フォロー相手にのみ表示されます + personal: 自分限定 + personal_long: 自分にのみ表示されます private: フォロワー限定 private_long: フォロワーにのみ表示されます public: 公開 diff --git a/config/locales/simple_form.en.yml b/config/locales/simple_form.en.yml index ecf93f178..84e0ced26 100644 --- a/config/locales/simple_form.en.yml +++ b/config/locales/simple_form.en.yml @@ -87,14 +87,18 @@ en: setting_enable_federated_timeline: 'Enable a federated timeline (default: enable)' setting_enable_limited_timeline: Enable a limited home to display private and circle and direct message setting_enable_local_timeline: 'Enable a local timeline (default: enable)' + setting_enable_personal_timeline: Enable a personal home to display personal post setting_enable_reaction: Enable the reaction display on the timeline and display the reaction button setting_enable_status_reference: Enable the feature where a post references another post 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_hexagon_avatar: Display everyone's avatar icon as a hollowed out hexagon (joke feature) setting_hide_bot_on_public_timeline: Disable Bot accounts from appearing on federation & hashtag & domain & group timelines (overridden by column setting) + setting_hide_direct_from_timeline: Hide direct messages from the home and list timelines setting_hide_followers_count: The number of followers will be hidden in your profile setting_hide_following_count: The number of following will be hidden in your profile setting_hide_network: Who you follow and who follows you will be hidden on your profile + setting_hide_personal_from_account: Hide personal posts from the account post lists + setting_hide_personal_from_timeline: Hide personal posts from the home and list timelines setting_hide_statuses_count: The number of post will be hidden in your profile setting_match_visibility_of_references: If the referenced post is private, default the visibility of the post to private behavior accordingly setting_new_features_policy: Set the acceptance policy when new features are added to Fedibird. The recommended setting will enable many new features, so set it to disabled if it is not desirable @@ -274,15 +278,19 @@ en: setting_enable_federated_timeline: Enable federated timeline setting_enable_limited_timeline: Enable limited timeline setting_enable_local_timeline: Enable local timeline + setting_enable_personal_timeline: Enable personal timeline setting_enable_reaction: Enable reaction setting_enable_status_reference: Enable reference setting_expand_spoilers: Always expand posts marked with content warnings setting_follow_button_to_list_adder: Open list add dialog with follow button setting_hexagon_avatar: Experience NFT Avatar setting_hide_bot_on_public_timeline: Hide bot account on public timeline + setting_hide_direct_from_timeline: Hide direct messages from the timeline setting_hide_followers_count: Hide your followers count setting_hide_following_count: Hide your following count setting_hide_network: Hide your social graph + setting_hide_personal_from_account: Hide personal posts from account posts + setting_hide_personal_from_timeline: Hide personal posts from the timeline setting_hide_statuses_count: Hide your post count setting_info_font_size: Information header font size setting_match_visibility_of_references: Match the visibility of the post to the references diff --git a/config/locales/simple_form.ja.yml b/config/locales/simple_form.ja.yml index cf819b97d..d530806bb 100644 --- a/config/locales/simple_form.ja.yml +++ b/config/locales/simple_form.ja.yml @@ -83,14 +83,18 @@ ja: setting_enable_federated_timeline: 連合タイムラインを有効にします(デフォルト) setting_enable_limited_timeline: フォロワー限定・サークル・ダイレクトメッセージを表示する限定ホームを有効にします setting_enable_local_timeline: ローカルタイムラインを有効にします(デフォルト) + setting_enable_personal_timeline: 自分限定を表示する自分限定ホームを有効にします setting_enable_reaction: タイムラインでリアクションの表示を有効にし、リアクションボタンを表示する setting_enable_status_reference: 投稿が別の投稿を参照する機能を有効にします setting_follow_button_to_list_adder: フォロー・購読ボタンの動作を変更し、フォロー・購読するリストを選択したり、ホームで受け取らないよう設定するダイアログを開きます setting_hexagon_avatar: 全員のアバターアイコンを6角形にくりぬいて表示します(ジョーク機能) setting_hide_bot_on_public_timeline: 連合・ハッシュタグ・ドメイン・グループタイムライン上にBotアカウントが表示されないようにします(※カラム設定を優先) + setting_hide_direct_from_timeline: ホームとリストタイムラインからダイレクトメッセージを隠します setting_hide_followers_count: フォロワー数をプロフィールページで見られないようにします setting_hide_following_count: フォロー数をプロフィールページで見られないようにします setting_hide_network: フォローとフォロワーの情報がプロフィールページで見られないようにします + setting_hide_personal_from_account: アカウントの投稿一覧から個人限定投稿を隠します + setting_hide_personal_from_timeline: ホームとリストタイムラインから個人限定投稿を隠します setting_hide_statuses_count: 投稿数をプロフィールページで見られないようにします setting_match_visibility_of_references: 参照先の投稿がフォロワー限定の場合、投稿の公開範囲をそれに合わせてフォロワー限定とする動作をデフォルトにします setting_new_features_policy: Fedibirdに新しい機能が追加された時の受け入れポリシーを設定します。推奨設定は多くの新機能を有効にするので、望ましくない場合は無効に設定してください @@ -270,15 +274,19 @@ ja: setting_enable_federated_timeline: 連合タイムラインを有効にする setting_enable_limited_timeline: 限定ホームを有効にする setting_enable_local_timeline: ローカルタイムラインを有効にする + setting_enable_personal_timeline: 自分限定ホームを有効にする setting_enable_reaction: リアクションを有効にする setting_enable_status_reference: 参照を有効にする setting_expand_spoilers: 閲覧注意としてマークされた投稿を常に展開する setting_follow_button_to_list_adder: フォローボタンでリスト追加ダイアログを開く setting_hexagon_avatar: NFTアイコンを体験する setting_hide_bot_on_public_timeline: 公開タイムラインのBotアカウントを非表示 + setting_hide_direct_from_timeline: ダイレクトメッセージをタイムラインから隠す setting_hide_followers_count: フォロワー数を隠す setting_hide_following_count: フォロー数を隠す setting_hide_network: 繋がりを隠す + setting_hide_personal_from_account: 自分限定投稿をアカウントの投稿一覧から隠す + setting_hide_personal_from_timeline: 自分限定投稿をタイムラインから隠す setting_hide_statuses_count: 投稿数を隠す setting_info_font_size: 情報ヘッダのフォントサイズ setting_match_visibility_of_references: 投稿の公開範囲を参照先に合わせる diff --git a/config/routes.rb b/config/routes.rb index 1b01952b7..65c58e8cc 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -379,6 +379,7 @@ Rails.application.routes.draw do resources :tag, only: :show resources :list, only: :show resources :group, only: :show + resource :personal, only: :show, controller: :personal end resources :streaming, only: [:index] diff --git a/config/settings.yml b/config/settings.yml index ecfdb90e9..1390bd378 100644 --- a/config/settings.yml +++ b/config/settings.yml @@ -55,6 +55,8 @@ defaults: &defaults show_tab_bar_label: false enable_federated_timeline: true enable_limited_timeline: false + enable_personal_timeline: false + enable_personal_account: false enable_local_timeline: true enable_reaction: true compact_reaction: false @@ -129,6 +131,8 @@ defaults: &defaults prohibited_visibilities: [] prohibited_words: '' disable_relative_time: false + hide_direct_from_timeline: false + hide_personal_from_timeline: false development: <<: *defaults diff --git a/db/migrate/20221230091953_add_personal_timeline_index.rb b/db/migrate/20221230091953_add_personal_timeline_index.rb new file mode 100644 index 000000000..c857cbe88 --- /dev/null +++ b/db/migrate/20221230091953_add_personal_timeline_index.rb @@ -0,0 +1,11 @@ +class AddPersonalTimelineIndex < ActiveRecord::Migration[6.1] + disable_ddl_transaction! + + def up + safety_assured { add_index :statuses, [:account_id, :id], where: 'visibility = 200 AND deleted_at IS NULL AND reblog_of_id IS NULL', order: { id: :desc }, algorithm: :concurrently, name: :index_statuses_personal_timeline } + end + + def down + remove_index :statuses, name: :index_statuses_personal_timeline + end +end diff --git a/db/schema.rb b/db/schema.rb index fc7919403..ab90d0585 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -1007,6 +1007,7 @@ ActiveRecord::Schema.define(version: 2023_01_29_193248) do t.index ["account_id", "id"], name: "index_statuses_private_searchable", order: { id: :desc }, where: "((deleted_at IS NULL) AND (expired_at IS NULL) AND (reblog_of_id IS NULL) AND (searchability = ANY (ARRAY[0, 1, 2])))" t.index ["id", "account_id"], name: "index_statuses_local_20190824", order: { id: :desc }, where: "((local OR (uri IS NULL)) AND (deleted_at IS NULL) AND (visibility = 0) AND (reblog_of_id IS NULL) AND ((NOT reply) OR (in_reply_to_account_id = account_id)))" t.index ["id", "account_id"], name: "index_statuses_public_20200119", order: { id: :desc }, where: "((deleted_at IS NULL) AND (visibility = 0) AND (reblog_of_id IS NULL) AND ((NOT reply) OR (in_reply_to_account_id = account_id)))" + t.index ["account_id", "id"], name: "index_statuses_personal_timeline", order: :desc, where: "((visibility = 200) AND (deleted_at IS NULL) AND (reblog_of_id IS NULL))" t.index ["in_reply_to_account_id"], name: "index_statuses_on_in_reply_to_account_id" t.index ["in_reply_to_id"], name: "index_statuses_on_in_reply_to_id" t.index ["quote_id"], name: "index_statuses_on_quote_id"