Merge commit '94d71c992e0fd88fc3a3fc92b598f139c874ab3f' into glitch-soc/merge-upstream

This commit is contained in:
Claire 2025-03-25 21:12:37 +01:00
commit 4ace929129
14 changed files with 171 additions and 121 deletions
app/javascript
mastodon
actions
components
features
standalone/compose
ui
models
reducers
selectors
store/middlewares
styles/mastodon
package.jsonyarn.lock

View file

@ -1,14 +1,11 @@
import { defineMessages } from 'react-intl';
import type { MessageDescriptor } from 'react-intl';
import { createAction } from '@reduxjs/toolkit';
import { AxiosError } from 'axios';
import type { AxiosResponse } from 'axios';
interface Alert {
title: string | MessageDescriptor;
message: string | MessageDescriptor;
values?: Record<string, string | number | Date>;
}
import type { Alert } from 'mastodon/models/alert';
interface ApiErrorResponse {
error?: string;
@ -30,24 +27,13 @@ const messages = defineMessages({
},
});
export const ALERT_SHOW = 'ALERT_SHOW';
export const ALERT_DISMISS = 'ALERT_DISMISS';
export const ALERT_CLEAR = 'ALERT_CLEAR';
export const ALERT_NOOP = 'ALERT_NOOP';
export const dismissAlert = createAction<{ key: number }>('alerts/dismiss');
export const dismissAlert = (alert: Alert) => ({
type: ALERT_DISMISS,
alert,
});
export const clearAlerts = createAction('alerts/clear');
export const clearAlert = () => ({
type: ALERT_CLEAR,
});
export const showAlert = createAction<Omit<Alert, 'key'>>('alerts/show');
export const showAlert = (alert: Alert) => ({
type: ALERT_SHOW,
alert,
});
const ignoreAlert = createAction('alerts/ignore');
export const showAlertForError = (error: unknown, skipNotFound = false) => {
if (error instanceof AxiosError && error.response) {
@ -56,7 +42,7 @@ export const showAlertForError = (error: unknown, skipNotFound = false) => {
// Skip these errors as they are reflected in the UI
if (skipNotFound && (status === 404 || status === 410)) {
return { type: ALERT_NOOP };
return ignoreAlert();
}
// Rate limit errors
@ -76,9 +62,9 @@ export const showAlertForError = (error: unknown, skipNotFound = false) => {
});
}
// An aborted request, e.g. due to reloading the browser window, it not really error
// An aborted request, e.g. due to reloading the browser window, is not really an error
if (error instanceof AxiosError && error.code === AxiosError.ECONNABORTED) {
return { type: ALERT_NOOP };
return ignoreAlert();
}
console.error(error);

View file

@ -0,0 +1,105 @@
import { useState, useEffect } from 'react';
import { useIntl } from 'react-intl';
import type { IntlShape } from 'react-intl';
import classNames from 'classnames';
import { dismissAlert } from 'mastodon/actions/alerts';
import type {
Alert,
TranslatableString,
TranslatableValues,
} from 'mastodon/models/alert';
import { useAppSelector, useAppDispatch } from 'mastodon/store';
const formatIfNeeded = (
intl: IntlShape,
message: TranslatableString,
values?: TranslatableValues,
) => {
if (typeof message === 'object') {
return intl.formatMessage(message, values);
}
return message;
};
const Alert: React.FC<{
alert: Alert;
dismissAfter: number;
}> = ({
alert: { key, title, message, values, action, onClick },
dismissAfter,
}) => {
const dispatch = useAppDispatch();
const intl = useIntl();
const [active, setActive] = useState(false);
useEffect(() => {
const setActiveTimeout = setTimeout(() => {
setActive(true);
}, 1);
return () => {
clearTimeout(setActiveTimeout);
};
}, []);
useEffect(() => {
const dismissTimeout = setTimeout(() => {
setActive(false);
// Allow CSS transition to finish before removing from the DOM
setTimeout(() => {
dispatch(dismissAlert({ key }));
}, 500);
}, dismissAfter);
return () => {
clearTimeout(dismissTimeout);
};
}, [dispatch, setActive, key, dismissAfter]);
return (
<div
className={classNames('notification-bar', {
'notification-bar-active': active,
})}
>
<div className='notification-bar-wrapper'>
{title && (
<span className='notification-bar-title'>
{formatIfNeeded(intl, title, values)}
</span>
)}
<span className='notification-bar-message'>
{formatIfNeeded(intl, message, values)}
</span>
{action && (
<button className='notification-bar-action' onClick={onClick}>
{formatIfNeeded(intl, action, values)}
</button>
)}
</div>
</div>
);
};
export const AlertsController: React.FC = () => {
const alerts = useAppSelector((state) => state.alerts);
if (alerts.length === 0) {
return null;
}
return (
<div className='notification-list'>
{alerts.map((alert, idx) => (
<Alert key={alert.key} alert={alert} dismissAfter={5000 + idx * 1000} />
))}
</div>
);
};

View file

@ -1,12 +1,12 @@
import { AlertsController } from 'mastodon/components/alerts_controller';
import ComposeFormContainer from 'mastodon/features/compose/containers/compose_form_container';
import LoadingBarContainer from 'mastodon/features/ui/containers/loading_bar_container';
import ModalContainer from 'mastodon/features/ui/containers/modal_container';
import NotificationsContainer from 'mastodon/features/ui/containers/notifications_container';
const Compose = () => (
<>
<ComposeFormContainer autoFocus withoutNavigation />
<NotificationsContainer />
<AlertsController />
<ModalContainer />
<LoadingBarContainer className='loading-bar' />
</>

View file

@ -1,20 +0,0 @@
import { injectIntl } from 'react-intl';
import { connect } from 'react-redux';
import { NotificationStack } from 'react-notification';
import { dismissAlert } from 'mastodon/actions/alerts';
import { getAlerts } from 'mastodon/selectors';
const mapStateToProps = (state, { intl }) => ({
notifications: getAlerts(state, { intl }),
});
const mapDispatchToProps = (dispatch) => ({
onDismiss (alert) {
dispatch(dismissAlert(alert));
},
});
export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(NotificationStack));

View file

@ -15,6 +15,7 @@ import { focusApp, unfocusApp, changeLayout } from 'mastodon/actions/app';
import { synchronouslySubmitMarkers, submitMarkers, fetchMarkers } from 'mastodon/actions/markers';
import { fetchNotifications } from 'mastodon/actions/notification_groups';
import { INTRODUCTION_VERSION } from 'mastodon/actions/onboarding';
import { AlertsController } from 'mastodon/components/alerts_controller';
import { HoverCardController } from 'mastodon/components/hover_card_controller';
import { PictureInPicture } from 'mastodon/features/picture_in_picture';
import { identityContextPropShape, withIdentity } from 'mastodon/identity_context';
@ -33,7 +34,6 @@ import UploadArea from './components/upload_area';
import ColumnsAreaContainer from './containers/columns_area_container';
import LoadingBarContainer from './containers/loading_bar_container';
import ModalContainer from './containers/modal_container';
import NotificationsContainer from './containers/notifications_container';
import {
Compose,
Status,
@ -607,7 +607,7 @@ class UI extends PureComponent {
</SwitchingColumnsArea>
{layout !== 'mobile' && <PictureInPicture />}
<NotificationsContainer />
<AlertsController />
{!disableHoverCards && <HoverCardController />}
<LoadingBarContainer className='loading-bar' />
<ModalContainer />

View file

@ -0,0 +1,14 @@
import type { MessageDescriptor } from 'react-intl';
export type TranslatableString = string | MessageDescriptor;
export type TranslatableValues = Record<string, string | number | Date>;
export interface Alert {
key: number;
title?: TranslatableString;
message: TranslatableString;
action?: TranslatableString;
values?: TranslatableValues;
onClick?: () => void;
}

View file

@ -1,30 +0,0 @@
import { List as ImmutableList } from 'immutable';
import {
ALERT_SHOW,
ALERT_DISMISS,
ALERT_CLEAR,
} from '../actions/alerts';
const initialState = ImmutableList([]);
let id = 0;
const addAlert = (state, alert) =>
state.push({
key: id++,
...alert,
});
export default function alerts(state = initialState, action) {
switch(action.type) {
case ALERT_SHOW:
return addAlert(state, action.alert);
case ALERT_DISMISS:
return state.filterNot(item => item.key === action.alert.key);
case ALERT_CLEAR:
return state.clear();
default:
return state;
}
}

View file

@ -0,0 +1,24 @@
import { createReducer } from '@reduxjs/toolkit';
import { showAlert, dismissAlert, clearAlerts } from 'mastodon/actions/alerts';
import type { Alert } from 'mastodon/models/alert';
const initialState: Alert[] = [];
let id = 0;
export const alertsReducer = createReducer(initialState, (builder) => {
builder
.addCase(showAlert, (state, { payload }) => {
state.push({
key: id++,
...payload,
});
})
.addCase(dismissAlert, (state, { payload: { key } }) => {
return state.filter((item) => item.key !== key);
})
.addCase(clearAlerts, () => {
return [];
});
});

View file

@ -5,7 +5,7 @@ import { combineReducers } from 'redux-immutable';
import { accountsReducer } from './accounts';
import accounts_map from './accounts_map';
import alerts from './alerts';
import { alertsReducer } from './alerts';
import announcements from './announcements';
import { composeReducer } from './compose';
import contexts from './contexts';
@ -45,7 +45,7 @@ const reducers = {
dropdownMenu: dropdownMenuReducer,
timelines,
meta,
alerts,
alerts: alertsReducer,
loadingBar: loadingBarReducer,
modal: modalReducer,
user_lists,

View file

@ -60,28 +60,6 @@ export const makeGetPictureInPicture = () => {
}));
};
const ALERT_DEFAULTS = {
dismissAfter: 5000,
style: false,
};
const formatIfNeeded = (intl, message, values) => {
if (typeof message === 'object') {
return intl.formatMessage(message, values);
}
return message;
};
export const getAlerts = createSelector([state => state.get('alerts'), (_, { intl }) => intl], (alerts, intl) =>
alerts.map(item => ({
...ALERT_DEFAULTS,
...item,
action: formatIfNeeded(intl, item.action, item.values),
title: formatIfNeeded(intl, item.title, item.values),
message: formatIfNeeded(intl, item.message, item.values),
})).toArray());
export const makeGetNotification = () => createSelector([
(_, base) => base,
(state, _, accountId) => state.getIn(['accounts', accountId]),

View file

@ -12,19 +12,21 @@ import type { AsyncThunkRejectValue } from '../typed_functions';
const defaultFailSuffix = 'FAIL';
const isFailedAction = new RegExp(`${defaultFailSuffix}$`, 'g');
interface ActionWithMaybeAlertParams extends Action, AsyncThunkRejectValue {}
interface RejectedAction extends Action {
payload: AsyncThunkRejectValue;
}
interface ActionWithMaybeAlertParams extends Action, AsyncThunkRejectValue {
payload?: AsyncThunkRejectValue;
}
function isRejectedActionWithPayload(
action: unknown,
): action is RejectedAction {
return isAsyncThunkAction(action) && isRejectedWithValue(action);
}
function isActionWithmaybeAlertParams(
function isActionWithMaybeAlertParams(
action: unknown,
): action is ActionWithMaybeAlertParams {
return isAction(action);
@ -40,11 +42,12 @@ export const errorsMiddleware: Middleware<{}, RootState> =
showAlertForError(action.payload.error, action.payload.skipNotFound),
);
} else if (
isActionWithmaybeAlertParams(action) &&
!action.skipAlert &&
isActionWithMaybeAlertParams(action) &&
!(action.payload?.skipAlert || action.skipAlert) &&
action.type.match(isFailedAction)
) {
dispatch(showAlertForError(action.error, action.skipNotFound));
const { error, skipNotFound } = action.payload ?? action;
dispatch(showAlertForError(error, skipNotFound));
}
return next(action);

View file

@ -9732,6 +9732,9 @@ noscript {
}
.notification-bar-action {
display: inline-block;
border: 0;
background: transparent;
text-transform: uppercase;
margin-inline-start: 10px;
cursor: pointer;

View file

@ -108,7 +108,6 @@
"react-immutable-pure-component": "^2.2.2",
"react-intl": "^7.0.0",
"react-motion": "^0.5.2",
"react-notification": "^6.8.5",
"react-overlays": "^5.2.1",
"react-redux": "^9.0.4",
"react-redux-loading-bar": "^5.0.8",

View file

@ -2854,7 +2854,6 @@ __metadata:
react-immutable-pure-component: "npm:^2.2.2"
react-intl: "npm:^7.0.0"
react-motion: "npm:^0.5.2"
react-notification: "npm:^6.8.5"
react-overlays: "npm:^5.2.1"
react-redux: "npm:^9.0.4"
react-redux-loading-bar: "npm:^5.0.8"
@ -14815,17 +14814,6 @@ __metadata:
languageName: node
linkType: hard
"react-notification@npm:^6.8.5":
version: 6.8.5
resolution: "react-notification@npm:6.8.5"
dependencies:
prop-types: "npm:^15.6.2"
peerDependencies:
react: ^0.14.0 || ^15.0.0 || ^16.0.0
checksum: 10c0/14ffb71a5b18301830699b814d1de2421f4f43f31df5b95efd95cd47548a0d7597ec58abc16a12191958cad398495eba9274193af3294863e2864d32ea79f2c6
languageName: node
linkType: hard
"react-overlays@npm:^5.2.1":
version: 5.2.1
resolution: "react-overlays@npm:5.2.1"