Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
This commit is contained in:
marcin mikołajczak 2022-11-27 23:29:45 +01:00
parent c61dcddd81
commit 6465423ccb
10 changed files with 81 additions and 56 deletions

View File

@ -19,12 +19,13 @@ const messages = defineMessages({
}); });
interface IEventPreview { interface IEventPreview {
status: StatusEntity, status: StatusEntity
className?: string, className?: string
hideAction?: boolean; hideAction?: boolean
floatingAction?: boolean
} }
const EventPreview: React.FC<IEventPreview> = ({ status, className, hideAction }) => { const EventPreview: React.FC<IEventPreview> = ({ status, className, hideAction, floatingAction = true }) => {
const intl = useIntl(); const intl = useIntl();
const me = useAppSelector((state) => state.me); const me = useAppSelector((state) => state.me);
@ -32,26 +33,37 @@ const EventPreview: React.FC<IEventPreview> = ({ status, className, hideAction }
const account = status.account as AccountEntity; const account = status.account as AccountEntity;
const event = status.event!; const event = status.event!;
const banner = status.media_attachments?.find(({ description }) => description === 'Banner'); const banner = event.banner;
return ( const action = !hideAction && (account.id === me ? (
<div className={classNames('w-full rounded-lg bg-gray-100 dark:bg-primary-800 relative overflow-hidden', className)}>
<div className='absolute top-28 right-3'>
{!hideAction && (account.id === me ? (
<Button <Button
size='sm' size='sm'
theme='secondary' theme={floatingAction ? 'secondary' : 'primary'}
to={`/@${account.acct}/events/${status.id}`} to={`/@${account.acct}/events/${status.id}`}
> >
<FormattedMessage id='event.manage' defaultMessage='Manage' /> <FormattedMessage id='event.manage' defaultMessage='Manage' />
</Button> </Button>
) : <EventActionButton status={status} />)} ) : (
<EventActionButton
status={status}
theme={floatingAction ? 'secondary' : 'primary'}
/>
));
return (
<div className={classNames('w-full rounded-lg bg-gray-100 dark:bg-primary-800 relative overflow-hidden', className)}>
<div className='absolute top-28 right-3'>
{floatingAction && action}
</div> </div>
<div className='bg-primary-200 dark:bg-gray-600 h-40'> <div className='bg-primary-200 dark:bg-gray-600 h-40'>
{banner && <img className='h-full w-full object-cover' src={banner.url} alt={intl.formatMessage(messages.bannerHeader)} />} {banner && <img className='h-full w-full object-cover' src={banner.url} alt={intl.formatMessage(messages.bannerHeader)} />}
</div> </div>
<Stack className='p-2.5' space={2}> <Stack className='p-2.5' space={2}>
<Text weight='semibold'>{event.name}</Text> <HStack space={2} alignItems='center' justifyContent='between'>
<Text weight='semibold' truncate>{event.name}</Text>
{!floatingAction && action}
</HStack>
<div className='flex gap-y-1 gap-x-2 flex-wrap text-gray-700 dark:text-gray-600'> <div className='flex gap-y-1 gap-x-2 flex-wrap text-gray-700 dark:text-gray-600'>
<HStack alignItems='center' space={2}> <HStack alignItems='center' space={2}>

View File

@ -24,8 +24,6 @@ interface IStatusMedia {
showMedia?: boolean, showMedia?: boolean,
/** Callback when visibility is toggled (eg clicked through NSFW). */ /** Callback when visibility is toggled (eg clicked through NSFW). */
onToggleVisibility?: () => void, onToggleVisibility?: () => void,
/** Whether or not to hide image describer as 'Banner' */
excludeBanner?: boolean,
} }
/** Render media attachments for a status. */ /** Render media attachments for a status. */
@ -35,7 +33,6 @@ const StatusMedia: React.FC<IStatusMedia> = ({
onClick, onClick,
showMedia = true, showMedia = true,
onToggleVisibility = () => { }, onToggleVisibility = () => { },
excludeBanner = false,
}) => { }) => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const settings = useSettings(); const settings = useSettings();
@ -43,10 +40,8 @@ const StatusMedia: React.FC<IStatusMedia> = ({
const [mediaWrapperWidth, setMediaWrapperWidth] = useState<number | undefined>(undefined); const [mediaWrapperWidth, setMediaWrapperWidth] = useState<number | undefined>(undefined);
const mediaAttachments = excludeBanner ? status.media_attachments.filter(({ description, pleroma }) => description !== 'Banner' && pleroma.get('mime_type') !== 'text/html') : status.media_attachments; const size = status.media_attachments.size;
const firstAttachment = status.media_attachments.first();
const size = mediaAttachments.size;
const firstAttachment = mediaAttachments.first();
let media: JSX.Element | null = null; let media: JSX.Element | null = null;
@ -76,7 +71,7 @@ const StatusMedia: React.FC<IStatusMedia> = ({
if (muted) { if (muted) {
media = ( media = (
<AttachmentThumbs <AttachmentThumbs
media={mediaAttachments} media={status.media_attachments}
onClick={onClick} onClick={onClick}
sensitive={status.sensitive} sensitive={status.sensitive}
/> />
@ -147,7 +142,7 @@ const StatusMedia: React.FC<IStatusMedia> = ({
<Bundle fetchComponent={MediaGallery} loading={renderLoadingMediaGallery}> <Bundle fetchComponent={MediaGallery} loading={renderLoadingMediaGallery}>
{(Component: any) => ( {(Component: any) => (
<Component <Component
media={mediaAttachments} media={status.media_attachments}
sensitive={status.sensitive} sensitive={status.sensitive}
height={285} height={285}
onOpenMedia={openMedia} onOpenMedia={openMedia}

View File

@ -6,6 +6,7 @@ import { openModal } from 'soapbox/actions/modals';
import { Button } from 'soapbox/components/ui'; import { Button } from 'soapbox/components/ui';
import { useAppDispatch, useAppSelector } from 'soapbox/hooks'; import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
import type { ButtonThemes } from 'soapbox/components/ui/button/useButtonStyles';
import type { Status as StatusEntity } from 'soapbox/types/entities'; import type { Status as StatusEntity } from 'soapbox/types/entities';
const messages = defineMessages({ const messages = defineMessages({
@ -14,10 +15,11 @@ const messages = defineMessages({
}); });
interface IEventAction { interface IEventAction {
status: StatusEntity, status: StatusEntity
theme?: ButtonThemes
} }
const EventActionButton: React.FC<IEventAction> = ({ status }) => { const EventActionButton: React.FC<IEventAction> = ({ status, theme = 'secondary' }) => {
const intl = useIntl(); const intl = useIntl();
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
@ -86,7 +88,7 @@ const EventActionButton: React.FC<IEventAction> = ({ status }) => {
return ( return (
<Button <Button
size='sm' size='sm'
theme='secondary' theme={theme}
icon={buttonIcon} icon={buttonIcon}
onClick={buttonAction} onClick={buttonAction}
disabled={buttonDisabled} disabled={buttonDisabled}

View File

@ -1,3 +1,4 @@
import { List as ImmutableList } from 'immutable';
import React from 'react'; import React from 'react';
import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
import { Link, useHistory } from 'react-router-dom'; import { Link, useHistory } from 'react-router-dom';
@ -86,15 +87,14 @@ const EventHeader: React.FC<IEventHeader> = ({ status }) => {
const account = status.account as AccountEntity; const account = status.account as AccountEntity;
const event = status.event; const event = status.event;
const banner = status.media_attachments?.find(({ description }) => description === 'Banner'); const banner = event.banner;
const username = account.username; const username = account.username;
const handleHeaderClick: React.MouseEventHandler<HTMLAnchorElement> = (e) => { const handleHeaderClick: React.MouseEventHandler<HTMLAnchorElement> = (e) => {
e.stopPropagation(); e.stopPropagation();
const index = status.media_attachments!.findIndex(({ description }) => description === 'Banner'); dispatch(openModal('MEDIA', { media: ImmutableList([event.banner]) }));
dispatch(openModal('MEDIA', { media: status.media_attachments, index }));
}; };
const handleExportClick = () => { const handleExportClick = () => {

View File

@ -4,8 +4,11 @@ import { FormattedDate, FormattedMessage } from 'react-intl';
import { openModal } from 'soapbox/actions/modals'; import { openModal } from 'soapbox/actions/modals';
import { fetchStatus } from 'soapbox/actions/statuses'; import { fetchStatus } from 'soapbox/actions/statuses';
import MissingIndicator from 'soapbox/components/missing-indicator'; import MissingIndicator from 'soapbox/components/missing-indicator';
import StatusContent from 'soapbox/components/status-content';
import StatusMedia from 'soapbox/components/status-media'; import StatusMedia from 'soapbox/components/status-media';
import TranslateButton from 'soapbox/components/translate-button';
import { HStack, Icon, Stack, Text } from 'soapbox/components/ui'; import { HStack, Icon, Stack, Text } from 'soapbox/components/ui';
import QuotedStatus from 'soapbox/features/status/containers/quoted-status-container';
import { useAppDispatch, useAppSelector, useSettings } from 'soapbox/hooks'; import { useAppDispatch, useAppSelector, useSettings } from 'soapbox/hooks';
import { makeGetStatus } from 'soapbox/selectors'; import { makeGetStatus } from 'soapbox/selectors';
import { defaultMediaVisibility } from 'soapbox/utils/status'; import { defaultMediaVisibility } from 'soapbox/utils/status';
@ -107,10 +110,7 @@ const EventInformation: React.FC<IEventInformation> = ({ params }) => {
}, [status]); }, [status]);
const renderLinks = useCallback(() => { const renderLinks = useCallback(() => {
const links = status?.media_attachments.filter(({ pleroma }) => pleroma.get('mime_type') === 'text/html'); if (!status.event?.links.size) return null;
if (!links?.size) return null;
return ( return (
<Stack space={1}> <Stack space={1}>
@ -118,11 +118,11 @@ const EventInformation: React.FC<IEventInformation> = ({ params }) => {
<FormattedMessage id='event.website' defaultMessage='External links' /> <FormattedMessage id='event.website' defaultMessage='External links' />
</Text> </Text>
{links.map(link => ( {status.event.links.map(link => (
<HStack space={2} alignItems='center'> <HStack space={2} alignItems='center'>
<Icon src={require('@tabler/icons/link.svg')} /> <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'> <a href={link.url} className='text-primary-600 dark:text-accent-blue hover:underline' target='_blank'>
{(link.remote_url || link.url).replace(/^https?:\/\//, '')} {link.url.replace(/^https?:\/\//, '')}
</a> </a>
</HStack> </HStack>
))} ))}
@ -143,21 +143,23 @@ const EventInformation: React.FC<IEventInformation> = ({ params }) => {
<Text size='xl' weight='bold'> <Text size='xl' weight='bold'>
<FormattedMessage id='event.description' defaultMessage='Description' /> <FormattedMessage id='event.description' defaultMessage='Description' />
</Text> </Text>
<Text
className='break-words status__content' <StatusContent status={status} collapsable={false} translatable />
size='sm'
dangerouslySetInnerHTML={{ __html: status.contentHtml }} <TranslateButton status={status} />
/>
</Stack> </Stack>
)} )}
<StatusMedia <StatusMedia
status={status} status={status}
excludeBanner
showMedia={showMedia} showMedia={showMedia}
onToggleVisibility={handleToggleMediaVisibility} onToggleVisibility={handleToggleMediaVisibility}
/> />
{status.quote && status.pleroma.get('quote_visible', true) && (
<QuotedStatus statusId={status.quote as string} />
)}
{renderEventLocation()} {renderEventLocation()}
{renderEventDate()} {renderEventDate()}

View File

@ -22,7 +22,7 @@ const Event = ({ id }: { id: string }) => {
className='w-full px-1' className='w-full px-1'
to={`/@${status.getIn(['account', 'acct'])}/events/${status.id}`} to={`/@${status.getIn(['account', 'acct'])}/events/${status.id}`}
> >
<EventPreview status={status} /> <EventPreview status={status} floatingAction={false} />
</Link> </Link>
); );
}; };
@ -56,7 +56,6 @@ const EventCarousel: React.FC<IEventCarousel> = ({ statusIds, isLoading, emptyMe
{index !== 0 && ( {index !== 0 && (
<div className='z-10 absolute left-3 top-1/2 -mt-4'> <div className='z-10 absolute left-3 top-1/2 -mt-4'>
<button <button
data-testid='prev-page'
onClick={() => handleChangeIndex(index - 1)} onClick={() => handleChangeIndex(index - 1)}
className='bg-white/50 dark:bg-gray-900/50 backdrop-blur rounded-full h-8 w-8 flex items-center justify-center' className='bg-white/50 dark:bg-gray-900/50 backdrop-blur rounded-full h-8 w-8 flex items-center justify-center'
> >
@ -70,7 +69,6 @@ const EventCarousel: React.FC<IEventCarousel> = ({ statusIds, isLoading, emptyMe
{index !== statusIds.size - 1 && ( {index !== statusIds.size - 1 && (
<div className='z-10 absolute right-3 top-1/2 -mt-4'> <div className='z-10 absolute right-3 top-1/2 -mt-4'>
<button <button
data-testid='next-page'
onClick={() => handleChangeIndex(index + 1)} onClick={() => handleChangeIndex(index + 1)}
className='bg-white/50 dark:bg-gray-900/50 backdrop-blur rounded-full h-8 w-8 flex items-center justify-center' className='bg-white/50 dark:bg-gray-900/50 backdrop-blur rounded-full h-8 w-8 flex items-center justify-center'
> >

View File

@ -1,4 +1,4 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect } from 'react';
import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
import { fetchJoinedEvents, fetchRecentEvents } from 'soapbox/actions/events'; import { fetchJoinedEvents, fetchRecentEvents } from 'soapbox/actions/events';

View File

@ -10,9 +10,7 @@ const PlaceholderEventPreview = () => {
return ( return (
<div className='w-full rounded-lg bg-gray-100 dark:bg-primary-800 relative overflow-hidden animate-pulse text-primary-50 dark:text-primary-800'> <div className='w-full rounded-lg bg-gray-100 dark:bg-primary-800 relative overflow-hidden animate-pulse text-primary-50 dark:text-primary-800'>
<div className='bg-primary-200 dark:bg-gray-600 h-40'> <div className='bg-primary-200 dark:bg-gray-600 h-40' />
{/* <img className='h-full w-full object-cover' src={banner.url} alt={intl.formatMessage(messages.bannerHeader)} />} */}
</div>
<Stack className='p-2.5' space={2}> <Stack className='p-2.5' space={2}>
<Text weight='semibold'>{generateText(eventNameLength)}</Text> <Text weight='semibold'>{generateText(eventNameLength)}</Text>

View File

@ -32,6 +32,8 @@ export const EventRecord = ImmutableRecord({
participants_count: 0, participants_count: 0,
location: null as ImmutableMap<string, any> | null, location: null as ImmutableMap<string, any> | null,
join_state: null as EventJoinState | null, join_state: null as EventJoinState | null,
banner: null as Attachment | null,
links: ImmutableList<Attachment>(),
}); });
// https://docs.joinmastodon.org/entities/status/ // https://docs.joinmastodon.org/entities/status/
@ -174,9 +176,27 @@ const fixSensitivity = (status: ImmutableMap<string, any>) => {
// Normalize event // Normalize event
const normalizeEvent = (status: ImmutableMap<string, any>) => { const normalizeEvent = (status: ImmutableMap<string, any>) => {
if (status.getIn(['pleroma', 'event'])) { if (status.getIn(['pleroma', 'event'])) {
return status.set('event', EventRecord(status.getIn(['pleroma', 'event']) as ImmutableMap<string, any>)); const firstAttachment = status.get('media_attachments').first();
} else { let banner = null;
return status.set('event', null); let mediaAttachments = status.get('media_attachments');
if (firstAttachment && firstAttachment.description === 'Banner' && firstAttachment.type === 'image') {
banner = normalizeAttachment(firstAttachment);
mediaAttachments = mediaAttachments.shift();
}
const links = mediaAttachments.filter((attachment: Attachment) => attachment.pleroma.get('mime_type') === 'text/html');
mediaAttachments = mediaAttachments.filter((attachment: Attachment) => attachment.pleroma.get('mime_type') !== 'text/html');
const event = EventRecord(
(status.getIn(['pleroma', 'event']) as ImmutableMap<string, any>)
.set('banner', banner)
.set('links', links),
);
status
.set('event', event)
.set('media_attachments', mediaAttachments);
} }
}; };

View File

@ -25,7 +25,6 @@ import { normalizeAttachment, normalizeLocation } from 'soapbox/normalizers';
import type { import type {
Attachment as AttachmentEntity, Attachment as AttachmentEntity,
Location as LocationEntity, Location as LocationEntity,
Status as StatusEntity,
} from 'soapbox/types/entities'; } from 'soapbox/types/entities';
export const ReducerRecord = ImmutableRecord({ export const ReducerRecord = ImmutableRecord({
@ -90,11 +89,10 @@ export default function compose_event(state = ReducerRecord(), action: AnyAction
return ReducerRecord({ return ReducerRecord({
name: action.status.event.name, name: action.status.event.name,
status: action.text, status: action.text,
// location: null as LocationEntity | null,
start_time: new Date(action.status.event.start_time), start_time: new Date(action.status.event.start_time),
end_time: action.status.event.end_time ? new Date(action.status.event.end_time) : null, end_time: action.status.event.end_time ? new Date(action.status.event.end_time) : null,
approval_required: action.status.event.join_mode !== 'free', approval_required: action.status.event.join_mode !== 'free',
banner: (action.status as StatusEntity).media_attachments.find(({ description }) => description === 'Banner') || null, banner: action.status.event.banner || null,
location: action.location ? normalizeLocation(action.location) : null, location: action.location ? normalizeLocation(action.location) : null,
progress: 0, progress: 0,
is_uploading: false, is_uploading: false,