diff --git a/app/controllers/api/v1/statuses_controller.rb b/app/controllers/api/v1/statuses_controller.rb index 0bfbac33a..cf6ab65b8 100644 --- a/app/controllers/api/v1/statuses_controller.rb +++ b/app/controllers/api/v1/statuses_controller.rb @@ -20,6 +20,7 @@ class Api::V1::StatusesController < Api::BaseController # conversations as quasi-unlimited, it would be too much work to render more # than this anyway CONTEXT_LIMIT = 4_096 + DURATION_RE = /^(?:(?\d+)y)?(?:(?\d+)m(?=[\do])o?)?(?:(?\d+)d)?(?:(?\d+)h)?(?:(?\d+)m)?$/ def index @statuses = cache_collection(@statuses, Status) @@ -128,7 +129,18 @@ class Api::V1::StatusesController < Api::BaseController end def set_expire - @expires_at = status_params[:expires_at] || (status_params[:expires_in].blank? ? nil : (@scheduled_at || Time.now.utc) + status_params[:expires_in].to_i.seconds) + expires_in = + if status_params.has_key?(:expires_in) + status_params[:expires_in].blank? ? nil : status_params[:expires_in].to_i.seconds + elsif (match = current_user.setting_default_expires_in.match(DURATION_RE)) + year, month, day, hour, minute = match.to_a.values_at(1,2,3,4,5).map(&:to_i) + seconds = (year.years + month.months + day.days + hour.hours + minute.minutes).to_i.seconds + seconds < 1.minutes ? nil : seconds + else + nil + end + + @expires_at = status_params[:expires_at] || (expires_in.nil? ? nil : (@scheduled_at || Time.now.utc) + expires_in) end def status_ids diff --git a/app/controllers/settings/preferences_controller.rb b/app/controllers/settings/preferences_controller.rb index d695afa35..21f3a809f 100644 --- a/app/controllers/settings/preferences_controller.rb +++ b/app/controllers/settings/preferences_controller.rb @@ -97,6 +97,8 @@ class Settings::PreferencesController < Settings::BaseController :setting_show_reload_button, :setting_default_column_width, :setting_confirm_domain_block, + :setting_default_expires_in, + :setting_default_expires_action, 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/javascript/mastodon/locales/ja.json b/app/javascript/mastodon/locales/ja.json index 9be27ad5a..a1a9ddb1d 100644 --- a/app/javascript/mastodon/locales/ja.json +++ b/app/javascript/mastodon/locales/ja.json @@ -213,7 +213,7 @@ "datetime_button.add_datetime": "日時を追加", "datetime_button.remove_datetime": "日時を削除", "datetime.expires": "終了日時", - "datetime.expires_action.mark": "保持", + "datetime.expires_action.mark": "期限切れマーク", "datetime.expires_action.delete": "削除", "datetime.open_calendar": "カレンダーを開く", "datetime.placeholder": "日付か期間を入力", diff --git a/app/javascript/mastodon/reducers/compose.js b/app/javascript/mastodon/reducers/compose.js index eecc1b333..129ae1281 100644 --- a/app/javascript/mastodon/reducers/compose.js +++ b/app/javascript/mastodon/reducers/compose.js @@ -160,10 +160,10 @@ const clearAll = state => { map.update('media_attachments', list => list.clear()); map.set('poll', null); map.set('idempotencyKey', uuid()); - map.set('datetime_form', null); + map.set('datetime_form', state.get('default_expires_in') ? true : null); map.set('scheduled', null); - map.set('expires', null); - map.set('expires_action', 'mark'); + map.set('expires', state.get('default_expires_in', null)); + map.set('expires_action', state.get('default_expires_action', 'mark')); map.update('references', set => set.clear()); map.update('context_references', set => set.clear()); }); @@ -410,10 +410,10 @@ export default function compose(state = initialState, action) { map.set('caretPosition', null); map.set('preselectDate', new Date()); map.set('idempotencyKey', uuid()); - map.set('datetime_form', null); + map.set('datetime_form', state.get('default_expires_in') ? true : null); map.set('scheduled', null); - map.set('expires', null); - map.set('expires_action', 'mark'); + map.set('expires', state.get('default_expires_in', null)); + map.set('expires_action', state.get('default_expires_action', 'mark')); map.update('context_references', set => set.clear().concat(action.context_references)); if (action.status.get('spoiler_text').length > 0) { @@ -438,10 +438,10 @@ export default function compose(state = initialState, action) { map.set('focusDate', new Date()); map.set('preselectDate', new Date()); map.set('idempotencyKey', uuid()); - map.set('datetime_form', null); + map.set('datetime_form', state.get('default_expires_in') ? true : null); map.set('scheduled', null); - map.set('expires', null); - map.set('expires_action', 'mark'); + map.set('expires', state.get('default_expires_in', null)); + map.set('expires_action', state.get('default_expires_action', 'mark')); map.update('context_references', set => set.clear().add(action.status.get('id'))); if (action.status.get('spoiler_text').length > 0) { @@ -468,10 +468,10 @@ export default function compose(state = initialState, action) { map.set('circle_id', null); map.set('poll', null); map.set('idempotencyKey', uuid()); - map.set('datetime_form', null); + map.set('datetime_form', state.get('default_expires_in') ? true : null); map.set('scheduled', null); - map.set('expires', null); - map.set('expires_action', 'mark'); + map.set('expires', state.get('default_expires_in', null)); + map.set('expires_action', state.get('default_expires_action', 'mark')); map.update('context_references', set => set.clear()); if (action.type == COMPOSE_RESET) { map.update('references', set => set.clear()); @@ -575,6 +575,8 @@ export default function compose(state = initialState, action) { })); case REDRAFT: return state.withMutations(map => { + const default_expires_in_exist = state.get('default_expires_in') ? true : null; + map.set('text', action.raw_text || unescapeHTML(rejectQuoteAltText(expandMentions(action.status)))); map.set('in_reply_to', action.status.get('in_reply_to_id')); map.set('quote_from', action.status.getIn(['quote', 'id'])); @@ -588,10 +590,10 @@ export default function compose(state = initialState, action) { map.set('caretPosition', null); map.set('idempotencyKey', uuid()); map.set('sensitive', action.status.get('sensitive')); - map.set('datetime_form', !!action.status.get('scheduled_at') || !!action.status.get('expires_at') ? true : null); + map.set('datetime_form', !!action.status.get('scheduled_at') || !!action.status.get('expires_at') ? true : default_expires_in_exist); map.set('scheduled', action.status.get('scheduled_at')); - map.set('expires', action.status.get('expires_at') ? format(parseISO(action.status.get('expires_at')), 'yyyy-MM-dd HH:mm') : null); - map.set('expires_action', action.status.get('expires_action') ?? 'mark'); + map.set('expires', action.status.get('expires_at') ? format(parseISO(action.status.get('expires_at')), 'yyyy-MM-dd HH:mm') : state.get('default_expires_in', null)); + map.set('expires_action', action.status.get('expires_action') ?? state.get('default_expires_action', 'mark')); map.update('references', set => set.clear().concat(action.status.get('status_reference_ids'))); map.update('context_references', set => set.clear().concat(action.context_references)); diff --git a/app/lib/user_settings_decorator.rb b/app/lib/user_settings_decorator.rb index 4d8092107..fefc932a5 100644 --- a/app/lib/user_settings_decorator.rb +++ b/app/lib/user_settings_decorator.rb @@ -91,7 +91,9 @@ class UserSettingsDecorator user.settings['default_search_searchability'] = default_search_searchability_preference if change?('setting_default_search_searchability') user.settings['show_reload_button'] = show_reload_button_preference if change?('setting_show_reload_button') user.settings['default_column_width'] = default_column_width_preference if change?('setting_default_column_width') - end + user.settings['default_expires_in'] = default_expires_in_preference if change?('setting_default_expires_in') + user.settings['default_expires_action'] = default_expires_action_preference if change?('setting_default_expires_action') +end def merged_notification_emails user.settings['notification_emails'].merge coerced_settings('notification_emails').to_h @@ -341,6 +343,14 @@ class UserSettingsDecorator settings['setting_default_column_width'] end + def default_expires_in_preference + settings['setting_default_expires_in'] + end + + def default_expires_action_preference + settings['setting_default_expires_action'] + end + def boolean_cast_setting(key) ActiveModel::Type::Boolean.new.cast(settings[key]) end diff --git a/app/models/user.rb b/app/models/user.rb index 8d10a4265..9b8b7babb 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -141,7 +141,7 @@ class User < ApplicationRecord :multi_column_customize, :multi_column_content_font_size, :multi_column_info_font_size, :multi_column_content_emoji_reaction_size, :mobile_customize, :mobile_content_font_size, :mobile_info_font_size, :mobile_content_emoji_reaction_size, :hide_bot_on_public_timeline, :confirm_follow_from_bot, - :default_search_searchability, + :default_search_searchability, :default_expires_in, :default_expires_action, :show_reload_button, :default_column_width, to: :settings, prefix: :setting, allow_nil: false diff --git a/app/serializers/initial_state_serializer.rb b/app/serializers/initial_state_serializer.rb index e53343e23..cc0052290 100644 --- a/app/serializers/initial_state_serializer.rb +++ b/app/serializers/initial_state_serializer.rb @@ -89,10 +89,12 @@ class InitialStateSerializer < ActiveModel::Serializer store = {} if object.current_account - store[:me] = object.current_account.id.to_s - store[:default_privacy] = object.visibility || object.current_account.user.setting_default_privacy - store[:default_searchability] = object.current_account.searchability - store[:default_sensitive] = object.current_account.user.setting_default_sensitive + store[:me] = object.current_account.id.to_s + store[:default_privacy] = object.visibility || object.current_account.user.setting_default_privacy + store[:default_searchability] = object.current_account.searchability + store[:default_sensitive] = object.current_account.user.setting_default_sensitive + store[:default_expires_in] = object.current_account.user.setting_default_expires_in + store[:default_expires_action] = object.current_account.user.setting_default_expires_action end store[:text] = object.text if object.text diff --git a/app/views/settings/preferences/other/show.html.haml b/app/views/settings/preferences/other/show.html.haml index 373979545..38fa01a73 100644 --- a/app/views/settings/preferences/other/show.html.haml +++ b/app/views/settings/preferences/other/show.html.haml @@ -40,6 +40,14 @@ .fields-group = f.input :setting_show_application, as: :boolean, wrapper: :with_label, recommended: true + .fields-row + .fields-group.fields-row__column.fields-row__column-6 + = f.input :setting_default_expires_in, wrapper: :with_label, input_html: { maxlength: 30, pattern: '^(?:(\d+)y)?(?:(\d+)m(?=[\do])o?)?(?:(\d+)d)?(?:(\d+)h)?(?:(\d+)m)?$' }, fedibird_features: true + + .fields-group.fields-row__column.fields-row__column-6 + %span.fedibird_features.float Fedibird + = f.input :setting_default_expires_action, collection: ['mark', 'delete'], label_method: lambda { |item| t("simple_form.labels.defaults.setting_default_expires_action_#{item}") }, hint: t("simple_form.hints.defaults.setting_default_expires_action"), as: :radio_buttons, collection_wrapper_tag: 'ul', item_wrapper_tag: 'li', wrapper: :with_floating_label + %h4= t 'preferences.searching_defaults' .fields-group diff --git a/config/locales/simple_form.en.yml b/config/locales/simple_form.en.yml index 5df9c8169..8d87ceaa6 100644 --- a/config/locales/simple_form.en.yml +++ b/config/locales/simple_form.en.yml @@ -62,6 +62,12 @@ en: setting_default_column_width_x100: 100% - Fix to Mastodon standard column width setting_default_column_width_x125: 125% - Fix to 125% of standard width setting_default_column_width_x150: 150% - Fix to 150% of standard width + setting_default_expires_action: | + Submissions whose publication period has ended will be marked or deleted. + Marked posts will no longer be visible, but only the poster and those who reacted (favourite, emoji reaction, bookmark) to the post will be able to see it. + setting_default_expires_in: | + Specify the end date and time as the starting point of the posting date and time. + The format is 1y2mo3d4h5m (1 year, 2 months, 3 days, 4 hours, 5 minutes later). setting_default_search_searchability: For clients that do not support advanced range settings, switch the settings here. Mastodon's standard behavior is "Reacted-users-only". Targeting "Public" makes it easier to discover unknown information, but if the results are noisy, narrowing the search range is effective. setting_default_sensitive: Sensitive media is hidden by default and can be revealed with a click setting_disable_joke_appearance: Disable April Fools' Day and other joke functions @@ -226,6 +232,10 @@ en: setting_content_font_size: Content font size setting_crop_images: Crop images in non-expanded posts to 16x9 setting_default_column_width: Default column width + setting_default_expires_action: Default expiry action + setting_default_expires_action_mark: Mark as expired + setting_default_expires_action_delete: Delete + setting_default_expires_in: Default expiry duration setting_default_language: Posting language setting_default_privacy: Posting privacy setting_default_search_searchability: Search range diff --git a/config/locales/simple_form.ja.yml b/config/locales/simple_form.ja.yml index 2669207fc..0b63f7ae7 100644 --- a/config/locales/simple_form.ja.yml +++ b/config/locales/simple_form.ja.yml @@ -62,6 +62,8 @@ ja: setting_default_column_width_x100: 100% - Mastodon標準のカラム幅に固定します setting_default_column_width_x125: 125% - 標準の125%の幅に固定します setting_default_column_width_x150: 150% - 標準の150%の幅に固定します + setting_default_expires_action: 公開期間が終了した投稿には、マークが付けられるか、削除されます。マークされた投稿は見えなくなり、投稿者とその投稿に反応(お気に入り・絵文字リアクション・ブックマーク)した人だけが見ることができるようになります。 + setting_default_expires_in: 投稿日時を起点とする終了日時を指定します。書式は1y2mo3d4h5m(1年2ヶ月3日4時間5分後)です。 setting_default_search_searchability: 範囲の詳細設定に対応していないクライアントでは、ここで設定を切り替えてください。Mastodonの標準動作は『リアクション限定』です。『公開』を対象にすると未知の情報を発見しやすくなりますが、結果にノイズが多い場合は検索範囲を狭めると効果的です。 setting_default_sensitive: 閲覧注意状態のメディアはデフォルトでは内容が伏せられ、クリックして初めて閲覧できるようになります setting_disable_joke_appearance: エイプリルフール等のジョーク機能を無効にします @@ -226,6 +228,10 @@ ja: setting_content_font_size: 投稿のフォントサイズ setting_crop_images: 投稿の詳細以外では画像を16:9に切り抜く setting_default_column_width: デフォルトのカラム幅 + setting_default_expires_action: デフォルトの終了時アクション + setting_default_expires_action_mark: 期限切れマーク + setting_default_expires_action_delete: 削除 + setting_default_expires_in: デフォルトの終了日時 setting_default_language: 投稿する言語 setting_default_privacy: 投稿の公開範囲 setting_default_search_searchability: 検索の対象とする範囲 diff --git a/config/settings.yml b/config/settings.yml index e8b1061b4..ea8b1b365 100644 --- a/config/settings.yml +++ b/config/settings.yml @@ -112,6 +112,8 @@ defaults: &defaults default_search_searchability: 'private' show_reload_button: true default_column_width: 'x100' + default_expires_in: '' + default_expires_action: 'mark' development: <<: *defaults