Minor improvements, add actions

Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
environments/review-events-5jp5it/deployments/1372
marcin mikołajczak 2022-10-06 00:01:26 +02:00
rodzic d1ab8c7cb6
commit 3c8c8048e5
6 zmienionych plików z 338 dodań i 166 usunięć

Wyświetl plik

@ -21,9 +21,10 @@ const messages = defineMessages({
interface IEventPreview {
status: StatusEntity,
className?: string,
hideAction?: boolean;
}
const EventPreview: React.FC<IEventPreview> = ({ status, className }) => {
const EventPreview: React.FC<IEventPreview> = ({ status, className, hideAction }) => {
const intl = useIntl();
const me = useAppSelector((state) => state.me);
@ -36,7 +37,7 @@ const EventPreview: React.FC<IEventPreview> = ({ status, className }) => {
return (
<div className={classNames('rounded-lg bg-gray-100 dark:bg-primary-800 relative overflow-hidden', className)}>
<div className='absolute top-28 right-3'>
{account.id === me ? (
{!hideAction && (account.id === me ? (
<Button
size='sm'
theme='secondary'
@ -44,7 +45,7 @@ const EventPreview: React.FC<IEventPreview> = ({ status, className }) => {
>
<FormattedMessage id='event.manage' defaultMessage='Manage' />
</Button>
) : <EventActionButton status={status} />}
) : <EventActionButton status={status} />)}
</div>
<div className='bg-primary-200 dark:bg-gray-600 h-40'>
{banner && <img className='h-full w-full object-cover' src={banner.url} alt={intl.formatMessage(messages.bannerHeader)} />}

Wyświetl plik

@ -147,7 +147,7 @@ const QuotedStatus: React.FC<IQuotedStatus> = ({ status, onCancel, compose }) =>
{renderReplyMentions()}
{status.event ? <EventPreview status={status} /> : (
{status.event ? <EventPreview status={status} hideAction /> : (
<>
<Text
className='break-words status__content status__content--quote'

Wyświetl plik

@ -1,16 +1,23 @@
import React from 'react';
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
import { Link } from 'react-router-dom';
import { Link, useHistory } from 'react-router-dom';
import { blockAccount } from 'soapbox/actions/accounts';
import { launchChat } from 'soapbox/actions/chats';
import { directCompose, mentionCompose, quoteCompose } from 'soapbox/actions/compose';
import { editEvent, fetchEventIcs } from 'soapbox/actions/events';
import { toggleBookmark, togglePin } from 'soapbox/actions/interactions';
import { openModal } from 'soapbox/actions/modals';
import { deleteStatusModal, toggleStatusSensitivityModal } from 'soapbox/actions/moderation';
import { initMuteModal } from 'soapbox/actions/mutes';
import { initReport } from 'soapbox/actions/reports';
import { deleteStatus } from 'soapbox/actions/statuses';
import Icon from 'soapbox/components/icon';
import StillImage from 'soapbox/components/still_image';
import { Button, HStack, IconButton, Menu, MenuButton, MenuDivider, MenuItem, MenuLink, MenuList, Stack, Text } from 'soapbox/components/ui';
import SvgIcon from 'soapbox/components/ui/icon/svg-icon';
import VerificationBadge from 'soapbox/components/verification_badge';
import { useAppDispatch, useOwnAccount } from 'soapbox/hooks';
import { useAppDispatch, useFeatures, useOwnAccount } from 'soapbox/hooks';
import { download } from 'soapbox/utils/download';
import { shortNumberFormat } from 'soapbox/utils/numbers';
@ -27,11 +34,28 @@ const messages = defineMessages({
copy: { id: 'event.copy', defaultMessage: 'Copy link to event' },
bookmark: { id: 'status.bookmark', defaultMessage: 'Bookmark' },
unbookmark: { id: 'status.unbookmark', defaultMessage: 'Remove bookmark' },
quotePost: { id: 'status.quote', defaultMessage: 'Quote post' },
pin: { id: 'status.pin', defaultMessage: 'Pin on profile' },
unpin: { id: 'status.unpin', defaultMessage: 'Unpin from profile' },
reblog_private: { id: 'status.reblog_private', defaultMessage: 'Repost to original audience' },
cancel_reblog_private: { id: 'status.cancel_reblog_private', defaultMessage: 'Un-repost' },
delete: { id: 'status.delete', defaultMessage: 'Delete' },
mention: { id: 'status.mention', defaultMessage: 'Mention @{name}' },
chat: { id: 'status.chat', defaultMessage: 'Chat with @{name}' },
direct: { id: 'status.direct', defaultMessage: 'Direct message @{name}' },
mute: { id: 'account.mute', defaultMessage: 'Mute @{name}' },
block: { id: 'account.block', defaultMessage: 'Block @{name}' },
report: { id: 'status.report', defaultMessage: 'Report @{name}' },
adminAccount: { id: 'status.admin_account', defaultMessage: 'Moderate @{name}' },
adminStatus: { id: 'status.admin_status', defaultMessage: 'Open this post in the moderation interface' },
markStatusSensitive: { id: 'admin.statuses.actions.mark_status_sensitive', defaultMessage: 'Mark post sensitive' },
markStatusNotSensitive: { id: 'admin.statuses.actions.mark_status_not_sensitive', defaultMessage: 'Mark post not sensitive' },
deleteStatus: { id: 'admin.statuses.actions.delete_status', defaultMessage: 'Delete post' },
blockConfirm: { id: 'confirmations.block.confirm', defaultMessage: 'Block' },
blockAndReport: { id: 'confirmations.block.block_and_report', defaultMessage: 'Block & Report' },
deleteConfirm: { id: 'confirmations.delete_event.confirm', defaultMessage: 'Delete' },
deleteHeading: { id: 'confirmations.delete_event.heading', defaultMessage: 'Delete event' },
deleteMessage: { id: 'confirmations.delete_event.message', defaultMessage: 'Are you sure you want to delete this event?' },
});
interface IEventHeader {
@ -41,7 +65,9 @@ interface IEventHeader {
const EventHeader: React.FC<IEventHeader> = ({ status }) => {
const intl = useIntl();
const dispatch = useAppDispatch();
const history = useHistory();
const features = useFeatures();
const ownAccount = useOwnAccount();
const isStaff = ownAccount ? ownAccount.staff : false;
const isAdmin = ownAccount ? ownAccount.admin : false;
@ -62,6 +88,8 @@ const EventHeader: React.FC<IEventHeader> = ({ status }) => {
const event = status.event;
const banner = status.media_attachments?.find(({ description }) => description === 'Banner');
const username = account.username;
const handleHeaderClick: React.MouseEventHandler<HTMLAnchorElement> = (e) => {
e.stopPropagation();
@ -94,6 +122,63 @@ const EventHeader: React.FC<IEventHeader> = ({ status }) => {
}
};
const handleBookmarkClick = () => {
dispatch(toggleBookmark(status));
};
const handleQuoteClick = () => {
dispatch(quoteCompose(status));
};
const handlePinClick = () => {
dispatch(togglePin(status));
};
const handleDeleteClick = () => {
dispatch(openModal('CONFIRM', {
icon: require('@tabler/icons/trash.svg'),
heading: intl.formatMessage(messages.deleteHeading),
message: intl.formatMessage(messages.deleteMessage),
confirm: intl.formatMessage(messages.deleteConfirm),
onConfirm: () => dispatch(deleteStatus(status.id)),
}));
};
const handleMentionClick = () => {
dispatch(mentionCompose(account));
};
const handleChatClick = () => {
dispatch(launchChat(account.id, history));
};
const handleDirectClick = () => {
dispatch(directCompose(account));
};
const handleMuteClick = () => {
dispatch(initMuteModal(account));
};
const handleBlockClick = () => {
dispatch(openModal('CONFIRM', {
icon: require('@tabler/icons/ban.svg'),
heading: <FormattedMessage id='confirmations.block.heading' defaultMessage='Block @{name}' values={{ name: account.acct }} />,
message: <FormattedMessage id='confirmations.block.message' defaultMessage='Are you sure you want to block {name}?' values={{ name: <strong>@{account.acct}</strong> }} />,
confirm: intl.formatMessage(messages.blockConfirm),
onConfirm: () => dispatch(blockAccount(account.id)),
secondary: intl.formatMessage(messages.blockAndReport),
onSecondary: () => {
dispatch(blockAccount(account.id));
dispatch(initReport(account, status));
},
}));
};
const handleReport = () => {
dispatch(initReport(account, status));
};
const handleModerate = () => {
dispatch(openModal('ACCOUNT_MODERATION', { accountId: account.id }));
};
@ -110,51 +195,129 @@ const EventHeader: React.FC<IEventHeader> = ({ status }) => {
dispatch(deleteStatusModal(intl, status.id));
};
const menu: MenuType = [
{
text: intl.formatMessage(messages.exportIcs),
action: handleExportClick,
icon: require('@tabler/icons/calendar-plus.svg'),
},
{
text: intl.formatMessage(messages.copy),
action: handleCopy,
icon: require('@tabler/icons/link.svg'),
},
];
const makeMenu = () => {
const menu: MenuType = [
{
text: intl.formatMessage(messages.exportIcs),
action: handleExportClick,
icon: require('@tabler/icons/calendar-plus.svg'),
},
{
text: intl.formatMessage(messages.copy),
action: handleCopy,
icon: require('@tabler/icons/link.svg'),
},
];
if (isStaff) {
menu.push(null);
if (!ownAccount) return menu;
menu.push({
text: intl.formatMessage(messages.adminAccount, { name: account.username }),
action: handleModerate,
icon: require('@tabler/icons/gavel.svg'),
});
if (isAdmin) {
if (features.bookmarks) {
menu.push({
text: intl.formatMessage(messages.adminStatus),
action: handleModerateStatus,
icon: require('@tabler/icons/pencil.svg'),
text: intl.formatMessage(status.bookmarked ? messages.unbookmark : messages.bookmark),
action: handleBookmarkClick,
icon: status.bookmarked ? require('@tabler/icons/bookmark-off.svg') : require('@tabler/icons/bookmark.svg'),
});
}
menu.push({
text: intl.formatMessage(status.sensitive === false ? messages.markStatusSensitive : messages.markStatusNotSensitive),
action: handleToggleStatusSensitivity,
icon: require('@tabler/icons/alert-triangle.svg'),
});
if (account.id !== ownAccount?.id) {
if (features.quotePosts) {
menu.push({
text: intl.formatMessage(messages.deleteStatus),
action: handleDeleteStatus,
text: intl.formatMessage(messages.quotePost),
action: handleQuoteClick,
icon: require('@tabler/icons/quote.svg'),
});
}
menu.push(null);
if (ownAccount.id === account.id) {
if (['public', 'unlisted'].includes(status.visibility)) {
menu.push({
text: intl.formatMessage(status.pinned ? messages.unpin : messages.pin),
action: handlePinClick,
icon: status.pinned ? require('@tabler/icons/pinned-off.svg') : require('@tabler/icons/pin.svg'),
});
}
menu.push({
text: intl.formatMessage(messages.delete),
action: handleDeleteClick,
icon: require('@tabler/icons/trash.svg'),
destructive: true,
});
} else {
menu.push({
text: intl.formatMessage(messages.mention, { name: username }),
action: handleMentionClick,
icon: require('@tabler/icons/at.svg'),
});
if (status.getIn(['account', 'pleroma', 'accepts_chat_messages']) === true) {
menu.push({
text: intl.formatMessage(messages.chat, { name: username }),
action: handleChatClick,
icon: require('@tabler/icons/messages.svg'),
});
} else if (features.privacyScopes) {
menu.push({
text: intl.formatMessage(messages.direct, { name: username }),
action: handleDirectClick,
icon: require('@tabler/icons/mail.svg'),
});
}
menu.push(null);
menu.push({
text: intl.formatMessage(messages.mute, { name: username }),
action: handleMuteClick,
icon: require('@tabler/icons/circle-x.svg'),
});
menu.push({
text: intl.formatMessage(messages.block, { name: username }),
action: handleBlockClick,
icon: require('@tabler/icons/ban.svg'),
});
menu.push({
text: intl.formatMessage(messages.report, { name: username }),
action: handleReport,
icon: require('@tabler/icons/flag.svg'),
});
}
}
if (isStaff) {
menu.push(null);
menu.push({
text: intl.formatMessage(messages.adminAccount, { name: account.username }),
action: handleModerate,
icon: require('@tabler/icons/gavel.svg'),
});
if (isAdmin) {
menu.push({
text: intl.formatMessage(messages.adminStatus),
action: handleModerateStatus,
icon: require('@tabler/icons/pencil.svg'),
});
}
menu.push({
text: intl.formatMessage(status.sensitive === false ? messages.markStatusSensitive : messages.markStatusNotSensitive),
action: handleToggleStatusSensitivity,
icon: require('@tabler/icons/alert-triangle.svg'),
});
if (account.id !== ownAccount?.id) {
menu.push({
text: intl.formatMessage(messages.deleteStatus),
action: handleDeleteStatus,
icon: require('@tabler/icons/trash.svg'),
destructive: true,
});
}
}
return menu;
};
const handleManageClick: React.MouseEventHandler = e => {
e.stopPropagation();
@ -199,7 +362,7 @@ const EventHeader: React.FC<IEventHeader> = ({ status }) => {
/>
<MenuList>
{menu.map((menuItem, idx) => {
{makeMenu().map((menuItem, idx) => {
if (typeof menuItem?.text === 'undefined') {
return <MenuDivider key={idx} />;
} else {

Wyświetl plik

@ -45,7 +45,7 @@ const EventInformation: React.FC<IEventInformation> = ({ params }) => {
};
const handleShowMap: React.MouseEventHandler<HTMLAnchorElement> = (e) => {
e.stopPropagation();
e.preventDefault();
dispatch(openModal('EVENT_MAP', {
statusId: status.id,
@ -138,16 +138,18 @@ const EventInformation: React.FC<IEventInformation> = ({ params }) => {
return (
<Stack className='mt-4 sm:p-2' space={2}>
<Stack space={1}>
<Text size='xl' weight='bold'>
<FormattedMessage id='event.description' defaultMessage='Description' />
</Text>
<Text
className='break-words status__content'
size='sm'
dangerouslySetInnerHTML={{ __html: status.contentHtml }}
/>
</Stack>
{!!status.contentHtml.trim() && (
<Stack space={1}>
<Text size='xl' weight='bold'>
<FormattedMessage id='event.description' defaultMessage='Description' />
</Text>
<Text
className='break-words status__content'
size='sm'
dangerouslySetInnerHTML={{ __html: status.contentHtml }}
/>
</Stack>
)}
<StatusMedia
status={status}

Wyświetl plik

@ -7,7 +7,7 @@ import { Button } from 'soapbox/components/ui';
const ComposeButton = () => {
const dispatch = useDispatch();
const onOpenCompose = () => dispatch(openModal('COMPOSE_EVENT'));
const onOpenCompose = () => dispatch(openModal('COMPOSE'));
return (
<div className='mt-4'>

Wyświetl plik

@ -185,6 +185,124 @@ const ComposeEventModal: React.FC<IComposeEventModal> = ({ onClose }) => {
return <Tabs items={items} activeItem={tab} />;
};
let body;
if (tab === 'edit') body = (
<Form>
<FormGroup
labelText={<FormattedMessage id='compose_event.fields.banner_label' defaultMessage='Event banner' />}
>
<div className='flex items-center justify-center bg-gray-200 dark:bg-gray-900/50 rounded-lg text-black dark:text-white sm:shadow dark:sm:shadow-inset overflow-hidden h-24 sm:h-32 relative'>
{banner ? (
<>
<img className='h-full w-full object-cover' src={banner.url} alt='' />
<IconButton className='absolute top-2 right-2' src={require('@tabler/icons/x.svg')} onClick={handleClearBanner} />
</>
) : (
<UploadButton disabled={isUploading} onSelectFile={handleFiles} />
)}
</div>
</FormGroup>
<FormGroup
labelText={<FormattedMessage id='compose_event.fields.name_label' defaultMessage='Event name' />}
>
<Input
type='text'
placeholder={intl.formatMessage(messages.eventNamePlaceholder)}
value={name}
onChange={onChangeName}
/>
</FormGroup>
<FormGroup
labelText={<FormattedMessage id='compose_event.fields.description_label' defaultMessage='Event description' />}
hintText={<FormattedMessage id='compose_event.fields.description_hint' defaultMessage='Markdown syntax is supported' />}
>
<Textarea
autoComplete='off'
placeholder={intl.formatMessage(messages.eventDescriptionPlaceholder)}
value={description}
onChange={onChangeDescription}
/>
</FormGroup>
<FormGroup
labelText={<FormattedMessage id='compose_event.fields.location_label' defaultMessage='Event location' />}
>
{location ? renderLocation() : (
<LocationSearch
onSelected={onChangeLocation}
/>
)}
</FormGroup>
<FormGroup
labelText={<FormattedMessage id='compose_event.fields.start_time_label' defaultMessage='Event 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.eventStartTimePlaceholder)}
filterDate={isCurrentOrFutureDate}
selected={startTime}
onChange={onChangeStartTime}
/>)}
</BundleContainer>
</FormGroup>
<HStack alignItems='center' space={2}>
<Toggle
icons={false}
checked={!!endTime}
onChange={onChangeHasEndTime}
/>
<Text tag='span' theme='muted'>
<FormattedMessage id='compose_event.fields.has_end_time' defaultMessage='The event has end date' />
</Text>
</HStack>
{endTime && (
<FormGroup
labelText={<FormattedMessage id='compose_event.fields.end_time_label' defaultMessage='Event 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.eventEndTimePlaceholder)}
filterDate={isCurrentOrFutureDate}
selected={endTime}
onChange={onChangeEndTime}
/>)}
</BundleContainer>
</FormGroup>
)}
{!id && (
<HStack alignItems='center' space={2}>
<Toggle
icons={false}
checked={approvalRequired}
onChange={onChangeApprovalRequired}
/>
<Text tag='span' theme='muted'>
<FormattedMessage id='compose_event.fields.approval_required' defaultMessage='I want to approve participation requests manually' />
</Text>
</HStack>
)}
</Form>
);
else body = accounts ? (
<Stack space={3}>
{accounts.size > 0 ? (
accounts.map(({ account, participation_message }) =>
<Account key={account} eventId={id!} id={account} participationMessage={participation_message} />,
)
) : (
<FormattedMessage id='empty_column.event_participant_requests' defaultMessage='There are no pending event participation requests.' />
)}
</Stack>
) : <Spinner />;
return (
<Modal
title={id
@ -199,119 +317,7 @@ const ComposeEventModal: React.FC<IComposeEventModal> = ({ onClose }) => {
>
<Stack space={2}>
{id && renderTabs()}
{tab === 'edit' ? (
<Form>
<FormGroup
labelText={<FormattedMessage id='compose_event.fields.banner_label' defaultMessage='Event banner' />}
>
<div className='flex items-center justify-center bg-gray-200 dark:bg-gray-900/50 rounded-lg text-black dark:text-white sm:shadow dark:sm:shadow-inset overflow-hidden h-24 sm:h-32 relative'>
{banner ? (
<>
<img className='h-full w-full object-cover' src={banner.url} alt='' />
<IconButton className='absolute top-2 right-2' src={require('@tabler/icons/x.svg')} onClick={handleClearBanner} />
</>
) : (
<UploadButton disabled={isUploading} onSelectFile={handleFiles} />
)}
</div>
</FormGroup>
<FormGroup
labelText={<FormattedMessage id='compose_event.fields.name_label' defaultMessage='Event name' />}
>
<Input
type='text'
placeholder={intl.formatMessage(messages.eventNamePlaceholder)}
value={name}
onChange={onChangeName}
/>
</FormGroup>
<FormGroup
labelText={<FormattedMessage id='compose_event.fields.description_label' defaultMessage='Event description' />}
hintText={<FormattedMessage id='compose_event.fields.description_hint' defaultMessage='Markdown syntax is supported' />}
>
<Textarea
autoComplete='off'
placeholder={intl.formatMessage(messages.eventDescriptionPlaceholder)}
value={description}
onChange={onChangeDescription}
/>
</FormGroup>
<FormGroup
labelText={<FormattedMessage id='compose_event.fields.location_label' defaultMessage='Event location' />}
>
{location ? renderLocation() : (
<LocationSearch
onSelected={onChangeLocation}
/>
)}
</FormGroup>
<FormGroup
labelText={<FormattedMessage id='compose_event.fields.start_time_label' defaultMessage='Event 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.eventStartTimePlaceholder)}
filterDate={isCurrentOrFutureDate}
selected={startTime}
onChange={onChangeStartTime}
/>)}
</BundleContainer>
</FormGroup>
<HStack alignItems='center' space={2}>
<Toggle
icons={false}
checked={!!endTime}
onChange={onChangeHasEndTime}
/>
<Text tag='span' theme='muted'>
<FormattedMessage id='compose_event.fields.has_end_time' defaultMessage='The event has end date' />
</Text>
</HStack>
{endTime && (
<FormGroup
labelText={<FormattedMessage id='compose_event.fields.end_time_label' defaultMessage='Event 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.eventEndTimePlaceholder)}
filterDate={isCurrentOrFutureDate}
selected={endTime}
onChange={onChangeEndTime}
/>)}
</BundleContainer>
</FormGroup>
)}
<HStack alignItems='center' space={2}>
<Toggle
icons={false}
checked={approvalRequired}
onChange={onChangeApprovalRequired}
/>
<Text tag='span' theme='muted'>
<FormattedMessage id='compose_event.fields.approval_required' defaultMessage='I want to approve participation requests manually' />
</Text>
</HStack>
</Form>
) : accounts ? (
<Stack space={3}>
{accounts.size > 0 ? (
accounts.map(({ account, participation_message }) =>
<Account key={account} eventId={id!} id={account} participationMessage={participation_message} />,
)
) : (
<FormattedMessage id='empty_column.event_participant_requests' defaultMessage='There are no pending event participation requests.' />
)}
</Stack>
) : <Spinner />}
{body}
</Stack>
</Modal>
);