Add support for notices in WebUI

This commit is contained in:
Claire 2023-09-11 17:24:33 +02:00
parent c6710769ca
commit 3f361c2bf2
6 changed files with 123 additions and 2 deletions

View file

@ -0,0 +1,36 @@
import { createAppAsyncThunk } from 'mastodon/store/typed_functions';
import api from '../api';
export interface ApiNoticeActionJSON {
label: string;
url: string;
}
export interface ApiNoticeJSON {
id: string;
title: string;
message: string;
icon?: string;
actions: ApiNoticeActionJSON[];
}
export const fetchNotices = createAppAsyncThunk(
'notices/fetch',
async (_, { getState }) => {
const response = await api(getState).get<ApiNoticeJSON[]>(
'/api/v1/notices',
);
return { notices: response.data };
},
);
export const dismissNotice = createAppAsyncThunk(
'notices/dismiss',
async (args: { id: string }, { getState }) => {
await api(getState).delete<unknown>(`/api/v1/notices/${args.id}`);
return {};
},
);

View file

@ -0,0 +1,61 @@
import { useCallback } from 'react';
import { defineMessages, useIntl } from 'react-intl';
import background from 'mastodon/../images/friends-cropped.png';
import type { ApiNoticeJSON } from 'mastodon/actions/notices';
import { dismissNotice } from 'mastodon/actions/notices';
import { IconButton } from 'mastodon/components/icon_button';
import { useAppDispatch } from 'mastodon/store';
const messages = defineMessages({
dismiss: { id: 'dismissable_banner.dismiss', defaultMessage: 'Dismiss' },
});
interface Props {
notice: ApiNoticeJSON;
}
export const Notice: React.FC<Props> = ({ notice }) => {
const intl = useIntl();
const dispatch = useAppDispatch();
const handleDismiss = useCallback(() => {
void dispatch(dismissNotice({ id: notice.id }));
}, [dispatch, notice.id]);
return (
<div className='dismissable-banner'>
<div className='dismissable-banner__message'>
<img
src={notice.icon ?? background}
alt=''
className='dismissable-banner__background-image'
/>
<h1>{notice.title}</h1>
<p>{notice.message}</p>
<div className='dismissable-banner__message__wrapper'>
<div className='dismissable-banner__message__actions'>
{notice.actions.map((action, i) => (
<a key={`action-${i}`} href={action.url} className='button'>
{action.label}
</a>
))}
</div>
</div>
</div>
<div className='dismissable-banner__action'>
<IconButton
icon='times'
title={intl.formatMessage(messages.dismiss)}
onClick={handleDismiss}
/>
</div>
</div>
);
};

View file

@ -25,6 +25,7 @@ import StatusListContainer from '../ui/containers/status_list_container';
import { ColumnSettings } from './components/column_settings'; import { ColumnSettings } from './components/column_settings';
import { CriticalUpdateBanner } from './components/critical_update_banner'; import { CriticalUpdateBanner } from './components/critical_update_banner';
import { ExplorePrompt } from './components/explore_prompt'; import { ExplorePrompt } from './components/explore_prompt';
import { Notice } from './components/notice';
const messages = defineMessages({ const messages = defineMessages({
title: { id: 'column.home', defaultMessage: 'Home' }, title: { id: 'column.home', defaultMessage: 'Home' },
@ -66,6 +67,7 @@ const mapStateToProps = state => ({
unreadAnnouncements: state.getIn(['announcements', 'items']).count(item => !item.get('read')), unreadAnnouncements: state.getIn(['announcements', 'items']).count(item => !item.get('read')),
showAnnouncements: state.getIn(['announcements', 'show']), showAnnouncements: state.getIn(['announcements', 'show']),
tooSlow: homeTooSlow(state), tooSlow: homeTooSlow(state),
notices: state.get('notices'),
}); });
class HomeTimeline extends PureComponent { class HomeTimeline extends PureComponent {
@ -85,6 +87,7 @@ class HomeTimeline extends PureComponent {
unreadAnnouncements: PropTypes.number, unreadAnnouncements: PropTypes.number,
showAnnouncements: PropTypes.bool, showAnnouncements: PropTypes.bool,
tooSlow: PropTypes.bool, tooSlow: PropTypes.bool,
notices: PropTypes.arrayOf(PropTypes.object),
}; };
handlePin = () => { handlePin = () => {
@ -154,7 +157,7 @@ class HomeTimeline extends PureComponent {
}; };
render () { render () {
const { intl, hasUnread, columnId, multiColumn, tooSlow, hasAnnouncements, unreadAnnouncements, showAnnouncements } = this.props; const { intl, hasUnread, columnId, multiColumn, tooSlow, hasAnnouncements, unreadAnnouncements, showAnnouncements, notices } = this.props;
const pinned = !!columnId; const pinned = !!columnId;
const { signedIn } = this.context.identity; const { signedIn } = this.context.identity;
const banners = []; const banners = [];
@ -179,7 +182,9 @@ class HomeTimeline extends PureComponent {
banners.push(<CriticalUpdateBanner key='critical-update-banner' />); banners.push(<CriticalUpdateBanner key='critical-update-banner' />);
} }
if (tooSlow) { if (notices.length) {
banners.push(<Notice key='notice' notice={notices[0]} />);
} else if (tooSlow) {
banners.push(<ExplorePrompt key='explore-prompt' />); banners.push(<ExplorePrompt key='explore-prompt' />);
} }

View file

@ -13,6 +13,7 @@ import { HotKeys } from 'react-hotkeys';
import { focusApp, unfocusApp, changeLayout } from 'mastodon/actions/app'; import { focusApp, unfocusApp, changeLayout } from 'mastodon/actions/app';
import { synchronouslySubmitMarkers, submitMarkers, fetchMarkers } from 'mastodon/actions/markers'; import { synchronouslySubmitMarkers, submitMarkers, fetchMarkers } from 'mastodon/actions/markers';
import { fetchNotices } from 'mastodon/actions/notices';
import { INTRODUCTION_VERSION } from 'mastodon/actions/onboarding'; import { INTRODUCTION_VERSION } from 'mastodon/actions/onboarding';
import PictureInPicture from 'mastodon/features/picture_in_picture'; import PictureInPicture from 'mastodon/features/picture_in_picture';
import { layoutFromWindow } from 'mastodon/is_mobile'; import { layoutFromWindow } from 'mastodon/is_mobile';
@ -401,6 +402,7 @@ class UI extends PureComponent {
} }
if (signedIn) { if (signedIn) {
this.props.dispatch(fetchNotices());
this.props.dispatch(fetchMarkers()); this.props.dispatch(fetchMarkers());
this.props.dispatch(expandHomeTimeline()); this.props.dispatch(expandHomeTimeline());
this.props.dispatch(expandNotifications()); this.props.dispatch(expandNotifications());

View file

@ -28,6 +28,7 @@ import media_attachments from './media_attachments';
import meta from './meta'; import meta from './meta';
import { modalReducer } from './modal'; import { modalReducer } from './modal';
import mutes from './mutes'; import mutes from './mutes';
import { noticesReducer } from './notices';
import notifications from './notifications'; import notifications from './notifications';
import picture_in_picture from './picture_in_picture'; import picture_in_picture from './picture_in_picture';
import polls from './polls'; import polls from './polls';
@ -86,6 +87,7 @@ const reducers = {
history, history,
tags, tags,
followed_tags, followed_tags,
notices: noticesReducer,
}; };
// We want the root state to be an ImmutableRecord, which is an object with a defined list of keys, // We want the root state to be an ImmutableRecord, which is an object with a defined list of keys,

View file

@ -0,0 +1,15 @@
import { createReducer } from '@reduxjs/toolkit';
import type { ApiNoticeJSON } from '../actions/notices';
import { fetchNotices, dismissNotice } from '../actions/notices';
const initialState: ApiNoticeJSON[] = [];
export const noticesReducer = createReducer(initialState, (builder) => {
builder
.addCase(
fetchNotices.fulfilled,
(_state, { payload: { notices } }) => notices,
)
.addCase(dismissNotice.fulfilled, () => []);
});