Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
update-emoji-mart^2
marcin mikołajczak 2022-07-09 23:47:58 +02:00
rodzic f5c3497ece
commit d5a6e978e6
13 zmienionych plików z 494 dodań i 299 usunięć

Wyświetl plik

@ -0,0 +1,63 @@
import React, { useEffect, useState } from 'react';
import { FormattedNumber } from 'react-intl';
import { TransitionMotion, spring } from 'react-motion';
import { useSettings } from 'soapbox/hooks';
const obfuscatedCount = (count: number) => {
if (count < 0) {
return 0;
} else if (count <= 1) {
return count;
} else {
return '1+';
}
};
interface IAnimatedNumber {
value: number;
obfuscate?: boolean;
}
const AnimatedNumber: React.FC<IAnimatedNumber> = ({ value, obfuscate }) => {
const reduceMotion = useSettings().get('reduceMotion');
const [direction, setDirection] = useState(1);
const [displayedValue, setDisplayedValue] = useState<number>(value);
useEffect(() => {
if (displayedValue !== undefined) {
if (value > displayedValue) setDirection(1);
else if (value < displayedValue) setDirection(-1);
}
setDisplayedValue(value);
}, [value]);
const willEnter = () => ({ y: -1 * direction });
const willLeave = () => ({ y: spring(1 * direction, { damping: 35, stiffness: 400 }) });
if (reduceMotion) {
return obfuscate ? <>{obfuscatedCount(displayedValue)}</> : <FormattedNumber value={displayedValue} />;
}
const styles = [{
key: `${displayedValue}`,
data: displayedValue,
style: { y: spring(0, { damping: 35, stiffness: 400 }) },
}];
return (
<TransitionMotion styles={styles} willEnter={willEnter} willLeave={willLeave}>
{items => (
<span className='inline-flex flex-col items-stretch relative overflow-hidden'>
{items.map(({ key, data, style }) => (
<span key={key} style={{ position: (direction * style.y) > 0 ? 'absolute' : 'static', transform: `translateY(${style.y * 100}%)` }}>{obfuscate ? obfuscatedCount(data) : <FormattedNumber value={data} />}</span>
))}
</span>
)}
</TransitionMotion>
);
};
export default AnimatedNumber;

Wyświetl plik

@ -0,0 +1,80 @@
import React, { useEffect, useRef } from 'react';
import { useHistory } from 'react-router-dom';
import type { Announcement as AnnouncementEntity, Mention as MentionEntity } from 'soapbox/types/entities';
interface IAnnouncementContent {
announcement: AnnouncementEntity;
}
const AnnouncementContent: React.FC<IAnnouncementContent> = ({ announcement }) => {
const history = useHistory();
const node = useRef<HTMLDivElement>(null);
useEffect(() => {
updateLinks();
});
const onMentionClick = (mention: MentionEntity, e: MouseEvent) => {
if (e.button === 0 && !(e.ctrlKey || e.metaKey)) {
e.preventDefault();
e.stopPropagation();
history.push(`/@${mention.acct}`);
}
};
const onHashtagClick = (hashtag: string, e: MouseEvent) => {
hashtag = hashtag.replace(/^#/, '').toLowerCase();
if (e.button === 0 && !(e.ctrlKey || e.metaKey)) {
e.preventDefault();
e.stopPropagation();
history.push(`/tags/${hashtag}`);
}
};
/** For regular links, just stop propogation */
const onLinkClick = (e: MouseEvent) => {
e.stopPropagation();
};
const updateLinks = () => {
if (!node.current) return;
const links = node.current.querySelectorAll('a');
links.forEach(link => {
// Skip already processed
if (link.classList.contains('status-link')) return;
// Add attributes
link.classList.add('status-link');
link.setAttribute('rel', 'nofollow noopener');
link.setAttribute('target', '_blank');
const mention = announcement.mentions.find(mention => link.href === `${mention.url}`);
// Add event listeners on mentions and hashtags
if (mention) {
link.addEventListener('click', onMentionClick.bind(link, mention), false);
link.setAttribute('title', mention.acct);
} 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 {
link.setAttribute('title', link.href);
link.addEventListener('click', onLinkClick.bind(link), false);
}
});
};
return (
<div
className='translate text-sm'
ref={node}
dangerouslySetInnerHTML={{ __html: announcement.contentHtml }}
/>
);
};
export default AnnouncementContent;

Wyświetl plik

@ -0,0 +1,70 @@
import React from 'react';
import { FormattedDate } from 'react-intl';
import { Stack, Text } from 'soapbox/components/ui';
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';
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 startsAt = announcement.starts_at && new Date(announcement.starts_at);
const endsAt = announcement.ends_at && new Date(announcement.ends_at);
const now = new Date();
const hasTimeRange = startsAt && endsAt;
const skipYear = hasTimeRange && startsAt.getFullYear() === endsAt.getFullYear() && endsAt.getFullYear() === now.getFullYear();
const skipEndDate = hasTimeRange && startsAt.getDate() === endsAt.getDate() && startsAt.getMonth() === endsAt.getMonth() && startsAt.getFullYear() === endsAt.getFullYear();
const skipTime = announcement.all_day;
return (
<div>
<Stack space={2}>
{hasTimeRange && (
<Text theme='muted'>
<FormattedDate
value={startsAt}
hour12={false}
year={(skipYear || startsAt.getFullYear() === now.getFullYear()) ? undefined : 'numeric'}
month='short'
day='2-digit'
hour={skipTime ? undefined : '2-digit'} minute={skipTime ? undefined : '2-digit'}
/>
{' '}
-
{' '}
<FormattedDate
value={endsAt}
hour12={false}
year={(skipYear || endsAt.getFullYear() === now.getFullYear()) ? undefined : 'numeric'}
month={skipEndDate ? undefined : 'short'}
day={skipEndDate ? undefined : '2-digit'}
hour={skipTime ? undefined : '2-digit'}
minute={skipTime ? undefined : '2-digit'}
/>
</Text>
)}
<AnnouncementContent announcement={announcement} />
<ReactionsBar
reactions={announcement.reactions}
announcementId={announcement.id}
addReaction={addReaction}
removeReaction={removeReaction}
emojiMap={emojiMap}
/>
</Stack>
</div>
);
};
export default Announcement;

Wyświetl plik

@ -0,0 +1,69 @@
import classNames from 'classnames';
import { List as ImmutableList, Map as ImmutableMap } from 'immutable';
import React, { useState } from 'react';
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 { Card, HStack, Widget } from 'soapbox/components/ui';
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
import Announcement from './announcement';
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 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;
const handleChangeIndex = (index: number) => {
setIndex(index % announcements.size);
};
return (
<Widget title={<FormattedMessage id='announcements.title' defaultMessage='Announcements' />}>
<Card className='relative' size='md' variant='rounded'>
<ReactSwipeableViews animateHeight index={index} onChangeIndex={handleChangeIndex}>
{announcements.map((announcement) => (
<Announcement
key={announcement.id}
announcement={announcement}
emojiMap={emojiMap}
addReaction={addReaction}
removeReaction={removeReaction}
/>
)).reverse()}
</ReactSwipeableViews>
{announcements.size > 2 && (
<HStack space={2} alignItems='center' justifyContent='center' className='relative'>
{announcements.map((_, i) => (
<button
key={i}
tabIndex={0}
onClick={() => setIndex(i)}
className={classNames({
'w-2 h-2 rounded-full focus:ring-primary-600 focus:ring-2 focus:ring-offset-2': true,
'bg-gray-200 hover:bg-gray-300': i !== index,
'bg-primary-600': i === index,
})}
/>
))}
</HStack>
)}
</Card>
</Widget>
);
};
export default AnnouncementsPanel;

Wyświetl plik

@ -0,0 +1,51 @@
import React from 'react';
import unicodeMapping from 'soapbox/features/emoji/emoji_unicode_mapping_light';
import { useSettings } from 'soapbox/hooks';
import { joinPublicPath } from 'soapbox/utils/static';
import type { Map as ImmutableMap } from 'immutable';
interface IEmoji {
emoji: string;
emojiMap: ImmutableMap<string, ImmutableMap<string, string>>;
hovered: boolean;
}
const Emoji: React.FC<IEmoji> = ({ emoji, emojiMap, hovered }) => {
const autoPlayGif = useSettings().get('autoPlayGif');
// @ts-ignore
if (unicodeMapping[emoji]) {
// @ts-ignore
const { filename, shortCode } = unicodeMapping[emoji];
const title = shortCode ? `:${shortCode}:` : '';
return (
<img
draggable='false'
className='emojione block m-0'
alt={emoji}
title={title}
src={joinPublicPath(`/emoji/${filename}.svg`)}
/>
);
} else if (emojiMap.get(emoji as any)) {
const filename = (autoPlayGif || hovered) ? emojiMap.getIn([emoji, 'url']) : emojiMap.getIn([emoji, 'static_url']);
const shortCode = `:${emoji}:`;
return (
<img
draggable='false'
className='emojione block m-0'
alt={shortCode}
title={shortCode}
src={filename as string}
/>
);
} else {
return null;
}
};
export default Emoji;

Wyświetl plik

@ -0,0 +1,66 @@
import classNames from 'classnames';
import React, { useState } from 'react';
import AnimatedNumber from 'soapbox/components/animated-number';
import unicodeMapping from 'soapbox/features/emoji/emoji_unicode_mapping_light';
import Emoji from './emoji';
import type { Map as ImmutableMap } from 'immutable';
import type { AnnouncementReaction } from 'soapbox/types/entities';
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 [hovered, setHovered] = useState(false);
const handleClick = () => {
if (reaction.me) {
removeReaction(announcementId, reaction.name);
} else {
addReaction(announcementId, reaction.name);
}
};
const handleMouseEnter = () => setHovered(true);
const handleMouseLeave = () => setHovered(false);
let shortCode = reaction.name;
// @ts-ignore
if (unicodeMapping[shortCode]) {
// @ts-ignore
shortCode = unicodeMapping[shortCode].shortCode;
}
return (
<button
className={classNames('flex shrink-0 items-center gap-1.5 bg-gray-100 dark:bg-primary-900 rounded-sm px-1.5 py-1 transition-colors', {
'bg-gray-200 dark:bg-primary-800': hovered,
'bg-primary-200 dark:bg-primary-500': reaction.me,
})}
onClick={handleClick}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
title={`:${shortCode}:`}
style={style}
>
<span className='block h-4 w-4'>
<Emoji hovered={hovered} emoji={reaction.name} emojiMap={emojiMap} />
</span>
<span className='block min-w-[9px] text-center text-xs font-medium text-primary-600 dark:text-white'>
<AnimatedNumber value={reaction.count} />
</span>
</button>
);
};
export default Reaction;

Wyświetl plik

@ -0,0 +1,65 @@
import classNames from 'classnames';
import React from 'react';
import { TransitionMotion, spring } from 'react-motion';
import { Icon } from 'soapbox/components/ui';
import EmojiPickerDropdown from 'soapbox/features/compose/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 { Emoji } from 'soapbox/components/autosuggest_emoji';
import type { AnnouncementReaction } from 'soapbox/types/entities';
interface IReactionsBar {
announcementId: string;
reactions: ImmutableList<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 reduceMotion = useSettings().get('reduceMotion');
const handleEmojiPick = (data: Emoji) => {
addReaction(announcementId, data.native.replace(/:/g, ''));
};
const willEnter = () => ({ scale: reduceMotion ? 1 : 0 });
const willLeave = () => ({ scale: reduceMotion ? 0 : spring(0, { stiffness: 170, damping: 26 }) });
const visibleReactions = reactions.filter(x => x.count > 0);
const styles = visibleReactions.map(reaction => ({
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={classNames('flex flex-wrap items-center gap-1', { 'reactions-bar--empty': visibleReactions.isEmpty() })}>
{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} button={<Icon className='h-4 w-4 text-gray-400 hover:text-gray-600 dark:hover:text-white' src={require('@tabler/icons/icons/plus.svg')} />} />}
</div>
)}
</TransitionMotion>
);
};
export default ReactionsBar;

Wyświetl plik

@ -290,6 +290,7 @@ class EmojiPickerDropdown extends React.PureComponent {
onPickEmoji: PropTypes.func.isRequired,
onSkinTone: PropTypes.func.isRequired,
skinTone: PropTypes.number.isRequired,
button: PropTypes.node,
};
state = {
@ -352,20 +353,14 @@ class EmojiPickerDropdown extends React.PureComponent {
}
render() {
const { intl, onPickEmoji, onSkinTone, skinTone, frequentlyUsedEmojis } = this.props;
const { intl, onPickEmoji, onSkinTone, skinTone, frequentlyUsedEmojis, button } = this.props;
const title = intl.formatMessage(messages.emoji);
const { active, loading, placement } = this.state;
return (
<div className='relative' onKeyDown={this.handleKeyDown}>
<IconButton
<div
ref={this.setTargetRef}
className={classNames({
'text-gray-400 hover:text-gray-600': true,
'pulse-loading': active && loading,
})}
alt='😀'
src={require('@tabler/icons/icons/mood-happy.svg')}
title={title}
aria-label={title}
aria-expanded={active}
@ -373,7 +368,16 @@ class EmojiPickerDropdown extends React.PureComponent {
onClick={this.onToggle}
onKeyDown={this.onToggle}
tabIndex={0}
/>
>
{button || <IconButton
className={classNames({
'text-gray-400 hover:text-gray-600': true,
'pulse-loading': active && loading,
})}
alt='😀'
src={require('@tabler/icons/icons/mood-happy.svg')}
/>}
</div>
<Overlay show={active} placement={placement} target={this.findTarget}>
<EmojiPickerMenu

Wyświetl plik

@ -67,7 +67,7 @@ const mapStateToProps = state => ({
frequentlyUsedEmojis: getFrequentlyUsedEmojis(state),
});
const mapDispatchToProps = (dispatch, { onPickEmoji }) => ({
const mapDispatchToProps = (dispatch, props) => ({
onSkinTone: skinTone => {
dispatch(changeSetting(['skinTone'], skinTone));
},
@ -75,8 +75,8 @@ const mapDispatchToProps = (dispatch, { onPickEmoji }) => ({
onPickEmoji: emoji => {
dispatch(useEmoji(emoji)); // eslint-disable-line react-hooks/rules-of-hooks
if (onPickEmoji) {
onPickEmoji(emoji);
if (props.onPickEmoji) {
props.onPickEmoji(emoji);
}
},
});

Wyświetl plik

@ -1,285 +0,0 @@
import classNames from 'classnames';
import { Map as ImmutableMap } from 'immutable';
import React, { useEffect, useRef, useState } from 'react';
import { FormattedDate, FormattedMessage } from 'react-intl';
import { TransitionMotion, spring } from 'react-motion';
import { useHistory } from 'react-router-dom';
import ReactSwipeableViews from 'react-swipeable-views';
import { createSelector } from 'reselect';
import { Card, HStack, Widget } from 'soapbox/components/ui';
import EmojiPickerDropdown from 'soapbox/features/compose/containers/emoji_picker_dropdown_container';
import unicodeMapping from 'soapbox/features/emoji/emoji_unicode_mapping_light';
import { useAppDispatch, useAppSelector, useSettings } from 'soapbox/hooks';
import { joinPublicPath } from 'soapbox/utils/static';
import type { RootState } from 'soapbox/store';
import type { Announcement as AnnouncementEntity, Mention as MentionEntity } from 'soapbox/types/entities';
const customEmojiMap = createSelector([(state: RootState) => state.custom_emojis], items => items.reduce((map, emoji) => map.set(emoji.shortcode, emoji), ImmutableMap()));
const AnnouncementContent = ({ announcement }: { announcement: AnnouncementEntity }) => {
const history = useHistory();
const node = useRef<HTMLDivElement>(null);
useEffect(() => {
updateLinks();
});
const onMentionClick = (mention: MentionEntity, e: MouseEvent) => {
if (e.button === 0 && !(e.ctrlKey || e.metaKey)) {
e.preventDefault();
e.stopPropagation();
history.push(`/@${mention.acct}`);
}
};
const onHashtagClick = (hashtag: string, e: MouseEvent) => {
hashtag = hashtag.replace(/^#/, '').toLowerCase();
if (e.button === 0 && !(e.ctrlKey || e.metaKey)) {
e.preventDefault();
e.stopPropagation();
history.push(`/tags/${hashtag}`);
}
};
/** For regular links, just stop propogation */
const onLinkClick = (e: MouseEvent) => {
e.stopPropagation();
};
const updateLinks = () => {
if (!node.current) return;
const links = node.current.querySelectorAll('a');
links.forEach(link => {
// Skip already processed
if (link.classList.contains('status-link')) return;
// Add attributes
link.classList.add('status-link');
link.setAttribute('rel', 'nofollow noopener');
link.setAttribute('target', '_blank');
const mention = announcement.mentions.find(mention => link.href === `${mention.url}`);
// Add event listeners on mentions and hashtags
if (mention) {
link.addEventListener('click', onMentionClick.bind(link, mention), false);
link.setAttribute('title', mention.acct);
} 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 {
link.setAttribute('title', link.href);
link.addEventListener('click', onLinkClick.bind(link), false);
}
});
};
return (
<div
className='translate text-sm'
ref={node}
dangerouslySetInnerHTML={{ __html: announcement.content }}
// onMouseEnter={handleMouseEnter}
// onMouseLeave={handleMouseLeave}
/>
);
};
const Emoji = ({ emoji, emojiMap, hovered }) => {
const autoPlayGif = useSettings().get('autoPlayGif');
if (unicodeMapping[emoji]) {
const { filename, shortCode } = unicodeMapping[emoji];
const title = shortCode ? `:${shortCode}:` : '';
return (
<img
draggable='false'
className='emojione'
alt={emoji}
title={title}
src={joinPublicPath(`/emoji/${filename}.svg`)}
/>
);
} else if (emojiMap.get(emoji)) {
const filename = (autoPlayGif || hovered) ? emojiMap.getIn([emoji, 'url']) : emojiMap.getIn([emoji, 'static_url']);
const shortCode = `:${emoji}:`;
return (
<img
draggable='false'
className='emojione custom-emoji'
alt={shortCode}
title={shortCode}
src={filename}
/>
);
} else {
return null;
}
};
const Reaction = ({ announcementId, reaction, addReaction, removeReaction, emojiMap, style }) => {
const [hovered, setHovered] = useState(false);
const handleClick = () => {
if (reaction.get('me')) {
removeReaction(announcementId, reaction.get('name'));
} else {
addReaction(announcementId, reaction.get('name'));
}
};
const handleMouseEnter = () => setHovered(true);
const handleMouseLeave = () => setHovered(false);
let shortCode = reaction.get('name');
if (unicodeMapping[shortCode]) {
shortCode = unicodeMapping[shortCode].shortCode;
}
return (
<button className={classNames('reactions-bar__item', { active: reaction.get('me') })} onClick={handleClick} onMouseEnter={handleMouseEnter} onMouseLeave={handleMouseLeave} title={`:${shortCode}:`} style={style}>
<span className='reactions-bar__item__emoji'><Emoji hovered={hovered} emoji={reaction.get('name')} emojiMap={emojiMap} /></span>
<span className='reactions-bar__item__count'>
{reaction.get('count')}
{/* <AnimatedNumber value={reaction.get('count')} /> */}
</span>
</button>
);
};
const ReactionsBar = ({ announcementId, reactions, addReaction, removeReaction, emojiMap }) => {
const reduceMotion = useSettings().get('reduceMotion');
const handleEmojiPick = data => {
addReaction(announcementId, data.native.replace(/:/g, ''));
};
const willEnter = () => reduceMotion ? 1 : 0;
const willLeave = () => reduceMotion ? 0 : spring(0, { stiffness: 170, damping: 26 });
const visibleReactions = reactions.filter(x => x.get('count') > 0);
const styles = visibleReactions.map(reaction => ({
key: reaction.get('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={classNames('reactions-bar', { 'reactions-bar--empty': visibleReactions.isEmpty() })}>
{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} />}
</div>
)}
</TransitionMotion>
);
};
const Announcement = ({ announcement, addReaction, removeReaction, emojiMap }: { announcement: AnnouncementEntity }) => {
const startsAt = announcement.starts_at && new Date(announcement.starts_at);
const endsAt = announcement.ends_at && new Date(announcement.ends_at);
const now = new Date();
const hasTimeRange = startsAt && endsAt;
const skipYear = hasTimeRange && startsAt.getFullYear() === endsAt.getFullYear() && endsAt.getFullYear() === now.getFullYear();
const skipEndDate = hasTimeRange && startsAt.getDate() === endsAt.getDate() && startsAt.getMonth() === endsAt.getMonth() && startsAt.getFullYear() === endsAt.getFullYear();
const skipTime = announcement.all_day;
return (
<div>
<strong>
{hasTimeRange && <span> · <FormattedDate value={startsAt} hour12={false} year={(skipYear || startsAt.getFullYear() === now.getFullYear()) ? undefined : 'numeric'} month='short' day='2-digit' hour={skipTime ? undefined : '2-digit'} minute={skipTime ? undefined : '2-digit'} /> - <FormattedDate value={endsAt} hour12={false} year={(skipYear || endsAt.getFullYear() === now.getFullYear()) ? undefined : 'numeric'} month={skipEndDate ? undefined : 'short'} day={skipEndDate ? undefined : '2-digit'} hour={skipTime ? undefined : '2-digit'} minute={skipTime ? undefined : '2-digit'} /></span>}
</strong>
<AnnouncementContent announcement={announcement} />
{/* <ReactionsBar
reactions={announcement.get('reactions')}
announcementId={announcement.get('id')}
addReaction={addReaction}
removeReaction={removeReaction}
emojiMap={emojiMap}
/> */}
</div>
);
};
const AnnouncementsPanel = () => {
const dispatch = useAppDispatch();
// const emojiMap = useAppSelector(state => customEmojiMap(state));
const [index, setIndex] = useState(0);
const announcements = useAppSelector((state) => state.announcements.items);
const addReaction = (id: string, name: string) => dispatch(addReaction(id, name));
const removeReaction = (id: string, name: string) => dispatch(removeReaction(id, name));
if (announcements.size === 0) return null;
const handleChangeIndex = (index: number) => {
setIndex(index % announcements.size);
};
return (
<Widget title={<FormattedMessage id='announcements.title' defaultMessage='Announcements' />}>
<Card className='relative' size='lg' variant='rounded'>
<ReactSwipeableViews animateHeight index={index} onChangeIndex={handleChangeIndex}>
{announcements.map((announcement) => (
<Announcement
key={announcement.id}
announcement={announcement}
// emojiMap={emojiMap}
addReaction={addReaction}
removeReaction={removeReaction}
// selected={index === idx}
// disabled={disableSwiping}
/>
)).reverse()}
</ReactSwipeableViews>
<HStack space={2} alignItems='center' justifyContent='center' className='relative'>
{announcements.map((_, i) => (
<button
key={i}
tabIndex={0}
onClick={() => setIndex(i)}
className={classNames({
'w-2 h-2 rounded-full focus:ring-primary-600 focus:ring-2 focus:ring-offset-2': true,
'bg-gray-200 hover:bg-gray-300': i !== index,
'bg-primary-600': i === index,
})}
/>
))}
</HStack>
</Card>
</Widget>
);
};
export default AnnouncementsPanel;

Wyświetl plik

@ -523,5 +523,5 @@ export function FamiliarFollowersModal() {
}
export function AnnouncementsPanel() {
return import(/* webpackChunkName: "features/announcements" */'../components/announcements-panel');
return import(/* webpackChunkName: "features/announcements" */'../../../components/announcements/announcements-panel');
}

Wyświetl plik

@ -10,7 +10,9 @@ import {
fromJS,
} from 'immutable';
import emojify from 'soapbox/features/emoji/emoji';
import { normalizeEmoji } from 'soapbox/normalizers/emoji';
import { makeEmojiMap } from 'soapbox/utils/normalizers';
import { normalizeAnnouncementReaction } from './announcement_reaction';
import { normalizeMention } from './mention';
@ -32,6 +34,9 @@ export const AnnouncementRecord = ImmutableRecord({
tags: ImmutableList<ImmutableMap<string, any>>(),
emojis: ImmutableList<Emoji>(),
updated_at: Date,
// Internal fields
contentHtml: '',
});
const normalizeMentions = (announcement: ImmutableMap<string, any>) => {
@ -54,12 +59,20 @@ const normalizeEmojis = (announcement: ImmutableMap<string, any>) => {
});
};
const normalizeContent = (announcement: ImmutableMap<string, any>) => {
const emojiMap = makeEmojiMap(announcement.get('emojis'));
const contentHtml = emojify(announcement.get('content'), emojiMap);
return announcement.set('contentHtml', contentHtml);
};
export const normalizeAnnouncement = (announcement: Record<string, any>) => {
return AnnouncementRecord(
ImmutableMap(fromJS(announcement)).withMutations(announcement => {
normalizeMentions(announcement);
normalizeReactions(announcement);
normalizeEmojis(announcement);
normalizeContent(announcement);
}),
);
};

Wyświetl plik

@ -1,6 +1,5 @@
import { List as ImmutableList, Record as ImmutableRecord, Set as ImmutableSet } from 'immutable';
import {
ANNOUNCEMENTS_FETCH_REQUEST,
ANNOUNCEMENTS_FETCH_SUCCESS,