Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
environments/review-events-25kg8c/deployments/1588
marcin mikołajczak 2022-11-26 22:15:58 +01:00
rodzic 859d93b289
commit c61dcddd81
11 zmienionych plików z 261 dodań i 22 usunięć

Wyświetl plik

@ -4,7 +4,7 @@ import api, { getLinks } from 'soapbox/api';
import { formatBytes } from 'soapbox/utils/media';
import resizeImage from 'soapbox/utils/resize-image';
import { importFetchedAccounts, importFetchedStatus } from './importer';
import { importFetchedAccounts, importFetchedStatus, importFetchedStatuses } from './importer';
import { fetchMedia, uploadMedia } from './media';
import { closeModal, openModal } from './modals';
import snackbar from './snackbar';
@ -76,6 +76,13 @@ const EVENT_COMPOSE_CANCEL = 'EVENT_COMPOSE_CANCEL';
const EVENT_FORM_SET = 'EVENT_FORM_SET';
const RECENT_EVENTS_FETCH_REQUEST = 'RECENT_EVENTS_FETCH_REQUEST';
const RECENT_EVENTS_FETCH_SUCCESS = 'RECENT_EVENTS_FETCH_SUCCESS';
const RECENT_EVENTS_FETCH_FAIL = 'RECENT_EVENTS_FETCH_FAIL';
const JOINED_EVENTS_FETCH_REQUEST = 'JOINED_EVENTS_FETCH_REQUEST';
const JOINED_EVENTS_FETCH_SUCCESS = 'JOINED_EVENTS_FETCH_SUCCESS';
const JOINED_EVENTS_FETCH_FAIL = 'JOINED_EVENTS_FETCH_FAIL';
const noOp = () => new Promise(f => f(undefined));
const messages = defineMessages({
@ -579,6 +586,48 @@ const editEvent = (id: string) => (dispatch: AppDispatch, getState: () => RootSt
});
};
const fetchRecentEvents = () =>
(dispatch: AppDispatch, getState: () => RootState) => {
if (getState().status_lists.get('recent_events')?.isLoading) {
return;
}
dispatch({ type: RECENT_EVENTS_FETCH_REQUEST });
api(getState).get('/api/v1/timelines/public?only_events=true').then(response => {
const next = getLinks(response).refs.find(link => link.rel === 'next');
dispatch(importFetchedStatuses(response.data));
dispatch({
type: RECENT_EVENTS_FETCH_SUCCESS,
statuses: response.data,
next: next ? next.uri : null,
});
}).catch(error => {
dispatch({ type: RECENT_EVENTS_FETCH_FAIL, error });
});
};
const fetchJoinedEvents = () =>
(dispatch: AppDispatch, getState: () => RootState) => {
if (getState().status_lists.get('joined_events')?.isLoading) {
return;
}
dispatch({ type: JOINED_EVENTS_FETCH_REQUEST });
api(getState).get('/api/v1/pleroma/events/joined_events').then(response => {
const next = getLinks(response).refs.find(link => link.rel === 'next');
dispatch(importFetchedStatuses(response.data));
dispatch({
type: JOINED_EVENTS_FETCH_SUCCESS,
statuses: response.data,
next: next ? next.uri : null,
});
}).catch(error => {
dispatch({ type: JOINED_EVENTS_FETCH_FAIL, error });
});
};
export {
LOCATION_SEARCH_REQUEST,
LOCATION_SEARCH_SUCCESS,
@ -624,6 +673,12 @@ export {
EVENT_PARTICIPATION_REQUEST_REJECT_FAIL,
EVENT_COMPOSE_CANCEL,
EVENT_FORM_SET,
RECENT_EVENTS_FETCH_REQUEST,
RECENT_EVENTS_FETCH_SUCCESS,
RECENT_EVENTS_FETCH_FAIL,
JOINED_EVENTS_FETCH_REQUEST,
JOINED_EVENTS_FETCH_SUCCESS,
JOINED_EVENTS_FETCH_FAIL,
locationSearch,
changeEditEventName,
changeEditEventDescription,
@ -677,4 +732,6 @@ export {
fetchEventIcs,
cancelEventCompose,
editEvent,
fetchRecentEvents,
fetchJoinedEvents,
};

Wyświetl plik

@ -35,7 +35,7 @@ const EventPreview: React.FC<IEventPreview> = ({ status, className, hideAction }
const banner = status.media_attachments?.find(({ description }) => description === 'Banner');
return (
<div className={classNames('rounded-lg bg-gray-100 dark:bg-primary-800 relative overflow-hidden', className)}>
<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

Wyświetl plik

@ -87,9 +87,14 @@ const CardTitle: React.FC<ICardTitle> = ({ title }): JSX.Element => (
<Text size='xl' weight='bold' tag='h1' data-testid='card-title' truncate>{title}</Text>
);
interface ICardBody {
/** Classnames for the <div> element. */
className?: string
}
/** A card's body. */
const CardBody: React.FC = ({ children }): JSX.Element => (
<div data-testid='card-body'>{children}</div>
const CardBody: React.FC<ICardBody> = ({ className, children }): JSX.Element => (
<div data-testid='card-body' className={className}>{children}</div>
);
export { Card, CardHeader, CardTitle, CardBody };

Wyświetl plik

@ -68,7 +68,6 @@ const messages = defineMessages({
userUnendorsed: { id: 'account.unendorse.success', defaultMessage: 'You are no longer featuring @{acct}' },
profileExternal: { id: 'account.profile_external', defaultMessage: 'View profile on {domain}' },
header: { id: 'account.header.alt', defaultMessage: 'Profile header' },
composeEvent: { od: 'navigation.compose_event', defaultMessage: 'Create new event' },
});
interface IHeader {
@ -214,10 +213,6 @@ const Header: React.FC<IHeader> = ({ account }) => {
history.push('/search');
};
const onComposeEvent = () => {
dispatch(openModal('COMPOSE_EVENT'));
};
const onAvatarClick = () => {
const avatar = normalizeAttachment({
type: 'image',
@ -302,13 +297,6 @@ const Header: React.FC<IHeader> = ({ account }) => {
to: '/blocks',
icon: require('@tabler/icons/ban.svg'),
});
if (features.events) {
menu.push({
text: intl.formatMessage(messages.composeEvent),
action: onComposeEvent,
icon: require('@tabler/icons/calendar.svg'),
});
}
} else {
menu.push({
text: intl.formatMessage(messages.mention, { name: account.username }),

Wyświetl plik

@ -342,7 +342,7 @@ const EventHeader: React.FC<IEventHeader> = ({ status }) => {
<StillImage
src={banner.url}
alt={intl.formatMessage(messages.bannerHeader)}
className='absolute inset-0 object-cover md:rounded-t-xl'
className='absolute inset-0 object-cover md:rounded-t-xl h-full'
/>
</a>
)}

Wyświetl plik

@ -175,7 +175,7 @@ const EventDiscussion: React.FC<IEventDiscussion> = (props) => {
return (
<Stack space={2}>
{me && <div className='sm:p-2 pt-0 border-b border-solid border-gray-200 dark:border-gray-800'>
{me && <div className='p-2 pt-0 border-b border-solid border-gray-200 dark:border-gray-800'>
<ComposeForm id={`reply:${status.id}`} autoFocus={false} event={status.id} />
</div>}
<div ref={node} className='thread p-0 sm:p-2 shadow-none'>

Wyświetl plik

@ -0,0 +1,85 @@
import React, { useCallback, useState } from 'react';
import { Link } from 'react-router-dom';
import ReactSwipeableViews from 'react-swipeable-views';
import EventPreview from 'soapbox/components/event-preview';
import { Card, Icon } from 'soapbox/components/ui';
import { useAppSelector } from 'soapbox/hooks';
import { makeGetStatus } from 'soapbox/selectors';
import PlaceholderEventPreview from '../../placeholder/components/placeholder-event-preview';
import type { OrderedSet as ImmutableOrderedSet } from 'immutable';
const Event = ({ id }: { id: string }) => {
const getStatus = useCallback(makeGetStatus(), []);
const status = useAppSelector(state => getStatus(state, { id }));
if (!status) return null;
return (
<Link
className='w-full px-1'
to={`/@${status.getIn(['account', 'acct'])}/events/${status.id}`}
>
<EventPreview status={status} />
</Link>
);
};
interface IEventCarousel {
statusIds: ImmutableOrderedSet<string>
isLoading?: boolean | null
emptyMessage: React.ReactNode
}
const EventCarousel: React.FC<IEventCarousel> = ({ statusIds, isLoading, emptyMessage }) => {
const [index, setIndex] = useState(0);
const handleChangeIndex = (index: number) => {
setIndex(index % statusIds.size);
};
if (isLoading) {
return <PlaceholderEventPreview />;
}
if (statusIds.size === 0) {
return (
<Card variant='rounded' size='lg'>
{emptyMessage}
</Card>
);
}
return (
<div className='relative -mx-1'>
{index !== 0 && (
<div className='z-10 absolute left-3 top-1/2 -mt-4'>
<button
data-testid='prev-page'
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'
>
<Icon src={require('@tabler/icons/chevron-left.svg')} className='text-black dark:text-white h-6 w-6' />
</button>
</div>
)}
<ReactSwipeableViews animateHeight index={index} onChangeIndex={handleChangeIndex}>
{statusIds.map(statusId => <Event key={statusId} id={statusId} />)}
</ReactSwipeableViews>
{index !== statusIds.size - 1 && (
<div className='z-10 absolute right-3 top-1/2 -mt-4'>
<button
data-testid='next-page'
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'
>
<Icon src={require('@tabler/icons/chevron-right.svg')} className='text-black dark:text-white h-6 w-6' />
</button>
</div>
)}
</div>
);
};
export default EventCarousel;

Wyświetl plik

@ -1,7 +1,12 @@
import React from 'react';
import { defineMessages, useIntl } from 'react-intl';
import React, { useEffect, useState } from 'react';
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
import { Column } from 'soapbox/components/ui';
import { fetchJoinedEvents, fetchRecentEvents } from 'soapbox/actions/events';
import { openModal } from 'soapbox/actions/modals';
import { Button, CardBody, CardHeader, CardTitle, Column, HStack } from 'soapbox/components/ui';
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
import EventCarousel from './components/event-carousel';
const messages = defineMessages({
title: { id: 'column.events', defaultMessage: 'Events' },
@ -10,8 +15,53 @@ const messages = defineMessages({
const Events = () => {
const intl = useIntl();
const dispatch = useAppDispatch();
const recentEvents = useAppSelector((state) => state.status_lists.get('recent_events')!.items);
const recentEventsLoading = useAppSelector((state) => state.status_lists.get('recent_events')!.isLoading);
const joinedEvents = useAppSelector((state) => state.status_lists.get('joined_events')!.items);
const joinedEventsLoading = useAppSelector((state) => state.status_lists.get('joined_events')!.isLoading);
const onComposeEvent = () => {
dispatch(openModal('COMPOSE_EVENT'));
};
useEffect(() => {
dispatch(fetchRecentEvents());
dispatch(fetchJoinedEvents());
}, []);
return (
<Column label={intl.formatMessage(messages.title)} />
<Column label={intl.formatMessage(messages.title)}>
<HStack className='mb-4' space={2} justifyContent='between'>
<CardTitle title='Recent events' />
<Button
className='ml-auto'
theme='primary'
size='sm'
onClick={onComposeEvent}
>
Create event
</Button>
</HStack>
<CardBody className='mb-2'>
<EventCarousel
statusIds={recentEvents}
isLoading={recentEventsLoading}
emptyMessage={<FormattedMessage id='events.recent_events.empty' defaultMessage='There are no public events yet.' />}
/>
</CardBody>
<CardHeader>
<CardTitle title='Joined events' />
</CardHeader>
<CardBody>
<EventCarousel
statusIds={joinedEvents}
isLoading={joinedEventsLoading}
emptyMessage={<FormattedMessage id='events.joined_events.empty' defaultMessage="You haven't joined any event yet." />}
/>
</CardBody>
</Column>
);
};

Wyświetl plik

@ -0,0 +1,29 @@
import React from 'react';
import { Stack, Text } from 'soapbox/components/ui';
import { generateText, randomIntFromInterval } from '../utils';
const PlaceholderEventPreview = () => {
const eventNameLength = randomIntFromInterval(5, 25);
const nameLength = randomIntFromInterval(5, 15);
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='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}>
<Text weight='semibold'>{generateText(eventNameLength)}</Text>
<div className='flex gap-y-1 gap-x-2 flex-wrap text-gray-700 dark:text-gray-600'>
<span>{generateText(nameLength)}</span>
<span>{generateText(nameLength)}</span>
<span>{generateText(nameLength)}</span>
</div>
</Stack>
</div>
);
};
export default PlaceholderEventPreview;

Wyświetl plik

@ -43,6 +43,9 @@ const LinkFooter: React.FC = (): JSX.Element => {
{features.profileDirectory && (
<FooterLink to='/directory'><FormattedMessage id='navigation_bar.profile_directory' defaultMessage='Profile directory' /></FooterLink>
)}
{features.events && (
<FooterLink to='/events'><FormattedMessage id='navigation_bar.events' defaultMessage='Events' /></FooterLink>
)}
<FooterLink to='/blocks'><FormattedMessage id='navigation_bar.blocks' defaultMessage='Blocks' /></FooterLink>
<FooterLink to='/mutes'><FormattedMessage id='navigation_bar.mutes' defaultMessage='Mutes' /></FooterLink>
{features.filters && (

Wyświetl plik

@ -12,6 +12,14 @@ import {
BOOKMARKED_STATUSES_EXPAND_SUCCESS,
BOOKMARKED_STATUSES_EXPAND_FAIL,
} from '../actions/bookmarks';
import {
RECENT_EVENTS_FETCH_REQUEST,
RECENT_EVENTS_FETCH_SUCCESS,
RECENT_EVENTS_FETCH_FAIL,
JOINED_EVENTS_FETCH_REQUEST,
JOINED_EVENTS_FETCH_SUCCESS,
JOINED_EVENTS_FETCH_FAIL,
} from '../actions/events';
import {
FAVOURITED_STATUSES_FETCH_REQUEST,
FAVOURITED_STATUSES_FETCH_SUCCESS,
@ -68,6 +76,8 @@ const initialState: State = ImmutableMap({
bookmarks: StatusListRecord(),
pins: StatusListRecord(),
scheduled_statuses: StatusListRecord(),
recent_events: StatusListRecord(),
joined_events: StatusListRecord(),
});
const getStatusId = (status: string | StatusEntity) => typeof status === 'string' ? status : status.id;
@ -168,6 +178,18 @@ export default function statusLists(state = initialState, action: AnyAction) {
case SCHEDULED_STATUS_CANCEL_REQUEST:
case SCHEDULED_STATUS_CANCEL_SUCCESS:
return removeOneFromList(state, 'scheduled_statuses', action.id || action.status.id);
case RECENT_EVENTS_FETCH_REQUEST:
return setLoading(state, 'recent_events', true);
case RECENT_EVENTS_FETCH_FAIL:
return setLoading(state, 'recent_events', false);
case RECENT_EVENTS_FETCH_SUCCESS:
return normalizeList(state, 'recent_events', action.statuses, action.next);
case JOINED_EVENTS_FETCH_REQUEST:
return setLoading(state, 'joined_events', true);
case JOINED_EVENTS_FETCH_FAIL:
return setLoading(state, 'joined_events', false);
case JOINED_EVENTS_FETCH_SUCCESS:
return normalizeList(state, 'joined_events', action.statuses, action.next);
default:
return state;
}