From 839a4e27a948ca2ff45a94ec0ea171ec9558e85d Mon Sep 17 00:00:00 2001 From: Claire Date: Fri, 3 Jan 2025 14:23:01 +0100 Subject: [PATCH 1/6] Fix incorrect `relationship_severance_event` attribute name in changelog (#33443) --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 743cc36ef519a7..ea18b3cb92a48c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -135,7 +135,7 @@ The following changelog entries focus on changes visible to users, administrator - **Add notifications of severed relationships** (#27511, #29665, #29668, #29670, #29700, #29714, #29712, and #29731 by @ClearlyClaire and @Gargron)\ Notify local users when they lose relationships as a result of a local moderator blocking a remote account or server, allowing the affected user to retrieve the list of broken relationships.\ Note that this does not notify remote users.\ - This adds the `severed_relationships` notification type to the REST API and streaming, with a new [`relationship_severance_event` attribute](https://docs.joinmastodon.org/entities/Notification/#relationship_severance_event). + This adds the `severed_relationships` notification type to the REST API and streaming, with a new [`event` attribute](https://docs.joinmastodon.org/entities/Notification/#relationship_severance_event). - **Add hover cards in web UI** (#30754, #30864, #30850, #30879, #30928, #30949, #30948, #30931, and #31300 by @ClearlyClaire, @Gargron, and @renchap)\ Hovering over an avatar or username will now display a hover card with the first two lines of the user's description and their first two profile fields.\ This can be disabled in the “Animations and accessibility” section of the preferences. From f9582bcfc3676404bd0053f58702246ed2d98431 Mon Sep 17 00:00:00 2001 From: Christian Schmidt Date: Fri, 3 Jan 2025 16:29:25 +0100 Subject: [PATCH 2/6] Localize language names (#33402) --- .../mastodon/components/status_content.jsx | 2 +- app/javascript/mastodon/initial_state.js | 13 ++++++++++++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/app/javascript/mastodon/components/status_content.jsx b/app/javascript/mastodon/components/status_content.jsx index 6b06b938dee4bd..aa8b0603843549 100644 --- a/app/javascript/mastodon/components/status_content.jsx +++ b/app/javascript/mastodon/components/status_content.jsx @@ -38,7 +38,7 @@ class TranslateButton extends PureComponent { if (translation) { const language = preloadedLanguages.find(lang => lang[0] === translation.get('detected_source_language')); - const languageName = language ? language[2] : translation.get('detected_source_language'); + const languageName = language ? language[1] : translation.get('detected_source_language'); const provider = translation.get('provider'); return ( diff --git a/app/javascript/mastodon/initial_state.js b/app/javascript/mastodon/initial_state.js index 0c59fc9ee00246..40942eb7ec79ef 100644 --- a/app/javascript/mastodon/initial_state.js +++ b/app/javascript/mastodon/initial_state.js @@ -115,11 +115,22 @@ export const trendsAsLanding = getMeta('trends_as_landing_page'); export const useBlurhash = getMeta('use_blurhash'); export const usePendingItems = getMeta('use_pending_items'); export const version = getMeta('version'); -export const languages = initialState?.languages; export const criticalUpdatesPending = initialState?.critical_updates_pending; export const statusPageUrl = getMeta('status_page_url'); export const sso_redirect = getMeta('sso_redirect'); export const termsOfServiceEnabled = getMeta('terms_of_service_enabled'); + +const displayNames = Intl.DisplayNames && new Intl.DisplayNames(getMeta('locale'), { + type: 'language', + fallback: 'none', + languageDisplay: 'standard', +}); + +export const languages = initialState?.languages?.map(lang => { + // zh-YUE is not a valid CLDR unicode_language_id + return [lang[0], displayNames?.of(lang[0].replace('zh-YUE', 'yue')) || lang[1], lang[2]]; +}); + /** * @returns {string | undefined} */ From 535866218e98e3ae2b7e2eaa26fa22e13979416f Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Fri, 3 Jan 2025 11:08:21 -0500 Subject: [PATCH 3/6] Update `rails` to version 8.0.1 (#32357) --- Gemfile | 4 +- Gemfile.lock | 118 +++++++++--------- config/application.rb | 2 +- .../initializers/filter_parameter_logging.rb | 2 +- lib/active_record/batches.rb | 4 +- spec/rails_helper.rb | 5 + 6 files changed, 71 insertions(+), 64 deletions(-) diff --git a/Gemfile b/Gemfile index 51f6d01c22b7f9..ad174cfdc42fb5 100644 --- a/Gemfile +++ b/Gemfile @@ -6,7 +6,7 @@ ruby '>= 3.2.0', '< 3.5' gem 'propshaft' gem 'puma', '~> 6.3' gem 'rack', '~> 2.2.7' -gem 'rails', '~> 7.2.0' +gem 'rails', '~> 8.0' gem 'thor', '~> 1.2' gem 'dotenv' @@ -73,7 +73,7 @@ gem 'public_suffix', '~> 6.0' gem 'pundit', '~> 2.3' gem 'rack-attack', '~> 6.6' gem 'rack-cors', '~> 2.0', require: 'rack/cors' -gem 'rails-i18n', '~> 7.0' +gem 'rails-i18n', '~> 8.0' gem 'redcarpet', '~> 3.6' gem 'redis', '~> 4.5', require: ['redis', 'redis/connection/hiredis'] gem 'redis-namespace', '~> 1.10' diff --git a/Gemfile.lock b/Gemfile.lock index 24c4d04766a086..f49de6bf3bdf5d 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -10,46 +10,45 @@ GIT GEM remote: https://rubygems.org/ specs: - actioncable (7.2.2.1) - actionpack (= 7.2.2.1) - activesupport (= 7.2.2.1) + actioncable (8.0.1) + actionpack (= 8.0.1) + activesupport (= 8.0.1) nio4r (~> 2.0) websocket-driver (>= 0.6.1) zeitwerk (~> 2.6) - actionmailbox (7.2.2.1) - actionpack (= 7.2.2.1) - activejob (= 7.2.2.1) - activerecord (= 7.2.2.1) - activestorage (= 7.2.2.1) - activesupport (= 7.2.2.1) + actionmailbox (8.0.1) + actionpack (= 8.0.1) + activejob (= 8.0.1) + activerecord (= 8.0.1) + activestorage (= 8.0.1) + activesupport (= 8.0.1) mail (>= 2.8.0) - actionmailer (7.2.2.1) - actionpack (= 7.2.2.1) - actionview (= 7.2.2.1) - activejob (= 7.2.2.1) - activesupport (= 7.2.2.1) + actionmailer (8.0.1) + actionpack (= 8.0.1) + actionview (= 8.0.1) + activejob (= 8.0.1) + activesupport (= 8.0.1) mail (>= 2.8.0) rails-dom-testing (~> 2.2) - actionpack (7.2.2.1) - actionview (= 7.2.2.1) - activesupport (= 7.2.2.1) + actionpack (8.0.1) + actionview (= 8.0.1) + activesupport (= 8.0.1) nokogiri (>= 1.8.5) - racc - rack (>= 2.2.4, < 3.2) + rack (>= 2.2.4) rack-session (>= 1.0.1) rack-test (>= 0.6.3) rails-dom-testing (~> 2.2) rails-html-sanitizer (~> 1.6) useragent (~> 0.16) - actiontext (7.2.2.1) - actionpack (= 7.2.2.1) - activerecord (= 7.2.2.1) - activestorage (= 7.2.2.1) - activesupport (= 7.2.2.1) + actiontext (8.0.1) + actionpack (= 8.0.1) + activerecord (= 8.0.1) + activestorage (= 8.0.1) + activesupport (= 8.0.1) globalid (>= 0.6.0) nokogiri (>= 1.8.5) - actionview (7.2.2.1) - activesupport (= 7.2.2.1) + actionview (8.0.1) + activesupport (= 8.0.1) builder (~> 3.1) erubi (~> 1.11) rails-dom-testing (~> 2.2) @@ -59,22 +58,22 @@ GEM activemodel (>= 4.1) case_transform (>= 0.2) jsonapi-renderer (>= 0.1.1.beta1, < 0.3) - activejob (7.2.2.1) - activesupport (= 7.2.2.1) + activejob (8.0.1) + activesupport (= 8.0.1) globalid (>= 0.3.6) - activemodel (7.2.2.1) - activesupport (= 7.2.2.1) - activerecord (7.2.2.1) - activemodel (= 7.2.2.1) - activesupport (= 7.2.2.1) + activemodel (8.0.1) + activesupport (= 8.0.1) + activerecord (8.0.1) + activemodel (= 8.0.1) + activesupport (= 8.0.1) timeout (>= 0.4.0) - activestorage (7.2.2.1) - actionpack (= 7.2.2.1) - activejob (= 7.2.2.1) - activerecord (= 7.2.2.1) - activesupport (= 7.2.2.1) + activestorage (8.0.1) + actionpack (= 8.0.1) + activejob (= 8.0.1) + activerecord (= 8.0.1) + activesupport (= 8.0.1) marcel (~> 1.0) - activesupport (7.2.2.1) + activesupport (8.0.1) base64 benchmark (>= 0.3) bigdecimal @@ -86,6 +85,7 @@ GEM minitest (>= 5.1) securerandom (>= 0.3) tzinfo (~> 2.0, >= 2.0.5) + uri (>= 0.13.1) addressable (2.8.7) public_suffix (>= 2.0.2, < 7.0) aes_key_wrap (1.1.0) @@ -613,20 +613,20 @@ GEM rackup (1.0.1) rack (< 3) webrick - rails (7.2.2.1) - actioncable (= 7.2.2.1) - actionmailbox (= 7.2.2.1) - actionmailer (= 7.2.2.1) - actionpack (= 7.2.2.1) - actiontext (= 7.2.2.1) - actionview (= 7.2.2.1) - activejob (= 7.2.2.1) - activemodel (= 7.2.2.1) - activerecord (= 7.2.2.1) - activestorage (= 7.2.2.1) - activesupport (= 7.2.2.1) + rails (8.0.1) + actioncable (= 8.0.1) + actionmailbox (= 8.0.1) + actionmailer (= 8.0.1) + actionpack (= 8.0.1) + actiontext (= 8.0.1) + actionview (= 8.0.1) + activejob (= 8.0.1) + activemodel (= 8.0.1) + activerecord (= 8.0.1) + activestorage (= 8.0.1) + activesupport (= 8.0.1) bundler (>= 1.15.0) - railties (= 7.2.2.1) + railties (= 8.0.1) rails-controller-testing (1.0.5) actionpack (>= 5.0.1.rc1) actionview (>= 5.0.1.rc1) @@ -638,12 +638,12 @@ GEM rails-html-sanitizer (1.6.2) loofah (~> 2.21) nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0) - rails-i18n (7.0.10) + rails-i18n (8.0.1) i18n (>= 0.7, < 2) - railties (>= 6.0.0, < 8) - railties (7.2.2.1) - actionpack (= 7.2.2.1) - activesupport (= 7.2.2.1) + railties (>= 8.0.0, < 9) + railties (8.0.1) + actionpack (= 8.0.1) + activesupport (= 8.0.1) irb (~> 1.13) rackup (>= 1.0.0) rake (>= 12.2) @@ -986,9 +986,9 @@ DEPENDENCIES rack-attack (~> 6.6) rack-cors (~> 2.0) rack-test (~> 2.1) - rails (~> 7.2.0) + rails (~> 8.0) rails-controller-testing (~> 1.0) - rails-i18n (~> 7.0) + rails-i18n (~> 8.0) rdf-normalize (~> 0.5) redcarpet (~> 3.6) redis (~> 4.5) diff --git a/config/application.rb b/config/application.rb index 8939ad91f465ca..92976e87abe7d0 100644 --- a/config/application.rb +++ b/config/application.rb @@ -59,7 +59,7 @@ module Mastodon class Application < Rails::Application # Initialize configuration defaults for originally generated Rails version. - config.load_defaults 7.2 + config.load_defaults 8.0 # Please, add to the `ignore` list any other `lib` subdirectories that do # not contain `.rb` files, or that should not be reloaded or eager loaded. diff --git a/config/initializers/filter_parameter_logging.rb b/config/initializers/filter_parameter_logging.rb index e88b020ff38bf2..497ac1325ccaa8 100644 --- a/config/initializers/filter_parameter_logging.rb +++ b/config/initializers/filter_parameter_logging.rb @@ -6,5 +6,5 @@ # Use this to limit dissemination of sensitive information. # See the ActiveSupport::ParameterFilter documentation for supported notations and behaviors. Rails.application.config.filter_parameters += [ - :passw, :email, :secret, :token, :_key, :crypt, :salt, :certificate, :otp, :ssn + :passw, :email, :secret, :token, :_key, :crypt, :salt, :certificate, :otp, :ssn, :cvv, :cvc ] diff --git a/lib/active_record/batches.rb b/lib/active_record/batches.rb index 7960b6e10de743..fb0b13954b91f9 100644 --- a/lib/active_record/batches.rb +++ b/lib/active_record/batches.rb @@ -13,7 +13,9 @@ def pluck_each(*column_names) column_names.unshift(primary_key) - relation = relation.reorder(build_batch_orders(order).to_h).limit(batch_limit) + cursor = Array(primary_key) + + relation = relation.reorder(build_batch_orders(cursor, order).to_h).limit(batch_limit) relation.skip_query_cache! batch_relation = relation diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb index 574d5438442fcc..6564f17372591b 100644 --- a/spec/rails_helper.rb +++ b/spec/rails_helper.rb @@ -119,6 +119,11 @@ def sign_in(resource, _deprecated = nil, scope: nil) config.include CommandLineHelpers, type: :cli config.include SystemHelpers, type: :system + # TODO: Remove when Devise fixes https://github.com/heartcombo/devise/issues/5705 + config.before do + Rails.application.reload_routes_unless_loaded + end + config.around(:each, use_transactional_tests: false) do |example| self.use_transactional_tests = false example.run From 7d6da219c0fccb8693282a99765135050173d7c4 Mon Sep 17 00:00:00 2001 From: Claire Date: Fri, 3 Jan 2025 17:12:08 +0100 Subject: [PATCH 4/6] Remove old notifications actions and reducers (#31843) --- .../mastodon/actions/notifications.js | 153 +--------- .../actions/notifications_migration.tsx | 10 - .../mastodon/actions/notifications_typed.ts | 1 - app/javascript/mastodon/actions/streaming.js | 5 +- .../containers/column_settings_container.js | 4 +- app/javascript/mastodon/features/ui/index.jsx | 4 +- .../mastodon/reducers/notifications.js | 281 +----------------- 7 files changed, 7 insertions(+), 451 deletions(-) delete mode 100644 app/javascript/mastodon/actions/notifications_migration.tsx diff --git a/app/javascript/mastodon/actions/notifications.js b/app/javascript/mastodon/actions/notifications.js index 4c6e27cd5f8d2e..3266df5a59d907 100644 --- a/app/javascript/mastodon/actions/notifications.js +++ b/app/javascript/mastodon/actions/notifications.js @@ -1,21 +1,13 @@ import { IntlMessageFormat } from 'intl-messageformat'; import { defineMessages } from 'react-intl'; -import { List as ImmutableList } from 'immutable'; - -import { compareId } from 'mastodon/compare_id'; -import { usePendingItems as preferPendingItems } from 'mastodon/initial_state'; - -import api, { getLinks } from '../api'; import { unescapeHTML } from '../utils/html'; import { requestNotificationPermission } from '../utils/notifications'; import { fetchFollowRequests } from './accounts'; import { importFetchedAccount, - importFetchedAccounts, importFetchedStatus, - importFetchedStatuses, } from './importer'; import { submitMarkers } from './markers'; import { notificationsUpdate } from "./notifications_typed"; @@ -26,27 +18,11 @@ export * from "./notifications_typed"; export const NOTIFICATIONS_UPDATE_NOOP = 'NOTIFICATIONS_UPDATE_NOOP'; -export const NOTIFICATIONS_EXPAND_REQUEST = 'NOTIFICATIONS_EXPAND_REQUEST'; -export const NOTIFICATIONS_EXPAND_SUCCESS = 'NOTIFICATIONS_EXPAND_SUCCESS'; -export const NOTIFICATIONS_EXPAND_FAIL = 'NOTIFICATIONS_EXPAND_FAIL'; - export const NOTIFICATIONS_FILTER_SET = 'NOTIFICATIONS_FILTER_SET'; -export const NOTIFICATIONS_SCROLL_TOP = 'NOTIFICATIONS_SCROLL_TOP'; -export const NOTIFICATIONS_LOAD_PENDING = 'NOTIFICATIONS_LOAD_PENDING'; - -export const NOTIFICATIONS_MOUNT = 'NOTIFICATIONS_MOUNT'; -export const NOTIFICATIONS_UNMOUNT = 'NOTIFICATIONS_UNMOUNT'; - -export const NOTIFICATIONS_MARK_AS_READ = 'NOTIFICATIONS_MARK_AS_READ'; - export const NOTIFICATIONS_SET_BROWSER_SUPPORT = 'NOTIFICATIONS_SET_BROWSER_SUPPORT'; export const NOTIFICATIONS_SET_BROWSER_PERMISSION = 'NOTIFICATIONS_SET_BROWSER_PERMISSION'; -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'; @@ -56,10 +32,6 @@ defineMessages({ group: { id: 'notifications.group', defaultMessage: '{count} notifications' }, }); -export const loadPending = () => ({ - type: NOTIFICATIONS_LOAD_PENDING, -}); - export function updateNotifications(notification, intlMessages, intlLocale) { return (dispatch, getState) => { const activeFilter = getState().getIn(['settings', 'notifications', 'quickFilter', 'active']); @@ -96,8 +68,7 @@ export function updateNotifications(notification, intlMessages, intlLocale) { dispatch(importFetchedAccount(notification.report.target_account)); } - - dispatch(notificationsUpdate({ notification, preferPendingItems, playSound: playSound && !filtered})); + dispatch(notificationsUpdate({ notification, playSound: playSound && !filtered})); } else if (playSound && !filtered) { dispatch({ type: NOTIFICATIONS_UPDATE_NOOP, @@ -120,116 +91,8 @@ export function updateNotifications(notification, intlMessages, intlLocale) { }; } -const excludeTypesFromSettings = state => state.getIn(['settings', 'notifications', 'shows']).filter(enabled => !enabled).keySeq().toJS(); - -const excludeTypesFromFilter = filter => { - const allTypes = ImmutableList([ - 'follow', - 'follow_request', - 'favourite', - 'reblog', - 'mention', - 'poll', - 'status', - 'update', - 'admin.sign_up', - 'admin.report', - ]); - - return allTypes.filterNot(item => item === filter).toJS(); -}; - const noOp = () => {}; -let expandNotificationsController = new AbortController(); - -export function expandNotifications({ maxId = undefined, forceLoad = false }) { - return async (dispatch, getState) => { - const activeFilter = getState().getIn(['settings', 'notifications', 'quickFilter', 'active']); - const notifications = getState().get('notifications'); - const isLoadingMore = !!maxId; - - if (notifications.get('isLoading')) { - if (forceLoad) { - expandNotificationsController.abort(); - expandNotificationsController = new AbortController(); - } else { - return; - } - } - - const params = { - max_id: maxId, - exclude_types: activeFilter === 'all' - ? excludeTypesFromSettings(getState()) - : excludeTypesFromFilter(activeFilter), - }; - - if (!params.max_id && (notifications.get('items', ImmutableList()).size + notifications.get('pendingItems', ImmutableList()).size) > 0) { - const a = notifications.getIn(['pendingItems', 0, 'id']); - const b = notifications.getIn(['items', 0, 'id']); - - if (a && b && compareId(a, b) > 0) { - params.since_id = a; - } else { - params.since_id = b || a; - } - } - - const isLoadingRecent = !!params.since_id; - - dispatch(expandNotificationsRequest(isLoadingMore)); - - try { - const response = await api().get('/api/v1/notifications', { params, signal: expandNotificationsController.signal }); - const next = getLinks(response).refs.find(link => link.rel === 'next'); - - dispatch(importFetchedAccounts(response.data.map(item => item.account))); - dispatch(importFetchedStatuses(response.data.map(item => item.status).filter(status => !!status))); - dispatch(importFetchedAccounts(response.data.filter(item => item.report).map(item => item.report.target_account))); - - dispatch(expandNotificationsSuccess(response.data, next ? next.uri : null, isLoadingMore, isLoadingRecent, isLoadingRecent && preferPendingItems)); - dispatch(submitMarkers()); - } catch(error) { - dispatch(expandNotificationsFail(error, isLoadingMore)); - } - }; -} - -export function expandNotificationsRequest(isLoadingMore) { - return { - type: NOTIFICATIONS_EXPAND_REQUEST, - skipLoading: !isLoadingMore, - }; -} - -export function expandNotificationsSuccess(notifications, next, isLoadingMore, isLoadingRecent, usePendingItems) { - return { - type: NOTIFICATIONS_EXPAND_SUCCESS, - notifications, - next, - isLoadingRecent: isLoadingRecent, - usePendingItems, - skipLoading: !isLoadingMore, - }; -} - -export function expandNotificationsFail(error, isLoadingMore) { - return { - type: NOTIFICATIONS_EXPAND_FAIL, - error, - skipLoading: !isLoadingMore, - skipAlert: !isLoadingMore || error.name === 'AbortError', - }; -} - -export function scrollTopNotifications(top) { - return { - type: NOTIFICATIONS_SCROLL_TOP, - top, - }; -} - export function setFilter (filterType) { return dispatch => { dispatch({ @@ -237,24 +100,10 @@ export function setFilter (filterType) { path: ['notifications', 'quickFilter', 'active'], value: filterType, }); - dispatch(expandNotifications({ forceLoad: true })); dispatch(saveSettings()); }; } -export const mountNotifications = () => ({ - type: NOTIFICATIONS_MOUNT, -}); - -export const unmountNotifications = () => ({ - type: NOTIFICATIONS_UNMOUNT, -}); - - -export const markNotificationsAsRead = () => ({ - type: NOTIFICATIONS_MARK_AS_READ, -}); - // Browser support export function setupBrowserNotifications() { return dispatch => { diff --git a/app/javascript/mastodon/actions/notifications_migration.tsx b/app/javascript/mastodon/actions/notifications_migration.tsx deleted file mode 100644 index cd9f5ca3d6d2bf..00000000000000 --- a/app/javascript/mastodon/actions/notifications_migration.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import { createAppAsyncThunk } from 'mastodon/store'; - -import { fetchNotifications } from './notification_groups'; - -export const initializeNotifications = createAppAsyncThunk( - 'notifications/initialize', - (_, { dispatch }) => { - void dispatch(fetchNotifications()); - }, -); diff --git a/app/javascript/mastodon/actions/notifications_typed.ts b/app/javascript/mastodon/actions/notifications_typed.ts index 88d942d45e54b5..3eb1230666dbc5 100644 --- a/app/javascript/mastodon/actions/notifications_typed.ts +++ b/app/javascript/mastodon/actions/notifications_typed.ts @@ -9,7 +9,6 @@ export const notificationsUpdate = createAction( ...args }: { notification: ApiNotificationJSON; - usePendingItems: boolean; playSound: boolean; }) => ({ payload: args, diff --git a/app/javascript/mastodon/actions/streaming.js b/app/javascript/mastodon/actions/streaming.js index 30e643363a1c0d..478e0cae4538d4 100644 --- a/app/javascript/mastodon/actions/streaming.js +++ b/app/javascript/mastodon/actions/streaming.js @@ -11,7 +11,7 @@ import { } from './announcements'; import { updateConversations } from './conversations'; import { processNewNotificationForGroups, refreshStaleNotificationGroups, pollRecentNotifications as pollRecentGroupNotifications } from './notification_groups'; -import { updateNotifications, expandNotifications } from './notifications'; +import { updateNotifications } from './notifications'; import { updateStatus } from './statuses'; import { updateTimeline, @@ -107,9 +107,6 @@ export const connectTimelineStream = (timelineId, channelName, params = {}, opti break; } case 'notifications_merged': { - const state = getState(); - if (state.notifications.top || !state.notifications.mounted) - dispatch(expandNotifications({ forceLoad: true, maxId: undefined })); dispatch(refreshStaleNotificationGroups()); break; } diff --git a/app/javascript/mastodon/features/notifications/containers/column_settings_container.js b/app/javascript/mastodon/features/notifications/containers/column_settings_container.js index 4ac6cfa629ba10..f061060ec7c4e2 100644 --- a/app/javascript/mastodon/features/notifications/containers/column_settings_container.js +++ b/app/javascript/mastodon/features/notifications/containers/column_settings_container.js @@ -3,7 +3,7 @@ import { defineMessages, injectIntl } from 'react-intl'; import { connect } from 'react-redux'; import { openModal } from 'mastodon/actions/modal'; -import { initializeNotifications } from 'mastodon/actions/notifications_migration'; +import { fetchNotifications } from 'mastodon/actions/notification_groups'; import { showAlert } from '../../../actions/alerts'; import { setFilter, requestBrowserPermission } from '../../../actions/notifications'; @@ -60,7 +60,7 @@ const mapDispatchToProps = (dispatch) => ({ dispatch(changeSetting(['notifications', ...path], checked)); if(path[0] === 'group' && path[1] === 'follow') { - dispatch(initializeNotifications()); + dispatch(fetchNotifications()); } } }, diff --git a/app/javascript/mastodon/features/ui/index.jsx b/app/javascript/mastodon/features/ui/index.jsx index bfc540a3313882..1ecc52b6bdfd6d 100644 --- a/app/javascript/mastodon/features/ui/index.jsx +++ b/app/javascript/mastodon/features/ui/index.jsx @@ -13,7 +13,7 @@ import { HotKeys } from 'react-hotkeys'; import { focusApp, unfocusApp, changeLayout } from 'mastodon/actions/app'; import { synchronouslySubmitMarkers, submitMarkers, fetchMarkers } from 'mastodon/actions/markers'; -import { initializeNotifications } from 'mastodon/actions/notifications_migration'; +import { fetchNotifications } from 'mastodon/actions/notification_groups'; import { INTRODUCTION_VERSION } from 'mastodon/actions/onboarding'; import { HoverCardController } from 'mastodon/components/hover_card_controller'; import { PictureInPicture } from 'mastodon/features/picture_in_picture'; @@ -418,7 +418,7 @@ class UI extends PureComponent { if (signedIn) { this.props.dispatch(fetchMarkers()); this.props.dispatch(expandHomeTimeline()); - this.props.dispatch(initializeNotifications()); + this.props.dispatch(fetchNotifications()); this.props.dispatch(fetchServerTranslationLanguages()); setTimeout(() => this.props.dispatch(fetchServer()), 3000); diff --git a/app/javascript/mastodon/reducers/notifications.js b/app/javascript/mastodon/reducers/notifications.js index c99a619b52dc69..b47e24e3979c77 100644 --- a/app/javascript/mastodon/reducers/notifications.js +++ b/app/javascript/mastodon/reducers/notifications.js @@ -1,50 +1,11 @@ -import { fromJS, Map as ImmutableMap, List as ImmutableList } from 'immutable'; +import { fromJS, Map as ImmutableMap } from 'immutable'; -import { blockDomainSuccess } from 'mastodon/actions/domain_blocks'; -import { timelineDelete } from 'mastodon/actions/timelines_typed'; - -import { - authorizeFollowRequestSuccess, - blockAccountSuccess, - muteAccountSuccess, - rejectFollowRequestSuccess, -} from '../actions/accounts'; -import { - focusApp, - unfocusApp, -} from '../actions/app'; import { - fetchMarkers, -} from '../actions/markers'; -import { clearNotifications } from '../actions/notification_groups'; -import { - notificationsUpdate, - NOTIFICATIONS_EXPAND_SUCCESS, - NOTIFICATIONS_EXPAND_REQUEST, - NOTIFICATIONS_EXPAND_FAIL, - NOTIFICATIONS_FILTER_SET, - NOTIFICATIONS_SCROLL_TOP, - NOTIFICATIONS_LOAD_PENDING, - NOTIFICATIONS_MOUNT, - NOTIFICATIONS_UNMOUNT, - NOTIFICATIONS_MARK_AS_READ, NOTIFICATIONS_SET_BROWSER_SUPPORT, NOTIFICATIONS_SET_BROWSER_PERMISSION, } from '../actions/notifications'; -import { disconnectTimeline } from '../actions/timelines'; -import { compareId } from '../compare_id'; const initialState = ImmutableMap({ - pendingItems: ImmutableList(), - items: ImmutableList(), - hasMore: true, - top: false, - mounted: 0, - unread: 0, - lastReadId: '0', - readMarkerId: '0', - isTabVisible: true, - isLoading: 0, browserSupport: false, browserPermission: 'default', }); @@ -60,248 +21,8 @@ export const notificationToMap = notification => ImmutableMap({ moderation_warning: notification.moderation_warning ? fromJS(notification.moderation_warning) : null, }); -const normalizeNotification = (state, notification, usePendingItems) => { - const top = state.get('top'); - - // Under currently unknown conditions, the client may receive duplicates from the server - if (state.get('pendingItems').some((item) => item?.get('id') === notification.id) || state.get('items').some((item) => item?.get('id') === notification.id)) { - return state; - } - - if (usePendingItems || !state.get('pendingItems').isEmpty()) { - return state.update('pendingItems', list => list.unshift(notificationToMap(notification))).update('unread', unread => unread + 1); - } - - if (shouldCountUnreadNotifications(state)) { - state = state.update('unread', unread => unread + 1); - } else { - state = state.set('lastReadId', notification.id); - } - - return state.update('items', list => { - if (top && list.size > 40) { - list = list.take(20); - } - - return list.unshift(notificationToMap(notification)); - }); -}; - -const expandNormalizedNotifications = (state, notifications, next, isLoadingMore, isLoadingRecent, usePendingItems) => { - // This method is pretty tricky because: - // - existing notifications might be out of order - // - the existing notifications may have gaps, most often explicitly noted with a `null` item - // - ideally, we don't want it to reorder existing items - // - `notifications` may include items that are already included - // - this function can be called either to fill in a gap, or load newer items - - const lastReadId = state.get('lastReadId'); - const newItems = ImmutableList(notifications.map(notificationToMap)); - - return state.withMutations(mutable => { - if (!newItems.isEmpty()) { - usePendingItems = isLoadingRecent && (usePendingItems || !mutable.get('pendingItems').isEmpty()); - - mutable.update(usePendingItems ? 'pendingItems' : 'items', oldItems => { - // If called to poll *new* notifications, we just need to add them on top without duplicates - if (isLoadingRecent) { - const idsToCheck = oldItems.map(item => item?.get('id')).toSet(); - const insertedItems = newItems.filterNot(item => idsToCheck.includes(item.get('id'))); - return insertedItems.concat(oldItems); - } - - // If called to expand more (presumably older than any known to the WebUI), we just have to - // add them to the bottom without duplicates - if (isLoadingMore) { - const idsToCheck = oldItems.map(item => item?.get('id')).toSet(); - const insertedItems = newItems.filterNot(item => idsToCheck.includes(item.get('id'))); - return oldItems.concat(insertedItems); - } - - // Now this gets tricky, as we don't necessarily know for sure where the gap to fill is, - // and some items in the timeline may not be properly ordered. - - // However, we know that `newItems.last()` is the oldest item that was requested and that - // there is no “hole” between `newItems.last()` and `newItems.first()`. - - // First, find the furthest (if properly sorted, oldest) item in the notifications that is - // newer than the oldest fetched one, as it's most likely that it delimits the gap. - // Start the gap *after* that item. - const lastIndex = oldItems.findLastIndex(item => item !== null && compareId(item.get('id'), newItems.last().get('id')) >= 0) + 1; - - // Then, try to find the furthest (if properly sorted, oldest) item in the notifications that - // is newer than the most recent fetched one, as it delimits a section comprised of only - // items older or within `newItems` (or that were deleted from the server, so should be removed - // anyway). - // Stop the gap *after* that item. - const firstIndex = oldItems.take(lastIndex).findLastIndex(item => item !== null && compareId(item.get('id'), newItems.first().get('id')) > 0) + 1; - - // At this point: - // - no `oldItems` after `firstIndex` is newer than any of the `newItems` - // - all `oldItems` after `lastIndex` are older than every of the `newItems` - // - it is possible for items in the replaced slice to be older than every `newItems` - // - it is possible for items before `firstIndex` to be in the `newItems` range - // Therefore: - // - to avoid losing items, items from the replaced slice that are older than `newItems` - // should be added in the back. - // - to avoid duplicates, `newItems` should be checked the first `firstIndex` items of - // `oldItems` - const idsToCheck = oldItems.take(firstIndex).map(item => item?.get('id')).toSet(); - const insertedItems = newItems.filterNot(item => idsToCheck.includes(item.get('id'))); - const olderItems = oldItems.slice(firstIndex, lastIndex).filter(item => item !== null && compareId(item.get('id'), newItems.last().get('id')) < 0); - - return oldItems.take(firstIndex).concat( - insertedItems, - olderItems, - oldItems.skip(lastIndex), - ); - }); - } - - if (!next) { - mutable.set('hasMore', false); - } - - if (shouldCountUnreadNotifications(state)) { - mutable.set('unread', mutable.get('pendingItems').count(item => item !== null) + mutable.get('items').count(item => item && compareId(item.get('id'), lastReadId) > 0)); - } else { - const mostRecent = newItems.find(item => item !== null); - if (mostRecent && compareId(lastReadId, mostRecent.get('id')) < 0) { - mutable.set('lastReadId', mostRecent.get('id')); - } - } - - mutable.update('isLoading', (nbLoading) => nbLoading - 1); - }); -}; - -const filterNotifications = (state, accountIds, type) => { - const helper = list => list.filterNot(item => item !== null && accountIds.includes(item.get('account')) && (type === undefined || type === item.get('type'))); - return state.update('items', helper).update('pendingItems', helper); -}; - -const clearUnread = (state) => { - state = state.set('unread', state.get('pendingItems').size); - const lastNotification = state.get('items').find(item => item !== null); - return state.set('lastReadId', lastNotification ? lastNotification.get('id') : '0'); -}; - -const updateTop = (state, top) => { - state = state.set('top', top); - - if (!shouldCountUnreadNotifications(state)) { - state = clearUnread(state); - } - - return state; -}; - -const deleteByStatus = (state, statusId) => { - const lastReadId = state.get('lastReadId'); - - if (shouldCountUnreadNotifications(state)) { - const deletedUnread = state.get('items').filter(item => item !== null && item.get('status') === statusId && compareId(item.get('id'), lastReadId) > 0); - state = state.update('unread', unread => unread - deletedUnread.size); - } - - const helper = list => list.filterNot(item => item !== null && item.get('status') === statusId); - const deletedUnread = state.get('pendingItems').filter(item => item !== null && item.get('status') === statusId && compareId(item.get('id'), lastReadId) > 0); - state = state.update('unread', unread => unread - deletedUnread.size); - return state.update('items', helper).update('pendingItems', helper); -}; - -const updateMounted = (state) => { - state = state.update('mounted', count => count + 1); - if (!shouldCountUnreadNotifications(state, state.get('mounted') === 1)) { - state = state.set('readMarkerId', state.get('lastReadId')); - state = clearUnread(state); - } - return state; -}; - -const updateVisibility = (state, visibility) => { - state = state.set('isTabVisible', visibility); - if (!shouldCountUnreadNotifications(state)) { - state = state.set('readMarkerId', state.get('lastReadId')); - state = clearUnread(state); - } - return state; -}; - -const shouldCountUnreadNotifications = (state, ignoreScroll = false) => { - const isTabVisible = state.get('isTabVisible'); - const isOnTop = state.get('top'); - const isMounted = state.get('mounted') > 0; - const lastReadId = state.get('lastReadId'); - const lastItem = state.get('items').findLast(item => item !== null); - const lastItemReached = !state.get('hasMore') || lastReadId === '0' || (lastItem && compareId(lastItem.get('id'), lastReadId) <= 0); - - return !(isTabVisible && (ignoreScroll || isOnTop) && isMounted && lastItemReached); -}; - -const recountUnread = (state, last_read_id) => { - return state.withMutations(mutable => { - if (compareId(last_read_id, mutable.get('lastReadId')) > 0) { - mutable.set('lastReadId', last_read_id); - } - - if (compareId(last_read_id, mutable.get('readMarkerId')) > 0) { - mutable.set('readMarkerId', last_read_id); - } - - if (state.get('unread') > 0 || shouldCountUnreadNotifications(state)) { - mutable.set('unread', mutable.get('pendingItems').count(item => item !== null) + mutable.get('items').count(item => item && compareId(item.get('id'), last_read_id) > 0)); - } - }); -}; - export default function notifications(state = initialState, action) { switch(action.type) { - case fetchMarkers.fulfilled.type: - return action.payload.markers.notifications ? recountUnread(state, action.payload.markers.notifications.last_read_id) : state; - case NOTIFICATIONS_MOUNT: - return updateMounted(state); - case NOTIFICATIONS_UNMOUNT: - return state.update('mounted', count => count - 1); - case focusApp.type: - return updateVisibility(state, true); - case unfocusApp.type: - return updateVisibility(state, false); - case NOTIFICATIONS_LOAD_PENDING: - return state.update('items', list => state.get('pendingItems').concat(list.take(40))).set('pendingItems', ImmutableList()).set('unread', 0); - case NOTIFICATIONS_EXPAND_REQUEST: - return state.update('isLoading', (nbLoading) => nbLoading + 1); - case NOTIFICATIONS_EXPAND_FAIL: - return state.update('isLoading', (nbLoading) => nbLoading - 1); - case NOTIFICATIONS_FILTER_SET: - return state.set('items', ImmutableList()).set('pendingItems', ImmutableList()).set('hasMore', true); - case NOTIFICATIONS_SCROLL_TOP: - return updateTop(state, action.top); - case notificationsUpdate.type: - return normalizeNotification(state, action.payload.notification, action.payload.usePendingItems); - case NOTIFICATIONS_EXPAND_SUCCESS: - return expandNormalizedNotifications(state, action.notifications, action.next, action.isLoadingMore, action.isLoadingRecent, action.usePendingItems); - case blockAccountSuccess.type: - return filterNotifications(state, [action.payload.relationship.id]); - case muteAccountSuccess.type: - return action.payload.relationship.muting_notifications ? filterNotifications(state, [action.payload.relationship.id]) : state; - case blockDomainSuccess.type: - return filterNotifications(state, action.payload.accounts); - case authorizeFollowRequestSuccess.type: - case rejectFollowRequestSuccess.type: - return filterNotifications(state, [action.payload.id], 'follow_request'); - case clearNotifications.pending.type: - return state.set('items', ImmutableList()).set('pendingItems', ImmutableList()).set('hasMore', false); - case timelineDelete.type: - return deleteByStatus(state, action.payload.statusId); - case disconnectTimeline.type: - return action.payload.timeline === 'home' ? - state.update(action.payload.usePendingItems ? 'pendingItems' : 'items', items => items.first() ? items.unshift(null) : items) : - state; - case NOTIFICATIONS_MARK_AS_READ: { - const lastNotification = state.get('items').find(item => item !== null); - return lastNotification ? recountUnread(state, lastNotification.get('id')) : state; - } case NOTIFICATIONS_SET_BROWSER_SUPPORT: return state.set('browserSupport', action.value); case NOTIFICATIONS_SET_BROWSER_PERMISSION: From 49c5791dec6da98d61344e90640144a9df5480af Mon Sep 17 00:00:00 2001 From: Christian Schmidt Date: Fri, 3 Jan 2025 16:29:25 +0100 Subject: [PATCH 5/6] [Glitch] Localize language names Port f9582bcfc3676404bd0053f58702246ed2d98431 to glitch-soc Signed-off-by: Claire --- .../flavours/glitch/components/status_content.jsx | 2 +- app/javascript/flavours/glitch/initial_state.js | 13 +++++++++++-- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/app/javascript/flavours/glitch/components/status_content.jsx b/app/javascript/flavours/glitch/components/status_content.jsx index 184fc65d39780d..af70ddcb47017d 100644 --- a/app/javascript/flavours/glitch/components/status_content.jsx +++ b/app/javascript/flavours/glitch/components/status_content.jsx @@ -94,7 +94,7 @@ class TranslateButton extends PureComponent { if (translation) { const language = preloadedLanguages.find(lang => lang[0] === translation.get('detected_source_language')); - const languageName = language ? language[2] : translation.get('detected_source_language'); + const languageName = language ? language[1] : translation.get('detected_source_language'); const provider = translation.get('provider'); return ( diff --git a/app/javascript/flavours/glitch/initial_state.js b/app/javascript/flavours/glitch/initial_state.js index 4777f31e299dc9..7fb25697ae0925 100644 --- a/app/javascript/flavours/glitch/initial_state.js +++ b/app/javascript/flavours/glitch/initial_state.js @@ -134,13 +134,22 @@ export const trendsAsLanding = getMeta('trends_as_landing_page'); export const useBlurhash = getMeta('use_blurhash'); export const usePendingItems = getMeta('use_pending_items'); export const version = getMeta('version'); -export const languages = initialState?.languages; export const criticalUpdatesPending = initialState?.critical_updates_pending; export const statusPageUrl = getMeta('status_page_url'); export const sso_redirect = getMeta('sso_redirect'); - export const termsOfServiceEnabled = getMeta('terms_of_service_enabled'); +const displayNames = Intl.DisplayNames && new Intl.DisplayNames(getMeta('locale'), { + type: 'language', + fallback: 'none', + languageDisplay: 'standard', +}); + +export const languages = initialState?.languages?.map(lang => { + // zh-YUE is not a valid CLDR unicode_language_id + return [lang[0], displayNames?.of(lang[0].replace('zh-YUE', 'yue')) || lang[1], lang[2]]; +}); + // Glitch-soc-specific settings export const maxFeedHashtags = (initialState && initialState.max_feed_hashtags) || 4; export const favouriteModal = getMeta('favourite_modal'); From 813921295e05175d52a15831d8089eaca6a264f4 Mon Sep 17 00:00:00 2001 From: Claire Date: Fri, 3 Jan 2025 17:12:08 +0100 Subject: [PATCH 6/6] [Glitch] Remove old notifications actions and reducers Port 7d6da219c0fccb8693282a99765135050173d7c4 to glitch-soc Signed-off-by: Claire --- .../flavours/glitch/actions/notifications.js | 243 +----------- .../actions/notifications_migration.tsx | 10 - .../glitch/actions/notifications_typed.ts | 1 - .../flavours/glitch/actions/streaming.js | 5 +- .../components/notification_purge_buttons.jsx | 64 ---- .../flavours/glitch/components/status.jsx | 7 - .../notification_purge_buttons_container.js | 53 --- .../notifications/components/notification.jsx | 5 - .../notifications/components/overlay.jsx | 61 --- .../containers/column_settings_container.js | 4 +- .../containers/notification_container.js | 3 +- .../containers/overlay_container.js | 19 - .../flavours/glitch/features/ui/index.jsx | 6 +- .../flavours/glitch/locales/en.json | 7 - .../flavours/glitch/reducers/notifications.js | 353 +----------------- 15 files changed, 11 insertions(+), 830 deletions(-) delete mode 100644 app/javascript/flavours/glitch/actions/notifications_migration.tsx delete mode 100644 app/javascript/flavours/glitch/components/notification_purge_buttons.jsx delete mode 100644 app/javascript/flavours/glitch/containers/notification_purge_buttons_container.js delete mode 100644 app/javascript/flavours/glitch/features/notifications/components/overlay.jsx delete mode 100644 app/javascript/flavours/glitch/features/notifications/containers/overlay_container.js diff --git a/app/javascript/flavours/glitch/actions/notifications.js b/app/javascript/flavours/glitch/actions/notifications.js index 7b80663f3ddb52..3266df5a59d907 100644 --- a/app/javascript/flavours/glitch/actions/notifications.js +++ b/app/javascript/flavours/glitch/actions/notifications.js @@ -1,21 +1,13 @@ import { IntlMessageFormat } from 'intl-messageformat'; import { defineMessages } from 'react-intl'; -import { List as ImmutableList } from 'immutable'; - -import { compareId } from 'flavours/glitch/compare_id'; -import { usePendingItems as preferPendingItems } from 'flavours/glitch/initial_state'; - -import api, { getLinks } from '../api'; import { unescapeHTML } from '../utils/html'; import { requestNotificationPermission } from '../utils/notifications'; import { fetchFollowRequests } from './accounts'; import { importFetchedAccount, - importFetchedAccounts, importFetchedStatus, - importFetchedStatuses, } from './importer'; import { submitMarkers } from './markers'; import { notificationsUpdate } from "./notifications_typed"; @@ -26,50 +18,18 @@ export * from "./notifications_typed"; export const NOTIFICATIONS_UPDATE_NOOP = 'NOTIFICATIONS_UPDATE_NOOP'; -// tracking the notif cleaning request -export const NOTIFICATIONS_DELETE_MARKED_REQUEST = 'NOTIFICATIONS_DELETE_MARKED_REQUEST'; -export const NOTIFICATIONS_DELETE_MARKED_SUCCESS = 'NOTIFICATIONS_DELETE_MARKED_SUCCESS'; -export const NOTIFICATIONS_DELETE_MARKED_FAIL = 'NOTIFICATIONS_DELETE_MARKED_FAIL'; -export const NOTIFICATIONS_MARK_ALL_FOR_DELETE = 'NOTIFICATIONS_MARK_ALL_FOR_DELETE'; -export const NOTIFICATIONS_ENTER_CLEARING_MODE = 'NOTIFICATIONS_ENTER_CLEARING_MODE'; // arg: yes -// Unmark notifications (when the cleaning mode is left) -export const NOTIFICATIONS_UNMARK_ALL_FOR_DELETE = 'NOTIFICATIONS_UNMARK_ALL_FOR_DELETE'; -// Mark one for delete -export const NOTIFICATION_MARK_FOR_DELETE = 'NOTIFICATION_MARK_FOR_DELETE'; - -export const NOTIFICATIONS_EXPAND_REQUEST = 'NOTIFICATIONS_EXPAND_REQUEST'; -export const NOTIFICATIONS_EXPAND_SUCCESS = 'NOTIFICATIONS_EXPAND_SUCCESS'; -export const NOTIFICATIONS_EXPAND_FAIL = 'NOTIFICATIONS_EXPAND_FAIL'; - export const NOTIFICATIONS_FILTER_SET = 'NOTIFICATIONS_FILTER_SET'; -export const NOTIFICATIONS_SCROLL_TOP = 'NOTIFICATIONS_SCROLL_TOP'; -export const NOTIFICATIONS_LOAD_PENDING = 'NOTIFICATIONS_LOAD_PENDING'; - -export const NOTIFICATIONS_MOUNT = 'NOTIFICATIONS_MOUNT'; -export const NOTIFICATIONS_UNMOUNT = 'NOTIFICATIONS_UNMOUNT'; - -export const NOTIFICATIONS_SET_VISIBILITY = 'NOTIFICATIONS_SET_VISIBILITY'; - -export const NOTIFICATIONS_MARK_AS_READ = 'NOTIFICATIONS_MARK_AS_READ'; - export const NOTIFICATIONS_SET_BROWSER_SUPPORT = 'NOTIFICATIONS_SET_BROWSER_SUPPORT'; export const NOTIFICATIONS_SET_BROWSER_PERMISSION = 'NOTIFICATIONS_SET_BROWSER_PERMISSION'; -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'; defineMessages({ mention: { id: 'notification.mention', defaultMessage: '{name} mentioned you' }, -}); - -export const loadPending = () => ({ - type: NOTIFICATIONS_LOAD_PENDING, + group: { id: 'notifications.group', defaultMessage: '{count} notifications' }, }); export function updateNotifications(notification, intlMessages, intlLocale) { @@ -108,8 +68,7 @@ export function updateNotifications(notification, intlMessages, intlLocale) { dispatch(importFetchedAccount(notification.report.target_account)); } - - dispatch(notificationsUpdate({ notification, preferPendingItems, playSound: playSound && !filtered})); + dispatch(notificationsUpdate({ notification, playSound: playSound && !filtered})); } else if (playSound && !filtered) { dispatch({ type: NOTIFICATIONS_UPDATE_NOOP, @@ -132,199 +91,8 @@ export function updateNotifications(notification, intlMessages, intlLocale) { }; } -const excludeTypesFromSettings = state => state.getIn(['settings', 'notifications', 'shows']).filter(enabled => !enabled).keySeq().toJS(); - -const excludeTypesFromFilter = filter => { - const allTypes = ImmutableList([ - 'follow', - 'follow_request', - 'favourite', - 'reblog', - 'mention', - 'poll', - 'status', - 'update', - 'admin.sign_up', - 'admin.report', - ]); - - return allTypes.filterNot(item => item === filter).toJS(); -}; - const noOp = () => {}; -let expandNotificationsController = new AbortController(); - -export function expandNotifications({ maxId = undefined, forceLoad = false }) { - return async (dispatch, getState) => { - const activeFilter = getState().getIn(['settings', 'notifications', 'quickFilter', 'active']); - const notifications = getState().get('notifications'); - const isLoadingMore = !!maxId; - - if (notifications.get('isLoading')) { - if (forceLoad) { - expandNotificationsController.abort(); - expandNotificationsController = new AbortController(); - } else { - return; - } - } - - const params = { - max_id: maxId, - exclude_types: activeFilter === 'all' - ? excludeTypesFromSettings(getState()) - : excludeTypesFromFilter(activeFilter), - }; - - if (!params.max_id && (notifications.get('items', ImmutableList()).size + notifications.get('pendingItems', ImmutableList()).size) > 0) { - const a = notifications.getIn(['pendingItems', 0, 'id']); - const b = notifications.getIn(['items', 0, 'id']); - - if (a && b && compareId(a, b) > 0) { - params.since_id = a; - } else { - params.since_id = b || a; - } - } - - const isLoadingRecent = !!params.since_id; - - dispatch(expandNotificationsRequest(isLoadingMore)); - - try { - const response = await api().get('/api/v1/notifications', { params, signal: expandNotificationsController.signal }); - const next = getLinks(response).refs.find(link => link.rel === 'next'); - - dispatch(importFetchedAccounts(response.data.map(item => item.account))); - dispatch(importFetchedStatuses(response.data.map(item => item.status).filter(status => !!status))); - dispatch(importFetchedAccounts(response.data.filter(item => item.report).map(item => item.report.target_account))); - - dispatch(expandNotificationsSuccess(response.data, next ? next.uri : null, isLoadingMore, isLoadingRecent, isLoadingRecent && preferPendingItems)); - dispatch(submitMarkers()); - } catch(error) { - dispatch(expandNotificationsFail(error, isLoadingMore)); - } - }; -} - -export function expandNotificationsRequest(isLoadingMore) { - return { - type: NOTIFICATIONS_EXPAND_REQUEST, - skipLoading: !isLoadingMore, - }; -} - -export function expandNotificationsSuccess(notifications, next, isLoadingMore, isLoadingRecent, usePendingItems) { - return { - type: NOTIFICATIONS_EXPAND_SUCCESS, - notifications, - next, - isLoadingRecent: isLoadingRecent, - usePendingItems, - skipLoading: !isLoadingMore, - }; -} - -export function expandNotificationsFail(error, isLoadingMore) { - return { - type: NOTIFICATIONS_EXPAND_FAIL, - error, - skipLoading: !isLoadingMore, - skipAlert: !isLoadingMore || error.name === 'AbortError', - }; -} - -export function scrollTopNotifications(top) { - return { - type: NOTIFICATIONS_SCROLL_TOP, - top, - }; -} - -export function deleteMarkedNotifications() { - return (dispatch, getState) => { - dispatch(deleteMarkedNotificationsRequest()); - - let ids = []; - getState().getIn(['notifications', 'items']).forEach((n) => { - if (n.get('markedForDelete')) { - ids.push(n.get('id')); - } - }); - - if (ids.length === 0) { - return; - } - - api().delete(`/api/v1/notifications/destroy_multiple?ids[]=${ids.join('&ids[]=')}`).then(() => { - dispatch(deleteMarkedNotificationsSuccess()); - }).catch(error => { - console.error(error); - dispatch(deleteMarkedNotificationsFail(error)); - }); - }; -} - -export function enterNotificationClearingMode(yes) { - return { - type: NOTIFICATIONS_ENTER_CLEARING_MODE, - yes: yes, - }; -} - -export function markAllNotifications(yes) { - return { - type: NOTIFICATIONS_MARK_ALL_FOR_DELETE, - yes: yes, // true, false or null. null = invert - }; -} - -export function deleteMarkedNotificationsRequest() { - return { - type: NOTIFICATIONS_DELETE_MARKED_REQUEST, - }; -} - -export function deleteMarkedNotificationsFail() { - return { - type: NOTIFICATIONS_DELETE_MARKED_FAIL, - }; -} - -export function markNotificationForDelete(id, yes) { - return { - type: NOTIFICATION_MARK_FOR_DELETE, - id: id, - yes: yes, - }; -} - -export function deleteMarkedNotificationsSuccess() { - return { - type: NOTIFICATIONS_DELETE_MARKED_SUCCESS, - }; -} - -export function mountNotifications() { - return { - type: NOTIFICATIONS_MOUNT, - }; -} - -export function unmountNotifications() { - return { - type: NOTIFICATIONS_UNMOUNT, - }; -} - -export function notificationsSetVisibility(visibility) { - return { - type: NOTIFICATIONS_SET_VISIBILITY, - visibility: visibility, - }; -} - export function setFilter (filterType) { return dispatch => { dispatch({ @@ -332,17 +100,10 @@ export function setFilter (filterType) { path: ['notifications', 'quickFilter', 'active'], value: filterType, }); - dispatch(expandNotifications({ forceLoad: true })); dispatch(saveSettings()); }; } -export function markNotificationsAsRead() { - return { - type: NOTIFICATIONS_MARK_AS_READ, - }; -} - // Browser support export function setupBrowserNotifications() { return dispatch => { diff --git a/app/javascript/flavours/glitch/actions/notifications_migration.tsx b/app/javascript/flavours/glitch/actions/notifications_migration.tsx deleted file mode 100644 index 6f2d38b9e94677..00000000000000 --- a/app/javascript/flavours/glitch/actions/notifications_migration.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import { createAppAsyncThunk } from 'flavours/glitch/store'; - -import { fetchNotifications } from './notification_groups'; - -export const initializeNotifications = createAppAsyncThunk( - 'notifications/initialize', - (_, { dispatch }) => { - void dispatch(fetchNotifications()); - }, -); diff --git a/app/javascript/flavours/glitch/actions/notifications_typed.ts b/app/javascript/flavours/glitch/actions/notifications_typed.ts index 55896f4c378f49..a9abfa0e60d7ec 100644 --- a/app/javascript/flavours/glitch/actions/notifications_typed.ts +++ b/app/javascript/flavours/glitch/actions/notifications_typed.ts @@ -9,7 +9,6 @@ export const notificationsUpdate = createAction( ...args }: { notification: ApiNotificationJSON; - usePendingItems: boolean; playSound: boolean; }) => ({ payload: args, diff --git a/app/javascript/flavours/glitch/actions/streaming.js b/app/javascript/flavours/glitch/actions/streaming.js index fa7af7055e6180..93bd7dc5f44098 100644 --- a/app/javascript/flavours/glitch/actions/streaming.js +++ b/app/javascript/flavours/glitch/actions/streaming.js @@ -11,7 +11,7 @@ import { } from './announcements'; import { updateConversations } from './conversations'; import { processNewNotificationForGroups, refreshStaleNotificationGroups, pollRecentNotifications as pollRecentGroupNotifications } from './notification_groups'; -import { updateNotifications, expandNotifications } from './notifications'; +import { updateNotifications } from './notifications'; import { updateStatus } from './statuses'; import { updateTimeline, @@ -107,9 +107,6 @@ export const connectTimelineStream = (timelineId, channelName, params = {}, opti break; } case 'notifications_merged': { - const state = getState(); - if (state.notifications.top || !state.notifications.mounted) - dispatch(expandNotifications({ forceLoad: true, maxId: undefined })); dispatch(refreshStaleNotificationGroups()); break; } diff --git a/app/javascript/flavours/glitch/components/notification_purge_buttons.jsx b/app/javascript/flavours/glitch/components/notification_purge_buttons.jsx deleted file mode 100644 index b17173832277ab..00000000000000 --- a/app/javascript/flavours/glitch/components/notification_purge_buttons.jsx +++ /dev/null @@ -1,64 +0,0 @@ -/** - * Buttons widget for controlling the notification clearing mode. - * In idle state, the cleaning mode button is shown. When the mode is active, - * a Confirm and Abort buttons are shown in its place. - */ - -import PropTypes from 'prop-types'; - -import { defineMessages, injectIntl } from 'react-intl'; - -import classNames from 'classnames'; - -import ImmutablePureComponent from 'react-immutable-pure-component'; - -import DeleteIcon from '@/material-icons/400-24px/delete.svg?react'; -import { Icon } from 'flavours/glitch/components/icon'; - - -const messages = defineMessages({ - btnAll : { id: 'notification_purge.btn_all', defaultMessage: 'Select\nall' }, - btnNone : { id: 'notification_purge.btn_none', defaultMessage: 'Select\nnone' }, - btnInvert : { id: 'notification_purge.btn_invert', defaultMessage: 'Invert\nselection' }, - btnApply : { id: 'notification_purge.btn_apply', defaultMessage: 'Clear\nselected' }, -}); - -class NotificationPurgeButtons extends ImmutablePureComponent { - - static propTypes = { - onDeleteMarked : PropTypes.func.isRequired, - onMarkAll : PropTypes.func.isRequired, - onMarkNone : PropTypes.func.isRequired, - onInvert : PropTypes.func.isRequired, - intl: PropTypes.object.isRequired, - markNewForDelete: PropTypes.bool, - }; - - render () { - const { intl, markNewForDelete } = this.props; - - //className='active' - return ( -
- - - - - - - -
- ); - } - -} - -export default injectIntl(NotificationPurgeButtons); diff --git a/app/javascript/flavours/glitch/components/status.jsx b/app/javascript/flavours/glitch/components/status.jsx index 4e24a3e2889d71..b38fac46fbd708 100644 --- a/app/javascript/flavours/glitch/components/status.jsx +++ b/app/javascript/flavours/glitch/components/status.jsx @@ -11,7 +11,6 @@ import { HotKeys } from 'react-hotkeys'; import { ContentWarning } from 'flavours/glitch/components/content_warning'; import PictureInPicturePlaceholder from 'flavours/glitch/components/picture_in_picture_placeholder'; -import NotificationOverlayContainer from 'flavours/glitch/features/notifications/containers/overlay_container'; import { autoUnfoldCW } from 'flavours/glitch/utils/content_warning'; import { withOptionalRouter, WithOptionalRouterPropTypes } from 'flavours/glitch/utils/react_router'; @@ -729,12 +728,6 @@ class Status extends ImmutablePureComponent { onFilter={matchedFilters ? this.handleFilterClick : null} {...other} /> - - {notification && ( - - )} diff --git a/app/javascript/flavours/glitch/containers/notification_purge_buttons_container.js b/app/javascript/flavours/glitch/containers/notification_purge_buttons_container.js deleted file mode 100644 index 144d77f135bbdb..00000000000000 --- a/app/javascript/flavours/glitch/containers/notification_purge_buttons_container.js +++ /dev/null @@ -1,53 +0,0 @@ -// Package imports. -import { defineMessages, injectIntl } from 'react-intl'; - -import { connect } from 'react-redux'; - -// Our imports. -import { openModal } from 'flavours/glitch/actions/modal'; -import { - deleteMarkedNotifications, - enterNotificationClearingMode, - markAllNotifications, -} from 'flavours/glitch/actions/notifications'; -import NotificationPurgeButtons from 'flavours/glitch/components/notification_purge_buttons'; - -const messages = defineMessages({ - clearMessage: { id: 'notifications.marked_clear_confirmation', defaultMessage: 'Are you sure you want to permanently clear all selected notifications?' }, - clearConfirm: { id: 'notifications.marked_clear', defaultMessage: 'Clear selected notifications' }, -}); - -const mapDispatchToProps = (dispatch, { intl }) => ({ - onEnterCleaningMode(yes) { - dispatch(enterNotificationClearingMode(yes)); - }, - - onDeleteMarked() { - dispatch(openModal({ - modalType: 'CONFIRM', - modalProps: { - message: intl.formatMessage(messages.clearMessage), - confirm: intl.formatMessage(messages.clearConfirm), - onConfirm: () => dispatch(deleteMarkedNotifications()), - }, - })); - }, - - onMarkAll() { - dispatch(markAllNotifications(true)); - }, - - onMarkNone() { - dispatch(markAllNotifications(false)); - }, - - onInvert() { - dispatch(markAllNotifications(null)); - }, -}); - -const mapStateToProps = state => ({ - markNewForDelete: state.getIn(['notifications', 'markNewForDelete']), -}); - -export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(NotificationPurgeButtons)); diff --git a/app/javascript/flavours/glitch/features/notifications/components/notification.jsx b/app/javascript/flavours/glitch/features/notifications/components/notification.jsx index 8518ddb2ca6f36..6ec613e2094b79 100644 --- a/app/javascript/flavours/glitch/features/notifications/components/notification.jsx +++ b/app/javascript/flavours/glitch/features/notifications/components/notification.jsx @@ -20,7 +20,6 @@ import StatusContainer from 'flavours/glitch/containers/status_container'; import { WithRouterPropTypes } from 'flavours/glitch/utils/react_router'; import FollowRequestContainer from '../containers/follow_request_container'; -import NotificationOverlayContainer from '../containers/overlay_container'; import { ModerationWarning } from './moderation_warning'; import { RelationshipsSeveranceEvent } from './relationships_severance_event'; @@ -133,7 +132,6 @@ class Notification extends ImmutablePureComponent {