diff --git a/Dockerfile b/Dockerfile index bc7cd3b682..a0af1eda6b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -211,7 +211,7 @@ FROM build AS ffmpeg # ffmpeg version to compile, change with [--build-arg FFMPEG_VERSION="7.0.x"] # renovate: datasource=repology depName=ffmpeg packageName=openpkg_current/ffmpeg -ARG FFMPEG_VERSION=7.0.1 +ARG FFMPEG_VERSION=7.0.2 # ffmpeg download URL, change with [--build-arg FFMPEG_URL="https://ffmpeg.org/releases"] ARG FFMPEG_URL=https://ffmpeg.org/releases diff --git a/Gemfile b/Gemfile index de9bc45f32..cf930dadb6 100644 --- a/Gemfile +++ b/Gemfile @@ -16,7 +16,7 @@ gem 'pghero' gem 'aws-sdk-s3', '~> 1.123', require: false gem 'blurhash', '~> 0.1' -gem 'fog-core', '<= 2.4.0' +gem 'fog-core', '<= 2.5.0' gem 'fog-openstack', '~> 1.0', require: false gem 'kt-paperclip', '~> 7.2' gem 'md-paperclip-azure', '~> 2.2', require: false diff --git a/Gemfile.lock b/Gemfile.lock index 49bd446d9b..e4d787eb64 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -100,8 +100,8 @@ GEM attr_required (1.0.2) awrence (1.2.1) aws-eventstream (1.3.0) - aws-partitions (1.950.0) - aws-sdk-core (3.201.0) + aws-partitions (1.961.0) + aws-sdk-core (3.201.3) aws-eventstream (~> 1, >= 1.3.0) aws-partitions (~> 1, >= 1.651.0) aws-sigv4 (~> 1.8) @@ -109,11 +109,11 @@ GEM aws-sdk-kms (1.88.0) aws-sdk-core (~> 3, >= 3.201.0) aws-sigv4 (~> 1.5) - aws-sdk-s3 (1.156.0) + aws-sdk-s3 (1.157.0) aws-sdk-core (~> 3, >= 3.201.0) aws-sdk-kms (~> 1) aws-sigv4 (~> 1.5) - aws-sigv4 (1.8.0) + aws-sigv4 (1.9.1) aws-eventstream (~> 1, >= 1.0.2) azure-storage-blob (2.0.3) azure-storage-common (~> 2.0) @@ -135,7 +135,7 @@ GEM binding_of_caller (1.0.1) debug_inspector (>= 1.2.0) blurhash (0.1.7) - bootsnap (1.18.3) + bootsnap (1.18.4) msgpack (~> 1.2) brakeman (6.1.2) racc @@ -229,7 +229,7 @@ GEM erubi (1.13.0) et-orbi (1.2.11) tzinfo - excon (0.110.0) + excon (0.111.0) fabrication (2.31.0) faker (3.4.2) i18n (>= 1.8.11, < 2) @@ -269,7 +269,7 @@ GEM flatware-rspec (2.3.2) flatware (= 2.3.2) rspec (>= 3.6) - fog-core (2.4.0) + fog-core (2.5.0) builder excon (~> 0.71) formatador (>= 0.2, < 2.0) @@ -429,7 +429,7 @@ GEM memory_profiler (1.0.2) mime-types (3.5.2) mime-types-data (~> 3.2015) - mime-types-data (3.2024.0604) + mime-types-data (3.2024.0702) mini_mime (1.1.5) mini_portile2 (2.8.7) minitest (5.24.1) @@ -758,7 +758,7 @@ GEM rack (>= 1.1) rubocop (>= 1.33.0, < 2.0) rubocop-ast (>= 1.31.1, < 2.0) - rubocop-rspec (3.0.3) + rubocop-rspec (3.0.4) rubocop (~> 1.61) rubocop-rspec_rails (2.30.0) rubocop (~> 1.61) @@ -796,7 +796,7 @@ GEM redis (>= 4.5.0, < 5) sidekiq-bulk (0.2.0) sidekiq - sidekiq-scheduler (5.0.5) + sidekiq-scheduler (5.0.6) rufus-scheduler (~> 3.2) sidekiq (>= 6, < 8) tilt (>= 1.4.0, < 3) @@ -945,7 +945,7 @@ DEPENDENCIES fast_blank (~> 1.0) fastimage flatware-rspec - fog-core (<= 2.4.0) + fog-core (<= 2.5.0) fog-openstack (~> 1.0) fuubar (~> 2.5) haml-rails (~> 2.0) diff --git a/app/controllers/api/v1/notifications/policies_controller.rb b/app/controllers/api/v1/notifications/policies_controller.rb index 1ec336f9a5..9d70c283be 100644 --- a/app/controllers/api/v1/notifications/policies_controller.rb +++ b/app/controllers/api/v1/notifications/policies_controller.rb @@ -8,12 +8,12 @@ class Api::V1::Notifications::PoliciesController < Api::BaseController before_action :set_policy def show - render json: @policy, serializer: REST::NotificationPolicySerializer + render json: @policy, serializer: REST::V1::NotificationPolicySerializer end def update @policy.update!(resource_params) - render json: @policy, serializer: REST::NotificationPolicySerializer + render json: @policy, serializer: REST::V1::NotificationPolicySerializer end private diff --git a/app/controllers/api/v1/notifications/requests_controller.rb b/app/controllers/api/v1/notifications/requests_controller.rb index b4207147c8..0710166d05 100644 --- a/app/controllers/api/v1/notifications/requests_controller.rb +++ b/app/controllers/api/v1/notifications/requests_controller.rb @@ -29,7 +29,7 @@ class Api::V1::Notifications::RequestsController < Api::BaseController end def dismiss - @request.destroy! + DismissNotificationRequestService.new.call(@request) render_empty end diff --git a/app/controllers/api/v2/notifications/policies_controller.rb b/app/controllers/api/v2/notifications/policies_controller.rb new file mode 100644 index 0000000000..637587967f --- /dev/null +++ b/app/controllers/api/v2/notifications/policies_controller.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +class Api::V2::Notifications::PoliciesController < Api::BaseController + before_action -> { doorkeeper_authorize! :read, :'read:notifications' }, only: :show + before_action -> { doorkeeper_authorize! :write, :'write:notifications' }, only: :update + + before_action :require_user! + before_action :set_policy + + def show + render json: @policy, serializer: REST::NotificationPolicySerializer + end + + def update + @policy.update!(resource_params) + render json: @policy, serializer: REST::NotificationPolicySerializer + end + + private + + def set_policy + @policy = NotificationPolicy.find_or_initialize_by(account: current_account) + + with_read_replica do + @policy.summarize! + end + end + + def resource_params + params.permit( + :for_not_following, + :for_not_followers, + :for_new_accounts, + :for_private_mentions, + :for_limited_accounts + ) + end +end diff --git a/app/controllers/api/v2_alpha/notifications_controller.rb b/app/controllers/api/v2_alpha/notifications_controller.rb index 837499e898..d0205ad6af 100644 --- a/app/controllers/api/v2_alpha/notifications_controller.rb +++ b/app/controllers/api/v2_alpha/notifications_controller.rb @@ -16,10 +16,10 @@ class Api::V2Alpha::NotificationsController < Api::BaseController @group_metadata = load_group_metadata @grouped_notifications = load_grouped_notifications @relationships = StatusRelationshipsPresenter.new(target_statuses_from_notifications, current_user&.account_id) - @sample_accounts = @grouped_notifications.flat_map(&:sample_accounts) + @presenter = GroupedNotificationsPresenter.new(@grouped_notifications, expand_accounts: expand_accounts_param) # Preload associations to avoid N+1s - ActiveRecord::Associations::Preloader.new(records: @sample_accounts, associations: [:account_stat, { user: :role }]).call + ActiveRecord::Associations::Preloader.new(records: @presenter.accounts, associations: [:account_stat, { user: :role }]).call end MastodonOTELTracer.in_span('Api::V2Alpha::NotificationsController#index rendering') do |span| @@ -27,14 +27,14 @@ class Api::V2Alpha::NotificationsController < Api::BaseController span.add_attributes( 'app.notification_grouping.count' => @grouped_notifications.size, - 'app.notification_grouping.sample_account.count' => @sample_accounts.size, - 'app.notification_grouping.sample_account.unique_count' => @sample_accounts.pluck(:id).uniq.size, + 'app.notification_grouping.account.count' => @presenter.accounts.size, + 'app.notification_grouping.partial_account.count' => @presenter.partial_accounts.size, 'app.notification_grouping.status.count' => statuses.size, - 'app.notification_grouping.status.unique_count' => statuses.uniq.size + 'app.notification_grouping.status.unique_count' => statuses.uniq.size, + 'app.notification_grouping.expand_accounts_param' => expand_accounts_param ) - presenter = GroupedNotificationsPresenter.new(@grouped_notifications) - render json: presenter, serializer: REST::DedupNotificationGroupSerializer, relationships: @relationships, group_metadata: @group_metadata + render json: @presenter, serializer: REST::DedupNotificationGroupSerializer, relationships: @relationships, group_metadata: @group_metadata, expand_accounts: expand_accounts_param end end @@ -131,4 +131,15 @@ class Api::V2Alpha::NotificationsController < Api::BaseController def pagination_params(core_params) params.slice(:limit, :types, :exclude_types, :include_filtered).permit(:limit, :include_filtered, types: [], exclude_types: []).merge(core_params) end + + def expand_accounts_param + case params[:expand_accounts] + when nil, 'full' + 'full' + when 'partial_avatars' + 'partial_avatars' + else + raise Mastodon::InvalidParameterError, "Invalid value for 'expand_accounts': '#{params[:expand_accounts]}', allowed values are 'full' and 'partial_avatars'" + end + end end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 2369cff7e6..ed89d81563 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -116,7 +116,7 @@ module ApplicationHelper def material_symbol(icon, attributes = {}) inline_svg_tag( "400-24px/#{icon}.svg", - class: %w(icon).concat(attributes[:class].to_s.split), + class: ['icon', "material-#{icon}"].concat(attributes[:class].to_s.split), role: :img ) end @@ -127,23 +127,23 @@ module ApplicationHelper def visibility_icon(status) if status.public_visibility? - fa_icon('globe', title: I18n.t('statuses.visibilities.public')) + material_symbol('globe', title: I18n.t('statuses.visibilities.public')) elsif status.unlisted_visibility? - fa_icon('unlock', title: I18n.t('statuses.visibilities.unlisted')) + material_symbol('lock_open', title: I18n.t('statuses.visibilities.unlisted')) elsif status.private_visibility? || status.limited_visibility? - fa_icon('lock', title: I18n.t('statuses.visibilities.private')) + material_symbol('lock', title: I18n.t('statuses.visibilities.private')) elsif status.direct_visibility? - fa_icon('at', title: I18n.t('statuses.visibilities.direct')) + material_symbol('alternate_email', title: I18n.t('statuses.visibilities.direct')) end end def interrelationships_icon(relationships, account_id) if relationships.following[account_id] && relationships.followed_by[account_id] - fa_icon('exchange', title: I18n.t('relationships.mutual'), class: 'fa-fw active passive') + material_symbol('sync_alt', title: I18n.t('relationships.mutual'), class: 'active passive') elsif relationships.following[account_id] - fa_icon(locale_direction == 'ltr' ? 'arrow-right' : 'arrow-left', title: I18n.t('relationships.following'), class: 'fa-fw active') + material_symbol(locale_direction == 'ltr' ? 'arrow_right_alt' : 'arrow_left_alt', title: I18n.t('relationships.following'), class: 'active') elsif relationships.followed_by[account_id] - fa_icon(locale_direction == 'ltr' ? 'arrow-left' : 'arrow-right', title: I18n.t('relationships.followers'), class: 'fa-fw passive') + material_symbol(locale_direction == 'ltr' ? 'arrow_left_alt' : 'arrow_right_alt', title: I18n.t('relationships.followers'), class: 'passive') end end diff --git a/app/helpers/statuses_helper.rb b/app/helpers/statuses_helper.rb index ca693a8a78..d956e4fcd8 100644 --- a/app/helpers/statuses_helper.rb +++ b/app/helpers/statuses_helper.rb @@ -60,13 +60,13 @@ module StatusesHelper def fa_visibility_icon(status) case status.visibility when 'public' - fa_icon 'globe fw' + material_symbol 'globe' when 'unlisted' - fa_icon 'unlock fw' + material_symbol 'lock_open' when 'private' - fa_icon 'lock fw' + material_symbol 'lock' when 'direct' - fa_icon 'at fw' + material_symbol 'alternate_email' end end diff --git a/app/javascript/entrypoints/public.tsx b/app/javascript/entrypoints/public.tsx index 149c1d28d0..b06675c2ee 100644 --- a/app/javascript/entrypoints/public.tsx +++ b/app/javascript/entrypoints/public.tsx @@ -431,6 +431,42 @@ Rails.delegate(document, 'img.custom-emoji', 'mouseout', ({ target }) => { target.src = target.dataset.static; }); +const setInputDisabled = ( + input: HTMLInputElement | HTMLSelectElement, + disabled: boolean, +) => { + input.disabled = disabled; + + const wrapper = input.closest('.with_label'); + if (wrapper) { + wrapper.classList.toggle('disabled', input.disabled); + + const hidden = + input.type === 'checkbox' && + wrapper.querySelector('input[type=hidden][value="0"]'); + if (hidden) { + hidden.disabled = input.disabled; + } + } +}; + +Rails.delegate( + document, + '#account_statuses_cleanup_policy_enabled', + 'change', + ({ target }) => { + if (!(target instanceof HTMLInputElement) || !target.form) return; + + target.form + .querySelectorAll< + HTMLInputElement | HTMLSelectElement + >('input:not([type=hidden], #account_statuses_cleanup_policy_enabled), select') + .forEach((input) => { + setInputDisabled(input, !target.checked); + }); + }, +); + // Empty the honeypot fields in JS in case something like an extension // automatically filled them. Rails.delegate(document, '#registration_new_user,#new_user', 'submit', () => { diff --git a/app/javascript/flavours/glitch/actions/notifications.js b/app/javascript/flavours/glitch/actions/notifications.js index cefb9e6e83..6dcda9f65c 100644 --- a/app/javascript/flavours/glitch/actions/notifications.js +++ b/app/javascript/flavours/glitch/actions/notifications.js @@ -77,6 +77,14 @@ export const NOTIFICATION_REQUEST_DISMISS_REQUEST = 'NOTIFICATION_REQUEST_DISMIS export const NOTIFICATION_REQUEST_DISMISS_SUCCESS = 'NOTIFICATION_REQUEST_DISMISS_SUCCESS'; export const NOTIFICATION_REQUEST_DISMISS_FAIL = 'NOTIFICATION_REQUEST_DISMISS_FAIL'; +export const NOTIFICATION_REQUESTS_ACCEPT_REQUEST = 'NOTIFICATION_REQUESTS_ACCEPT_REQUEST'; +export const NOTIFICATION_REQUESTS_ACCEPT_SUCCESS = 'NOTIFICATION_REQUESTS_ACCEPT_SUCCESS'; +export const NOTIFICATION_REQUESTS_ACCEPT_FAIL = 'NOTIFICATION_REQUESTS_ACCEPT_FAIL'; + +export const NOTIFICATION_REQUESTS_DISMISS_REQUEST = 'NOTIFICATION_REQUESTS_DISMISS_REQUEST'; +export const NOTIFICATION_REQUESTS_DISMISS_SUCCESS = 'NOTIFICATION_REQUESTS_DISMISS_SUCCESS'; +export const NOTIFICATION_REQUESTS_DISMISS_FAIL = 'NOTIFICATION_REQUESTS_DISMISS_FAIL'; + export const NOTIFICATIONS_FOR_REQUEST_FETCH_REQUEST = 'NOTIFICATIONS_FOR_REQUEST_FETCH_REQUEST'; export const NOTIFICATIONS_FOR_REQUEST_FETCH_SUCCESS = 'NOTIFICATIONS_FOR_REQUEST_FETCH_SUCCESS'; export const NOTIFICATIONS_FOR_REQUEST_FETCH_FAIL = 'NOTIFICATIONS_FOR_REQUEST_FETCH_FAIL'; @@ -585,6 +593,62 @@ export const dismissNotificationRequestFail = (id, error) => ({ error, }); +export const acceptNotificationRequests = (ids) => (dispatch, getState) => { + const count = ids.reduce((count, id) => count + selectNotificationCountForRequest(getState(), id), 0); + dispatch(acceptNotificationRequestsRequest(ids)); + + api().post(`/api/v1/notifications/requests/accept`, { id: ids }).then(() => { + dispatch(acceptNotificationRequestsSuccess(ids)); + dispatch(decreasePendingNotificationsCount(count)); + }).catch(err => { + dispatch(acceptNotificationRequestFail(ids, err)); + }); +}; + +export const acceptNotificationRequestsRequest = ids => ({ + type: NOTIFICATION_REQUESTS_ACCEPT_REQUEST, + ids, +}); + +export const acceptNotificationRequestsSuccess = ids => ({ + type: NOTIFICATION_REQUESTS_ACCEPT_SUCCESS, + ids, +}); + +export const acceptNotificationRequestsFail = (ids, error) => ({ + type: NOTIFICATION_REQUESTS_ACCEPT_FAIL, + ids, + error, +}); + +export const dismissNotificationRequests = (ids) => (dispatch, getState) => { + const count = ids.reduce((count, id) => count + selectNotificationCountForRequest(getState(), id), 0); + dispatch(acceptNotificationRequestsRequest(ids)); + + api().post(`/api/v1/notifications/requests/dismiss`, { id: ids }).then(() => { + dispatch(dismissNotificationRequestsSuccess(ids)); + dispatch(decreasePendingNotificationsCount(count)); + }).catch(err => { + dispatch(dismissNotificationRequestFail(ids, err)); + }); +}; + +export const dismissNotificationRequestsRequest = ids => ({ + type: NOTIFICATION_REQUESTS_DISMISS_REQUEST, + ids, +}); + +export const dismissNotificationRequestsSuccess = ids => ({ + type: NOTIFICATION_REQUESTS_DISMISS_SUCCESS, + ids, +}); + +export const dismissNotificationRequestsFail = (ids, error) => ({ + type: NOTIFICATION_REQUESTS_DISMISS_FAIL, + ids, + error, +}); + export const fetchNotificationsForRequest = accountId => (dispatch, getState) => { const current = getState().getIn(['notificationRequests', 'current']); const params = { account_id: accountId }; diff --git a/app/javascript/flavours/glitch/api/notification_policies.ts b/app/javascript/flavours/glitch/api/notification_policies.ts index e52ea64f41..1db79a6e74 100644 --- a/app/javascript/flavours/glitch/api/notification_policies.ts +++ b/app/javascript/flavours/glitch/api/notification_policies.ts @@ -2,8 +2,8 @@ import { apiRequestGet, apiRequestPut } from 'flavours/glitch/api'; import type { NotificationPolicyJSON } from 'flavours/glitch/api_types/notification_policies'; export const apiGetNotificationPolicy = () => - apiRequestGet('/v1/notifications/policy'); + apiRequestGet('/v2/notifications/policy'); export const apiUpdateNotificationsPolicy = ( policy: Partial, -) => apiRequestPut('/v1/notifications/policy', policy); +) => apiRequestPut('/v2/notifications/policy', policy); diff --git a/app/javascript/flavours/glitch/api_types/notification_policies.ts b/app/javascript/flavours/glitch/api_types/notification_policies.ts index 0f4a2d132e..1c3970782c 100644 --- a/app/javascript/flavours/glitch/api_types/notification_policies.ts +++ b/app/javascript/flavours/glitch/api_types/notification_policies.ts @@ -1,10 +1,13 @@ // See app/serializers/rest/notification_policy_serializer.rb +export type NotificationPolicyValue = 'accept' | 'filter' | 'drop'; + export interface NotificationPolicyJSON { - filter_not_following: boolean; - filter_not_followers: boolean; - filter_new_accounts: boolean; - filter_private_mentions: boolean; + for_not_following: NotificationPolicyValue; + for_not_followers: NotificationPolicyValue; + for_new_accounts: NotificationPolicyValue; + for_private_mentions: NotificationPolicyValue; + for_limited_accounts: NotificationPolicyValue; summary: { pending_requests_count: number; pending_notifications_count: number; diff --git a/app/javascript/flavours/glitch/components/account.jsx b/app/javascript/flavours/glitch/components/account.jsx index e2ea899524..29ffc60c6b 100644 --- a/app/javascript/flavours/glitch/components/account.jsx +++ b/app/javascript/flavours/glitch/components/account.jsx @@ -106,7 +106,7 @@ const Account = ({ size = 46, account, onFollow, onBlock, onMute, onMuteNotifica ); } else if (defaultAction === 'mute') { - buttons =