@@ -59,7 +59,7 @@ const ReportStatus: React.FC<IReportStatus> = ({ status }) => {
|
||||
const video = firstAttachment;
|
||||
|
||||
return (
|
||||
<Bundle fetchComponent={Video} >
|
||||
<Bundle fetchComponent={Video}>
|
||||
{(Component: any) => (
|
||||
<Component
|
||||
preview={video.preview_url}
|
||||
|
||||
@@ -6,25 +6,12 @@ import { getSubscribersCsv, getUnsubscribersCsv, getCombinedCsv } from 'soapbox/
|
||||
import { Text } from 'soapbox/components/ui';
|
||||
import { useAppSelector, useAppDispatch, useOwnAccount, useFeatures } from 'soapbox/hooks';
|
||||
import sourceCode from 'soapbox/utils/code';
|
||||
import { download } from 'soapbox/utils/download';
|
||||
import { parseVersion } from 'soapbox/utils/features';
|
||||
import { isNumber } from 'soapbox/utils/numbers';
|
||||
|
||||
import RegistrationModePicker from '../components/registration_mode_picker';
|
||||
|
||||
import type { AxiosResponse } from 'axios';
|
||||
|
||||
/** Download the file from the response instead of opening it in a tab. */
|
||||
// https://stackoverflow.com/a/53230807
|
||||
const download = (response: AxiosResponse, filename: string) => {
|
||||
const url = URL.createObjectURL(new Blob([response.data]));
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.setAttribute('download', filename);
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
link.remove();
|
||||
};
|
||||
|
||||
const Dashboard: React.FC = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const instance = useAppSelector(state => state.instance);
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
import React from 'react';
|
||||
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 type { Status as StatusEntity } from 'soapbox/types/entities';
|
||||
|
||||
const messages = defineMessages({
|
||||
leaveConfirm: { id: 'confirmations.leave_event.confirm', defaultMessage: 'Leave event' },
|
||||
leaveMessage: { id: 'confirmations.leave_event.message', defaultMessage: 'If you want to rejoin the event, the request will be manually reviewed again. Are you sure you want to proceed?' },
|
||||
});
|
||||
|
||||
interface IEventAction {
|
||||
status: StatusEntity,
|
||||
}
|
||||
|
||||
const EventActionButton: React.FC<IEventAction> = ({ status }) => {
|
||||
const intl = useIntl();
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const event = status.event!;
|
||||
|
||||
const handleJoin: React.EventHandler<React.MouseEvent> = (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (event.join_mode === 'free') {
|
||||
dispatch(joinEvent(status.id));
|
||||
} else {
|
||||
dispatch(openModal('JOIN_EVENT', {
|
||||
statusId: status.id,
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
const handleLeave: React.EventHandler<React.MouseEvent> = (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (event.join_mode === 'restricted') {
|
||||
dispatch(openModal('CONFIRM', {
|
||||
message: intl.formatMessage(messages.leaveMessage),
|
||||
confirm: intl.formatMessage(messages.leaveConfirm),
|
||||
onConfirm: () => dispatch(leaveEvent(status.id)),
|
||||
}));
|
||||
} else {
|
||||
dispatch(leaveEvent(status.id));
|
||||
}
|
||||
};
|
||||
|
||||
let buttonLabel;
|
||||
let buttonIcon;
|
||||
let buttonDisabled = false;
|
||||
let buttonAction = handleLeave;
|
||||
|
||||
switch (event.join_state) {
|
||||
case 'accept':
|
||||
buttonLabel = <FormattedMessage id='event.join_state.accept' defaultMessage='Going' />;
|
||||
buttonIcon = require('@tabler/icons/check.svg');
|
||||
break;
|
||||
case 'pending':
|
||||
buttonLabel = <FormattedMessage id='event.join_state.pending' defaultMessage='Pending' />;
|
||||
break;
|
||||
case 'reject':
|
||||
buttonLabel = <FormattedMessage id='event.join_state.rejected' defaultMessage='Going' />;
|
||||
buttonIcon = require('@tabler/icons/ban.svg');
|
||||
buttonDisabled = true;
|
||||
break;
|
||||
default:
|
||||
buttonLabel = <FormattedMessage id='event.join_state.empty' defaultMessage='Participate' />;
|
||||
buttonAction = handleJoin;
|
||||
}
|
||||
|
||||
return (
|
||||
<Button
|
||||
size='sm'
|
||||
theme='secondary'
|
||||
icon={buttonIcon}
|
||||
onClick={buttonAction}
|
||||
disabled={buttonDisabled}
|
||||
>
|
||||
{buttonLabel}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
export default EventActionButton;
|
||||
58
app/soapbox/features/event/components/event-date.tsx
Normal file
58
app/soapbox/features/event/components/event-date.tsx
Normal file
@@ -0,0 +1,58 @@
|
||||
import React from 'react';
|
||||
import { FormattedDate } from 'react-intl';
|
||||
|
||||
import Icon from 'soapbox/components/icon';
|
||||
import { HStack } from 'soapbox/components/ui';
|
||||
|
||||
import type { Status as StatusEntity } from 'soapbox/types/entities';
|
||||
|
||||
interface IEventDate {
|
||||
status: StatusEntity,
|
||||
}
|
||||
|
||||
const EventDate: React.FC<IEventDate> = ({ status }) => {
|
||||
const event = status.event!;
|
||||
|
||||
if (!event.start_time) return null;
|
||||
|
||||
const startDate = new Date(event.start_time);
|
||||
|
||||
let date;
|
||||
|
||||
if (event.end_time) {
|
||||
const endDate = new Date(event.end_time);
|
||||
|
||||
const sameDay = startDate.getDate() === endDate.getDate() && startDate.getMonth() === endDate.getMonth() && startDate.getFullYear() === endDate.getFullYear();
|
||||
|
||||
if (sameDay) {
|
||||
date = (
|
||||
<>
|
||||
<FormattedDate value={event.start_time} year='2-digit' month='short' day='2-digit' weekday='short' hour='2-digit' minute='2-digit' />
|
||||
{' - '}
|
||||
<FormattedDate value={event.end_time} hour='2-digit' minute='2-digit' />
|
||||
</>
|
||||
);
|
||||
} else {
|
||||
date = (
|
||||
<>
|
||||
<FormattedDate value={event.start_time} year='2-digit' month='short' day='2-digit' weekday='short' />
|
||||
{' - '}
|
||||
<FormattedDate value={event.end_time} year='2-digit' month='short' day='2-digit' weekday='short' />
|
||||
</>
|
||||
);
|
||||
}
|
||||
} else {
|
||||
date = (
|
||||
<FormattedDate value={event.start_time} year='2-digit' month='short' day='2-digit' weekday='short' hour='2-digit' minute='2-digit' />
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<HStack alignItems='center' space={2}>
|
||||
<Icon src={require('@tabler/icons/calendar.svg')} />
|
||||
<span>{date}</span>
|
||||
</HStack>
|
||||
);
|
||||
};
|
||||
|
||||
export default EventDate;
|
||||
163
app/soapbox/features/event/components/event-header.tsx
Normal file
163
app/soapbox/features/event/components/event-header.tsx
Normal file
@@ -0,0 +1,163 @@
|
||||
import React from 'react';
|
||||
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import { fetchEventIcs } from 'soapbox/actions/events';
|
||||
import { openModal } from 'soapbox/actions/modals';
|
||||
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 SvgIcon from 'soapbox/components/ui/icon/svg-icon';
|
||||
import VerificationBadge from 'soapbox/components/verification_badge';
|
||||
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
|
||||
import { download } from 'soapbox/utils/download';
|
||||
|
||||
import PlaceholderEventHeader from '../../placeholder/components/placeholder_event_header';
|
||||
import EventActionButton from '../components/event-action-button';
|
||||
import EventDate from '../components/event-date';
|
||||
|
||||
import type { Menu as MenuType } from 'soapbox/components/dropdown_menu';
|
||||
import type { Account as AccountEntity, Status as StatusEntity } from 'soapbox/types/entities';
|
||||
|
||||
const messages = defineMessages({
|
||||
bannerHeader: { id: 'event.banner', defaultMessage: 'Event banner' },
|
||||
});
|
||||
|
||||
interface IEventHeader {
|
||||
status?: StatusEntity,
|
||||
}
|
||||
|
||||
const EventHeader: React.FC<IEventHeader> = ({ status }) => {
|
||||
const intl = useIntl();
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const me = useAppSelector(state => state.me);
|
||||
|
||||
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>
|
||||
|
||||
<PlaceholderEventHeader />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const account = status.account as AccountEntity;
|
||||
const event = status.event;
|
||||
const banner = status.media_attachments?.find(({ description }) => description === 'Banner');
|
||||
|
||||
const handleHeaderClick: React.MouseEventHandler<HTMLAnchorElement> = (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const index = status.media_attachments!.findIndex(({ description }) => description === 'Banner');
|
||||
dispatch(openModal('MEDIA', { media: status.media_attachments, index }));
|
||||
};
|
||||
|
||||
const handleExportClick: React.MouseEventHandler = e => {
|
||||
dispatch(fetchEventIcs(status.id)).then((response) => {
|
||||
download(response, 'calendar.ics');
|
||||
}).catch(() => {});
|
||||
e.preventDefault();
|
||||
};
|
||||
|
||||
const menu: MenuType = [
|
||||
{
|
||||
text: 'Export to your calendar',
|
||||
action: handleExportClick,
|
||||
icon: require('@tabler/icons/calendar-plus.svg'),
|
||||
},
|
||||
];
|
||||
|
||||
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'>
|
||||
{banner && (
|
||||
<a href={banner.url} onClick={handleHeaderClick} target='_blank'>
|
||||
<StillImage
|
||||
src={banner.url}
|
||||
alt={intl.formatMessage(messages.bannerHeader)}
|
||||
className='absolute inset-0 object-cover md:rounded-t-xl'
|
||||
/>
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<Stack space={2}>
|
||||
<HStack className='w-full' alignItems='start' space={2}>
|
||||
<Text className='flex-grow' size='lg' weight='bold'>{event.name}</Text>
|
||||
<Menu>
|
||||
<MenuButton
|
||||
as={IconButton}
|
||||
src={require('@tabler/icons/dots.svg')}
|
||||
theme='outlined'
|
||||
className='px-2 h-[30px]'
|
||||
iconClassName='w-4 h-4'
|
||||
children={null}
|
||||
/>
|
||||
|
||||
<MenuList>
|
||||
{menu.map((menuItem, idx) => {
|
||||
if (typeof menuItem?.text === 'undefined') {
|
||||
return <MenuDivider key={idx} />;
|
||||
} else {
|
||||
const Comp = (menuItem.action ? MenuItem : MenuLink) as any;
|
||||
const itemProps = menuItem.action ? { onSelect: menuItem.action } : { to: menuItem.to, as: Link, target: menuItem.newTab ? '_blank' : '_self' };
|
||||
|
||||
return (
|
||||
<Comp key={idx} {...itemProps} className='group'>
|
||||
<div className='flex items-center'>
|
||||
{menuItem.icon && (
|
||||
<SvgIcon src={menuItem.icon} className='mr-3 h-5 w-5 text-gray-400 flex-none group-hover:text-gray-500' />
|
||||
)}
|
||||
|
||||
<div className='truncate'>{menuItem.text}</div>
|
||||
</div>
|
||||
</Comp>
|
||||
);
|
||||
}
|
||||
})}
|
||||
</MenuList>
|
||||
</Menu>
|
||||
{account.id !== me && <EventActionButton status={status} />}
|
||||
</HStack>
|
||||
|
||||
<Stack space={1}>
|
||||
<HStack alignItems='center' space={2}>
|
||||
<Icon src={require('@tabler/icons/user.svg')} />
|
||||
<span>
|
||||
<FormattedMessage
|
||||
id='event.organized_by'
|
||||
defaultMessage='Organized by {name}'
|
||||
values={{
|
||||
name: (
|
||||
<Link className='mention' to={`/@${account.acct}`}>
|
||||
<span dangerouslySetInnerHTML={{ __html: account.display_name_html }} />
|
||||
{account.verified && <VerificationBadge />}
|
||||
</Link>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
</HStack>
|
||||
|
||||
<EventDate status={status} />
|
||||
|
||||
{event.location && (
|
||||
<HStack alignItems='center' space={2}>
|
||||
<Icon src={require('@tabler/icons/map-pin.svg')} />
|
||||
<span>
|
||||
{event.location.get('name')}
|
||||
</span>
|
||||
</HStack>
|
||||
)}
|
||||
</Stack>
|
||||
</Stack>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default EventHeader;
|
||||
227
app/soapbox/features/event/event-discussion.tsx
Normal file
227
app/soapbox/features/event/event-discussion.tsx
Normal file
@@ -0,0 +1,227 @@
|
||||
import { List as ImmutableList, OrderedSet as ImmutableOrderedSet } from 'immutable';
|
||||
import debounce from 'lodash/debounce';
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { createSelector } from 'reselect';
|
||||
|
||||
import { fetchStatusWithContext, fetchNext } from 'soapbox/actions/statuses';
|
||||
import MissingIndicator from 'soapbox/components/missing_indicator';
|
||||
import PullToRefresh from 'soapbox/components/pull-to-refresh';
|
||||
import ScrollableList from 'soapbox/components/scrollable_list';
|
||||
import Tombstone from 'soapbox/components/tombstone';
|
||||
import { Stack } from 'soapbox/components/ui';
|
||||
import PlaceholderStatus from 'soapbox/features/placeholder/components/placeholder_status';
|
||||
import PendingStatus from 'soapbox/features/ui/components/pending_status';
|
||||
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
|
||||
import { makeGetStatus } from 'soapbox/selectors';
|
||||
|
||||
import ThreadStatus from '../status/components/thread-status';
|
||||
|
||||
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,
|
||||
], (statusId, contextReplies) => {
|
||||
let descendantsIds = ImmutableOrderedSet<string>();
|
||||
const ids = [statusId];
|
||||
|
||||
while (ids.length > 0) {
|
||||
const id = ids.shift();
|
||||
if (!id) break;
|
||||
|
||||
const replies = contextReplies.get(id);
|
||||
|
||||
if (descendantsIds.includes(id)) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (statusId !== id) {
|
||||
descendantsIds = descendantsIds.union([id]);
|
||||
}
|
||||
|
||||
if (replies) {
|
||||
replies.reverse().forEach((reply: string) => {
|
||||
ids.unshift(reply);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return descendantsIds;
|
||||
});
|
||||
|
||||
type RouteParams = { statusId: string };
|
||||
|
||||
interface IThread {
|
||||
params: RouteParams,
|
||||
onOpenMedia: (media: ImmutableList<AttachmentEntity>, index: number) => void,
|
||||
onOpenVideo: (video: AttachmentEntity, time: number) => void,
|
||||
}
|
||||
|
||||
const Thread: React.FC<IThread> = (props) => {
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const status = useAppSelector(state => getStatus(state, { id: props.params.statusId }));
|
||||
|
||||
const descendantsIds = useAppSelector(state => {
|
||||
let descendantsIds = ImmutableOrderedSet<string>();
|
||||
|
||||
if (status) {
|
||||
const statusId = status.id;
|
||||
descendantsIds = getDescendantsIds(state, statusId);
|
||||
descendantsIds = descendantsIds.delete(statusId);
|
||||
}
|
||||
|
||||
return descendantsIds;
|
||||
});
|
||||
|
||||
const [isLoaded, setIsLoaded] = useState<boolean>(!!status);
|
||||
const [next, setNext] = useState<string>();
|
||||
|
||||
const node = useRef<HTMLDivElement>(null);
|
||||
const scroller = useRef<VirtuosoHandle>(null);
|
||||
|
||||
/** Fetch the status (and context) from the API. */
|
||||
const fetchData = async() => {
|
||||
const { params } = props;
|
||||
const { statusId } = params;
|
||||
const { next } = await dispatch(fetchStatusWithContext(statusId));
|
||||
setNext(next);
|
||||
};
|
||||
|
||||
// Load data.
|
||||
useEffect(() => {
|
||||
fetchData().then(() => {
|
||||
setIsLoaded(true);
|
||||
}).catch(() => {
|
||||
setIsLoaded(true);
|
||||
});
|
||||
}, [props.params.statusId]);
|
||||
|
||||
const handleMoveUp = (id: string) => {
|
||||
const index = ImmutableList(descendantsIds).indexOf(id);
|
||||
_selectChild(index - 1);
|
||||
};
|
||||
|
||||
const handleMoveDown = (id: string) => {
|
||||
const index = ImmutableList(descendantsIds).indexOf(id);
|
||||
_selectChild(index + 1);
|
||||
};
|
||||
|
||||
const _selectChild = (index: number) => {
|
||||
scroller.current?.scrollIntoView({
|
||||
index,
|
||||
behavior: 'smooth',
|
||||
done: () => {
|
||||
const element = document.querySelector<HTMLDivElement>(`#thread [data-index="${index}"] .focusable`);
|
||||
|
||||
if (element) {
|
||||
element.focus();
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const renderTombstone = (id: string) => {
|
||||
return (
|
||||
<div className='py-4 pb-8'>
|
||||
<Tombstone
|
||||
key={id}
|
||||
id={id}
|
||||
onMoveUp={handleMoveUp}
|
||||
onMoveDown={handleMoveDown}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const renderStatus = (id: string) => {
|
||||
return (
|
||||
<ThreadStatus
|
||||
key={id}
|
||||
id={id}
|
||||
focusedStatusId={status!.id}
|
||||
onMoveUp={handleMoveUp}
|
||||
onMoveDown={handleMoveDown}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const renderPendingStatus = (id: string) => {
|
||||
const idempotencyKey = id.replace(/^末pending-/, '');
|
||||
|
||||
return (
|
||||
<PendingStatus
|
||||
key={id}
|
||||
idempotencyKey={idempotencyKey}
|
||||
thread
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const renderChildren = (list: ImmutableOrderedSet<string>) => {
|
||||
return list.map(id => {
|
||||
if (id.endsWith('-tombstone')) {
|
||||
return renderTombstone(id);
|
||||
} else if (id.startsWith('末pending-')) {
|
||||
return renderPendingStatus(id);
|
||||
} else {
|
||||
return renderStatus(id);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const handleRefresh = () => {
|
||||
return fetchData();
|
||||
};
|
||||
|
||||
const handleLoadMore = useCallback(debounce(() => {
|
||||
if (next && status) {
|
||||
dispatch(fetchNext(status.id, next)).then(({ next }) => {
|
||||
setNext(next);
|
||||
}).catch(() => {});
|
||||
}
|
||||
}, 300, { leading: true }), [next, status]);
|
||||
|
||||
const hasDescendants = descendantsIds.size > 0;
|
||||
|
||||
if (!status && isLoaded) {
|
||||
return (
|
||||
<MissingIndicator />
|
||||
);
|
||||
} else if (!status) {
|
||||
return (
|
||||
<PlaceholderStatus />
|
||||
);
|
||||
}
|
||||
|
||||
const children: JSX.Element[] = [];
|
||||
|
||||
if (hasDescendants) {
|
||||
children.push(...renderChildren(descendantsIds).toArray());
|
||||
}
|
||||
|
||||
return (
|
||||
<PullToRefresh onRefresh={handleRefresh}>
|
||||
<Stack space={2}>
|
||||
<div ref={node} className='thread p-0 sm:p-2 shadow-none'>
|
||||
<ScrollableList
|
||||
id='thread'
|
||||
ref={scroller}
|
||||
hasMore={!!next}
|
||||
onLoadMore={handleLoadMore}
|
||||
placeholderComponent={() => <PlaceholderStatus thread />}
|
||||
initialTopMostItemIndex={0}
|
||||
>
|
||||
{children}
|
||||
</ScrollableList>
|
||||
</div>
|
||||
</Stack>
|
||||
</PullToRefresh>
|
||||
);
|
||||
};
|
||||
|
||||
export default Thread;
|
||||
69
app/soapbox/features/event/event-information.tsx
Normal file
69
app/soapbox/features/event/event-information.tsx
Normal file
@@ -0,0 +1,69 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
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 { 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 {
|
||||
params: RouteParams,
|
||||
}
|
||||
|
||||
const EventInformation: React.FC<IEventInformation> = ({ params }) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const status = useAppSelector(state => getStatus(state, { id: params.statusId })) as StatusEntity;
|
||||
|
||||
const settings = useSettings();
|
||||
const displayMedia = settings.get('displayMedia') as string;
|
||||
|
||||
const [isLoaded, setIsLoaded] = useState<boolean>(!!status);
|
||||
const [showMedia, setShowMedia] = useState<boolean>(defaultMediaVisibility(status, displayMedia));
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(fetchStatus(params.statusId)).then(() => {
|
||||
setIsLoaded(true);
|
||||
}).catch(() => {
|
||||
setIsLoaded(true);
|
||||
});
|
||||
|
||||
setShowMedia(defaultMediaVisibility(status, displayMedia));
|
||||
}, [params.statusId]);
|
||||
|
||||
const handleToggleMediaVisibility = (): void => {
|
||||
setShowMedia(!showMedia);
|
||||
};
|
||||
|
||||
if (!status && isLoaded) {
|
||||
return (
|
||||
<MissingIndicator />
|
||||
);
|
||||
} else if (!status) return null;
|
||||
|
||||
return (
|
||||
<Stack className='mt-4 sm:p-2' space={2}>
|
||||
<Text
|
||||
className='break-words status__content'
|
||||
size='sm'
|
||||
dangerouslySetInnerHTML={{ __html: status.contentHtml }}
|
||||
/>
|
||||
|
||||
<StatusMedia
|
||||
status={status}
|
||||
excludeBanner
|
||||
showMedia={showMedia}
|
||||
onToggleVisibility={handleToggleMediaVisibility}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default EventInformation;
|
||||
@@ -0,0 +1,26 @@
|
||||
import React from 'react';
|
||||
|
||||
import { Stack } from 'soapbox/components/ui';
|
||||
|
||||
import { generateText, randomIntFromInterval } from '../utils';
|
||||
|
||||
const PlaceholderEventHeader = () => {
|
||||
const eventNameLength = randomIntFromInterval(5, 25);
|
||||
const organizerNameLength = randomIntFromInterval(5, 30);
|
||||
const dateLength = randomIntFromInterval(5, 30);
|
||||
const locationLength = randomIntFromInterval(5, 30);
|
||||
|
||||
return (
|
||||
<Stack className='animate-pulse text-primary-50 dark:text-primary-800' space={2}>
|
||||
<p className='text-lg'>{generateText(eventNameLength)}</p>
|
||||
|
||||
<Stack space={1}>
|
||||
<p>{generateText(organizerNameLength)}</p>
|
||||
<p>{generateText(dateLength)}</p>
|
||||
<p>{generateText(locationLength)}</p>
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default PlaceholderEventHeader;
|
||||
@@ -35,7 +35,7 @@ const StatusCheckBox: React.FC<IStatusCheckBox> = ({ id, disabled }) => {
|
||||
|
||||
if (video) {
|
||||
media = (
|
||||
<Bundle fetchComponent={Video} >
|
||||
<Bundle fetchComponent={Video}>
|
||||
{(Component: any) => (
|
||||
<Component
|
||||
preview={video.preview_url}
|
||||
@@ -58,7 +58,7 @@ const StatusCheckBox: React.FC<IStatusCheckBox> = ({ id, disabled }) => {
|
||||
|
||||
if (audio) {
|
||||
media = (
|
||||
<Bundle fetchComponent={Audio} >
|
||||
<Bundle fetchComponent={Audio}>
|
||||
{(Component: any) => (
|
||||
<Component
|
||||
src={audio.url}
|
||||
@@ -73,7 +73,7 @@ const StatusCheckBox: React.FC<IStatusCheckBox> = ({ id, disabled }) => {
|
||||
}
|
||||
} else {
|
||||
media = (
|
||||
<Bundle fetchComponent={MediaGallery} >
|
||||
<Bundle fetchComponent={MediaGallery}>
|
||||
{(Component: any) => <Component media={status.media_attachments} sensitive={status.sensitive} height={110} onOpenMedia={noop} />}
|
||||
</Bundle>
|
||||
);
|
||||
|
||||
@@ -32,7 +32,7 @@ const AccountNoteModal: React.FC<IAccountNoteModal> = ({ statusId }) => {
|
||||
|
||||
const handleSubmit = () => {
|
||||
setIsSubmitting(true);
|
||||
dispatch(joinEvent(statusId, participationMessage))?.then(() => {
|
||||
dispatch(joinEvent(statusId, participationMessage)).then(() => {
|
||||
onClose();
|
||||
}).catch(() => {});
|
||||
};
|
||||
|
||||
@@ -29,6 +29,7 @@ import { Layout } from 'soapbox/components/ui';
|
||||
import { useAppSelector, useOwnAccount, useSoapboxConfig, useFeatures } from 'soapbox/hooks';
|
||||
import AdminPage from 'soapbox/pages/admin_page';
|
||||
import DefaultPage from 'soapbox/pages/default_page';
|
||||
import EventPage from 'soapbox/pages/event_page';
|
||||
// import GroupsPage from 'soapbox/pages/groups_page';
|
||||
// import GroupPage from 'soapbox/pages/group_page';
|
||||
import HomePage from 'soapbox/pages/home_page';
|
||||
@@ -113,6 +114,8 @@ import {
|
||||
TestTimeline,
|
||||
LogoutPage,
|
||||
AuthTokenList,
|
||||
EventInformation,
|
||||
EventDiscussion,
|
||||
} from './util/async-components';
|
||||
import { WrappedRoute } from './util/react_router_helpers';
|
||||
|
||||
@@ -280,6 +283,8 @@ const SwitchingColumnsArea: React.FC = ({ children }) => {
|
||||
<WrappedRoute path='/@:username/favorites' component={FavouritedStatuses} page={ProfilePage} content={children} />
|
||||
<WrappedRoute path='/@:username/pins' component={PinnedStatuses} page={ProfilePage} content={children} />
|
||||
<WrappedRoute path='/@:username/posts/:statusId' publicRoute exact page={StatusPage} component={Status} content={children} />
|
||||
<WrappedRoute path='/@:username/events/:statusId' publicRoute exact page={EventPage} component={EventInformation} content={children} />
|
||||
<WrappedRoute path='/@:username/events/:statusId/discussion' publicRoute exact page={EventPage} component={EventDiscussion} content={children} />
|
||||
<Redirect from='/@:username/:statusId' to='/@:username/posts/:statusId' />
|
||||
|
||||
<WrappedRoute path='/statuses/new' page={DefaultPage} component={NewStatus} content={children} exact />
|
||||
|
||||
@@ -533,3 +533,15 @@ export function CreateEventModal() {
|
||||
export function JoinEventModal() {
|
||||
return import(/* webpackChunkName: "features/join_event_modal" */'../components/modals/join-event-modal');
|
||||
}
|
||||
|
||||
export function EventHeader() {
|
||||
return import(/* webpackChunkName: "features/event" */'../../event/components/event-header');
|
||||
}
|
||||
|
||||
export function EventInformation() {
|
||||
return import(/* webpackChunkName: "features/event" */'../../event/event-information');
|
||||
}
|
||||
|
||||
export function EventDiscussion() {
|
||||
return import(/* webpackChunkName: "features/event" */'../../event/event-discussion');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user