Use react-query and zod for announcements

Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
This commit is contained in:
marcin mikołajczak
2024-04-09 23:38:09 +02:00
parent d4beb15d71
commit 161db37ba0
28 changed files with 391 additions and 905 deletions

View File

@ -3,7 +3,7 @@ import { useHistory } from 'react-router-dom';
import { getTextDirection } from 'soapbox/utils/rtl';
import type { Announcement as AnnouncementEntity, Mention as MentionEntity } from 'soapbox/types/entities';
import type { Announcement as AnnouncementEntity, Mention as MentionEntity } from 'soapbox/schemas';
interface IAnnouncementContent {
announcement: AnnouncementEntity;
@ -67,7 +67,7 @@ const AnnouncementContent: React.FC<IAnnouncementContent> = ({ announcement }) =
} else if (link.textContent?.charAt(0) === '#' || (link.previousSibling?.textContent?.charAt(link.previousSibling.textContent.length - 1) === '#')) {
link.addEventListener('click', onHashtagClick.bind(link, link.text), false);
} else {
const status = announcement.statuses.get(link.href);
const status = announcement.statuses[link.href];
if (status) {
link.addEventListener('click', onStatusClick.bind(this, status), false);
}

View File

@ -9,16 +9,14 @@ import AnnouncementContent from './announcement-content';
import ReactionsBar from './reactions-bar';
import type { Map as ImmutableMap } from 'immutable';
import type { Announcement as AnnouncementEntity } from 'soapbox/types/entities';
import type { Announcement as AnnouncementEntity } from 'soapbox/schemas';
interface IAnnouncement {
announcement: AnnouncementEntity;
addReaction: (id: string, name: string) => void;
removeReaction: (id: string, name: string) => void;
emojiMap: ImmutableMap<string, ImmutableMap<string, string>>;
}
const Announcement: React.FC<IAnnouncement> = ({ announcement, addReaction, removeReaction, emojiMap }) => {
const Announcement: React.FC<IAnnouncement> = ({ announcement, emojiMap }) => {
const features = useFeatures();
const startsAt = announcement.starts_at && new Date(announcement.starts_at);
@ -64,8 +62,6 @@ const Announcement: React.FC<IAnnouncement> = ({ announcement, addReaction, remo
<ReactionsBar
reactions={announcement.reactions}
announcementId={announcement.id}
addReaction={addReaction}
removeReaction={removeReaction}
emojiMap={emojiMap}
/>
)}

View File

@ -5,9 +5,9 @@ import { FormattedMessage } from 'react-intl';
import ReactSwipeableViews from 'react-swipeable-views';
import { createSelector } from 'reselect';
import { addReaction as addReactionAction, removeReaction as removeReactionAction } from 'soapbox/actions/announcements';
import { useAnnouncements } from 'soapbox/api/hooks/announcements';
import { Card, HStack, Widget } from 'soapbox/components/ui';
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
import { useAppSelector } from 'soapbox/hooks';
import Announcement from './announcement';
@ -16,36 +16,30 @@ import type { RootState } from 'soapbox/store';
const customEmojiMap = createSelector([(state: RootState) => state.custom_emojis], items => (items as ImmutableList<ImmutableMap<string, string>>).reduce((map, emoji) => map.set(emoji.get('shortcode')!, emoji), ImmutableMap<string, ImmutableMap<string, string>>()));
const AnnouncementsPanel = () => {
const dispatch = useAppDispatch();
const emojiMap = useAppSelector(state => customEmojiMap(state));
const [index, setIndex] = useState(0);
const announcements = useAppSelector((state) => state.announcements.items);
const { data: announcements } = useAnnouncements();
const addReaction = (id: string, name: string) => dispatch(addReactionAction(id, name));
const removeReaction = (id: string, name: string) => dispatch(removeReactionAction(id, name));
if (announcements.size === 0) return null;
if (!announcements || announcements.length === 0) return null;
const handleChangeIndex = (index: number) => {
setIndex(index % announcements.size);
setIndex(index % announcements.length);
};
return (
<Widget title={<FormattedMessage id='announcements.title' defaultMessage='Announcements' />}>
<Card className='relative black:rounded-xl black:border black:border-gray-800' size='md' variant='rounded'>
<ReactSwipeableViews animateHeight index={index} onChangeIndex={handleChangeIndex}>
{announcements.map((announcement) => (
{announcements!.map((announcement) => (
<Announcement
key={announcement.id}
announcement={announcement}
emojiMap={emojiMap}
addReaction={addReaction}
removeReaction={removeReaction}
/>
)).reverse()}
</ReactSwipeableViews>
{announcements.size > 1 && (
{announcements.length > 1 && (
<HStack space={2} alignItems='center' justifyContent='center' className='relative'>
{announcements.map((_, i) => (
<button

View File

@ -1,31 +1,32 @@
import clsx from 'clsx';
import React, { useState } from 'react';
import { useAnnouncements } from 'soapbox/api/hooks/announcements';
import AnimatedNumber from 'soapbox/components/animated-number';
import unicodeMapping from 'soapbox/features/emoji/mapping';
import Emoji from './emoji';
import type { Map as ImmutableMap } from 'immutable';
import type { AnnouncementReaction } from 'soapbox/types/entities';
import type { AnnouncementReaction } from 'soapbox/schemas';
interface IReaction {
announcementId: string;
reaction: AnnouncementReaction;
emojiMap: ImmutableMap<string, ImmutableMap<string, string>>;
addReaction: (id: string, name: string) => void;
removeReaction: (id: string, name: string) => void;
style: React.CSSProperties;
}
const Reaction: React.FC<IReaction> = ({ announcementId, reaction, addReaction, removeReaction, emojiMap, style }) => {
const Reaction: React.FC<IReaction> = ({ announcementId, reaction, emojiMap, style }) => {
const [hovered, setHovered] = useState(false);
const { addReaction, removeReaction } = useAnnouncements();
const handleClick = () => {
if (reaction.me) {
removeReaction(announcementId, reaction.name);
removeReaction({ announcementId, name: reaction.name });
} else {
addReaction(announcementId, reaction.name);
addReaction({ announcementId, name: reaction.name });
}
};

View File

@ -2,28 +2,28 @@ import clsx from 'clsx';
import React from 'react';
import { TransitionMotion, spring } from 'react-motion';
import { useAnnouncements } from 'soapbox/api/hooks/announcements';
import EmojiPickerDropdown from 'soapbox/features/emoji/containers/emoji-picker-dropdown-container';
import { useSettings } from 'soapbox/hooks';
import Reaction from './reaction';
import type { List as ImmutableList, Map as ImmutableMap } from 'immutable';
import type { Map as ImmutableMap } from 'immutable';
import type { Emoji, NativeEmoji } from 'soapbox/features/emoji';
import type { AnnouncementReaction } from 'soapbox/types/entities';
import type { AnnouncementReaction } from 'soapbox/schemas';
interface IReactionsBar {
announcementId: string;
reactions: ImmutableList<AnnouncementReaction>;
reactions: AnnouncementReaction[];
emojiMap: ImmutableMap<string, ImmutableMap<string, string>>;
addReaction: (id: string, name: string) => void;
removeReaction: (id: string, name: string) => void;
}
const ReactionsBar: React.FC<IReactionsBar> = ({ announcementId, reactions, addReaction, removeReaction, emojiMap }) => {
const ReactionsBar: React.FC<IReactionsBar> = ({ announcementId, reactions, emojiMap }) => {
const { reduceMotion } = useSettings();
const { addReaction } = useAnnouncements();
const handleEmojiPick = (data: Emoji) => {
addReaction(announcementId, (data as NativeEmoji).native.replace(/:/g, ''));
addReaction({ announcementId, name: (data as NativeEmoji).native.replace(/:/g, '') });
};
const willEnter = () => ({ scale: reduceMotion ? 1 : 0 });
@ -36,25 +36,23 @@ const ReactionsBar: React.FC<IReactionsBar> = ({ announcementId, reactions, addR
key: reaction.name,
data: reaction,
style: { scale: reduceMotion ? 1 : spring(1, { stiffness: 150, damping: 13 }) },
})).toArray();
}));
return (
<TransitionMotion styles={styles} willEnter={willEnter} willLeave={willLeave}>
{items => (
<div className={clsx('flex flex-wrap items-center gap-1', { 'reactions-bar--empty': visibleReactions.isEmpty() })}>
<div className={clsx('flex flex-wrap items-center gap-1', { 'reactions-bar--empty': visibleReactions.length === 0 })}>
{items.map(({ key, data, style }) => (
<Reaction
key={key}
reaction={data}
style={{ transform: `scale(${style.scale})`, position: style.scale < 0.5 ? 'absolute' : 'static' }}
announcementId={announcementId}
addReaction={addReaction}
removeReaction={removeReaction}
emojiMap={emojiMap}
/>
))}
{visibleReactions.size < 8 && <EmojiPickerDropdown onPickEmoji={handleEmojiPick} />}
{visibleReactions.length < 8 && <EmojiPickerDropdown onPickEmoji={handleEmojiPick} />}
</div>
)}
</TransitionMotion>