diff --git a/CHANGELOG.md b/CHANGELOG.md
index 743cc36ef5..ea18b3cb92 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.
diff --git a/Gemfile b/Gemfile
index 51f6d01c22..ad174cfdc4 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 24c4d04766..f49de6bf3b 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/app/javascript/flavours/glitch/actions/notifications.js b/app/javascript/flavours/glitch/actions/notifications.js
index 7b80663f3d..3266df5a59 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 6f2d38b9e9..0000000000
--- 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 55896f4c37..a9abfa0e60 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 fa7af7055e..93bd7dc5f4 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 b171738322..0000000000
--- 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 4e24a3e288..b38fac46fb 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/components/status_content.jsx b/app/javascript/flavours/glitch/components/status_content.jsx
index 184fc65d39..af70ddcb47 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/containers/notification_purge_buttons_container.js b/app/javascript/flavours/glitch/containers/notification_purge_buttons_container.js
deleted file mode 100644
index 144d77f135..0000000000
--- 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 8518ddb2ca..6ec613e209 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 {
-
);
@@ -154,7 +152,6 @@ class Notification extends ImmutablePureComponent {
-
);
@@ -366,7 +363,6 @@ class Notification extends ImmutablePureComponent {
-
);
@@ -406,7 +402,6 @@ class Notification extends ImmutablePureComponent {
-
);
diff --git a/app/javascript/flavours/glitch/features/notifications/components/overlay.jsx b/app/javascript/flavours/glitch/features/notifications/components/overlay.jsx
deleted file mode 100644
index faf7e42e71..0000000000
--- a/app/javascript/flavours/glitch/features/notifications/components/overlay.jsx
+++ /dev/null
@@ -1,61 +0,0 @@
-/**
- * Notification overlay
- */
-
-import PropTypes from 'prop-types';
-
-import { defineMessages, injectIntl } from 'react-intl';
-
-import ImmutablePropTypes from 'react-immutable-proptypes';
-import ImmutablePureComponent from 'react-immutable-pure-component';
-
-import CheckIcon from '@/material-icons/400-24px/check.svg?react';
-import { Icon } from 'flavours/glitch/components/icon';
-
-
-const messages = defineMessages({
- markForDeletion: { id: 'notification.markForDeletion', defaultMessage: 'Mark for deletion' },
-});
-
-class NotificationOverlay extends ImmutablePureComponent {
-
- static propTypes = {
- notification : ImmutablePropTypes.map.isRequired,
- onMarkForDelete : PropTypes.func.isRequired,
- show : PropTypes.bool.isRequired,
- intl : PropTypes.object.isRequired,
- };
-
- onToggleMark = () => {
- const mark = !this.props.notification.get('markedForDelete');
- const id = this.props.notification.get('id');
- this.props.onMarkForDelete(id, mark);
- };
-
- render () {
- const { notification, show, intl } = this.props;
-
- const active = notification.get('markedForDelete');
- const label = intl.formatMessage(messages.markForDeletion);
-
- return show ? (
-
-
-
- {active ? () : ''}
-
-
-
- ) : null;
- }
-
-}
-
-export default injectIntl(NotificationOverlay);
diff --git a/app/javascript/flavours/glitch/features/notifications/containers/column_settings_container.js b/app/javascript/flavours/glitch/features/notifications/containers/column_settings_container.js
index 3e0da03e33..6a933237f9 100644
--- a/app/javascript/flavours/glitch/features/notifications/containers/column_settings_container.js
+++ b/app/javascript/flavours/glitch/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 'flavours/glitch/actions/modal';
-import { initializeNotifications } from 'flavours/glitch/actions/notifications_migration';
+import { fetchNotifications } from 'flavours/glitch/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/flavours/glitch/features/notifications/containers/notification_container.js b/app/javascript/flavours/glitch/features/notifications/containers/notification_container.js
index 8ecea4bc73..269e83b8a0 100644
--- a/app/javascript/flavours/glitch/features/notifications/containers/notification_container.js
+++ b/app/javascript/flavours/glitch/features/notifications/containers/notification_container.js
@@ -2,8 +2,8 @@ import { connect } from 'react-redux';
import { mentionCompose } from '../../../actions/compose';
import {
- toggleReblog,
toggleFavourite,
+ toggleReblog,
} from '../../../actions/interactions';
import { makeGetNotification, makeGetStatus, makeGetReport } from '../../../selectors';
import Notification from '../components/notification';
@@ -19,7 +19,6 @@ const makeMapStateToProps = () => {
notification: notification,
status: notification.get('status') ? getStatus(state, { id: notification.get('status'), contextType: 'notifications' }) : null,
report: notification.get('report') ? getReport(state, notification.get('report'), notification.getIn(['report', 'target_account', 'id'])) : null,
- notifCleaning: state.getIn(['notifications', 'cleaningMode']),
};
};
diff --git a/app/javascript/flavours/glitch/features/notifications/containers/overlay_container.js b/app/javascript/flavours/glitch/features/notifications/containers/overlay_container.js
deleted file mode 100644
index e4b8902a81..0000000000
--- a/app/javascript/flavours/glitch/features/notifications/containers/overlay_container.js
+++ /dev/null
@@ -1,19 +0,0 @@
-// Package imports.
-import { connect } from 'react-redux';
-
-// Our imports.
-import { markNotificationForDelete } from 'flavours/glitch/actions/notifications';
-
-import NotificationOverlay from '../components/overlay';
-
-const mapDispatchToProps = dispatch => ({
- onMarkForDelete(id, yes) {
- dispatch(markNotificationForDelete(id, yes));
- },
-});
-
-const mapStateToProps = state => ({
- show: state.getIn(['notifications', 'cleaningMode']),
-});
-
-export default connect(mapStateToProps, mapDispatchToProps)(NotificationOverlay);
diff --git a/app/javascript/flavours/glitch/features/ui/index.jsx b/app/javascript/flavours/glitch/features/ui/index.jsx
index 96a8fe89a3..db6d6a1fca 100644
--- a/app/javascript/flavours/glitch/features/ui/index.jsx
+++ b/app/javascript/flavours/glitch/features/ui/index.jsx
@@ -14,7 +14,7 @@ import { HotKeys } from 'react-hotkeys';
import { focusApp, unfocusApp, changeLayout } from 'flavours/glitch/actions/app';
import { synchronouslySubmitMarkers, submitMarkers, fetchMarkers } from 'flavours/glitch/actions/markers';
-import { initializeNotifications } from 'flavours/glitch/actions/notifications_migration';
+import { fetchNotifications } from 'flavours/glitch/actions/notification_groups';
import { INTRODUCTION_VERSION } from 'flavours/glitch/actions/onboarding';
import { HoverCardController } from 'flavours/glitch/components/hover_card_controller';
import { Permalink } from 'flavours/glitch/components/permalink';
@@ -26,7 +26,6 @@ import { WithRouterPropTypes } from 'flavours/glitch/utils/react_router';
import { uploadCompose, resetCompose, changeComposeSpoilerness } from '../../actions/compose';
import { clearHeight } from '../../actions/height_cache';
-import { notificationsSetVisibility } from '../../actions/notifications';
import { fetchServer, fetchServerTranslationLanguages } from '../../actions/server';
import { expandHomeTimeline } from '../../actions/timelines';
import initialState, { me, owner, singleUserMode, trendsEnabled, trendsAsLanding, disableHoverCards } from '../../initial_state';
@@ -316,7 +315,6 @@ class UI extends PureComponent {
handleVisibilityChange = () => {
const visibility = !document[this.visibilityHiddenProp];
- this.props.dispatch(notificationsSetVisibility(visibility));
if (visibility) {
this.props.dispatch(focusApp());
this.props.dispatch(submitMarkers({ immediate: true }));
@@ -436,7 +434,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/flavours/glitch/initial_state.js b/app/javascript/flavours/glitch/initial_state.js
index 4777f31e29..7fb25697ae 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');
diff --git a/app/javascript/flavours/glitch/locales/en.json b/app/javascript/flavours/glitch/locales/en.json
index 6384a97187..567bd72459 100644
--- a/app/javascript/flavours/glitch/locales/en.json
+++ b/app/javascript/flavours/glitch/locales/en.json
@@ -51,14 +51,7 @@
"navigation_bar.app_settings": "App settings",
"navigation_bar.keyboard_shortcuts": "Keyboard shortcuts",
"navigation_bar.misc": "Misc",
- "notification.markForDeletion": "Mark for deletion",
- "notification_purge.btn_all": "Select\nall",
- "notification_purge.btn_apply": "Clear\nselected",
- "notification_purge.btn_invert": "Invert\nselection",
- "notification_purge.btn_none": "Select\nnone",
"notifications.column_settings.filter_bar.show_bar": "Show filter bar",
- "notifications.marked_clear": "Clear selected notifications",
- "notifications.marked_clear_confirmation": "Are you sure you want to permanently clear all selected notifications?",
"settings.always_show_spoilers_field": "Always enable the Content Warning field",
"settings.close": "Close",
"settings.compose_box_opts": "Compose box",
diff --git a/app/javascript/flavours/glitch/reducers/notifications.js b/app/javascript/flavours/glitch/reducers/notifications.js
index 8453dd7038..b47e24e397 100644
--- a/app/javascript/flavours/glitch/reducers/notifications.js
+++ b/app/javascript/flavours/glitch/reducers/notifications.js
@@ -1,379 +1,32 @@
-import { fromJS, Map as ImmutableMap, List as ImmutableList } from 'immutable';
-
-import { blockDomainSuccess } from 'flavours/glitch/actions/domain_blocks';
-import { timelineDelete } from 'flavours/glitch/actions/timelines_typed';
+import { fromJS, Map as ImmutableMap } from 'immutable';
import {
- authorizeFollowRequestSuccess,
- blockAccountSuccess,
- muteAccountSuccess,
- rejectFollowRequestSuccess,
-} from '../actions/accounts';
-import {
- fetchMarkers,
-} from '../actions/markers';
-import { clearNotifications } from '../actions/notification_groups';
-import {
- NOTIFICATIONS_MOUNT,
- NOTIFICATIONS_UNMOUNT,
- NOTIFICATIONS_SET_VISIBILITY,
- notificationsUpdate,
- NOTIFICATIONS_EXPAND_SUCCESS,
- NOTIFICATIONS_EXPAND_REQUEST,
- NOTIFICATIONS_EXPAND_FAIL,
- NOTIFICATIONS_FILTER_SET,
- NOTIFICATIONS_SCROLL_TOP,
- NOTIFICATIONS_LOAD_PENDING,
- NOTIFICATIONS_DELETE_MARKED_REQUEST,
- NOTIFICATIONS_DELETE_MARKED_SUCCESS,
- NOTIFICATION_MARK_FOR_DELETE,
- NOTIFICATIONS_DELETE_MARKED_FAIL,
- NOTIFICATIONS_ENTER_CLEARING_MODE,
- NOTIFICATIONS_MARK_ALL_FOR_DELETE,
- 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',
- isLoading: 0,
- cleaningMode: false,
- isTabVisible: true,
browserSupport: false,
browserPermission: 'default',
- // notification removal mark of new notifs loaded whilst cleaningMode is true.
- markNewForDelete: false,
});
-export const notificationToMap = (notification) => ImmutableMap({
+export const notificationToMap = notification => ImmutableMap({
id: notification.id,
type: notification.type,
account: notification.account.id,
- markedForDelete: false,
+ created_at: notification.created_at,
status: notification.status ? notification.status.id : null,
report: notification.report ? fromJS(notification.report) : null,
event: notification.event ? fromJS(notification.event) : null,
moderation_warning: notification.moderation_warning ? fromJS(notification.moderation_warning) : null,
});
-const normalizeNotification = (state, notification, usePendingItems) => {
- const markNewForDelete = state.get('markNewForDelete');
- 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).set('markForDelete', markNewForDelete))).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).set('markForDelete', markNewForDelete));
- });
-};
-
-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 markNewForDelete = state.get('markNewForDelete');
- const lastReadId = state.get('lastReadId');
- const newItems = ImmutableList(notifications.map((notification) => notificationToMap(notification).set('markForDelete', markNewForDelete)));
-
- 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 markForDelete = (state, notificationId, yes) => {
- return state.update('items', list => list.map(item => {
- if (item === null) {
- return null;
- } else if(item.get('id') === notificationId) {
- return item.set('markedForDelete', yes);
- } else {
- return item;
- }
- }));
-};
-
-const markAllForDelete = (state, yes) => {
- return state.update('items', list => list.map(item => {
- if (item === null) {
- return null;
- } else if(yes !== null) {
- return item.set('markedForDelete', yes);
- } else {
- return item.set('markedForDelete', !item.get('markedForDelete'));
- }
- }));
-};
-
-const unmarkAllForDelete = (state) => {
- return state.update('items', list => list.map(item => item === null ? item : item.set('markedForDelete', false)));
-};
-
-const deleteMarkedNotifs = (state) => {
- return state.update('items', list => list.filterNot(item => item === null ? item : item.get('markedForDelete')));
-};
-
-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) {
- let st;
-
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 NOTIFICATIONS_SET_VISIBILITY:
- return updateVisibility(state, action.visibility);
- 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:
- case NOTIFICATIONS_DELETE_MARKED_REQUEST:
- return state.update('isLoading', (nbLoading) => nbLoading + 1);
- case NOTIFICATIONS_DELETE_MARKED_FAIL:
- 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_SET_BROWSER_SUPPORT:
return state.set('browserSupport', action.value);
case NOTIFICATIONS_SET_BROWSER_PERMISSION:
return state.set('browserPermission', action.value);
-
- case NOTIFICATION_MARK_FOR_DELETE:
- return markForDelete(state, action.id, action.yes);
-
- case NOTIFICATIONS_DELETE_MARKED_SUCCESS:
- return deleteMarkedNotifs(state).update('isLoading', (nbLoading) => nbLoading - 1);
-
- case NOTIFICATIONS_ENTER_CLEARING_MODE:
- st = state.set('cleaningMode', action.yes);
- if (!action.yes) {
- return unmarkAllForDelete(st).set('markNewForDelete', false);
- } else {
- return st;
- }
-
- case NOTIFICATIONS_MARK_ALL_FOR_DELETE:
- st = state;
- if (action.yes === null) {
- // Toggle - this is a bit confusing, as it toggles the all-none mode
- //st = st.set('markNewForDelete', !st.get('markNewForDelete'));
- } else {
- st = st.set('markNewForDelete', action.yes);
- }
- return markAllForDelete(st, action.yes);
-
- case NOTIFICATIONS_MARK_AS_READ: {
- const lastNotification = state.get('items').find(item => item !== null);
- return lastNotification ? recountUnread(state, lastNotification.get('id')) : state;
- }
-
default:
return state;
}
diff --git a/app/javascript/mastodon/actions/notifications.js b/app/javascript/mastodon/actions/notifications.js
index 4c6e27cd5f..3266df5a59 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 cd9f5ca3d6..0000000000
--- 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 88d942d45e..3eb1230666 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 30e643363a..478e0cae45 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/components/status_content.jsx b/app/javascript/mastodon/components/status_content.jsx
index 6b06b938de..aa8b060384 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/features/notifications/containers/column_settings_container.js b/app/javascript/mastodon/features/notifications/containers/column_settings_container.js
index 4ac6cfa629..f061060ec7 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 bfc540a331..1ecc52b6bd 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/initial_state.js b/app/javascript/mastodon/initial_state.js
index 0c59fc9ee0..40942eb7ec 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}
*/
diff --git a/app/javascript/mastodon/reducers/notifications.js b/app/javascript/mastodon/reducers/notifications.js
index c99a619b52..b47e24e397 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 { blockDomainSuccess } from 'mastodon/actions/domain_blocks';
-import { timelineDelete } from 'mastodon/actions/timelines_typed';
+import { fromJS, Map as ImmutableMap } from 'immutable';
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:
diff --git a/config/application.rb b/config/application.rb
index 8939ad91f4..92976e87ab 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -59,7 +59,7 @@ Bundler.require(:pam_authentication) if ENV['PAM_ENABLED'] == 'true'
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 e88b020ff3..497ac1325c 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 7960b6e10d..fb0b13954b 100644
--- a/lib/active_record/batches.rb
+++ b/lib/active_record/batches.rb
@@ -13,7 +13,9 @@ module ActiveRecord
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 574d543844..6564f17372 100644
--- a/spec/rails_helper.rb
+++ b/spec/rails_helper.rb
@@ -119,6 +119,11 @@ RSpec.configure do |config|
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