Merge branch 'admin-announcements' into 'develop'
Dashboard: Allow to create announcements See merge request soapbox-pub/soapbox!2276
This commit is contained in:
commit
2cfd402bec
14 changed files with 652 additions and 122 deletions
|
@ -19,6 +19,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||||
- Profile: add RSS link to user profiles.
|
- Profile: add RSS link to user profiles.
|
||||||
- Posts: fix posts filtering.
|
- Posts: fix posts filtering.
|
||||||
- Chats: reset chat message field height after sending a message.
|
- Chats: reset chat message field height after sending a message.
|
||||||
|
- Admin: allow to manage announcements.
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
- Chats: improved display of media attachments.
|
- Chats: improved display of media attachments.
|
||||||
|
|
|
@ -1,13 +1,18 @@
|
||||||
|
import { defineMessages } from 'react-intl';
|
||||||
|
|
||||||
import { fetchRelationships } from 'soapbox/actions/accounts';
|
import { fetchRelationships } from 'soapbox/actions/accounts';
|
||||||
import { importFetchedAccount, importFetchedAccounts, importFetchedStatuses } from 'soapbox/actions/importer';
|
import { importFetchedAccount, importFetchedAccounts, importFetchedStatuses } from 'soapbox/actions/importer';
|
||||||
|
import toast from 'soapbox/toast';
|
||||||
import { filterBadges, getTagDiff } from 'soapbox/utils/badges';
|
import { filterBadges, getTagDiff } from 'soapbox/utils/badges';
|
||||||
import { getFeatures } from 'soapbox/utils/features';
|
import { getFeatures } from 'soapbox/utils/features';
|
||||||
|
|
||||||
import api, { getLinks } from '../api';
|
import api, { getLinks } from '../api';
|
||||||
|
|
||||||
|
import { openModal } from './modals';
|
||||||
|
|
||||||
import type { AxiosResponse } from 'axios';
|
import type { AxiosResponse } from 'axios';
|
||||||
import type { AppDispatch, RootState } from 'soapbox/store';
|
import type { AppDispatch, RootState } from 'soapbox/store';
|
||||||
import type { APIEntity } from 'soapbox/types/entities';
|
import type { APIEntity, Announcement } from 'soapbox/types/entities';
|
||||||
|
|
||||||
const ADMIN_CONFIG_FETCH_REQUEST = 'ADMIN_CONFIG_FETCH_REQUEST';
|
const ADMIN_CONFIG_FETCH_REQUEST = 'ADMIN_CONFIG_FETCH_REQUEST';
|
||||||
const ADMIN_CONFIG_FETCH_SUCCESS = 'ADMIN_CONFIG_FETCH_SUCCESS';
|
const ADMIN_CONFIG_FETCH_SUCCESS = 'ADMIN_CONFIG_FETCH_SUCCESS';
|
||||||
|
@ -77,16 +82,45 @@ const ADMIN_USERS_UNSUGGEST_REQUEST = 'ADMIN_USERS_UNSUGGEST_REQUEST';
|
||||||
const ADMIN_USERS_UNSUGGEST_SUCCESS = 'ADMIN_USERS_UNSUGGEST_SUCCESS';
|
const ADMIN_USERS_UNSUGGEST_SUCCESS = 'ADMIN_USERS_UNSUGGEST_SUCCESS';
|
||||||
const ADMIN_USERS_UNSUGGEST_FAIL = 'ADMIN_USERS_UNSUGGEST_FAIL';
|
const ADMIN_USERS_UNSUGGEST_FAIL = 'ADMIN_USERS_UNSUGGEST_FAIL';
|
||||||
|
|
||||||
const ADMIN_USER_INDEX_EXPAND_FAIL = 'ADMIN_USER_INDEX_EXPAND_FAIL';
|
const ADMIN_USER_INDEX_EXPAND_FAIL = 'ADMIN_USER_INDEX_EXPAND_FAIL';
|
||||||
const ADMIN_USER_INDEX_EXPAND_REQUEST = 'ADMIN_USER_INDEX_EXPAND_REQUEST';
|
const ADMIN_USER_INDEX_EXPAND_REQUEST = 'ADMIN_USER_INDEX_EXPAND_REQUEST';
|
||||||
const ADMIN_USER_INDEX_EXPAND_SUCCESS = 'ADMIN_USER_INDEX_EXPAND_SUCCESS';
|
const ADMIN_USER_INDEX_EXPAND_SUCCESS = 'ADMIN_USER_INDEX_EXPAND_SUCCESS';
|
||||||
|
|
||||||
const ADMIN_USER_INDEX_FETCH_FAIL = 'ADMIN_USER_INDEX_FETCH_FAIL';
|
const ADMIN_USER_INDEX_FETCH_FAIL = 'ADMIN_USER_INDEX_FETCH_FAIL';
|
||||||
const ADMIN_USER_INDEX_FETCH_REQUEST = 'ADMIN_USER_INDEX_FETCH_REQUEST';
|
const ADMIN_USER_INDEX_FETCH_REQUEST = 'ADMIN_USER_INDEX_FETCH_REQUEST';
|
||||||
const ADMIN_USER_INDEX_FETCH_SUCCESS = 'ADMIN_USER_INDEX_FETCH_SUCCESS';
|
const ADMIN_USER_INDEX_FETCH_SUCCESS = 'ADMIN_USER_INDEX_FETCH_SUCCESS';
|
||||||
|
|
||||||
const ADMIN_USER_INDEX_QUERY_SET = 'ADMIN_USER_INDEX_QUERY_SET';
|
const ADMIN_USER_INDEX_QUERY_SET = 'ADMIN_USER_INDEX_QUERY_SET';
|
||||||
|
|
||||||
|
const ADMIN_ANNOUNCEMENTS_FETCH_FAIL = 'ADMIN_ANNOUNCEMENTS_FETCH_FAILS';
|
||||||
|
const ADMIN_ANNOUNCEMENTS_FETCH_REQUEST = 'ADMIN_ANNOUNCEMENTS_FETCH_REQUEST';
|
||||||
|
const ADMIN_ANNOUNCEMENTS_FETCH_SUCCESS = 'ADMIN_ANNOUNCEMENTS_FETCH_SUCCESS';
|
||||||
|
|
||||||
|
const ADMIN_ANNOUNCEMENTS_EXPAND_FAIL = 'ADMIN_ANNOUNCEMENTS_EXPAND_FAILS';
|
||||||
|
const ADMIN_ANNOUNCEMENTS_EXPAND_REQUEST = 'ADMIN_ANNOUNCEMENTS_EXPAND_REQUEST';
|
||||||
|
const ADMIN_ANNOUNCEMENTS_EXPAND_SUCCESS = 'ADMIN_ANNOUNCEMENTS_EXPAND_SUCCESS';
|
||||||
|
|
||||||
|
const ADMIN_ANNOUNCEMENT_CHANGE_CONTENT = 'ADMIN_ANNOUNCEMENT_CHANGE_CONTENT';
|
||||||
|
const ADMIN_ANNOUNCEMENT_CHANGE_START_TIME = 'ADMIN_ANNOUNCEMENT_CHANGE_START_TIME';
|
||||||
|
const ADMIN_ANNOUNCEMENT_CHANGE_END_TIME = 'ADMIN_ANNOUNCEMENT_CHANGE_END_TIME';
|
||||||
|
const ADMIN_ANNOUNCEMENT_CHANGE_ALL_DAY = 'ADMIN_ANNOUNCEMENT_CHANGE_ALL_DAY';
|
||||||
|
|
||||||
|
const ADMIN_ANNOUNCEMENT_CREATE_REQUEST = 'ADMIN_ANNOUNCEMENT_CREATE_REQUEST';
|
||||||
|
const ADMIN_ANNOUNCEMENT_CREATE_SUCCESS = 'ADMIN_ANNOUNCEMENT_CREATE_REQUEST';
|
||||||
|
const ADMIN_ANNOUNCEMENT_CREATE_FAIL = 'ADMIN_ANNOUNCEMENT_CREATE_FAIL';
|
||||||
|
|
||||||
|
const ADMIN_ANNOUNCEMENT_DELETE_REQUEST = 'ADMIN_ANNOUNCEMENT_DELETE_REQUEST';
|
||||||
|
const ADMIN_ANNOUNCEMENT_DELETE_SUCCESS = 'ADMIN_ANNOUNCEMENT_DELETE_REQUEST';
|
||||||
|
const ADMIN_ANNOUNCEMENT_DELETE_FAIL = 'ADMIN_ANNOUNCEMENT_DELETE_FAIL';
|
||||||
|
|
||||||
|
const ADMIN_ANNOUNCEMENT_MODAL_INIT = 'ADMIN_ANNOUNCEMENT_MODAL_INIT';
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
announcementCreateSuccess: { id: 'admin.edit_announcement.created', defaultMessage: 'Announcement created' },
|
||||||
|
announcementDeleteSuccess: { id: 'admin.edit_announcement.deleted', defaultMessage: 'Announcement deleted' },
|
||||||
|
announcementUpdateSuccess: { id: 'admin.edit_announcement.updated', defaultMessage: 'Announcement edited' },
|
||||||
|
});
|
||||||
|
|
||||||
const nicknamesFromIds = (getState: () => RootState, ids: string[]) => ids.map(id => getState().accounts.get(id)!.acct);
|
const nicknamesFromIds = (getState: () => RootState, ids: string[]) => ids.map(id => getState().accounts.get(id)!.acct);
|
||||||
|
|
||||||
const fetchConfig = () =>
|
const fetchConfig = () =>
|
||||||
|
@ -598,6 +632,93 @@ const expandUserIndex = () =>
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const fetchAdminAnnouncements = () =>
|
||||||
|
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||||
|
dispatch({ type: ADMIN_ANNOUNCEMENTS_FETCH_REQUEST });
|
||||||
|
return api(getState)
|
||||||
|
.get('/api/pleroma/admin/announcements', { params: { limit: 50 } })
|
||||||
|
.then(({ data }) => {
|
||||||
|
dispatch({ type: ADMIN_ANNOUNCEMENTS_FETCH_SUCCESS, announcements: data });
|
||||||
|
return data;
|
||||||
|
}).catch(error => {
|
||||||
|
dispatch({ type: ADMIN_ANNOUNCEMENTS_FETCH_FAIL, error });
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const expandAdminAnnouncements = () =>
|
||||||
|
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||||
|
const page = getState().admin_announcements.page;
|
||||||
|
|
||||||
|
dispatch({ type: ADMIN_ANNOUNCEMENTS_EXPAND_REQUEST });
|
||||||
|
return api(getState)
|
||||||
|
.get('/api/pleroma/admin/announcements', { params: { limit: 50, offset: page * 50 } })
|
||||||
|
.then(({ data }) => {
|
||||||
|
dispatch({ type: ADMIN_ANNOUNCEMENTS_EXPAND_SUCCESS, announcements: data });
|
||||||
|
return data;
|
||||||
|
}).catch(error => {
|
||||||
|
dispatch({ type: ADMIN_ANNOUNCEMENTS_EXPAND_FAIL, error });
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const changeAnnouncementContent = (content: string) => ({
|
||||||
|
type: ADMIN_ANNOUNCEMENT_CHANGE_CONTENT,
|
||||||
|
value: content,
|
||||||
|
});
|
||||||
|
|
||||||
|
const changeAnnouncementStartTime = (time: Date | null) => ({
|
||||||
|
type: ADMIN_ANNOUNCEMENT_CHANGE_START_TIME,
|
||||||
|
value: time,
|
||||||
|
});
|
||||||
|
|
||||||
|
const changeAnnouncementEndTime = (time: Date | null) => ({
|
||||||
|
type: ADMIN_ANNOUNCEMENT_CHANGE_END_TIME,
|
||||||
|
value: time,
|
||||||
|
});
|
||||||
|
|
||||||
|
const changeAnnouncementAllDay = (allDay: boolean) => ({
|
||||||
|
type: ADMIN_ANNOUNCEMENT_CHANGE_ALL_DAY,
|
||||||
|
value: allDay,
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleCreateAnnouncement = () =>
|
||||||
|
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||||
|
dispatch({ type: ADMIN_ANNOUNCEMENT_CREATE_REQUEST });
|
||||||
|
|
||||||
|
const { id, content, starts_at, ends_at, all_day } = getState().admin_announcements.form;
|
||||||
|
|
||||||
|
return api(getState)[id ? 'patch' : 'post'](
|
||||||
|
id ? `/api/pleroma/admin/announcements/${id}` : '/api/pleroma/admin/announcements',
|
||||||
|
{ content, starts_at, ends_at, all_day },
|
||||||
|
).then(({ data }) => {
|
||||||
|
dispatch({ type: ADMIN_ANNOUNCEMENT_CREATE_SUCCESS, announcement: data });
|
||||||
|
toast.success(id ? messages.announcementUpdateSuccess : messages.announcementCreateSuccess);
|
||||||
|
dispatch(fetchAdminAnnouncements());
|
||||||
|
return data;
|
||||||
|
}).catch(error => {
|
||||||
|
dispatch({ type: ADMIN_ANNOUNCEMENT_CREATE_FAIL, error });
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteAnnouncement = (id: string) =>
|
||||||
|
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||||
|
dispatch({ type: ADMIN_ANNOUNCEMENT_DELETE_REQUEST, id });
|
||||||
|
|
||||||
|
return api(getState).delete(`/api/pleroma/admin/announcements/${id}`).then(({ data }) => {
|
||||||
|
dispatch({ type: ADMIN_ANNOUNCEMENT_DELETE_SUCCESS, id });
|
||||||
|
toast.success(messages.announcementDeleteSuccess);
|
||||||
|
dispatch(fetchAdminAnnouncements());
|
||||||
|
return data;
|
||||||
|
}).catch(error => {
|
||||||
|
dispatch({ type: ADMIN_ANNOUNCEMENT_DELETE_FAIL, id, error });
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const initAnnouncementModal = (announcement?: Announcement) =>
|
||||||
|
(dispatch: AppDispatch) => {
|
||||||
|
dispatch({ type: ADMIN_ANNOUNCEMENT_MODAL_INIT, announcement });
|
||||||
|
dispatch(openModal('EDIT_ANNOUNCEMENT'));
|
||||||
|
};
|
||||||
|
|
||||||
export {
|
export {
|
||||||
ADMIN_CONFIG_FETCH_REQUEST,
|
ADMIN_CONFIG_FETCH_REQUEST,
|
||||||
ADMIN_CONFIG_FETCH_SUCCESS,
|
ADMIN_CONFIG_FETCH_SUCCESS,
|
||||||
|
@ -657,6 +778,23 @@ export {
|
||||||
ADMIN_USER_INDEX_FETCH_REQUEST,
|
ADMIN_USER_INDEX_FETCH_REQUEST,
|
||||||
ADMIN_USER_INDEX_FETCH_SUCCESS,
|
ADMIN_USER_INDEX_FETCH_SUCCESS,
|
||||||
ADMIN_USER_INDEX_QUERY_SET,
|
ADMIN_USER_INDEX_QUERY_SET,
|
||||||
|
ADMIN_ANNOUNCEMENTS_FETCH_FAIL,
|
||||||
|
ADMIN_ANNOUNCEMENTS_FETCH_REQUEST,
|
||||||
|
ADMIN_ANNOUNCEMENTS_FETCH_SUCCESS,
|
||||||
|
ADMIN_ANNOUNCEMENTS_EXPAND_FAIL,
|
||||||
|
ADMIN_ANNOUNCEMENTS_EXPAND_REQUEST,
|
||||||
|
ADMIN_ANNOUNCEMENTS_EXPAND_SUCCESS,
|
||||||
|
ADMIN_ANNOUNCEMENT_CHANGE_CONTENT,
|
||||||
|
ADMIN_ANNOUNCEMENT_CHANGE_START_TIME,
|
||||||
|
ADMIN_ANNOUNCEMENT_CHANGE_END_TIME,
|
||||||
|
ADMIN_ANNOUNCEMENT_CHANGE_ALL_DAY,
|
||||||
|
ADMIN_ANNOUNCEMENT_CREATE_FAIL,
|
||||||
|
ADMIN_ANNOUNCEMENT_CREATE_REQUEST,
|
||||||
|
ADMIN_ANNOUNCEMENT_CREATE_SUCCESS,
|
||||||
|
ADMIN_ANNOUNCEMENT_DELETE_FAIL,
|
||||||
|
ADMIN_ANNOUNCEMENT_DELETE_REQUEST,
|
||||||
|
ADMIN_ANNOUNCEMENT_DELETE_SUCCESS,
|
||||||
|
ADMIN_ANNOUNCEMENT_MODAL_INIT,
|
||||||
fetchConfig,
|
fetchConfig,
|
||||||
updateConfig,
|
updateConfig,
|
||||||
updateSoapboxConfig,
|
updateSoapboxConfig,
|
||||||
|
@ -686,4 +824,13 @@ export {
|
||||||
setUserIndexQuery,
|
setUserIndexQuery,
|
||||||
fetchUserIndex,
|
fetchUserIndex,
|
||||||
expandUserIndex,
|
expandUserIndex,
|
||||||
|
fetchAdminAnnouncements,
|
||||||
|
expandAdminAnnouncements,
|
||||||
|
changeAnnouncementContent,
|
||||||
|
changeAnnouncementStartTime,
|
||||||
|
changeAnnouncementEndTime,
|
||||||
|
changeAnnouncementAllDay,
|
||||||
|
handleCreateAnnouncement,
|
||||||
|
deleteAnnouncement,
|
||||||
|
initAnnouncementModal,
|
||||||
};
|
};
|
||||||
|
|
128
app/soapbox/features/admin/announcements.tsx
Normal file
128
app/soapbox/features/admin/announcements.tsx
Normal file
|
@ -0,0 +1,128 @@
|
||||||
|
import React, { useEffect } from 'react';
|
||||||
|
import { FormattedDate, FormattedMessage, defineMessages, useIntl } from 'react-intl';
|
||||||
|
|
||||||
|
import { deleteAnnouncement, fetchAdminAnnouncements, initAnnouncementModal } from 'soapbox/actions/admin';
|
||||||
|
import { openModal } from 'soapbox/actions/modals';
|
||||||
|
import ScrollableList from 'soapbox/components/scrollable-list';
|
||||||
|
import { Button, Column, HStack, Stack, Text } from 'soapbox/components/ui';
|
||||||
|
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
|
||||||
|
import { Announcement as AnnouncementEntity } from 'soapbox/types/entities';
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
heading: { id: 'column.admin.announcements', defaultMessage: 'Announcements' },
|
||||||
|
deleteConfirm: { id: 'confirmations.admin.delete_announcement.confirm', defaultMessage: 'Delete' },
|
||||||
|
deleteHeading: { id: 'confirmations.admin.delete_announcement.heading', defaultMessage: 'Delete announcement' },
|
||||||
|
deleteMessage: { id: 'confirmations.admin.delete_announcement.message', defaultMessage: 'Are you sure you want to delete the announcement?' },
|
||||||
|
});
|
||||||
|
|
||||||
|
interface IAnnouncement {
|
||||||
|
announcement: AnnouncementEntity
|
||||||
|
}
|
||||||
|
|
||||||
|
const Announcement: React.FC<IAnnouncement> = ({ announcement }) => {
|
||||||
|
const intl = useIntl();
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
|
const handleEditAnnouncement = (announcement: AnnouncementEntity) => () => {
|
||||||
|
dispatch(initAnnouncementModal(announcement));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeleteAnnouncement = (id: string) => () => {
|
||||||
|
dispatch(openModal('CONFIRM', {
|
||||||
|
heading: intl.formatMessage(messages.deleteHeading),
|
||||||
|
message: intl.formatMessage(messages.deleteMessage),
|
||||||
|
confirm: intl.formatMessage(messages.deleteConfirm),
|
||||||
|
onConfirm: () => dispatch(deleteAnnouncement(id)),
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={announcement.id} className='rounded-lg bg-gray-100 p-4 dark:bg-primary-800'>
|
||||||
|
<Stack space={2}>
|
||||||
|
<Text dangerouslySetInnerHTML={{ __html: announcement.contentHtml }} />
|
||||||
|
{(announcement.starts_at || announcement.ends_at || announcement.all_day) && (
|
||||||
|
<HStack space={2} wrap>
|
||||||
|
{announcement.starts_at && (
|
||||||
|
<Text size='sm'>
|
||||||
|
<Text tag='span' size='sm' weight='medium'>
|
||||||
|
<FormattedMessage id='admin.announcements.starts_at' defaultMessage='Starts at:' />
|
||||||
|
</Text>
|
||||||
|
{' '}
|
||||||
|
<FormattedDate value={announcement.starts_at} year='2-digit' month='short' day='2-digit' weekday='short' />
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
{announcement.ends_at && (
|
||||||
|
<Text size='sm'>
|
||||||
|
<Text tag='span' size='sm' weight='medium'>
|
||||||
|
<FormattedMessage id='admin.announcements.ends_at' defaultMessage='Ends at:' />
|
||||||
|
</Text>
|
||||||
|
{' '}
|
||||||
|
<FormattedDate value={announcement.ends_at} year='2-digit' month='short' day='2-digit' weekday='short' />
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
{announcement.all_day && (
|
||||||
|
<Text weight='medium' size='sm'>
|
||||||
|
<FormattedMessage id='admin.announcements.all_day' defaultMessage='All day' />
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</HStack>
|
||||||
|
)}
|
||||||
|
<HStack justifyContent='end' space={2}>
|
||||||
|
<Button theme='primary' onClick={handleEditAnnouncement(announcement)}>
|
||||||
|
<FormattedMessage id='admin.announcements.edit' defaultMessage='Edit' />
|
||||||
|
</Button>
|
||||||
|
<Button theme='primary' onClick={handleDeleteAnnouncement(announcement.id)}>
|
||||||
|
<FormattedMessage id='admin.announcements.delete' defaultMessage='Delete' />
|
||||||
|
</Button>
|
||||||
|
</HStack>
|
||||||
|
</Stack>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const Announcements: React.FC = () => {
|
||||||
|
const intl = useIntl();
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
|
const announcements = useAppSelector((state) => state.admin_announcements.items);
|
||||||
|
const isLoading = useAppSelector((state) => state.admin_announcements.isLoading);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
dispatch(fetchAdminAnnouncements());
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleCreateAnnouncement = () => {
|
||||||
|
dispatch(initAnnouncementModal());
|
||||||
|
};
|
||||||
|
|
||||||
|
const emptyMessage = <FormattedMessage id='empty_column.admin.announcements' defaultMessage='There are no announcements yet.' />;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Column label={intl.formatMessage(messages.heading)}>
|
||||||
|
<Stack className='gap-4'>
|
||||||
|
<Button
|
||||||
|
className='sm:w-fit sm:self-end'
|
||||||
|
icon={require('@tabler/icons/plus.svg')}
|
||||||
|
onClick={handleCreateAnnouncement}
|
||||||
|
theme='secondary'
|
||||||
|
block
|
||||||
|
>
|
||||||
|
<FormattedMessage id='admin.announcements.action' defaultMessage='Create announcement' />
|
||||||
|
</Button>
|
||||||
|
<ScrollableList
|
||||||
|
scrollKey='announcements'
|
||||||
|
emptyMessage={emptyMessage}
|
||||||
|
itemClassName='py-3 first:pt-0 last:pb-0'
|
||||||
|
isLoading={isLoading}
|
||||||
|
showLoading={isLoading && !announcements.count()}
|
||||||
|
>
|
||||||
|
{announcements.map((announcement) => (
|
||||||
|
<Announcement key={announcement.id} announcement={announcement} />
|
||||||
|
))}
|
||||||
|
</ScrollableList>
|
||||||
|
</Stack>
|
||||||
|
</Column>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Announcements;
|
|
@ -43,6 +43,7 @@ const Dashboard: React.FC = () => {
|
||||||
|
|
||||||
const navigateToSoapboxConfig = () => history.push('/soapbox/config');
|
const navigateToSoapboxConfig = () => history.push('/soapbox/config');
|
||||||
const navigateToModerationLog = () => history.push('/soapbox/admin/log');
|
const navigateToModerationLog = () => history.push('/soapbox/admin/log');
|
||||||
|
const navigateToAnnouncements = () => history.push('/soapbox/admin/announcements');
|
||||||
|
|
||||||
const v = parseVersion(instance.version);
|
const v = parseVersion(instance.version);
|
||||||
|
|
||||||
|
@ -95,6 +96,13 @@ const Dashboard: React.FC = () => {
|
||||||
onClick={navigateToModerationLog}
|
onClick={navigateToModerationLog}
|
||||||
label={<FormattedMessage id='column.admin.moderation_log' defaultMessage='Moderation Log' />}
|
label={<FormattedMessage id='column.admin.moderation_log' defaultMessage='Moderation Log' />}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{features.announcements && (
|
||||||
|
<ListItem
|
||||||
|
onClick={navigateToAnnouncements}
|
||||||
|
label={<FormattedMessage id='column.admin.announcements' defaultMessage='Announcements' />}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</List>
|
</List>
|
||||||
|
|
||||||
{account.admin && (
|
{account.admin && (
|
||||||
|
|
|
@ -67,4 +67,4 @@ const ChatTextarea: React.FC<IChatTextarea> = ({
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ChatTextarea;
|
export default ChatTextarea;
|
||||||
|
|
|
@ -2,41 +2,42 @@ import React from 'react';
|
||||||
|
|
||||||
import Base from 'soapbox/components/modal-root';
|
import Base from 'soapbox/components/modal-root';
|
||||||
import {
|
import {
|
||||||
MediaModal,
|
|
||||||
VideoModal,
|
|
||||||
BoostModal,
|
|
||||||
ConfirmationModal,
|
|
||||||
MuteModal,
|
|
||||||
ReportModal,
|
|
||||||
EmbedModal,
|
|
||||||
CryptoDonateModal,
|
|
||||||
ListEditor,
|
|
||||||
ListAdder,
|
|
||||||
MissingDescriptionModal,
|
|
||||||
ActionsModal,
|
|
||||||
HotkeysModal,
|
|
||||||
ComposeModal,
|
|
||||||
ReplyMentionsModal,
|
|
||||||
UnauthorizedModal,
|
|
||||||
EditFederationModal,
|
|
||||||
ComponentModal,
|
|
||||||
ReactionsModal,
|
|
||||||
FavouritesModal,
|
|
||||||
ReblogsModal,
|
|
||||||
MentionsModal,
|
|
||||||
LandingPageModal,
|
|
||||||
BirthdaysModal,
|
|
||||||
AccountNoteModal,
|
|
||||||
CompareHistoryModal,
|
|
||||||
VerifySmsModal,
|
|
||||||
FamiliarFollowersModal,
|
|
||||||
ComposeEventModal,
|
|
||||||
JoinEventModal,
|
|
||||||
AccountModerationModal,
|
AccountModerationModal,
|
||||||
|
AccountNoteModal,
|
||||||
|
ActionsModal,
|
||||||
|
BirthdaysModal,
|
||||||
|
BoostModal,
|
||||||
|
CompareHistoryModal,
|
||||||
|
ComponentModal,
|
||||||
|
ComposeEventModal,
|
||||||
|
ComposeModal,
|
||||||
|
ConfirmationModal,
|
||||||
|
CryptoDonateModal,
|
||||||
|
EditAnnouncementModal,
|
||||||
|
EditFederationModal,
|
||||||
|
EmbedModal,
|
||||||
EventMapModal,
|
EventMapModal,
|
||||||
EventParticipantsModal,
|
EventParticipantsModal,
|
||||||
PolicyModal,
|
FamiliarFollowersModal,
|
||||||
|
FavouritesModal,
|
||||||
|
HotkeysModal,
|
||||||
|
JoinEventModal,
|
||||||
|
LandingPageModal,
|
||||||
|
ListAdder,
|
||||||
|
ListEditor,
|
||||||
ManageGroupModal,
|
ManageGroupModal,
|
||||||
|
MediaModal,
|
||||||
|
MentionsModal,
|
||||||
|
MissingDescriptionModal,
|
||||||
|
MuteModal,
|
||||||
|
PolicyModal,
|
||||||
|
ReactionsModal,
|
||||||
|
ReblogsModal,
|
||||||
|
ReplyMentionsModal,
|
||||||
|
ReportModal,
|
||||||
|
UnauthorizedModal,
|
||||||
|
VerifySmsModal,
|
||||||
|
VideoModal,
|
||||||
} from 'soapbox/features/ui/util/async-components';
|
} from 'soapbox/features/ui/util/async-components';
|
||||||
|
|
||||||
import BundleContainer from '../containers/bundle-container';
|
import BundleContainer from '../containers/bundle-container';
|
||||||
|
@ -45,42 +46,44 @@ import { BundleProps } from './bundle';
|
||||||
import BundleModalError from './bundle-modal-error';
|
import BundleModalError from './bundle-modal-error';
|
||||||
import ModalLoading from './modal-loading';
|
import ModalLoading from './modal-loading';
|
||||||
|
|
||||||
|
/* eslint sort-keys: "error" */
|
||||||
const MODAL_COMPONENTS = {
|
const MODAL_COMPONENTS = {
|
||||||
'MEDIA': MediaModal,
|
|
||||||
'VIDEO': VideoModal,
|
|
||||||
'BOOST': BoostModal,
|
|
||||||
'CONFIRM': ConfirmationModal,
|
|
||||||
'MISSING_DESCRIPTION': MissingDescriptionModal,
|
|
||||||
'MUTE': MuteModal,
|
|
||||||
'REPORT': ReportModal,
|
|
||||||
'ACTIONS': ActionsModal,
|
|
||||||
'EMBED': EmbedModal,
|
|
||||||
'LIST_EDITOR': ListEditor,
|
|
||||||
'LIST_ADDER': ListAdder,
|
|
||||||
'HOTKEYS': HotkeysModal,
|
|
||||||
'COMPOSE': ComposeModal,
|
|
||||||
'REPLY_MENTIONS': ReplyMentionsModal,
|
|
||||||
'UNAUTHORIZED': UnauthorizedModal,
|
|
||||||
'CRYPTO_DONATE': CryptoDonateModal,
|
|
||||||
'EDIT_FEDERATION': EditFederationModal,
|
|
||||||
'COMPONENT': ComponentModal,
|
|
||||||
'REBLOGS': ReblogsModal,
|
|
||||||
'FAVOURITES': FavouritesModal,
|
|
||||||
'REACTIONS': ReactionsModal,
|
|
||||||
'MENTIONS': MentionsModal,
|
|
||||||
'LANDING_PAGE': LandingPageModal,
|
|
||||||
'BIRTHDAYS': BirthdaysModal,
|
|
||||||
'ACCOUNT_NOTE': AccountNoteModal,
|
|
||||||
'COMPARE_HISTORY': CompareHistoryModal,
|
|
||||||
'VERIFY_SMS': VerifySmsModal,
|
|
||||||
'FAMILIAR_FOLLOWERS': FamiliarFollowersModal,
|
|
||||||
'COMPOSE_EVENT': ComposeEventModal,
|
|
||||||
'JOIN_EVENT': JoinEventModal,
|
|
||||||
'ACCOUNT_MODERATION': AccountModerationModal,
|
'ACCOUNT_MODERATION': AccountModerationModal,
|
||||||
|
'ACCOUNT_NOTE': AccountNoteModal,
|
||||||
|
'ACTIONS': ActionsModal,
|
||||||
|
'BIRTHDAYS': BirthdaysModal,
|
||||||
|
'BOOST': BoostModal,
|
||||||
|
'COMPARE_HISTORY': CompareHistoryModal,
|
||||||
|
'COMPONENT': ComponentModal,
|
||||||
|
'COMPOSE': ComposeModal,
|
||||||
|
'COMPOSE_EVENT': ComposeEventModal,
|
||||||
|
'CONFIRM': ConfirmationModal,
|
||||||
|
'CRYPTO_DONATE': CryptoDonateModal,
|
||||||
|
'EDIT_ANNOUNCEMENT': EditAnnouncementModal,
|
||||||
|
'EDIT_FEDERATION': EditFederationModal,
|
||||||
|
'EMBED': EmbedModal,
|
||||||
'EVENT_MAP': EventMapModal,
|
'EVENT_MAP': EventMapModal,
|
||||||
'EVENT_PARTICIPANTS': EventParticipantsModal,
|
'EVENT_PARTICIPANTS': EventParticipantsModal,
|
||||||
'POLICY': PolicyModal,
|
'FAMILIAR_FOLLOWERS': FamiliarFollowersModal,
|
||||||
|
'FAVOURITES': FavouritesModal,
|
||||||
|
'HOTKEYS': HotkeysModal,
|
||||||
|
'JOIN_EVENT': JoinEventModal,
|
||||||
|
'LANDING_PAGE': LandingPageModal,
|
||||||
|
'LIST_ADDER': ListAdder,
|
||||||
|
'LIST_EDITOR': ListEditor,
|
||||||
'MANAGE_GROUP': ManageGroupModal,
|
'MANAGE_GROUP': ManageGroupModal,
|
||||||
|
'MEDIA': MediaModal,
|
||||||
|
'MENTIONS': MentionsModal,
|
||||||
|
'MISSING_DESCRIPTION': MissingDescriptionModal,
|
||||||
|
'MUTE': MuteModal,
|
||||||
|
'POLICY': PolicyModal,
|
||||||
|
'REACTIONS': ReactionsModal,
|
||||||
|
'REBLOGS': ReblogsModal,
|
||||||
|
'REPLY_MENTIONS': ReplyMentionsModal,
|
||||||
|
'REPORT': ReportModal,
|
||||||
|
'UNAUTHORIZED': UnauthorizedModal,
|
||||||
|
'VERIFY_SMS': VerifySmsModal,
|
||||||
|
'VIDEO': VideoModal,
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ModalType = keyof typeof MODAL_COMPONENTS | null;
|
export type ModalType = keyof typeof MODAL_COMPONENTS | null;
|
||||||
|
|
|
@ -0,0 +1,119 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { FormattedMessage, defineMessages, useIntl } from 'react-intl';
|
||||||
|
|
||||||
|
import { changeAnnouncementAllDay, changeAnnouncementContent, changeAnnouncementEndTime, changeAnnouncementStartTime, handleCreateAnnouncement } from 'soapbox/actions/admin';
|
||||||
|
import { closeModal } from 'soapbox/actions/modals';
|
||||||
|
import { Form, FormGroup, HStack, Modal, Stack, Text, Textarea, Toggle } from 'soapbox/components/ui';
|
||||||
|
import BundleContainer from 'soapbox/features/ui/containers/bundle-container';
|
||||||
|
import { DatePicker } from 'soapbox/features/ui/util/async-components';
|
||||||
|
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
save: { id: 'admin.edit_announcement.save', defaultMessage: 'Save' },
|
||||||
|
announcementContentPlaceholder: { id: 'admin.edit_announcement.fields.content_placeholder', defaultMessage: 'Announcement content' },
|
||||||
|
announcementStartTimePlaceholder: { id: 'admin.edit_announcement.fields.start_time_placeholder', defaultMessage: 'Announcement starts on…' },
|
||||||
|
announcementEndTimePlaceholder: { id: 'admin.edit_announcement.fields.end_time_placeholder', defaultMessage: 'Announcement ends on…' },
|
||||||
|
});
|
||||||
|
|
||||||
|
interface IEditAnnouncementModal {
|
||||||
|
onClose: (type?: string) => void,
|
||||||
|
}
|
||||||
|
|
||||||
|
const EditAnnouncementModal: React.FC<IEditAnnouncementModal> = ({ onClose }) => {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const intl = useIntl();
|
||||||
|
|
||||||
|
const id = useAppSelector((state) => state.admin_announcements.form.id);
|
||||||
|
const content = useAppSelector((state) => state.admin_announcements.form.content);
|
||||||
|
const startTime = useAppSelector((state) => state.admin_announcements.form.starts_at);
|
||||||
|
const endTime = useAppSelector((state) => state.admin_announcements.form.ends_at);
|
||||||
|
const allDay = useAppSelector((state) => state.admin_announcements.form.all_day);
|
||||||
|
|
||||||
|
const onChangeContent: React.ChangeEventHandler<HTMLTextAreaElement> = ({ target }) =>
|
||||||
|
dispatch(changeAnnouncementContent(target.value));
|
||||||
|
|
||||||
|
const onChangeStartTime = (date: Date | null) => dispatch(changeAnnouncementStartTime(date));
|
||||||
|
|
||||||
|
const onChangeEndTime = (date: Date | null) => dispatch(changeAnnouncementEndTime(date));
|
||||||
|
|
||||||
|
const onChangeAllDay: React.ChangeEventHandler<HTMLInputElement> = ({ target }) => dispatch(changeAnnouncementAllDay(target.checked));
|
||||||
|
|
||||||
|
const onClickClose = () => {
|
||||||
|
onClose('EDIT_ANNOUNCEMENT');
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = () => dispatch(handleCreateAnnouncement()).then(() => dispatch(closeModal('EDIT_ANNOUNCEMENT')));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
onClose={onClickClose}
|
||||||
|
title={id
|
||||||
|
? <FormattedMessage id='column.admin.edit_announcement' defaultMessage='Edit announcement' />
|
||||||
|
: <FormattedMessage id='column.admin.create_announcement' defaultMessage='Create announcement' />}
|
||||||
|
confirmationAction={handleSubmit}
|
||||||
|
confirmationText={intl.formatMessage(messages.save)}
|
||||||
|
>
|
||||||
|
<Form>
|
||||||
|
<FormGroup
|
||||||
|
labelText={<FormattedMessage id='admin.edit_announcement.fields.content_label' defaultMessage='Content' />}
|
||||||
|
>
|
||||||
|
<Textarea
|
||||||
|
autoComplete='off'
|
||||||
|
placeholder={intl.formatMessage(messages.announcementContentPlaceholder)}
|
||||||
|
value={content}
|
||||||
|
onChange={onChangeContent}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
<FormGroup
|
||||||
|
labelText={<FormattedMessage id='admin.edit_announcement.fields.start_time_label' defaultMessage='Start date' />}
|
||||||
|
>
|
||||||
|
<BundleContainer fetchComponent={DatePicker}>
|
||||||
|
{Component => (<Component
|
||||||
|
showTimeSelect
|
||||||
|
dateFormat='MMMM d, yyyy h:mm aa'
|
||||||
|
timeIntervals={15}
|
||||||
|
wrapperClassName='react-datepicker-wrapper'
|
||||||
|
placeholderText={intl.formatMessage(messages.announcementStartTimePlaceholder)}
|
||||||
|
selected={startTime}
|
||||||
|
onChange={onChangeStartTime}
|
||||||
|
isClearable
|
||||||
|
/>)}
|
||||||
|
</BundleContainer>
|
||||||
|
</FormGroup>
|
||||||
|
<FormGroup
|
||||||
|
labelText={<FormattedMessage id='admin.edit_announcement.fields.end_time_label' defaultMessage='End date' />}
|
||||||
|
>
|
||||||
|
<BundleContainer fetchComponent={DatePicker}>
|
||||||
|
{Component => (<Component
|
||||||
|
showTimeSelect
|
||||||
|
dateFormat='MMMM d, yyyy h:mm aa'
|
||||||
|
timeIntervals={15}
|
||||||
|
wrapperClassName='react-datepicker-wrapper'
|
||||||
|
placeholderText={intl.formatMessage(messages.announcementEndTimePlaceholder)}
|
||||||
|
selected={endTime}
|
||||||
|
onChange={onChangeEndTime}
|
||||||
|
isClearable
|
||||||
|
/>)}
|
||||||
|
</BundleContainer>
|
||||||
|
</FormGroup>
|
||||||
|
<HStack alignItems='center' space={2}>
|
||||||
|
<Toggle
|
||||||
|
icons={false}
|
||||||
|
checked={allDay}
|
||||||
|
onChange={onChangeAllDay}
|
||||||
|
/>
|
||||||
|
<Stack>
|
||||||
|
<Text tag='span' theme='muted'>
|
||||||
|
<FormattedMessage id='admin.edit_announcement.fields.all_day_label' defaultMessage='All-day event' />
|
||||||
|
</Text>
|
||||||
|
<Text size='xs' tag='span' theme='muted'>
|
||||||
|
<FormattedMessage id='admin.edit_announcement.fields.all_day_hint' defaultMessage='When checked, only the dates of the time range will be displayed' />
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
</HStack>
|
||||||
|
</Form>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EditAnnouncementModal;
|
|
@ -39,7 +39,7 @@ const ManageGroupModal: React.FC<IManageGroupModal> = ({ onClose }) => {
|
||||||
const [currentStep, setCurrentStep] = useState<Steps>(id ? Steps.TWO : Steps.ONE);
|
const [currentStep, setCurrentStep] = useState<Steps>(id ? Steps.TWO : Steps.ONE);
|
||||||
|
|
||||||
const onClickClose = () => {
|
const onClickClose = () => {
|
||||||
onClose('manage_group');
|
onClose('MANAGE_GROUP');
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmit = () => {
|
const handleSubmit = () => {
|
||||||
|
|
|
@ -120,6 +120,7 @@ import {
|
||||||
ManageGroup,
|
ManageGroup,
|
||||||
GroupBlockedMembers,
|
GroupBlockedMembers,
|
||||||
GroupMembershipRequests,
|
GroupMembershipRequests,
|
||||||
|
Announcements,
|
||||||
} from './util/async-components';
|
} from './util/async-components';
|
||||||
import { WrappedRoute } from './util/react-router-helpers';
|
import { WrappedRoute } from './util/react-router-helpers';
|
||||||
|
|
||||||
|
@ -311,6 +312,7 @@ const SwitchingColumnsArea: React.FC<ISwitchingColumnsArea> = ({ children }) =>
|
||||||
<WrappedRoute path='/soapbox/admin/log' staffOnly page={AdminPage} component={ModerationLog} content={children} exact />
|
<WrappedRoute path='/soapbox/admin/log' staffOnly page={AdminPage} component={ModerationLog} content={children} exact />
|
||||||
<WrappedRoute path='/soapbox/admin/users' staffOnly page={AdminPage} component={UserIndex} content={children} exact />
|
<WrappedRoute path='/soapbox/admin/users' staffOnly page={AdminPage} component={UserIndex} content={children} exact />
|
||||||
<WrappedRoute path='/soapbox/admin/theme' staffOnly page={AdminPage} component={ThemeEditor} content={children} exact />
|
<WrappedRoute path='/soapbox/admin/theme' staffOnly page={AdminPage} component={ThemeEditor} content={children} exact />
|
||||||
|
<WrappedRoute path='/soapbox/admin/announcements' staffOnly page={AdminPage} component={Announcements} content={children} exact />
|
||||||
<WrappedRoute path='/info' page={EmptyPage} component={ServerInfo} content={children} />
|
<WrappedRoute path='/info' page={EmptyPage} component={ServerInfo} content={children} />
|
||||||
|
|
||||||
<WrappedRoute path='/developers/apps/create' developerOnly page={DefaultPage} component={CreateApp} content={children} />
|
<WrappedRoute path='/developers/apps/create' developerOnly page={DefaultPage} component={CreateApp} content={children} />
|
||||||
|
|
|
@ -577,3 +577,11 @@ export function GroupMediaPanel() {
|
||||||
export function NewEventPanel() {
|
export function NewEventPanel() {
|
||||||
return import(/* webpackChunkName: "features/events" */'../components/panels/new-event-panel');
|
return import(/* webpackChunkName: "features/events" */'../components/panels/new-event-panel');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function Announcements() {
|
||||||
|
return import(/* webpackChunkName: "features/admin/announcements" */'../../admin/announcements');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function EditAnnouncementModal() {
|
||||||
|
return import(/* webpackChunkName: "features/admin/announcements" */'../components/modals/edit-announcement-modal');
|
||||||
|
}
|
||||||
|
|
|
@ -85,6 +85,12 @@
|
||||||
"account_search.placeholder": "Search for an account",
|
"account_search.placeholder": "Search for an account",
|
||||||
"actualStatus.edited": "Edited {date}",
|
"actualStatus.edited": "Edited {date}",
|
||||||
"actualStatuses.quote_tombstone": "Post is unavailable.",
|
"actualStatuses.quote_tombstone": "Post is unavailable.",
|
||||||
|
"admin.announcements.action": "Create announcement",
|
||||||
|
"admin.announcements.all_day": "All day",
|
||||||
|
"admin.announcements.delete": "Delete",
|
||||||
|
"admin.announcements.edit": "Edit",
|
||||||
|
"admin.announcements.ends_at": "Ends at:",
|
||||||
|
"admin.announcements.starts_at": "Starts at:",
|
||||||
"admin.awaiting_approval.approved_message": "{acct} was approved!",
|
"admin.awaiting_approval.approved_message": "{acct} was approved!",
|
||||||
"admin.awaiting_approval.empty_message": "There is nobody waiting for approval. When a new user signs up, you can review them here.",
|
"admin.awaiting_approval.empty_message": "There is nobody waiting for approval. When a new user signs up, you can review them here.",
|
||||||
"admin.awaiting_approval.rejected_message": "{acct} was rejected.",
|
"admin.awaiting_approval.rejected_message": "{acct} was rejected.",
|
||||||
|
@ -103,6 +109,18 @@
|
||||||
"admin.dashcounters.user_count_label": "total users",
|
"admin.dashcounters.user_count_label": "total users",
|
||||||
"admin.dashwidgets.email_list_header": "Email list",
|
"admin.dashwidgets.email_list_header": "Email list",
|
||||||
"admin.dashwidgets.software_header": "Software",
|
"admin.dashwidgets.software_header": "Software",
|
||||||
|
"admin.edit_announcement.created": "Announcement created",
|
||||||
|
"admin.edit_announcement.deleted": "Announcement deleted",
|
||||||
|
"admin.edit_announcement.fields.all_day_hint": "When checked, only the dates of the time range will be displayed",
|
||||||
|
"admin.edit_announcement.fields.all_day_label": "All-day event",
|
||||||
|
"admin.edit_announcement.fields.content_label": "Content",
|
||||||
|
"admin.edit_announcement.fields.content_placeholder": "Announcement content",
|
||||||
|
"admin.edit_announcement.fields.end_time_label": "End date",
|
||||||
|
"admin.edit_announcement.fields.end_time_placeholder": "Announcement ends on:",
|
||||||
|
"admin.edit_announcement.fields.start_time_label": "Start date",
|
||||||
|
"admin.edit_announcement.fields.start_time_placeholder": "Announcement starts on:",
|
||||||
|
"admin.edit_announcement.save": "Save",
|
||||||
|
"admin.edit_announcement.updated": "Announcement edited",
|
||||||
"admin.latest_accounts_panel.more": "Click to see {count, plural, one {# account} other {# accounts}}",
|
"admin.latest_accounts_panel.more": "Click to see {count, plural, one {# account} other {# accounts}}",
|
||||||
"admin.latest_accounts_panel.title": "Latest Accounts",
|
"admin.latest_accounts_panel.title": "Latest Accounts",
|
||||||
"admin.moderation_log.empty_message": "You have not performed any moderation actions yet. When you do, a history will be shown here.",
|
"admin.moderation_log.empty_message": "You have not performed any moderation actions yet. When you do, a history will be shown here.",
|
||||||
|
@ -265,8 +283,11 @@
|
||||||
"chats.main.blankslate_with_chats.subtitle": "Select from one of your open chats or create a new message.",
|
"chats.main.blankslate_with_chats.subtitle": "Select from one of your open chats or create a new message.",
|
||||||
"chats.main.blankslate_with_chats.title": "Select a chat",
|
"chats.main.blankslate_with_chats.title": "Select a chat",
|
||||||
"chats.search_placeholder": "Start a chat with…",
|
"chats.search_placeholder": "Start a chat with…",
|
||||||
|
"column.admin.announcements": "Announcements",
|
||||||
"column.admin.awaiting_approval": "Awaiting Approval",
|
"column.admin.awaiting_approval": "Awaiting Approval",
|
||||||
|
"column.admin.create_announcement": "Create announcement",
|
||||||
"column.admin.dashboard": "Dashboard",
|
"column.admin.dashboard": "Dashboard",
|
||||||
|
"column.admin.edit_announcement": "Edit announcement",
|
||||||
"column.admin.moderation_log": "Moderation Log",
|
"column.admin.moderation_log": "Moderation Log",
|
||||||
"column.admin.reports": "Reports",
|
"column.admin.reports": "Reports",
|
||||||
"column.admin.reports.menu.moderation_log": "Moderation Log",
|
"column.admin.reports.menu.moderation_log": "Moderation Log",
|
||||||
|
@ -416,6 +437,9 @@
|
||||||
"confirmations.admin.deactivate_user.confirm": "Deactivate @{name}",
|
"confirmations.admin.deactivate_user.confirm": "Deactivate @{name}",
|
||||||
"confirmations.admin.deactivate_user.heading": "Deactivate @{acct}",
|
"confirmations.admin.deactivate_user.heading": "Deactivate @{acct}",
|
||||||
"confirmations.admin.deactivate_user.message": "You are about to deactivate @{acct}. Deactivating a user is a reversible action.",
|
"confirmations.admin.deactivate_user.message": "You are about to deactivate @{acct}. Deactivating a user is a reversible action.",
|
||||||
|
"confirmations.admin.delete_announcement.confirm": "Delete",
|
||||||
|
"confirmations.admin.delete_announcement.heading": "Delete announcement",
|
||||||
|
"confirmations.admin.delete_announcement.message": "Are you sure you want to delete the announcement?",
|
||||||
"confirmations.admin.delete_local_user.checkbox": "I understand that I am about to delete a local user.",
|
"confirmations.admin.delete_local_user.checkbox": "I understand that I am about to delete a local user.",
|
||||||
"confirmations.admin.delete_status.confirm": "Delete post",
|
"confirmations.admin.delete_status.confirm": "Delete post",
|
||||||
"confirmations.admin.delete_status.heading": "Delete post",
|
"confirmations.admin.delete_status.heading": "Delete post",
|
||||||
|
@ -613,6 +637,7 @@
|
||||||
"empty_column.account_favourited_statuses": "This user doesn't have any liked posts yet.",
|
"empty_column.account_favourited_statuses": "This user doesn't have any liked posts yet.",
|
||||||
"empty_column.account_timeline": "No posts here!",
|
"empty_column.account_timeline": "No posts here!",
|
||||||
"empty_column.account_unavailable": "Profile unavailable",
|
"empty_column.account_unavailable": "Profile unavailable",
|
||||||
|
"empty_column.admin.announcements": "There are no announcements yet.",
|
||||||
"empty_column.aliases": "You haven't created any account alias yet.",
|
"empty_column.aliases": "You haven't created any account alias yet.",
|
||||||
"empty_column.aliases.suggestions": "There are no account suggestions available for the provided term.",
|
"empty_column.aliases.suggestions": "There are no account suggestions available for the provided term.",
|
||||||
"empty_column.blocks": "You haven't blocked any users yet.",
|
"empty_column.blocks": "You haven't blocked any users yet.",
|
||||||
|
|
|
@ -35,6 +35,8 @@ export const AnnouncementRecord = ImmutableRecord({
|
||||||
emojis: ImmutableList<Emoji>(),
|
emojis: ImmutableList<Emoji>(),
|
||||||
updated_at: Date,
|
updated_at: Date,
|
||||||
|
|
||||||
|
pleroma: ImmutableMap<string, any>(),
|
||||||
|
|
||||||
// Internal fields
|
// Internal fields
|
||||||
contentHtml: '',
|
contentHtml: '',
|
||||||
});
|
});
|
||||||
|
|
85
app/soapbox/reducers/admin-announcements.ts
Normal file
85
app/soapbox/reducers/admin-announcements.ts
Normal file
|
@ -0,0 +1,85 @@
|
||||||
|
import { List as ImmutableList, Record as ImmutableRecord } from 'immutable';
|
||||||
|
|
||||||
|
import {
|
||||||
|
ADMIN_ANNOUNCEMENT_CHANGE_ALL_DAY,
|
||||||
|
ADMIN_ANNOUNCEMENT_CHANGE_CONTENT,
|
||||||
|
ADMIN_ANNOUNCEMENT_CHANGE_END_TIME,
|
||||||
|
ADMIN_ANNOUNCEMENT_CHANGE_START_TIME,
|
||||||
|
ADMIN_ANNOUNCEMENT_CREATE_FAIL,
|
||||||
|
ADMIN_ANNOUNCEMENT_CREATE_REQUEST,
|
||||||
|
ADMIN_ANNOUNCEMENT_CREATE_SUCCESS,
|
||||||
|
ADMIN_ANNOUNCEMENT_DELETE_SUCCESS,
|
||||||
|
ADMIN_ANNOUNCEMENT_MODAL_INIT,
|
||||||
|
ADMIN_ANNOUNCEMENTS_FETCH_FAIL,
|
||||||
|
ADMIN_ANNOUNCEMENTS_FETCH_REQUEST,
|
||||||
|
ADMIN_ANNOUNCEMENTS_FETCH_SUCCESS,
|
||||||
|
} from 'soapbox/actions/admin';
|
||||||
|
import { normalizeAnnouncement } from 'soapbox/normalizers';
|
||||||
|
|
||||||
|
import type { AnyAction } from 'redux';
|
||||||
|
import type { Announcement, APIEntity } from 'soapbox/types/entities';
|
||||||
|
|
||||||
|
const AnnouncementFormRecord = ImmutableRecord({
|
||||||
|
id: null as string | null,
|
||||||
|
content: '',
|
||||||
|
starts_at: null as Date | null,
|
||||||
|
ends_at: null as Date | null,
|
||||||
|
all_day: false,
|
||||||
|
is_submitting: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const ReducerRecord = ImmutableRecord({
|
||||||
|
items: ImmutableList<Announcement>(),
|
||||||
|
isLoading: false,
|
||||||
|
page: -1,
|
||||||
|
form: AnnouncementFormRecord(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default function adminAnnouncementsReducer(state = ReducerRecord(), action: AnyAction) {
|
||||||
|
switch (action.type) {
|
||||||
|
case ADMIN_ANNOUNCEMENTS_FETCH_REQUEST:
|
||||||
|
return state.set('isLoading', true);
|
||||||
|
case ADMIN_ANNOUNCEMENTS_FETCH_SUCCESS:
|
||||||
|
return state.withMutations(map => {
|
||||||
|
const items = ImmutableList<Announcement>((action.announcements).map((announcement: APIEntity) => normalizeAnnouncement(announcement)));
|
||||||
|
|
||||||
|
map.set('items', items);
|
||||||
|
map.set('isLoading', false);
|
||||||
|
});
|
||||||
|
case ADMIN_ANNOUNCEMENTS_FETCH_FAIL:
|
||||||
|
return state.set('isLoading', false);
|
||||||
|
case ADMIN_ANNOUNCEMENT_DELETE_SUCCESS:
|
||||||
|
return state.update('items', list => {
|
||||||
|
const idx = list.findIndex(x => x.id === action.id);
|
||||||
|
|
||||||
|
if (idx > -1) {
|
||||||
|
return list.delete(idx);
|
||||||
|
}
|
||||||
|
|
||||||
|
return list;
|
||||||
|
});
|
||||||
|
case ADMIN_ANNOUNCEMENT_CHANGE_CONTENT:
|
||||||
|
return state.setIn(['form', 'content'], action.value);
|
||||||
|
case ADMIN_ANNOUNCEMENT_CHANGE_START_TIME:
|
||||||
|
return state.setIn(['form', 'starts_at'], action.value);
|
||||||
|
case ADMIN_ANNOUNCEMENT_CHANGE_END_TIME:
|
||||||
|
return state.setIn(['form', 'ends_at'], action.value);
|
||||||
|
case ADMIN_ANNOUNCEMENT_CHANGE_ALL_DAY:
|
||||||
|
return state.setIn(['form', 'all_day'], action.value);
|
||||||
|
case ADMIN_ANNOUNCEMENT_CREATE_REQUEST:
|
||||||
|
return state.setIn(['form', 'is_submitting'], true);
|
||||||
|
case ADMIN_ANNOUNCEMENT_CREATE_SUCCESS:
|
||||||
|
case ADMIN_ANNOUNCEMENT_CREATE_FAIL:
|
||||||
|
return state.setIn(['form', 'is_submitting'], true);
|
||||||
|
case ADMIN_ANNOUNCEMENT_MODAL_INIT:
|
||||||
|
return state.set('form', action.announcement ? AnnouncementFormRecord({
|
||||||
|
id: action.announcement.id,
|
||||||
|
content: action.announcement.content,
|
||||||
|
starts_at: action.announcement.starts_at ? new Date(action.announcement.starts_at) : null,
|
||||||
|
ends_at: action.announcement.ends_at ? new Date(action.announcement.ends_at) : null,
|
||||||
|
all_day: action.announcement.all_day,
|
||||||
|
}) : AnnouncementFormRecord());
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
|
@ -9,6 +9,7 @@ import accounts from './accounts';
|
||||||
import accounts_counters from './accounts-counters';
|
import accounts_counters from './accounts-counters';
|
||||||
import accounts_meta from './accounts-meta';
|
import accounts_meta from './accounts-meta';
|
||||||
import admin from './admin';
|
import admin from './admin';
|
||||||
|
import admin_announcements from './admin-announcements';
|
||||||
import admin_log from './admin-log';
|
import admin_log from './admin-log';
|
||||||
import admin_user_index from './admin-user-index';
|
import admin_user_index from './admin-user-index';
|
||||||
import aliases from './aliases';
|
import aliases from './aliases';
|
||||||
|
@ -67,67 +68,68 @@ import user_lists from './user-lists';
|
||||||
import verification from './verification';
|
import verification from './verification';
|
||||||
|
|
||||||
const reducers = {
|
const reducers = {
|
||||||
dropdown_menu,
|
|
||||||
timelines,
|
|
||||||
meta,
|
|
||||||
modals,
|
|
||||||
user_lists,
|
|
||||||
domain_lists,
|
|
||||||
status_lists,
|
|
||||||
account_notes,
|
account_notes,
|
||||||
accounts,
|
accounts,
|
||||||
accounts_counters,
|
accounts_counters,
|
||||||
statuses,
|
|
||||||
relationships,
|
|
||||||
settings,
|
|
||||||
push_notifications,
|
|
||||||
mutes,
|
|
||||||
reports,
|
|
||||||
contexts,
|
|
||||||
compose,
|
|
||||||
search,
|
|
||||||
notifications,
|
|
||||||
custom_emojis,
|
|
||||||
lists,
|
|
||||||
listEditor,
|
|
||||||
listAdder,
|
|
||||||
locations,
|
|
||||||
filters,
|
|
||||||
conversations,
|
|
||||||
suggestions,
|
|
||||||
polls,
|
|
||||||
trends,
|
|
||||||
sidebar,
|
|
||||||
patron,
|
|
||||||
soapbox,
|
|
||||||
instance,
|
|
||||||
me,
|
|
||||||
auth,
|
|
||||||
admin,
|
|
||||||
chats,
|
|
||||||
chat_messages,
|
|
||||||
chat_message_lists,
|
|
||||||
profile_hover_card,
|
|
||||||
status_hover_card,
|
|
||||||
backups,
|
|
||||||
admin_log,
|
|
||||||
security,
|
|
||||||
scheduled_statuses,
|
|
||||||
pending_statuses,
|
|
||||||
aliases,
|
|
||||||
accounts_meta,
|
accounts_meta,
|
||||||
trending_statuses,
|
admin,
|
||||||
verification,
|
admin_announcements,
|
||||||
onboarding,
|
admin_log,
|
||||||
rules,
|
|
||||||
history,
|
|
||||||
announcements,
|
|
||||||
compose_event,
|
|
||||||
admin_user_index,
|
admin_user_index,
|
||||||
groups,
|
aliases,
|
||||||
group_relationships,
|
announcements,
|
||||||
group_memberships,
|
auth,
|
||||||
|
backups,
|
||||||
|
chat_message_lists,
|
||||||
|
chat_messages,
|
||||||
|
chats,
|
||||||
|
compose,
|
||||||
|
compose_event,
|
||||||
|
contexts,
|
||||||
|
conversations,
|
||||||
|
custom_emojis,
|
||||||
|
domain_lists,
|
||||||
|
dropdown_menu,
|
||||||
|
filters,
|
||||||
group_editor,
|
group_editor,
|
||||||
|
group_memberships,
|
||||||
|
group_relationships,
|
||||||
|
groups,
|
||||||
|
history,
|
||||||
|
instance,
|
||||||
|
listAdder,
|
||||||
|
listEditor,
|
||||||
|
lists,
|
||||||
|
locations,
|
||||||
|
me,
|
||||||
|
meta,
|
||||||
|
modals,
|
||||||
|
mutes,
|
||||||
|
notifications,
|
||||||
|
onboarding,
|
||||||
|
patron,
|
||||||
|
pending_statuses,
|
||||||
|
polls,
|
||||||
|
profile_hover_card,
|
||||||
|
push_notifications,
|
||||||
|
relationships,
|
||||||
|
reports,
|
||||||
|
rules,
|
||||||
|
scheduled_statuses,
|
||||||
|
search,
|
||||||
|
security,
|
||||||
|
settings,
|
||||||
|
sidebar,
|
||||||
|
soapbox,
|
||||||
|
status_hover_card,
|
||||||
|
status_lists,
|
||||||
|
statuses,
|
||||||
|
suggestions,
|
||||||
|
timelines,
|
||||||
|
trending_statuses,
|
||||||
|
trends,
|
||||||
|
user_lists,
|
||||||
|
verification,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Build a default state from all reducers: it has the key and `undefined`
|
// Build a default state from all reducers: it has the key and `undefined`
|
||||||
|
|
Loading…
Reference in a new issue