diff --git a/app/javascript/mastodon/actions/bookmark_categories.js b/app/javascript/mastodon/actions/bookmark_categories.js index 9fa2ffc9f2..b6f82b49c8 100644 --- a/app/javascript/mastodon/actions/bookmark_categories.js +++ b/app/javascript/mastodon/actions/bookmark_categories.js @@ -1,6 +1,10 @@ +import { bookmarkCategoryNeeded } from 'mastodon/initial_state'; +import { makeGetStatus } from 'mastodon/selectors'; + import api, { getLinks } from '../api'; import { importFetchedStatuses } from './importer'; +import { unbookmark } from './interactions'; export const BOOKMARK_CATEGORY_FETCH_REQUEST = 'BOOKMARK_CATEGORY_FETCH_REQUEST'; export const BOOKMARK_CATEGORY_FETCH_SUCCESS = 'BOOKMARK_CATEGORY_FETCH_SUCCESS'; @@ -330,7 +334,17 @@ export const addToBookmarkCategoryAdder = bookmarkCategoryId => (dispatch, getSt }; export const removeFromBookmarkCategoryAdder = bookmarkCategoryId => (dispatch, getState) => { - dispatch(removeFromBookmarkCategory(bookmarkCategoryId, getState().getIn(['bookmarkCategoryAdder', 'statusId']))); + if (bookmarkCategoryNeeded) { + const categories = getState().getIn(['bookmarkCategoryAdder', 'bookmarkCategories', 'items']); + if (categories && categories.count() <= 1) { + const status = makeGetStatus()(getState(), { id: getState().getIn(['bookmarkCategoryAdder', 'statusId']) }); + dispatch(unbookmark(status)); + } else { + dispatch(removeFromBookmarkCategory(bookmarkCategoryId, getState().getIn(['bookmarkCategoryAdder', 'statusId']))); + } + } else { + dispatch(removeFromBookmarkCategory(bookmarkCategoryId, getState().getIn(['bookmarkCategoryAdder', 'statusId']))); + } }; export function expandBookmarkCategoryStatuses(bookmarkCategoryId) { diff --git a/app/javascript/mastodon/components/status_action_bar.jsx b/app/javascript/mastodon/components/status_action_bar.jsx index cd647cb379..7c6f3d1bd3 100644 --- a/app/javascript/mastodon/components/status_action_bar.jsx +++ b/app/javascript/mastodon/components/status_action_bar.jsx @@ -12,7 +12,7 @@ import { PERMISSION_MANAGE_USERS, PERMISSION_MANAGE_FEDERATION } from 'mastodon/ import DropdownMenuContainer from '../containers/dropdown_menu_container'; import EmojiPickerDropdown from '../features/compose/containers/emoji_picker_dropdown_container'; -import { me } from '../initial_state'; +import { bookmarkCategoryNeeded, me } from '../initial_state'; import { IconButton } from './icon_button'; @@ -166,13 +166,21 @@ class StatusActionBar extends ImmutablePureComponent { }; handleBookmarkClick = () => { - this.props.onBookmark(this.props.status); + if (bookmarkCategoryNeeded) { + this.handleBookmarkCategoryAdderClick(); + } else { + this.props.onBookmark(this.props.status); + } }; handleBookmarkCategoryAdderClick = () => { this.props.onBookmarkCategoryAdder(this.props.status); }; + handleBookmarkClickOriginal = () => { + this.props.onBookmark(this.props.status); + }; + handleDeleteClick = () => { this.props.onDelete(this.props.status, this.context.router.history); }; @@ -305,7 +313,7 @@ class StatusActionBar extends ImmutablePureComponent { menu.push({ text: intl.formatMessage(messages.reference), action: this.handleReference }); } - menu.push({ text: intl.formatMessage(status.get('bookmarked') ? messages.removeBookmark : messages.bookmark), action: this.handleBookmarkClick }); + menu.push({ text: intl.formatMessage(status.get('bookmarked') ? messages.removeBookmark : messages.bookmark), action: this.handleBookmarkClickOriginal }); menu.push({ text: intl.formatMessage(messages.bookmarkCategory), action: this.handleBookmarkCategoryAdderClick }); if (writtenByMe && pinnableStatus) { diff --git a/app/javascript/mastodon/features/status/components/action_bar.jsx b/app/javascript/mastodon/features/status/components/action_bar.jsx index 7831b02922..c5980461e9 100644 --- a/app/javascript/mastodon/features/status/components/action_bar.jsx +++ b/app/javascript/mastodon/features/status/components/action_bar.jsx @@ -12,7 +12,7 @@ import { PERMISSION_MANAGE_USERS, PERMISSION_MANAGE_FEDERATION } from 'mastodon/ import { IconButton } from '../../../components/icon_button'; import DropdownMenuContainer from '../../../containers/dropdown_menu_container'; -import { me } from '../../../initial_state'; +import { bookmarkCategoryNeeded, me } from '../../../initial_state'; import EmojiPickerDropdown from '../../compose/containers/emoji_picker_dropdown_container'; const messages = defineMessages({ @@ -107,7 +107,11 @@ class ActionBar extends PureComponent { }; handleBookmarkClick = (e) => { - this.props.onBookmark(this.props.status, e); + if (bookmarkCategoryNeeded) { + this.props.onBookmarkCategoryAdder(this.props.status); + } else { + this.props.onBookmark(this.props.status, e); + } }; handleDeleteClick = () => { diff --git a/app/javascript/mastodon/features/status/index.jsx b/app/javascript/mastodon/features/status/index.jsx index 8d88308923..cf4c2bd6ce 100644 --- a/app/javascript/mastodon/features/status/index.jsx +++ b/app/javascript/mastodon/features/status/index.jsx @@ -63,7 +63,7 @@ import { import ColumnHeader from '../../components/column_header'; import { textForScreenReader, defaultMediaVisibility } from '../../components/status'; import StatusContainer from '../../containers/status_container'; -import { boostModal, deleteModal } from '../../initial_state'; +import { bookmarkCategoryNeeded, boostModal, deleteModal } from '../../initial_state'; import { makeGetStatus, makeGetPictureInPicture } from '../../selectors'; import Column from '../ui/components/column'; import { attachFullscreenListener, detachFullscreenListener, isFullscreen } from '../ui/util/fullscreen'; @@ -367,6 +367,11 @@ class Status extends ImmutablePureComponent { }; handleBookmarkClick = (status) => { + if (bookmarkCategoryNeeded) { + this.handleBookmarkCategoryAdderClick(status); + return; + } + if (status.get('bookmarked')) { this.props.dispatch(unbookmark(status)); } else { diff --git a/app/javascript/mastodon/initial_state.js b/app/javascript/mastodon/initial_state.js index c171c56d99..fd8e514f98 100644 --- a/app/javascript/mastodon/initial_state.js +++ b/app/javascript/mastodon/initial_state.js @@ -50,6 +50,7 @@ * @property {boolean} auto_play_gif * @property {boolean} activity_api_enabled * @property {string} admin + * @property {boolean} bookmark_category_needed * @property {boolean=} boost_modal * @property {boolean=} delete_modal * @property {boolean=} disable_swiping @@ -113,6 +114,7 @@ const getMeta = (prop) => initialState?.meta && initialState.meta[prop]; export const activityApiEnabled = getMeta('activity_api_enabled'); export const autoPlayGif = getMeta('auto_play_gif'); +export const bookmarkCategoryNeeded = getMeta('bookmark_category_needed'); export const boostModal = getMeta('boost_modal'); export const deleteModal = getMeta('delete_modal'); export const disableSwiping = getMeta('disable_swiping'); diff --git a/app/javascript/mastodon/reducers/bookmark_categories.js b/app/javascript/mastodon/reducers/bookmark_categories.js index 561dea1824..86df1e6565 100644 --- a/app/javascript/mastodon/reducers/bookmark_categories.js +++ b/app/javascript/mastodon/reducers/bookmark_categories.js @@ -20,7 +20,14 @@ import { const initialState = ImmutableMap(); -const normalizeBookmarkCategory = (state, category) => state.set(category.id, fromJS(category)); +const normalizeBookmarkCategory = (state, category) => { + const old = state.get(category.id); + state = state.set(category.id, fromJS(category)); + if (old) { + state = state.setIn([category.id, 'items'], old.get('items')); + } + return state; +}; const normalizeBookmarkCategories = (state, bookmarkCategories) => { bookmarkCategories.forEach(bookmarkCategory => { diff --git a/app/javascript/mastodon/reducers/bookmark_category_adder.js b/app/javascript/mastodon/reducers/bookmark_category_adder.js index 05148d3336..c47ecb3b40 100644 --- a/app/javascript/mastodon/reducers/bookmark_category_adder.js +++ b/app/javascript/mastodon/reducers/bookmark_category_adder.js @@ -9,6 +9,9 @@ import { BOOKMARK_CATEGORY_EDITOR_ADD_SUCCESS, BOOKMARK_CATEGORY_EDITOR_REMOVE_SUCCESS, } from '../actions/bookmark_categories'; +import { + UNBOOKMARK_SUCCESS, +} from '../actions/interactions'; const initialState = ImmutableMap({ statusId: null, @@ -42,6 +45,8 @@ export default function bookmarkCategoryAdderReducer(state = initialState, actio return state.updateIn(['bookmarkCategories', 'items'], bookmarkCategory => bookmarkCategory.unshift(action.bookmarkCategoryId)); case BOOKMARK_CATEGORY_EDITOR_REMOVE_SUCCESS: return state.updateIn(['bookmarkCategories', 'items'], bookmarkCategory => bookmarkCategory.filterNot(item => item === action.bookmarkCategoryId)); + case UNBOOKMARK_SUCCESS: + return action.status.get('id') === state.get('statusId') ? state.setIn(['bookmarkCategories', 'items'], ImmutableList()) : state; default: return state; } diff --git a/app/models/concerns/has_user_settings.rb b/app/models/concerns/has_user_settings.rb index b602081988..dfb37c3517 100644 --- a/app/models/concerns/has_user_settings.rb +++ b/app/models/concerns/has_user_settings.rb @@ -31,6 +31,10 @@ module HasUserSettings settings['web.enable_login_privacy'] end + def setting_bookmark_category_needed + settings['web.bookmark_category_needed'] + end + def setting_hide_recent_emojis settings['web.hide_recent_emojis'] end diff --git a/app/models/user_settings.rb b/app/models/user_settings.rb index 40c146f596..32bfa563cd 100644 --- a/app/models/user_settings.rb +++ b/app/models/user_settings.rb @@ -42,6 +42,7 @@ class UserSettings setting :use_blurhash, default: true setting :use_pending_items, default: false setting :use_system_font, default: false + setting :bookmark_category_needed, default: false setting :disable_swiping, default: false setting :delete_modal, default: true setting :enable_login_privacy, default: false diff --git a/app/serializers/initial_state_serializer.rb b/app/serializers/initial_state_serializer.rb index 06e1bf2393..063c17e57d 100644 --- a/app/serializers/initial_state_serializer.rb +++ b/app/serializers/initial_state_serializer.rb @@ -52,6 +52,7 @@ class InitialStateSerializer < ActiveModel::Serializer store[:use_blurhash] = object.current_account.user.setting_use_blurhash store[:use_pending_items] = object.current_account.user.setting_use_pending_items store[:show_trends] = Setting.trends && object.current_account.user.setting_trends + store[:bookmark_category_needed] = object.current_account.user.setting_bookmark_category_needed else store[:auto_play_gif] = Setting.auto_play_gif store[:display_media] = Setting.display_media diff --git a/app/views/settings/preferences/appearance/show.html.haml b/app/views/settings/preferences/appearance/show.html.haml index bc16bf9da1..c01d9a8311 100644 --- a/app/views/settings/preferences/appearance/show.html.haml +++ b/app/views/settings/preferences/appearance/show.html.haml @@ -40,6 +40,9 @@ .fields-group = ff.input :'web.hide_recent_emojis', wrapper: :with_label, kmyblue: true, label: I18n.t('simple_form.labels.defaults.setting_hide_recent_emojis'), hint: false + .fields-group + = ff.input :'web.bookmark_category_needed', wrapper: :with_label, kmyblue: true, label: I18n.t('simple_form.labels.defaults.setting_bookmark_category_needed'), hint: I18n.t('simple_form.hints.defaults.setting_bookmark_category_needed') + %h4= t 'appearance.discovery' .fields-group diff --git a/config/locales/simple_form.en.yml b/config/locales/simple_form.en.yml index 148e8a109f..88d9ded9d5 100644 --- a/config/locales/simple_form.en.yml +++ b/config/locales/simple_form.en.yml @@ -61,6 +61,7 @@ en: scopes: Which APIs the application will be allowed to access. If you select a top-level scope, you don't need to select individual ones. setting_aggregate_reblogs: Do not show new boosts for posts that have been recently boosted (only affects newly-received boosts) setting_always_send_emails: Normally e-mail notifications won't be sent when you are actively using Mastodon + setting_bookmark_category_needed: When removing from all category, unbookmarked automatically setting_default_sensitive: Sensitive media is hidden by default and can be revealed with a click setting_display_media_default: Hide media marked as sensitive setting_display_media_hide_all: Always hide media @@ -218,6 +219,7 @@ en: setting_always_send_emails: Always send e-mail notifications setting_auto_play_gif: Auto-play animated GIFs setting_bio_markdown: Enable profile markdown + setting_bookmark_category_needed: Category selection needed when registering bookmark on web setting_boost_modal: Show confirmation dialog before boosting setting_default_language: Posting language setting_default_privacy: Posting privacy diff --git a/config/locales/simple_form.ja.yml b/config/locales/simple_form.ja.yml index 4617d6c849..05e27f62a3 100644 --- a/config/locales/simple_form.ja.yml +++ b/config/locales/simple_form.ja.yml @@ -60,6 +60,7 @@ ja: setting_send_without_domain_blocks: 管理人が同人コンテンツの配送にふさわしくないと判断したサーバーに、制限に関係なく全ての投稿を配送します。ただし何が起きても自己責任になります setting_aggregate_reblogs: 最近ブーストされた投稿が新たにブーストされても表示しません (設定後受信したものにのみ影響) setting_always_send_emails: 通常、Mastodon からメール通知は行われません。 + setting_bookmark_category_needed: すべてのカテゴリから削除したとき、ブックマークが自動で外れるようになります setting_boost_modal: ブーストの公開範囲が指定できるようになります setting_default_sensitive: 閲覧注意状態のメディアはデフォルトでは内容が伏せられ、クリックして初めて閲覧できるようになります setting_disallow_unlisted_public_searchability: この設定を有効にすると、未収載投稿と検索範囲「全て」は両立できず不特定多数からの検索が不可になります。Fedibirdと同じ挙動になります @@ -227,6 +228,7 @@ ja: setting_always_send_emails: 常にメール通知を送信する setting_auto_play_gif: アニメーションGIFを自動再生する setting_bio_markdown: プロフィールのMarkdownを有効にする + setting_bookmark_category_needed: Webでブックマーク時にカテゴリの選択を必須にする setting_boost_modal: ブーストする前に確認ダイアログを表示する setting_default_language: 投稿する言語 setting_default_privacy: 投稿の公開範囲