TS/functional: Emoji picker
Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
This commit is contained in:
@ -177,7 +177,6 @@ const toggleFavourite = (status: StatusEntity) =>
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const favouriteRequest = (status: StatusEntity) => ({
|
||||
type: FAVOURITE_REQUEST,
|
||||
status: status,
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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) && (
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
@ -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';
|
||||
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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>
|
||||
|
||||
|
||||
@ -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);
|
||||
@ -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);
|
||||
@ -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);
|
||||
@ -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;
|
||||
|
||||
@ -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({
|
||||
|
||||
@ -35,7 +35,6 @@ type Suggestion = {
|
||||
account: Account
|
||||
}
|
||||
|
||||
|
||||
export default function useOnboardingSuggestions() {
|
||||
const api = useApi();
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
2
app/soapbox/react-notification/index.d.ts
vendored
2
app/soapbox/react-notification/index.d.ts
vendored
@ -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.
|
||||
|
||||
@ -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({
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user