mirror of
https://github.com/mastodon/mastodon.git
synced 2024-11-21 21:57:19 +00:00
Add support for notices in WebUI
This commit is contained in:
parent
c6710769ca
commit
3f361c2bf2
36
app/javascript/mastodon/actions/notices.ts
Normal file
36
app/javascript/mastodon/actions/notices.ts
Normal 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 {};
|
||||||
|
},
|
||||||
|
);
|
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
|
@ -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' />);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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());
|
||||||
|
|
|
@ -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,
|
||||||
|
|
15
app/javascript/mastodon/reducers/notices.ts
Normal file
15
app/javascript/mastodon/reducers/notices.ts
Normal 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, () => []);
|
||||||
|
});
|
Loading…
Reference in a new issue