TS/functional: Emoji picker

Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
This commit is contained in:
marcin mikołajczak
2022-09-11 19:28:12 +02:00
parent 0c20739d71
commit 87877a3f96
18 changed files with 38 additions and 568 deletions

View File

@ -177,7 +177,6 @@ const toggleFavourite = (status: StatusEntity) =>
}
};
const favouriteRequest = (status: StatusEntity) => ({
type: FAVOURITE_REQUEST,
status: status,

View File

@ -3,7 +3,7 @@ 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 EmojiPickerDropdown from 'soapbox/features/compose/components/emoji-picker/emoji-picker-dropdown';
import { useSettings } from 'soapbox/hooks';
import Reaction from './reaction';

View File

@ -45,7 +45,6 @@ const PollFooter: React.FC<IPollFooter> = ({ poll, showResults, selected }): JSX
votesCount = <FormattedMessage id='poll.total_votes' defaultMessage='{count, plural, one {# vote} other {# votes}}' values={{ count: poll.get('votes_count') }} />;
}
return (
<Stack space={4} data-testid='poll-footer'>
{(!showResults && poll?.multiple) && (

View File

@ -21,6 +21,7 @@ import { Button, Stack } from 'soapbox/components/ui';
import { useAppDispatch, useAppSelector, useFeatures } from 'soapbox/hooks';
import { isMobile } from 'soapbox/is_mobile';
import EmojiPickerDropdown from '../components/emoji-picker/emoji-picker-dropdown';
import MarkdownButton from '../components/markdown_button';
import PollButton from '../components/poll_button';
import PollForm from '../components/polls/poll-form';
@ -30,7 +31,6 @@ import ScheduleButton from '../components/schedule_button';
import SpoilerButton from '../components/spoiler_button';
import UploadForm from '../components/upload_form';
import Warning from '../components/warning';
import EmojiPickerDropdown from '../containers/emoji_picker_dropdown_container';
import QuotedStatusContainer from '../containers/quoted_status_container';
import ReplyIndicatorContainer from '../containers/reply_indicator_container';
import ScheduleFormContainer from '../containers/schedule_form_container';

View File

@ -1,397 +0,0 @@
import classNames from 'clsx';
import { supportsPassiveEvents } from 'detect-passive-events';
import PropTypes from 'prop-types';
import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { defineMessages, injectIntl } from 'react-intl';
import Overlay from 'react-overlays/lib/Overlay';
import { IconButton } from 'soapbox/components/ui';
import { buildCustomEmojis } from '../../emoji/emoji';
import { EmojiPicker as EmojiPickerAsync } from '../../ui/util/async-components';
const messages = defineMessages({
emoji: { id: 'emoji_button.label', defaultMessage: 'Insert emoji' },
emoji_search: { id: 'emoji_button.search', defaultMessage: 'Search…' },
emoji_not_found: { id: 'emoji_button.not_found', defaultMessage: 'No emoji\'s found.' },
custom: { id: 'emoji_button.custom', defaultMessage: 'Custom' },
recent: { id: 'emoji_button.recent', defaultMessage: 'Frequently used' },
search_results: { id: 'emoji_button.search_results', defaultMessage: 'Search results' },
people: { id: 'emoji_button.people', defaultMessage: 'People' },
nature: { id: 'emoji_button.nature', defaultMessage: 'Nature' },
food: { id: 'emoji_button.food', defaultMessage: 'Food & Drink' },
activity: { id: 'emoji_button.activity', defaultMessage: 'Activity' },
travel: { id: 'emoji_button.travel', defaultMessage: 'Travel & Places' },
objects: { id: 'emoji_button.objects', defaultMessage: 'Objects' },
symbols: { id: 'emoji_button.symbols', defaultMessage: 'Symbols' },
flags: { id: 'emoji_button.flags', defaultMessage: 'Flags' },
});
let EmojiPicker, Emoji; // load asynchronously
const backgroundImageFn = () => require('emoji-datasource/img/twitter/sheets/32.png');
const listenerOptions = supportsPassiveEvents ? { passive: true } : false;
const categoriesSort = [
'recent',
'custom',
'people',
'nature',
'foods',
'activity',
'places',
'objects',
'symbols',
'flags',
];
class ModifierPickerMenu extends React.PureComponent {
static propTypes = {
active: PropTypes.bool,
onSelect: PropTypes.func.isRequired,
onClose: PropTypes.func.isRequired,
};
handleClick = e => {
this.props.onSelect(e.currentTarget.getAttribute('data-index') * 1);
}
componentDidUpdate(prevProps) {
if (this.props.active) {
this.attachListeners();
} else {
this.removeListeners();
}
}
componentWillUnmount() {
this.removeListeners();
}
handleDocumentClick = e => {
if (this.node && !this.node.contains(e.target)) {
this.props.onClose();
}
}
attachListeners() {
document.addEventListener('click', this.handleDocumentClick, false);
document.addEventListener('touchend', this.handleDocumentClick, listenerOptions);
}
removeListeners() {
document.removeEventListener('click', this.handleDocumentClick, false);
document.removeEventListener('touchend', this.handleDocumentClick, listenerOptions);
}
setRef = c => {
this.node = c;
}
render() {
const { active } = this.props;
return (
<div className='emoji-picker-dropdown__modifiers__menu' style={{ display: active ? 'block' : 'none' }} ref={this.setRef}>
<button onClick={this.handleClick} data-index={1}><Emoji emoji='thumbsup' set='twitter' size={22} sheetSize={32} skin={1} backgroundImageFn={backgroundImageFn} /></button>
<button onClick={this.handleClick} data-index={2}><Emoji emoji='thumbsup' set='twitter' size={22} sheetSize={32} skin={2} backgroundImageFn={backgroundImageFn} /></button>
<button onClick={this.handleClick} data-index={3}><Emoji emoji='thumbsup' set='twitter' size={22} sheetSize={32} skin={3} backgroundImageFn={backgroundImageFn} /></button>
<button onClick={this.handleClick} data-index={4}><Emoji emoji='thumbsup' set='twitter' size={22} sheetSize={32} skin={4} backgroundImageFn={backgroundImageFn} /></button>
<button onClick={this.handleClick} data-index={5}><Emoji emoji='thumbsup' set='twitter' size={22} sheetSize={32} skin={5} backgroundImageFn={backgroundImageFn} /></button>
<button onClick={this.handleClick} data-index={6}><Emoji emoji='thumbsup' set='twitter' size={22} sheetSize={32} skin={6} backgroundImageFn={backgroundImageFn} /></button>
</div>
);
}
}
class ModifierPicker extends React.PureComponent {
static propTypes = {
active: PropTypes.bool,
modifier: PropTypes.number,
onChange: PropTypes.func,
onClose: PropTypes.func,
onOpen: PropTypes.func,
};
handleClick = () => {
if (this.props.active) {
this.props.onClose();
} else {
this.props.onOpen();
}
}
handleSelect = modifier => {
this.props.onChange(modifier);
this.props.onClose();
}
render() {
const { active, modifier } = this.props;
return (
<div className='emoji-picker-dropdown__modifiers'>
<Emoji emoji='thumbsup' set='twitter' size={22} sheetSize={32} skin={modifier} onClick={this.handleClick} backgroundImageFn={backgroundImageFn} />
<ModifierPickerMenu active={active} onSelect={this.handleSelect} onClose={this.props.onClose} />
</div>
);
}
}
@injectIntl
class EmojiPickerMenu extends React.PureComponent {
static propTypes = {
custom_emojis: ImmutablePropTypes.list,
frequentlyUsedEmojis: PropTypes.arrayOf(PropTypes.string),
loading: PropTypes.bool,
onClose: PropTypes.func.isRequired,
onPick: PropTypes.func.isRequired,
style: PropTypes.object,
placement: PropTypes.string,
arrowOffsetLeft: PropTypes.string,
arrowOffsetTop: PropTypes.string,
intl: PropTypes.object.isRequired,
skinTone: PropTypes.number.isRequired,
onSkinTone: PropTypes.func.isRequired,
};
static defaultProps = {
style: {},
loading: true,
frequentlyUsedEmojis: [],
};
state = {
modifierOpen: false,
placement: null,
};
handleDocumentClick = e => {
if (this.node && !this.node.contains(e.target)) {
this.props.onClose();
}
}
componentDidMount() {
document.addEventListener('click', this.handleDocumentClick, false);
document.addEventListener('touchend', this.handleDocumentClick, listenerOptions);
}
componentWillUnmount() {
document.removeEventListener('click', this.handleDocumentClick, false);
document.removeEventListener('touchend', this.handleDocumentClick, listenerOptions);
}
setRef = c => {
this.node = c;
}
getI18n = () => {
const { intl } = this.props;
return {
search: intl.formatMessage(messages.emoji_search),
notfound: intl.formatMessage(messages.emoji_not_found),
categories: {
search: intl.formatMessage(messages.search_results),
recent: intl.formatMessage(messages.recent),
people: intl.formatMessage(messages.people),
nature: intl.formatMessage(messages.nature),
foods: intl.formatMessage(messages.food),
activity: intl.formatMessage(messages.activity),
places: intl.formatMessage(messages.travel),
objects: intl.formatMessage(messages.objects),
symbols: intl.formatMessage(messages.symbols),
flags: intl.formatMessage(messages.flags),
custom: intl.formatMessage(messages.custom),
},
};
}
handleClick = emoji => {
if (!emoji.native) {
emoji.native = emoji.colons;
}
this.props.onClose();
this.props.onPick(emoji);
}
handleModifierOpen = () => {
this.setState({ modifierOpen: true });
}
handleModifierClose = () => {
this.setState({ modifierOpen: false });
}
handleModifierChange = modifier => {
this.props.onSkinTone(modifier);
}
render() {
const { loading, style, intl, custom_emojis, skinTone, frequentlyUsedEmojis } = this.props;
if (loading) {
return <div style={{ width: 299 }} />;
}
const title = intl.formatMessage(messages.emoji);
const { modifierOpen } = this.state;
return (
<div className={classNames('emoji-picker-dropdown__menu', { selecting: modifierOpen })} style={style} ref={this.setRef}>
<EmojiPicker
perLine={8}
emojiSize={22}
sheetSize={32}
custom={buildCustomEmojis(custom_emojis)}
color=''
emoji=''
set='twitter'
title={title}
i18n={this.getI18n()}
onClick={this.handleClick}
include={categoriesSort}
recent={frequentlyUsedEmojis}
skin={skinTone}
showPreview={false}
backgroundImageFn={backgroundImageFn}
autoFocus
emojiTooltip
/>
<ModifierPicker
active={modifierOpen}
modifier={skinTone}
onOpen={this.handleModifierOpen}
onClose={this.handleModifierClose}
onChange={this.handleModifierChange}
/>
</div>
);
}
}
export default @injectIntl
class EmojiPickerDropdown extends React.PureComponent {
static propTypes = {
custom_emojis: ImmutablePropTypes.list,
frequentlyUsedEmojis: PropTypes.arrayOf(PropTypes.string),
intl: PropTypes.object.isRequired,
onPickEmoji: PropTypes.func.isRequired,
onSkinTone: PropTypes.func.isRequired,
skinTone: PropTypes.number.isRequired,
button: PropTypes.node,
};
state = {
active: false,
loading: false,
};
setRef = (c) => {
this.dropdown = c;
}
onShowDropdown = (e) => {
e.stopPropagation();
this.setState({ active: true });
if (!EmojiPicker) {
this.setState({ loading: true });
EmojiPickerAsync().then(EmojiMart => {
EmojiPicker = EmojiMart.Picker;
Emoji = EmojiMart.Emoji;
this.setState({ loading: false });
}).catch(() => {
this.setState({ loading: false });
});
}
const { top } = e.target.getBoundingClientRect();
this.setState({ placement: top * 2 < innerHeight ? 'bottom' : 'top' });
}
onHideDropdown = () => {
this.setState({ active: false });
}
onToggle = (e) => {
if (!this.state.loading && (!e.key || e.key === 'Enter')) {
if (this.state.active) {
this.onHideDropdown();
} else {
this.onShowDropdown(e);
}
}
}
handleKeyDown = e => {
if (e.key === 'Escape') {
this.onHideDropdown();
}
}
setTargetRef = c => {
this.target = c;
}
findTarget = () => {
return this.target;
}
render() {
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}>
<div
ref={this.setTargetRef}
title={title}
aria-label={title}
aria-expanded={active}
role='button'
onClick={this.onToggle}
onKeyDown={this.onToggle}
tabIndex={0}
>
{button || <IconButton
className={classNames({
'text-gray-600 hover:text-gray-700 dark:hover:text-gray-500': true,
'pulse-loading': active && loading,
})}
alt='😀'
src={require('@tabler/icons/mood-happy.svg')}
/>}
</div>
<Overlay show={active} placement={placement} target={this.findTarget}>
<EmojiPickerMenu
custom_emojis={this.props.custom_emojis}
loading={loading}
onClose={this.onHideDropdown}
onPick={onPickEmoji}
onSkinTone={onSkinTone}
skinTone={skinTone}
frequentlyUsedEmojis={frequentlyUsedEmojis}
/>
</Overlay>
</div>
);
}
}

View File

@ -10,8 +10,8 @@ import { changeComposeVisibility } from 'soapbox/actions/compose';
import { closeModal, openModal } from 'soapbox/actions/modals';
import Icon from 'soapbox/components/icon';
import { IconButton } from 'soapbox/components/ui';
import { isUserTouching } from 'soapbox/is_mobile';
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
import { isUserTouching } from 'soapbox/is_mobile';
import Motion from '../../ui/util/optional_motion';

View File

@ -1,17 +1,19 @@
import classNames from 'clsx';
import { List as ImmutableList } from 'immutable';
import React, { useState } from 'react';
import { defineMessages, useIntl, FormattedMessage } from 'react-intl';
import { spring } from 'react-motion';
import { useHistory } from 'react-router-dom';
import { undoUploadCompose, changeUploadCompose, submitCompose } from 'soapbox/actions/compose';
import { openModal } from 'soapbox/actions/modals';
import Blurhash from 'soapbox/components/blurhash';
import Icon from 'soapbox/components/icon';
import IconButton from 'soapbox/components/icon_button';
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
import Motion from '../../ui/util/optional_motion';
import type { Map as ImmutableMap } from 'immutable';
const bookIcon = require('@tabler/icons/book.svg');
const fileCodeIcon = require('@tabler/icons/file-code.svg');
const fileSpreadsheetIcon = require('@tabler/icons/file-spreadsheet.svg');
@ -60,18 +62,17 @@ const messages = defineMessages({
});
interface IUpload {
media: ImmutableMap<string, any>,
descriptionLimit: number,
onUndo: (attachmentId: string) => void,
onDescriptionChange: (attachmentId: string, description: string) => void,
onOpenFocalPoint: (attachmentId: string) => void,
onOpenModal: (attachments: ImmutableMap<string, any>) => void,
onSubmit: (history: ReturnType<typeof useHistory>) => void,
id: string,
composeId: string,
}
const Upload: React.FC<IUpload> = (props) => {
const Upload: React.FC<IUpload> = ({ composeId, id }) => {
const intl = useIntl();
const history = useHistory();
const dispatch = useAppDispatch();
const media = useAppSelector((state) => state.compose.get(composeId)!.media_attachments.find(item => item.get('id') === id)!);
const descriptionLimit = useAppSelector((state) => state.instance.get('description_limit'));
const [hovered, setHovered] = useState(false);
const [focused, setFocused] = useState(false);
@ -85,12 +86,12 @@ const Upload: React.FC<IUpload> = (props) => {
const handleSubmit = () => {
handleInputBlur();
props.onSubmit(history);
dispatch(submitCompose(composeId, history));
};
const handleUndoClick: React.MouseEventHandler = e => {
e.stopPropagation();
props.onUndo(props.media.get('id'));
dispatch(undoUploadCompose(media.id));
};
const handleInputChange: React.ChangeEventHandler<HTMLTextAreaElement> = e => {
@ -118,22 +119,22 @@ const Upload: React.FC<IUpload> = (props) => {
setDirtyDescription(null);
if (dirtyDescription !== null) {
props.onDescriptionChange(props.media.get('id'), dirtyDescription);
dispatch(changeUploadCompose(media.id, { dirtyDescription }));
}
};
const handleOpenModal = () => {
props.onOpenModal(props.media);
dispatch(openModal('MEDIA', { media: ImmutableList.of(media), index: 0 }));
};
const active = hovered || focused;
const description = dirtyDescription || (dirtyDescription !== '' && props.media.get('description')) || '';
const focusX = props.media.getIn(['meta', 'focus', 'x']) as number | undefined;
const focusY = props.media.getIn(['meta', 'focus', 'y']) as number | undefined;
const description = dirtyDescription || (dirtyDescription !== '' && media.description) || '';
const focusX = media.meta.getIn(['focus', 'x']) as number | undefined;
const focusY = media.meta.getIn(['focus', 'y']) as number | undefined;
const x = focusX ? ((focusX / 2) + .5) * 100 : undefined;
const y = focusY ? ((focusY / -2) + .5) * 100 : undefined;
const mediaType = props.media.get('type');
const mimeType = props.media.getIn(['pleroma', 'mime_type']) as string | undefined;
const mediaType = media.type;
const mimeType = media.pleroma.get('mime_type') as string | undefined;
const uploadIcon = mediaType === 'unknown' && (
<Icon
@ -144,14 +145,14 @@ const Upload: React.FC<IUpload> = (props) => {
return (
<div className='compose-form__upload' tabIndex={0} onMouseEnter={handleMouseEnter} onMouseLeave={handleMouseLeave} onClick={handleClick} role='button'>
<Blurhash hash={props.media.get('blurhash')} className='media-gallery__preview' />
<Blurhash hash={media.blurhash} className='media-gallery__preview' />
<Motion defaultStyle={{ scale: 0.8 }} style={{ scale: spring(1, { stiffness: 180, damping: 12 }) }}>
{({ scale }) => (
<div
className={classNames('compose-form__upload-thumbnail', `${mediaType}`)}
style={{
transform: `scale(${scale})`,
backgroundImage: mediaType === 'image' ? `url(${props.media.get('preview_url')})` : undefined,
backgroundImage: mediaType === 'image' ? `url(${media.preview_url})` : undefined,
backgroundPosition: typeof x === 'number' && typeof y === 'number' ? `${x}% ${y}%` : undefined }}
>
<div className={classNames('compose-form__upload__actions', { active })}>
@ -162,7 +163,7 @@ const Upload: React.FC<IUpload> = (props) => {
/>
{/* Only display the "Preview" button for a valid attachment with a URL */}
{(mediaType !== 'unknown' && Boolean(props.media.get('url'))) && (
{(mediaType !== 'unknown' && Boolean(media.url)) && (
<IconButton
onClick={handleOpenModal}
src={require('@tabler/icons/zoom-in.svg')}
@ -178,7 +179,7 @@ const Upload: React.FC<IUpload> = (props) => {
<textarea
placeholder={intl.formatMessage(messages.description)}
value={description}
maxLength={props.descriptionLimit}
maxLength={descriptionLimit}
onFocus={handleInputFocus}
onChange={handleInputChange}
onBlur={handleInputBlur}
@ -190,7 +191,7 @@ const Upload: React.FC<IUpload> = (props) => {
<div className='compose-form__upload-preview'>
{mediaType === 'video' && (
<video autoPlay playsInline muted loop>
<source src={props.media.get('preview_url')} />
<source src={media.preview_url} />
</video>
)}
{uploadIcon}

View File

@ -3,9 +3,9 @@ import React from 'react';
import { useAppSelector } from 'soapbox/hooks';
import SensitiveButton from '../components/sensitive-button';
import UploadProgress from '../components/upload-progress';
import UploadContainer from '../containers/upload_container';
import SensitiveButton from './sensitive-button';
import Upload from './upload';
import UploadProgress from './upload-progress';
import type { Attachment as AttachmentEntity } from 'soapbox/types/entities';
@ -25,7 +25,7 @@ const UploadForm: React.FC<IUploadForm> = ({ composeId }) => {
<div className={classes}>
{mediaIds.map((id: string) => (
<UploadContainer id={id} key={id} />
<Upload id={id} key={id} composeId={composeId} />
))}
</div>

View File

@ -1,84 +0,0 @@
import { Map as ImmutableMap } from 'immutable';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { useEmoji } from '../../../actions/emojis';
import { getSettings, changeSetting } from '../../../actions/settings';
import EmojiPickerDropdown from '../components/emoji_picker_dropdown';
const perLine = 8;
const lines = 2;
const DEFAULTS = [
'+1',
'grinning',
'kissing_heart',
'heart_eyes',
'laughing',
'stuck_out_tongue_winking_eye',
'sweat_smile',
'joy',
'yum',
'disappointed',
'thinking_face',
'weary',
'sob',
'sunglasses',
'heart',
'ok_hand',
];
const getFrequentlyUsedEmojis = createSelector([
state => state.getIn(['settings', 'frequentlyUsedEmojis'], ImmutableMap()),
], emojiCounters => {
let emojis = emojiCounters
.keySeq()
.sort((a, b) => emojiCounters.get(a) - emojiCounters.get(b))
.reverse()
.slice(0, perLine * lines)
.toArray();
if (emojis.length < DEFAULTS.length) {
const uniqueDefaults = DEFAULTS.filter(emoji => !emojis.includes(emoji));
emojis = emojis.concat(uniqueDefaults.slice(0, DEFAULTS.length - emojis.length));
}
return emojis;
});
const getCustomEmojis = createSelector([
state => state.get('custom_emojis'),
], emojis => emojis.filter(e => e.get('visible_in_picker')).sort((a, b) => {
const aShort = a.get('shortcode').toLowerCase();
const bShort = b.get('shortcode').toLowerCase();
if (aShort < bShort) {
return -1;
} else if (aShort > bShort) {
return 1;
} else {
return 0;
}
}));
const mapStateToProps = state => ({
custom_emojis: getCustomEmojis(state),
skinTone: getSettings(state).get('skinTone'),
frequentlyUsedEmojis: getFrequentlyUsedEmojis(state),
});
const mapDispatchToProps = (dispatch, props) => ({
onSkinTone: skinTone => {
dispatch(changeSetting(['skinTone'], skinTone));
},
onPickEmoji: emoji => {
dispatch(useEmoji(emoji)); // eslint-disable-line react-hooks/rules-of-hooks
if (props.onPickEmoji) {
props.onPickEmoji(emoji);
}
},
});
export default connect(mapStateToProps, mapDispatchToProps)(EmojiPickerDropdown);

View File

@ -1,37 +0,0 @@
import { List as ImmutableList } from 'immutable';
import { connect } from 'react-redux';
import { undoUploadCompose, changeUploadCompose, submitCompose } from '../../../actions/compose';
import { openModal } from '../../../actions/modals';
import Upload from '../components/upload';
const mapStateToProps = (state, { id }) => ({
media: state.getIn(['compose', 'media_attachments']).find(item => item.get('id') === id),
descriptionLimit: state.getIn(['instance', 'description_limit']),
});
const mapDispatchToProps = dispatch => ({
onUndo: id => {
dispatch(undoUploadCompose(id));
},
onDescriptionChange: (id, description) => {
dispatch(changeUploadCompose(id, { description }));
},
onOpenFocalPoint: id => {
dispatch(openModal('FOCAL_POINT', { id }));
},
onOpenModal: media => {
dispatch(openModal('MEDIA', { media: ImmutableList.of(media), index: 0 }));
},
onSubmit(router) {
dispatch(submitCompose(router));
},
});
export default connect(mapStateToProps, mapDispatchToProps)(Upload);

View File

@ -1,10 +0,0 @@
import { connect } from 'react-redux';
import UploadProgress from '../components/upload-progress';
const mapStateToProps = state => ({
active: state.getIn(['compose', 'is_uploading']),
progress: state.getIn(['compose', 'progress']),
});
export default connect(mapStateToProps)(UploadProgress);

View File

@ -10,7 +10,6 @@ import useOnboardingSuggestions from 'soapbox/queries/suggestions';
const SuggestedAccountsStep = ({ onNext }: { onNext: () => void }) => {
const { data, fetchNextPage, hasNextPage, isFetching } = useOnboardingSuggestions();
const handleLoadMore = debounce(() => {
if (isFetching) {
return null;

View File

@ -17,7 +17,7 @@ import { makeEmojiMap } from 'soapbox/utils/normalizers';
import { normalizeAnnouncementReaction } from './announcement_reaction';
import { normalizeMention } from './mention';
import type { AnnouncementReaction, Emoji, Mention } from 'soapbox/types/entities';
import type { AnnouncementReaction, Emoji, Mention } from 'soapbox/types/entities';
// https://docs.joinmastodon.org/entities/announcement/
export const AnnouncementRecord = ImmutableRecord({

View File

@ -35,7 +35,6 @@ type Suggestion = {
account: Account
}
export default function useOnboardingSuggestions() {
const api = useApi();
const dispatch = useAppDispatch();

View File

@ -49,7 +49,7 @@ declare module 'soapbox/react-notification' {
title?: string | ReactElement<any>;
/** Custom title styles. */
titleStyle?: object;
/**
* Callback function to run when dismissAfter timer runs out
* @param notification Notification currently being dismissed.

View File

@ -21,10 +21,10 @@ import {
ADMIN_USERS_APPROVE_SUCCESS,
} from 'soapbox/actions/admin';
import { normalizeAdminReport, normalizeAdminAccount } from 'soapbox/normalizers';
import { APIEntity } from 'soapbox/types/entities';
import { normalizeId } from 'soapbox/utils/normalizers';
import type { AnyAction } from 'redux';
import type { APIEntity } from 'soapbox/types/entities';
import type { Config } from 'soapbox/utils/config_db';
const ReducerRecord = ImmutableRecord({

View File

@ -17,7 +17,7 @@ import {
import { normalizeAnnouncement, normalizeAnnouncementReaction } from 'soapbox/normalizers';
import type { AnyAction } from 'redux';
import type{ Announcement, AnnouncementReaction, APIEntity } from 'soapbox/types/entities';
import type { Announcement, AnnouncementReaction, APIEntity } from 'soapbox/types/entities';
const ReducerRecord = ImmutableRecord({
items: ImmutableList<Announcement>(),
@ -107,4 +107,4 @@ export default function announcementsReducer(state = ReducerRecord(), action: An
default:
return state;
}
}
}

View File

@ -157,6 +157,7 @@ const rootReducer: typeof appReducer = (state, action) => {
case AUTH_LOGGED_OUT:
return appReducer(logOut(state), action);
default:
console.log(action.type);
return appReducer(state, action);
}
};