Refactor carousel components (#36425)

Co-authored-by: diondiondion <mail@diondiondion.com>
This commit is contained in:
Echoanddiondiondion authored and GitHub committed 2025-10-29 14:44:46 +00:00
1 parent 2a9c7d2b9e
commit e7cd5a430e
10 files changed
+591 -301

No files matched your search

@@ -15,24 +15,24 @@ export interface IAnnouncement extends ApiAnnouncementJSON {
interface AnnouncementProps {
announcement: IAnnouncement;
selected: boolean;
active?: boolean;
}
export const Announcement: FC<AnnouncementProps> = ({
announcement,
selected,
active,
}) => {
const [unread, setUnread] = useState(!announcement.read);
useEffect(() => {
// Only update `unread` marker once the announcement is out of view
if (!selected && unread !== !announcement.read) {
if (!active && unread !== !announcement.read) {
setUnread(!announcement.read);
}
}, [announcement.read, selected, unread]);
}, [announcement.read, active, unread]);
return (
<AnimateEmojiProvider className='announcements__item'>
<strong className='announcements__item__range'>
<AnimateEmojiProvider>
<strong className='announcements__range'>
<FormattedMessage
id='announcement.announcement'
defaultMessage='Announcement'
@@ -44,14 +44,14 @@ export const Announcement: FC<AnnouncementProps> = ({
</strong>
<EmojiHTML
className='announcements__item__content translate'
className='announcements__content translate'
htmlString={announcement.contentHtml}
extraEmojis={announcement.emojis}
/>
<ReactionsBar reactions={announcement.reactions} id={announcement.id} />
{unread && <span className='announcements__item__unread' />}
{unread && <span className='announcements__unread' />}
</AnimateEmojiProvider>
);
};
@@ -1,63 +1,50 @@
import { useCallback, useState } from 'react';
import { useCallback } from 'react';
import type { FC } from 'react';
import { defineMessages, useIntl } from 'react-intl';
import type { Map, List } from 'immutable';
import ReactSwipeableViews from 'react-swipeable-views';
import elephantUIPlane from '@/images/elephant_ui_plane.svg';
import type { RenderSlideFn } from '@/mastodon/components/carousel';
import { Carousel } from '@/mastodon/components/carousel';
import { CustomEmojiProvider } from '@/mastodon/components/emoji/context';
import { IconButton } from '@/mastodon/components/icon_button';
import { mascot, reduceMotion } from '@/mastodon/initial_state';
import { mascot } from '@/mastodon/initial_state';
import { createAppSelector, useAppSelector } from '@/mastodon/store';
import ChevronLeftIcon from '@/material-icons/400-24px/chevron_left.svg?react';
import ChevronRightIcon from '@/material-icons/400-24px/chevron_right.svg?react';
import type { IAnnouncement } from './announcement';
import { Announcement } from './announcement';
const messages = defineMessages({
close: { id: 'lightbox.close', defaultMessage: 'Close' },
previous: { id: 'lightbox.previous', defaultMessage: 'Previous' },
next: { id: 'lightbox.next', defaultMessage: 'Next' },
});
const announcementSelector = createAppSelector(
[(state) => state.announcements as Map<string, List<Map<string, unknown>>>],
(announcements) =>
(announcements.get('items')?.toJS() as IAnnouncement[] | undefined) ?? [],
((announcements.get('items')?.toJS() as IAnnouncement[] | undefined) ?? [])
.map((announcement) => ({ announcement, id: announcement.id }))
.toReversed(),
);
export const Announcements: FC = () => {
const intl = useIntl();
const announcements = useAppSelector(announcementSelector);
const emojis = useAppSelector((state) => state.custom_emojis);
const [index, setIndex] = useState(0);
const handleChangeIndex = useCallback(
(idx: number) => {
setIndex(idx % announcements.length);
},
[announcements.length],
const renderSlide: RenderSlideFn<{
id: string;
announcement: IAnnouncement;
}> = useCallback(
(item, active) => (
<Announcement
announcement={item.announcement}
active={active}
key={item.id}
/>
),
[],
);
const handleNextIndex = useCallback(() => {
setIndex((prevIndex) => (prevIndex + 1) % announcements.length);
}, [announcements.length]);
const handlePrevIndex = useCallback(() => {
setIndex((prevIndex) =>
prevIndex === 0 ? announcements.length - 1 : prevIndex - 1,
);
}, [announcements.length]);
if (announcements.length === 0) {
return null;
}
return (
<div className='announcements'>
<div className='announcements__root'>
<img
className='announcements__mastodon'
alt=''
@@ -65,48 +52,13 @@ export const Announcements: FC = () => {
src={mascot ?? elephantUIPlane}
/>
<div className='announcements__container'>
<CustomEmojiProvider emojis={emojis}>
<ReactSwipeableViews
animateHeight
animateTransitions={!reduceMotion}
index={index}
onChangeIndex={handleChangeIndex}
>
{announcements
.map((announcement, idx) => (
<Announcement
key={announcement.id}
announcement={announcement}
selected={index === idx}
/>
))
.reverse()}
</ReactSwipeableViews>
</CustomEmojiProvider>
{announcements.length > 1 && (
<div className='announcements__pagination'>
<IconButton
disabled={announcements.length === 1}
title={intl.formatMessage(messages.previous)}
icon='chevron-left'
iconComponent={ChevronLeftIcon}
onClick={handlePrevIndex}
/>
<span>
{index + 1} / {announcements.length}
</span>
<IconButton
disabled={announcements.length === 1}
title={intl.formatMessage(messages.next)}
icon='chevron-right'
iconComponent={ChevronRightIcon}
onClick={handleNextIndex}
/>
</div>
)}
</div>
<CustomEmojiProvider emojis={emojis}>
<Carousel
classNamePrefix='announcements'
renderItem={renderSlide}
items={announcements}
/>
</CustomEmojiProvider>
</div>
);
};