Information page, improvements
Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
This commit is contained in:
parent
309bd2c34f
commit
fe7333ddb0
28 changed files with 478 additions and 75 deletions
|
@ -11,7 +11,7 @@ import snackbar from './snackbar';
|
|||
|
||||
import type { AxiosError } from 'axios';
|
||||
import type { AppDispatch, RootState } from 'soapbox/store';
|
||||
import type { APIEntity } from 'soapbox/types/entities';
|
||||
import type { APIEntity, Status as StatusEntity } from 'soapbox/types/entities';
|
||||
|
||||
const LOCATION_SEARCH_REQUEST = 'LOCATION_SEARCH_REQUEST';
|
||||
const LOCATION_SEARCH_SUCCESS = 'LOCATION_SEARCH_SUCCESS';
|
||||
|
@ -259,7 +259,7 @@ const joinEvent = (id: string, participationMessage?: string) =>
|
|||
return dispatch(noOp);
|
||||
}
|
||||
|
||||
dispatch(joinEventRequest());
|
||||
dispatch(joinEventRequest(status));
|
||||
|
||||
return api(getState).post(`/api/v1/pleroma/events/${id}/join`, { participationMessage }).then(({ data }) => {
|
||||
dispatch(importFetchedStatus(data));
|
||||
|
@ -270,22 +270,24 @@ const joinEvent = (id: string, participationMessage?: string) =>
|
|||
`/@${data.account.acct}/events/${data.id}`,
|
||||
));
|
||||
}).catch(function(error) {
|
||||
dispatch(joinEventFail(error, status?.event?.join_state || null));
|
||||
dispatch(joinEventFail(error, status, status?.event?.join_state || null));
|
||||
});
|
||||
};
|
||||
|
||||
const joinEventRequest = () => ({
|
||||
const joinEventRequest = (status: StatusEntity) => ({
|
||||
type: EVENT_JOIN_REQUEST,
|
||||
id: status.id,
|
||||
});
|
||||
|
||||
const joinEventSuccess = (status: APIEntity) => ({
|
||||
type: EVENT_JOIN_SUCCESS,
|
||||
status,
|
||||
id: status.id,
|
||||
});
|
||||
|
||||
const joinEventFail = (error: AxiosError, previousState: string | null) => ({
|
||||
const joinEventFail = (error: AxiosError, status: StatusEntity, previousState: string | null) => ({
|
||||
type: EVENT_JOIN_FAIL,
|
||||
error,
|
||||
id: status.id,
|
||||
previousState,
|
||||
});
|
||||
|
||||
|
@ -297,27 +299,29 @@ const leaveEvent = (id: string) =>
|
|||
return dispatch(noOp);
|
||||
}
|
||||
|
||||
dispatch(leaveEventRequest());
|
||||
dispatch(leaveEventRequest(status));
|
||||
|
||||
return api(getState).post(`/api/v1/pleroma/events/${id}/leave`).then(({ data }) => {
|
||||
dispatch(importFetchedStatus(data));
|
||||
dispatch(leaveEventSuccess(data));
|
||||
}).catch(function(error) {
|
||||
dispatch(leaveEventFail(error));
|
||||
dispatch(leaveEventFail(error, status));
|
||||
});
|
||||
};
|
||||
|
||||
const leaveEventRequest = () => ({
|
||||
const leaveEventRequest = (status: StatusEntity) => ({
|
||||
type: EVENT_LEAVE_REQUEST,
|
||||
id: status.id,
|
||||
});
|
||||
|
||||
const leaveEventSuccess = (status: APIEntity) => ({
|
||||
type: EVENT_LEAVE_SUCCESS,
|
||||
status,
|
||||
id: status.id,
|
||||
});
|
||||
|
||||
const leaveEventFail = (error: AxiosError) => ({
|
||||
const leaveEventFail = (error: AxiosError, status: StatusEntity) => ({
|
||||
type: EVENT_LEAVE_FAIL,
|
||||
id: status.id,
|
||||
error,
|
||||
});
|
||||
|
||||
|
|
|
@ -38,7 +38,7 @@ const StatusMedia: React.FC<IStatusMedia> = ({
|
|||
const dispatch = useAppDispatch();
|
||||
const [mediaWrapperWidth, setMediaWrapperWidth] = useState<number | undefined>(undefined);
|
||||
|
||||
const mediaAttachments = excludeBanner ? status.media_attachments.filter(({ description }) => description !== 'Banner') : status.media_attachments;
|
||||
const mediaAttachments = excludeBanner ? status.media_attachments.filter(({ description, pleroma }) => description !== 'Banner' && pleroma.get('mime_type') !== 'text/html') : status.media_attachments;
|
||||
|
||||
const size = mediaAttachments.size;
|
||||
const firstAttachment = mediaAttachments.first();
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import classNames from 'clsx';
|
||||
import React from 'react';
|
||||
|
||||
type SIZES = 0 | 0.5 | 1 | 1.5 | 2 | 3 | 4 | 5 | 10
|
||||
type SIZES = 0 | 0.5 | 1 | 1.5 | 2 | 3 | 4 | 5 | 6 | 10
|
||||
|
||||
const spaces = {
|
||||
0: 'space-y-0',
|
||||
|
@ -12,6 +12,7 @@ const spaces = {
|
|||
3: 'space-y-3',
|
||||
4: 'space-y-4',
|
||||
5: 'space-y-5',
|
||||
6: 'space-y-6',
|
||||
10: 'space-y-10',
|
||||
};
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@ import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
|||
import { joinEvent, leaveEvent } from 'soapbox/actions/events';
|
||||
import { openModal } from 'soapbox/actions/modals';
|
||||
import { Button } from 'soapbox/components/ui';
|
||||
import { useAppDispatch } from 'soapbox/hooks';
|
||||
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
|
||||
|
||||
import type { Status as StatusEntity } from 'soapbox/types/entities';
|
||||
|
||||
|
@ -21,6 +21,8 @@ const EventActionButton: React.FC<IEventAction> = ({ status }) => {
|
|||
const intl = useIntl();
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const me = useAppSelector((state) => state.me);
|
||||
|
||||
const event = status.event!;
|
||||
|
||||
const handleJoin: React.EventHandler<React.MouseEvent> = (e) => {
|
||||
|
@ -49,6 +51,15 @@ const EventActionButton: React.FC<IEventAction> = ({ status }) => {
|
|||
}
|
||||
};
|
||||
|
||||
const handleOpenUnauthorizedModal: React.EventHandler<React.MouseEvent> = (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
dispatch(openModal('UNAUTHORIZED', {
|
||||
action: 'JOIN',
|
||||
ap_id: status.url,
|
||||
}));
|
||||
};
|
||||
|
||||
let buttonLabel;
|
||||
let buttonIcon;
|
||||
let buttonDisabled = false;
|
||||
|
@ -69,7 +80,7 @@ const EventActionButton: React.FC<IEventAction> = ({ status }) => {
|
|||
break;
|
||||
default:
|
||||
buttonLabel = <FormattedMessage id='event.join_state.empty' defaultMessage='Participate' />;
|
||||
buttonAction = handleJoin;
|
||||
buttonAction = me ? handleJoin : handleOpenUnauthorizedModal;
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
|
@ -4,13 +4,15 @@ import { Link } from 'react-router-dom';
|
|||
|
||||
import { fetchEventIcs } from 'soapbox/actions/events';
|
||||
import { openModal } from 'soapbox/actions/modals';
|
||||
import { deleteStatusModal, toggleStatusSensitivityModal } from 'soapbox/actions/moderation';
|
||||
import Icon from 'soapbox/components/icon';
|
||||
import StillImage from 'soapbox/components/still_image';
|
||||
import { HStack, IconButton, Menu, MenuButton, MenuDivider, MenuItem, MenuLink, MenuList, Stack, Text } from 'soapbox/components/ui';
|
||||
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, useAppSelector } from 'soapbox/hooks';
|
||||
import { useAppDispatch, useOwnAccount } from 'soapbox/hooks';
|
||||
import { download } from 'soapbox/utils/download';
|
||||
import { shortNumberFormat } from 'soapbox/utils/numbers';
|
||||
|
||||
import PlaceholderEventHeader from '../../placeholder/components/placeholder_event_header';
|
||||
import EventActionButton from '../components/event-action-button';
|
||||
|
@ -23,6 +25,13 @@ const messages = defineMessages({
|
|||
bannerHeader: { id: 'event.banner', defaultMessage: 'Event banner' },
|
||||
exportIcs: { id: 'event.export_ics', defaultMessage: 'Export to your calendar' },
|
||||
copy: { id: 'event.copy', defaultMessage: 'Copy link to event' },
|
||||
bookmark: { id: 'status.bookmark', defaultMessage: 'Bookmark' },
|
||||
unbookmark: { id: 'status.unbookmark', defaultMessage: 'Remove bookmark' },
|
||||
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' },
|
||||
});
|
||||
|
||||
interface IEventHeader {
|
||||
|
@ -33,13 +42,15 @@ const EventHeader: React.FC<IEventHeader> = ({ status }) => {
|
|||
const intl = useIntl();
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const me = useAppSelector(state => state.me);
|
||||
const ownAccount = useOwnAccount();
|
||||
const isStaff = ownAccount ? ownAccount.staff : false;
|
||||
const isAdmin = ownAccount ? ownAccount.admin : false;
|
||||
|
||||
if (!status || !status.event) {
|
||||
return (
|
||||
<>
|
||||
<div className='-mt-4 -mx-4'>
|
||||
<div className='relative h-48 w-full lg:h-64 md:rounded-t-xl bg-gray-200 dark:bg-gray-900/50' />
|
||||
<div className='relative h-32 w-full lg:h-48 md:rounded-t-xl bg-gray-200 dark:bg-gray-900/50' />
|
||||
</div>
|
||||
|
||||
<PlaceholderEventHeader />
|
||||
|
@ -52,17 +63,51 @@ const EventHeader: React.FC<IEventHeader> = ({ status }) => {
|
|||
const banner = status.media_attachments?.find(({ description }) => description === 'Banner');
|
||||
|
||||
const handleHeaderClick: React.MouseEventHandler<HTMLAnchorElement> = (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
const index = status.media_attachments!.findIndex(({ description }) => description === 'Banner');
|
||||
dispatch(openModal('MEDIA', { media: status.media_attachments, index }));
|
||||
};
|
||||
|
||||
const handleExportClick: React.MouseEventHandler = e => {
|
||||
const handleExportClick = () => {
|
||||
dispatch(fetchEventIcs(status.id)).then((response) => {
|
||||
download(response, 'calendar.ics');
|
||||
}).catch(() => {});
|
||||
e.preventDefault();
|
||||
};
|
||||
|
||||
const handleCopy = () => {
|
||||
const { uri } = status;
|
||||
const textarea = document.createElement('textarea');
|
||||
|
||||
textarea.textContent = uri;
|
||||
textarea.style.position = 'fixed';
|
||||
|
||||
document.body.appendChild(textarea);
|
||||
|
||||
try {
|
||||
textarea.select();
|
||||
document.execCommand('copy');
|
||||
} catch {
|
||||
// Do nothing
|
||||
} finally {
|
||||
document.body.removeChild(textarea);
|
||||
}
|
||||
};
|
||||
|
||||
const handleModerate = () => {
|
||||
dispatch(openModal('ACCOUNT_MODERATION', { accountId: account.id }));
|
||||
};
|
||||
|
||||
const handleModerateStatus = () => {
|
||||
window.open(`/pleroma/admin/#/statuses/${status.id}/`, '_blank');
|
||||
};
|
||||
|
||||
const handleToggleStatusSensitivity = () => {
|
||||
dispatch(toggleStatusSensitivityModal(intl, status.id, status.sensitive));
|
||||
};
|
||||
|
||||
const handleDeleteStatus = () => {
|
||||
dispatch(deleteStatusModal(intl, status.id));
|
||||
};
|
||||
|
||||
const menu: MenuType = [
|
||||
|
@ -71,12 +116,66 @@ const EventHeader: React.FC<IEventHeader> = ({ status }) => {
|
|||
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);
|
||||
|
||||
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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const handleManageClick: React.MouseEventHandler = e => {
|
||||
e.stopPropagation();
|
||||
|
||||
dispatch(openModal('MANAGE_EVENT', {
|
||||
statusId: status.id,
|
||||
}));
|
||||
};
|
||||
|
||||
const handleParticipantsClick: React.MouseEventHandler = e => {
|
||||
e.stopPropagation();
|
||||
|
||||
dispatch(openModal('EVENT_PARTICIPANTS', {
|
||||
statusId: status.id,
|
||||
}));
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className='-mt-4 -mx-4'>
|
||||
<div className='relative h-48 w-full lg:h-64 md:rounded-t-xl bg-gray-200 dark:bg-gray-900/50'>
|
||||
<div className='relative h-32 w-full lg:h-48 md:rounded-t-xl bg-gray-200 dark:bg-gray-900/50'>
|
||||
{banner && (
|
||||
<a href={banner.url} onClick={handleHeaderClick} target='_blank'>
|
||||
<StillImage
|
||||
|
@ -124,12 +223,21 @@ const EventHeader: React.FC<IEventHeader> = ({ status }) => {
|
|||
})}
|
||||
</MenuList>
|
||||
</Menu>
|
||||
{account.id !== me && <EventActionButton status={status} />}
|
||||
{account.id === ownAccount?.id ? (
|
||||
<Button
|
||||
size='sm'
|
||||
theme='secondary'
|
||||
onClick={handleManageClick}
|
||||
to={`/@${account.acct}/events/${status.id}`}
|
||||
>
|
||||
<FormattedMessage id='event.manage' defaultMessage='Manage' />
|
||||
</Button>
|
||||
) : <EventActionButton status={status} />}
|
||||
</HStack>
|
||||
|
||||
<Stack space={1}>
|
||||
<HStack alignItems='center' space={2}>
|
||||
<Icon src={require('@tabler/icons/user.svg')} />
|
||||
<Icon src={require('@tabler/icons/flag-3.svg')} />
|
||||
<span>
|
||||
<FormattedMessage
|
||||
id='event.organized_by'
|
||||
|
@ -146,6 +254,22 @@ const EventHeader: React.FC<IEventHeader> = ({ status }) => {
|
|||
</span>
|
||||
</HStack>
|
||||
|
||||
<HStack alignItems='center' space={2}>
|
||||
<Icon src={require('@tabler/icons/users.svg')} />
|
||||
<a href='#' className='hover:underline' onClick={handleParticipantsClick}>
|
||||
<span>
|
||||
<FormattedMessage
|
||||
id='event.participants'
|
||||
defaultMessage='{count} {rawCount, plural, one {person} other {people}} going'
|
||||
values={{
|
||||
rawCount: event.participants_count || 0,
|
||||
count: shortNumberFormat(event.participants_count || 0),
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
</a>
|
||||
</HStack>
|
||||
|
||||
<EventDate status={status} />
|
||||
|
||||
{event.location && (
|
||||
|
|
|
@ -22,8 +22,6 @@ import type { VirtuosoHandle } from 'react-virtuoso';
|
|||
import type { RootState } from 'soapbox/store';
|
||||
import type { Attachment as AttachmentEntity } from 'soapbox/types/entities';
|
||||
|
||||
const getStatus = makeGetStatus();
|
||||
|
||||
const getDescendantsIds = createSelector([
|
||||
(_: RootState, statusId: string) => statusId,
|
||||
(state: RootState) => state.contexts.replies,
|
||||
|
@ -66,8 +64,11 @@ interface IEventDiscussion {
|
|||
const EventDiscussion: React.FC<IEventDiscussion> = (props) => {
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const getStatus = useCallback(makeGetStatus(), []);
|
||||
const status = useAppSelector(state => getStatus(state, { id: props.params.statusId }));
|
||||
|
||||
const me = useAppSelector((state) => state.me);
|
||||
|
||||
const descendantsIds = useAppSelector(state => {
|
||||
let descendantsIds = ImmutableOrderedSet<string>();
|
||||
|
||||
|
@ -104,8 +105,8 @@ const EventDiscussion: React.FC<IEventDiscussion> = (props) => {
|
|||
}, [props.params.statusId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isLoaded) dispatch(eventDiscussionCompose(`reply:${props.params.statusId}`, status!));
|
||||
}, [isLoaded]);
|
||||
if (isLoaded && me) dispatch(eventDiscussionCompose(`reply:${props.params.statusId}`, status!));
|
||||
}, [isLoaded, me]);
|
||||
|
||||
const handleMoveUp = (id: string) => {
|
||||
const index = ImmutableList(descendantsIds).indexOf(id);
|
||||
|
@ -208,9 +209,9 @@ const EventDiscussion: React.FC<IEventDiscussion> = (props) => {
|
|||
|
||||
return (
|
||||
<Stack space={2}>
|
||||
<div className='sm:p-2 pt-0 border-b border-solid border-gray-200 dark:border-gray-800'>
|
||||
{me && <div className='sm:p-2 pt-0 border-b border-solid border-gray-200 dark:border-gray-800'>
|
||||
<ComposeForm id={`reply:${status.id}`} autoFocus={false} eventDiscussion />
|
||||
</div>
|
||||
</div>}
|
||||
<div ref={node} className='thread p-0 sm:p-2 shadow-none'>
|
||||
<ScrollableList
|
||||
id='thread'
|
||||
|
|
|
@ -1,17 +1,17 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { FormattedDate, FormattedMessage } from 'react-intl';
|
||||
|
||||
import { openModal } from 'soapbox/actions/modals';
|
||||
import { fetchStatus } from 'soapbox/actions/statuses';
|
||||
import MissingIndicator from 'soapbox/components/missing_indicator';
|
||||
import StatusMedia from 'soapbox/components/status-media';
|
||||
import { Stack, Text } from 'soapbox/components/ui';
|
||||
import { HStack, Icon, Stack, Text } from 'soapbox/components/ui';
|
||||
import { useAppDispatch, useAppSelector, useSettings } from 'soapbox/hooks';
|
||||
import { makeGetStatus } from 'soapbox/selectors';
|
||||
import { defaultMediaVisibility } from 'soapbox/utils/status';
|
||||
|
||||
import type { Status as StatusEntity } from 'soapbox/types/entities';
|
||||
|
||||
const getStatus = makeGetStatus();
|
||||
|
||||
type RouteParams = { statusId: string };
|
||||
|
||||
interface IEventInformation {
|
||||
|
@ -20,6 +20,8 @@ interface IEventInformation {
|
|||
|
||||
const EventInformation: React.FC<IEventInformation> = ({ params }) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const getStatus = useCallback(makeGetStatus(), []);
|
||||
|
||||
const status = useAppSelector(state => getStatus(state, { id: params.statusId })) as StatusEntity;
|
||||
|
||||
const settings = useSettings();
|
||||
|
@ -38,10 +40,96 @@ const EventInformation: React.FC<IEventInformation> = ({ params }) => {
|
|||
setShowMedia(defaultMediaVisibility(status, displayMedia));
|
||||
}, [params.statusId]);
|
||||
|
||||
const handleToggleMediaVisibility = (): void => {
|
||||
const handleToggleMediaVisibility = () => {
|
||||
setShowMedia(!showMedia);
|
||||
};
|
||||
|
||||
const handleShowMap: React.MouseEventHandler<HTMLAnchorElement> = (e) => {
|
||||
e.stopPropagation();
|
||||
|
||||
dispatch(openModal('EVENT_MAP', {
|
||||
statusId: status.id,
|
||||
}));
|
||||
};
|
||||
|
||||
const renderEventLocation = useCallback(() => {
|
||||
const event = status?.event;
|
||||
|
||||
return event?.location && (
|
||||
<Stack space={1}>
|
||||
<Text size='xl' weight='bold'>
|
||||
<FormattedMessage id='event.location' defaultMessage='Location' />
|
||||
</Text>
|
||||
<HStack space={2} alignItems='center'>
|
||||
<Icon src={require('@tabler/icons/map-pin.svg')} />
|
||||
<Text>
|
||||
{event.location.get('name')}
|
||||
<br />
|
||||
{!!event.location.get('street')?.trim() && (<>
|
||||
{event.location.get('street')}
|
||||
<br />
|
||||
</>)}
|
||||
{[event.location.get('postalCode'), event.location.get('locality'), event.location.get('country')].filter(text => text).join(', ')}
|
||||
{event.location.get('latitude') && (<>
|
||||
<br />
|
||||
<a href='#' className='text-primary-600 dark:text-accent-blue hover:underline' onClick={handleShowMap}>
|
||||
<FormattedMessage id='event.show_on_map' defaultMessage='Show on map' />
|
||||
</a>
|
||||
</>)}
|
||||
</Text>
|
||||
</HStack>
|
||||
</Stack>
|
||||
);
|
||||
}, [status]);
|
||||
|
||||
const renderEventDate = useCallback(() => {
|
||||
const event = status?.event;
|
||||
|
||||
if (!event?.start_time) return null;
|
||||
|
||||
return (
|
||||
<Stack space={1}>
|
||||
<Text size='xl' weight='bold'>
|
||||
<FormattedMessage id='event.date' defaultMessage='Date' />
|
||||
</Text>
|
||||
<HStack space={2} alignItems='center'>
|
||||
<Icon src={require('@tabler/icons/calendar.svg')} />
|
||||
<Text>
|
||||
<FormattedDate value={event.start_time} year='numeric' month='long' day='2-digit' weekday='long' hour='2-digit' minute='2-digit' />
|
||||
{event.end_time && (<>
|
||||
{' - '}
|
||||
<FormattedDate value={event.end_time} year='numeric' month='long' day='2-digit' weekday='long' hour='2-digit' minute='2-digit' />
|
||||
</>)}
|
||||
</Text>
|
||||
</HStack>
|
||||
</Stack>
|
||||
);
|
||||
}, [status]);
|
||||
|
||||
const renderLinks = useCallback(() => {
|
||||
const links = status?.media_attachments.filter(({ pleroma }) => pleroma.get('mime_type') === 'text/html');
|
||||
|
||||
if (!links?.size) return null;
|
||||
|
||||
|
||||
return (
|
||||
<Stack space={1}>
|
||||
<Text size='xl' weight='bold'>
|
||||
<FormattedMessage id='event.website' defaultMessage='External links' />
|
||||
</Text>
|
||||
|
||||
{links.map(link => (
|
||||
<HStack space={2} alignItems='center'>
|
||||
<Icon src={require('@tabler/icons/link.svg')} />
|
||||
<a href={link.remote_url || link.url} className='text-primary-600 dark:text-accent-blue hover:underline' target='_blank'>
|
||||
{(link.remote_url || link.url).replace(/^https?:\/\//, '')}
|
||||
</a>
|
||||
</HStack>
|
||||
))}
|
||||
</Stack>
|
||||
);
|
||||
}, [status]);
|
||||
|
||||
if (!status && isLoaded) {
|
||||
return (
|
||||
<MissingIndicator />
|
||||
|
@ -50,11 +138,16 @@ const EventInformation: React.FC<IEventInformation> = ({ params }) => {
|
|||
|
||||
return (
|
||||
<Stack className='mt-4 sm:p-2' space={2}>
|
||||
<Text
|
||||
className='break-words status__content'
|
||||
size='sm'
|
||||
dangerouslySetInnerHTML={{ __html: status.contentHtml }}
|
||||
/>
|
||||
<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}
|
||||
|
@ -62,6 +155,12 @@ const EventInformation: React.FC<IEventInformation> = ({ params }) => {
|
|||
showMedia={showMedia}
|
||||
onToggleVisibility={handleToggleMediaVisibility}
|
||||
/>
|
||||
|
||||
{renderEventLocation()}
|
||||
|
||||
{renderEventDate()}
|
||||
|
||||
{renderLinks()}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import React from 'react';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
|
||||
import IconButton from 'soapbox/components/icon_button';
|
||||
import { Modal } from 'soapbox/components/ui';
|
||||
|
||||
const messages = defineMessages({
|
||||
error: { id: 'bundle_modal_error.message', defaultMessage: 'Something went wrong while loading this page.' },
|
||||
error: { id: 'bundle_modal_error.message', defaultMessage: 'Something went wrong while loading this modal.' },
|
||||
retry: { id: 'bundle_modal_error.retry', defaultMessage: 'Try again' },
|
||||
close: { id: 'bundle_modal_error.close', defaultMessage: 'Close' },
|
||||
});
|
||||
|
@ -22,23 +22,13 @@ const BundleModalError: React.FC<IBundleModalError> = ({ onRetry, onClose }) =>
|
|||
};
|
||||
|
||||
return (
|
||||
<div className='modal-root__modal error-modal'>
|
||||
<div className='error-modal__body'>
|
||||
<IconButton title={intl.formatMessage(messages.retry)} icon='refresh' onClick={handleRetry} size={64} />
|
||||
{intl.formatMessage(messages.error)}
|
||||
</div>
|
||||
|
||||
<div className='error-modal__footer'>
|
||||
<div>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className='error-modal__nav onboarding-modal__skip'
|
||||
>
|
||||
{intl.formatMessage(messages.close)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Modal
|
||||
title={intl.formatMessage(messages.error)}
|
||||
confirmationAction={onClose}
|
||||
confirmationText={intl.formatMessage(messages.close)}
|
||||
secondaryAction={handleRetry}
|
||||
secondaryText={intl.formatMessage(messages.retry)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@ import { Button } from 'soapbox/components/ui';
|
|||
|
||||
const ComposeButton = () => {
|
||||
const dispatch = useDispatch();
|
||||
const onOpenCompose = () => dispatch(openModal('COMPOSE'));
|
||||
const onOpenCompose = () => dispatch(openModal('CREATE_EVENT'));
|
||||
|
||||
return (
|
||||
<div className='mt-4'>
|
||||
|
|
|
@ -35,6 +35,8 @@ import {
|
|||
CreateEventModal,
|
||||
JoinEventModal,
|
||||
AccountModerationModal,
|
||||
EventMapModal,
|
||||
EventParticipantsModal,
|
||||
} from 'soapbox/features/ui/util/async-components';
|
||||
|
||||
import BundleContainer from '../containers/bundle_container';
|
||||
|
@ -75,6 +77,8 @@ const MODAL_COMPONENTS = {
|
|||
'CREATE_EVENT': CreateEventModal,
|
||||
'JOIN_EVENT': JoinEventModal,
|
||||
'ACCOUNT_MODERATION': AccountModerationModal,
|
||||
'EVENT_MAP': EventMapModal,
|
||||
'EVENT_PARTICIPANTS': EventParticipantsModal,
|
||||
};
|
||||
|
||||
export default class ModalRoot extends React.PureComponent {
|
||||
|
|
|
@ -0,0 +1,79 @@
|
|||
import L from 'leaflet';
|
||||
import React, { useCallback, useEffect, useRef } from 'react';
|
||||
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
||||
|
||||
import { Button, Modal, Stack } from 'soapbox/components/ui';
|
||||
import { useAppSelector } from 'soapbox/hooks';
|
||||
import { makeGetStatus } from 'soapbox/selectors';
|
||||
|
||||
import type { Status as StatusEntity } from 'soapbox/types/entities';
|
||||
|
||||
import 'leaflet/dist/leaflet.css';
|
||||
|
||||
L.Icon.Default.mergeOptions({
|
||||
iconRetinaUrl: require('leaflet/dist/images/marker-icon-2x.png'),
|
||||
iconUrl: require('leaflet/dist/images/marker-icon.png'),
|
||||
shadowUrl: require('leaflet/dist/images/marker-shadow.png'),
|
||||
});
|
||||
|
||||
interface IEventMapModal {
|
||||
onClose: (type: string) => void,
|
||||
statusId: string,
|
||||
}
|
||||
|
||||
const messages = defineMessages({
|
||||
osmAttribution: { id: 'event_map.osm_attribution', defaultMessage: '© OpenStreetMap Contributors' },
|
||||
});
|
||||
|
||||
const EventMapModal: React.FC<IEventMapModal> = ({ onClose, statusId }) => {
|
||||
const intl = useIntl();
|
||||
|
||||
const getStatus = useCallback(makeGetStatus(), []);
|
||||
const status = useAppSelector(state => getStatus(state, { id: statusId })) as StatusEntity;
|
||||
const location = status.event!.location!;
|
||||
|
||||
const map = useRef<L.Map>();
|
||||
|
||||
useEffect(() => {
|
||||
const latlng: [number, number] = [+location.get('latitude'), +location.get('longitude')];
|
||||
|
||||
map.current = L.map('event-map').setView(latlng, 15);
|
||||
|
||||
L.marker(latlng, {
|
||||
title: location.get('name'),
|
||||
}).addTo(map.current);
|
||||
|
||||
L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||
attribution: intl.formatMessage(messages.osmAttribution),
|
||||
}).addTo(map.current);
|
||||
|
||||
return () => {
|
||||
map.current?.remove();
|
||||
};
|
||||
}, []);
|
||||
|
||||
const onClickClose = () => {
|
||||
onClose('EVENT_MAP');
|
||||
};
|
||||
|
||||
const onClickNavigate = () => {
|
||||
window.open(`https://www.openstreetmap.org/directions?from=&to=${location.get('latitude')},${location.get('longitude')}#map=14/${location.get('latitude')}/${location.get('longitude')}`, '_blank');
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={<FormattedMessage id='column.event_map' defaultMessage='Event location' />}
|
||||
onClose={onClickClose}
|
||||
width='2xl'
|
||||
>
|
||||
<Stack alignItems='center' space={6}>
|
||||
<div className='h-96 w-full' id='event-map' />
|
||||
<Button onClick={onClickNavigate} icon={require('@tabler/icons/gps.svg')}>
|
||||
<FormattedMessage id='event_map.navigate' defaultMessage='Navigate' />
|
||||
</Button>
|
||||
</Stack>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default EventMapModal;
|
|
@ -0,0 +1,59 @@
|
|||
import React, { useEffect } from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import { fetchEventParticipations } from 'soapbox/actions/events';
|
||||
import { Modal, Spinner, Stack } from 'soapbox/components/ui';
|
||||
import AccountContainer from 'soapbox/containers/account_container';
|
||||
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
|
||||
|
||||
interface IEventParticipantsModal {
|
||||
onClose: (type: string) => void,
|
||||
statusId: string,
|
||||
}
|
||||
|
||||
const EventParticipantsModal: React.FC<IEventParticipantsModal> = ({ onClose, statusId }) => {
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const accountIds = useAppSelector((state) => state.user_lists.event_participations.get(statusId)?.items);
|
||||
|
||||
const fetchData = () => {
|
||||
dispatch(fetchEventParticipations(statusId));
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
}, []);
|
||||
|
||||
const onClickClose = () => {
|
||||
onClose('EVENT_PARTICIPANTS');
|
||||
};
|
||||
|
||||
let body;
|
||||
|
||||
if (!accountIds) {
|
||||
body = <Spinner />;
|
||||
} else {
|
||||
body = (
|
||||
<Stack space={3}>
|
||||
{accountIds.size > 0 ? (
|
||||
accountIds.map((id) =>
|
||||
<AccountContainer key={id} id={id} />,
|
||||
)
|
||||
) : (
|
||||
<FormattedMessage id='empty_column.event_participants' defaultMessage='No one joined this event yet. When someone does, they will show up here.' />
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={<FormattedMessage id='column.event_participants' defaultMessage='Event participants' />}
|
||||
onClose={onClickClose}
|
||||
>
|
||||
{body}
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default EventParticipantsModal;
|
|
@ -15,7 +15,7 @@ const messages = defineMessages({
|
|||
|
||||
interface IUnauthorizedModal {
|
||||
/** Unauthorized action type. */
|
||||
action: 'FOLLOW' | 'REPLY' | 'REBLOG' | 'FAVOURITE' | 'POLL_VOTE',
|
||||
action: 'FOLLOW' | 'REPLY' | 'REBLOG' | 'FAVOURITE' | 'POLL_VOTE' | 'JOIN',
|
||||
/** Close event handler. */
|
||||
onClose: (modalType: string) => void,
|
||||
/** ActivityPub ID of the account OR status being acted upon. */
|
||||
|
@ -89,6 +89,9 @@ const UnauthorizedModal: React.FC<IUnauthorizedModal> = ({ action, onClose, acco
|
|||
} else if (action === 'POLL_VOTE') {
|
||||
header = <FormattedMessage id='remote_interaction.poll_vote_title' defaultMessage='Vote in a poll remotely' />;
|
||||
button = <FormattedMessage id='remote_interaction.poll_vote' defaultMessage='Proceed to vote' />;
|
||||
} else if (action === 'JOIN') {
|
||||
header = <FormattedMessage id='remote_interaction.event_join_title' defaultMessage='Join an event remotely' />;
|
||||
button = <FormattedMessage id='remote_interaction.event_join' defaultMessage='Proceed to join' />;
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
|
@ -525,3 +525,11 @@ export function EventInformation() {
|
|||
export function EventDiscussion() {
|
||||
return import(/* webpackChunkName: "features/event" */'../../event/event-discussion');
|
||||
}
|
||||
|
||||
export function EventMapModal() {
|
||||
return import(/* webpackChunkName: "modals/event-map-modal" */'../components/modals/event-map-modal');
|
||||
}
|
||||
|
||||
export function EventParticipantsModal() {
|
||||
return import(/* webpackChunkName: "modals/event-participants-modal" */'../components/modals/event-participants-modal');
|
||||
}
|
||||
|
|
|
@ -161,7 +161,7 @@
|
|||
"bundle_column_error.retry": "Try again",
|
||||
"bundle_column_error.title": "Network error",
|
||||
"bundle_modal_error.close": "Close",
|
||||
"bundle_modal_error.message": "Something went wrong while loading this page.",
|
||||
"bundle_modal_error.message": "Something went wrong while loading this modal.",
|
||||
"bundle_modal_error.retry": "Try again",
|
||||
"card.back.label": "Back",
|
||||
"chat_box.actions.send": "Send",
|
||||
|
|
|
@ -161,7 +161,7 @@
|
|||
"bundle_column_error.retry": "Опитай отново",
|
||||
"bundle_column_error.title": "Network error",
|
||||
"bundle_modal_error.close": "Close",
|
||||
"bundle_modal_error.message": "Something went wrong while loading this page.",
|
||||
"bundle_modal_error.message": "Something went wrong while loading this modal.",
|
||||
"bundle_modal_error.retry": "Try again",
|
||||
"card.back.label": "Back",
|
||||
"chat_box.actions.send": "Send",
|
||||
|
|
|
@ -161,7 +161,7 @@
|
|||
"bundle_column_error.retry": "Klask endro",
|
||||
"bundle_column_error.title": "Fazi rouedad",
|
||||
"bundle_modal_error.close": "Serriñ",
|
||||
"bundle_modal_error.message": "Something went wrong while loading this page.",
|
||||
"bundle_modal_error.message": "Something went wrong while loading this modal.",
|
||||
"bundle_modal_error.retry": "Klask endro",
|
||||
"card.back.label": "Back",
|
||||
"chat_box.actions.send": "Send",
|
||||
|
|
|
@ -161,7 +161,7 @@
|
|||
"bundle_column_error.retry": "Try again",
|
||||
"bundle_column_error.title": "Network error",
|
||||
"bundle_modal_error.close": "Close",
|
||||
"bundle_modal_error.message": "Something went wrong while loading this page.",
|
||||
"bundle_modal_error.message": "Something went wrong while loading this modal.",
|
||||
"bundle_modal_error.retry": "Try again",
|
||||
"card.back.label": "Back",
|
||||
"chat_box.actions.send": "Send",
|
||||
|
|
|
@ -161,7 +161,7 @@
|
|||
"bundle_column_error.retry": "Try again",
|
||||
"bundle_column_error.title": "Network error",
|
||||
"bundle_modal_error.close": "Close",
|
||||
"bundle_modal_error.message": "Something went wrong while loading this page.",
|
||||
"bundle_modal_error.message": "Something went wrong while loading this modal.",
|
||||
"bundle_modal_error.retry": "Try again",
|
||||
"card.back.label": "Back",
|
||||
"chat_box.actions.send": "Send",
|
||||
|
|
|
@ -161,7 +161,7 @@
|
|||
"bundle_column_error.retry": "Try again",
|
||||
"bundle_column_error.title": "Network error",
|
||||
"bundle_modal_error.close": "Close",
|
||||
"bundle_modal_error.message": "Something went wrong while loading this page.",
|
||||
"bundle_modal_error.message": "Something went wrong while loading this modal.",
|
||||
"bundle_modal_error.retry": "Try again",
|
||||
"card.back.label": "Back",
|
||||
"chat_box.actions.send": "Send",
|
||||
|
|
|
@ -161,7 +161,7 @@
|
|||
"bundle_column_error.retry": "Try again",
|
||||
"bundle_column_error.title": "Network error",
|
||||
"bundle_modal_error.close": "Close",
|
||||
"bundle_modal_error.message": "Something went wrong while loading this page.",
|
||||
"bundle_modal_error.message": "Something went wrong while loading this modal.",
|
||||
"bundle_modal_error.retry": "Try again",
|
||||
"card.back.label": "Back",
|
||||
"chat_box.actions.send": "Send",
|
||||
|
|
|
@ -161,7 +161,7 @@
|
|||
"bundle_column_error.retry": "Try again",
|
||||
"bundle_column_error.title": "Network error",
|
||||
"bundle_modal_error.close": "Close",
|
||||
"bundle_modal_error.message": "Something went wrong while loading this page.",
|
||||
"bundle_modal_error.message": "Something went wrong while loading this modal.",
|
||||
"bundle_modal_error.retry": "Try again",
|
||||
"card.back.label": "Back",
|
||||
"chat_box.actions.send": "Send",
|
||||
|
|
|
@ -161,7 +161,7 @@
|
|||
"bundle_column_error.retry": "Try again",
|
||||
"bundle_column_error.title": "Network error",
|
||||
"bundle_modal_error.close": "Close",
|
||||
"bundle_modal_error.message": "Something went wrong while loading this page.",
|
||||
"bundle_modal_error.message": "Something went wrong while loading this modal.",
|
||||
"bundle_modal_error.retry": "Try again",
|
||||
"card.back.label": "Back",
|
||||
"chat_box.actions.send": "Send",
|
||||
|
|
|
@ -161,7 +161,7 @@
|
|||
"bundle_column_error.retry": "Try again",
|
||||
"bundle_column_error.title": "Network error",
|
||||
"bundle_modal_error.close": "Close",
|
||||
"bundle_modal_error.message": "Something went wrong while loading this page.",
|
||||
"bundle_modal_error.message": "Something went wrong while loading this modal.",
|
||||
"bundle_modal_error.retry": "Try again",
|
||||
"card.back.label": "Back",
|
||||
"chat_box.actions.send": "Send",
|
||||
|
|
|
@ -261,12 +261,12 @@ export default function statuses(state = initialState, action: AnyAction): State
|
|||
case TIMELINE_DELETE:
|
||||
return deleteStatus(state, action.id, action.references);
|
||||
case EVENT_JOIN_REQUEST:
|
||||
return state.setIn([action.status.get('id'), 'event', 'join_state'], 'pending');
|
||||
return state.setIn([action.id, 'event', 'join_state'], 'pending');
|
||||
case EVENT_JOIN_FAIL:
|
||||
case EVENT_LEAVE_REQUEST:
|
||||
return state.setIn([action.status.get('id'), 'event', 'join_state'], null);
|
||||
return state.setIn([action.id, 'event', 'join_state'], null);
|
||||
case EVENT_LEAVE_FAIL:
|
||||
return state.setIn([action.status.get('id'), 'event', 'join_state'], action.previousState);
|
||||
return state.setIn([action.id, 'event', 'join_state'], action.previousState);
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
|
|
|
@ -75,6 +75,7 @@
|
|||
"@types/escape-html": "^1.0.1",
|
||||
"@types/http-link-header": "^1.0.3",
|
||||
"@types/jest": "^28.1.4",
|
||||
"@types/leaflet": "^1.8.0",
|
||||
"@types/lodash": "^4.14.180",
|
||||
"@types/object-assign": "^4.0.30",
|
||||
"@types/object-fit-images": "^3.2.3",
|
||||
|
@ -135,6 +136,7 @@
|
|||
"intl-pluralrules": "^1.3.1",
|
||||
"is-nan": "^1.2.1",
|
||||
"jsdoc": "~3.6.7",
|
||||
"leaflet": "^1.8.0",
|
||||
"libphonenumber-js": "^1.10.8",
|
||||
"line-awesome": "^1.3.0",
|
||||
"localforage": "^1.10.0",
|
||||
|
|
|
@ -11,6 +11,7 @@ module.exports = [{
|
|||
include: [
|
||||
resolve('app', 'images'),
|
||||
resolve('node_modules', 'emoji-datasource'),
|
||||
resolve('node_modules', 'leaflet'),
|
||||
],
|
||||
generator: {
|
||||
filename: 'packs/images/[name]-[contenthash:8][ext]',
|
||||
|
|
17
yarn.lock
17
yarn.lock
|
@ -2527,6 +2527,11 @@
|
|||
dependencies:
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/geojson@*":
|
||||
version "7946.0.10"
|
||||
resolved "https://registry.yarnpkg.com/@types/geojson/-/geojson-7946.0.10.tgz#6dfbf5ea17142f7f9a043809f1cd4c448cb68249"
|
||||
integrity sha512-Nmh0K3iWQJzniTuPRcJn5hxXkfB1T1pgB89SBig5PlJQU5yocazeu4jATJlaA0GYFKWMqDdvYemoSnF2pXgLVA==
|
||||
|
||||
"@types/graceful-fs@^4.1.3":
|
||||
version "4.1.5"
|
||||
resolved "https://registry.yarnpkg.com/@types/graceful-fs/-/graceful-fs-4.1.5.tgz#21ffba0d98da4350db64891f92a9e5db3cdb4e15"
|
||||
|
@ -2635,6 +2640,13 @@
|
|||
resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee"
|
||||
integrity sha1-7ihweulOEdK4J7y+UnC86n8+ce4=
|
||||
|
||||
"@types/leaflet@^1.8.0":
|
||||
version "1.8.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/leaflet/-/leaflet-1.8.0.tgz#dc92d3e868fb6d5067b4b59fa08cd4441f84fabe"
|
||||
integrity sha512-+sXFmiJTFdhaXXIGFlV5re9AdqtAODoXbGAvxx02e5SHXL3ir7ClP5J7pahO8VmzKY3dth4RUS1nf2BTT+DW1A==
|
||||
dependencies:
|
||||
"@types/geojson" "*"
|
||||
|
||||
"@types/lodash@^4.14.180":
|
||||
version "4.14.180"
|
||||
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.180.tgz#4ab7c9ddfc92ec4a887886483bc14c79fb380670"
|
||||
|
@ -7823,6 +7835,11 @@ language-tags@^1.0.5:
|
|||
dependencies:
|
||||
language-subtag-registry "~0.3.2"
|
||||
|
||||
leaflet@^1.8.0:
|
||||
version "1.8.0"
|
||||
resolved "https://registry.yarnpkg.com/leaflet/-/leaflet-1.8.0.tgz#4615db4a22a304e8e692cae9270b983b38a2055e"
|
||||
integrity sha512-gwhMjFCQiYs3x/Sf+d49f10ERXaEFCPr+nVTryhAW8DWbMGqJqt9G4XuIaHmFW08zYvhgdzqXGr8AlW8v8dQkA==
|
||||
|
||||
leven@^3.1.0:
|
||||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/leven/-/leven-3.1.0.tgz#77891de834064cccba82ae7842bb6b14a13ed7f2"
|
||||
|
|
Loading…
Reference in a new issue