Information page, improvements

Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
environments/review-events-5jp5it/deployments/1372
marcin mikołajczak 2022-09-21 23:27:53 +02:00
rodzic 309bd2c34f
commit fe7333ddb0
28 zmienionych plików z 478 dodań i 75 usunięć

Wyświetl plik

@ -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,
});

Wyświetl plik

@ -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();

Wyświetl plik

@ -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',
};

Wyświetl plik

@ -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 (

Wyświetl plik

@ -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 && (

Wyświetl plik

@ -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'

Wyświetl plik

@ -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>
);
};

Wyświetl plik

@ -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)}
/>
);
};

Wyświetl plik

@ -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'>

Wyświetl plik

@ -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 {

Wyświetl plik

@ -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;

Wyświetl plik

@ -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;

Wyświetl plik

@ -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 (

Wyświetl plik

@ -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');
}

Wyświetl plik

@ -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",

Wyświetl plik

@ -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",

Wyświetl plik

@ -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",

Wyświetl plik

@ -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",

Wyświetl plik

@ -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",

Wyświetl plik

@ -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",

Wyświetl plik

@ -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",

Wyświetl plik

@ -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",

Wyświetl plik

@ -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",

Wyświetl plik

@ -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",

Wyświetl plik

@ -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;
}

Wyświetl plik

@ -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",

Wyświetl plik

@ -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]',

Wyświetl plik

@ -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"