forked from glitch-soc-github-mirror/mastodon
Merge commit '94d71c992e0fd88fc3a3fc92b598f139c874ab3f' into glitch-soc/merge-upstream
This commit is contained in:
commit
4ace929129
14 changed files with 171 additions and 121 deletions
app/javascript
mastodon
actions
components
features
models
reducers
selectors
store/middlewares
styles/mastodon
|
@ -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);
|
||||
|
|
105
app/javascript/mastodon/components/alerts_controller.tsx
Normal file
105
app/javascript/mastodon/components/alerts_controller.tsx
Normal 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>
|
||||
);
|
||||
};
|
|
@ -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' />
|
||||
</>
|
||||
|
|
|
@ -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));
|
|
@ -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 />
|
||||
|
|
14
app/javascript/mastodon/models/alert.ts
Normal file
14
app/javascript/mastodon/models/alert.ts
Normal 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;
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
24
app/javascript/mastodon/reducers/alerts.ts
Normal file
24
app/javascript/mastodon/reducers/alerts.ts
Normal 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 [];
|
||||
});
|
||||
});
|
|
@ -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,
|
||||
|
|
|
@ -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]),
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -9732,6 +9732,9 @@ noscript {
|
|||
}
|
||||
|
||||
.notification-bar-action {
|
||||
display: inline-block;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
text-transform: uppercase;
|
||||
margin-inline-start: 10px;
|
||||
cursor: pointer;
|
||||
|
|
|
@ -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",
|
||||
|
|
12
yarn.lock
12
yarn.lock
|
@ -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"
|
||||
|
|
Loading…
Add table
Reference in a new issue