Add reaction deck

This commit is contained in:
KMY 2023-05-26 15:15:48 +09:00
parent db5c358f4f
commit f1625fe101
24 changed files with 404 additions and 13 deletions

View file

@ -11,6 +11,7 @@ const messages = defineMessages({
edit_profile: { id: 'account.edit_profile', defaultMessage: 'Edit profile' },
pins: { id: 'navigation_bar.pins', defaultMessage: 'Pinned posts' },
preferences: { id: 'navigation_bar.preferences', defaultMessage: 'Preferences' },
reaction_deck: { id: 'navigation_bar.reaction_deck', defaultMessage: 'Reaction deck' },
follow_requests: { id: 'navigation_bar.follow_requests', defaultMessage: 'Follow requests' },
favourites: { id: 'navigation_bar.favourites', defaultMessage: 'Favourites' },
emoji_reactions: { id: 'navigation_bar.emoji_reactions', defaultMessage: 'Stamps' },
@ -44,6 +45,7 @@ class ActionBar extends PureComponent {
menu.push({ text: intl.formatMessage(messages.edit_profile), href: '/settings/profile' });
menu.push({ text: intl.formatMessage(messages.preferences), href: '/settings/preferences' });
menu.push({ text: intl.formatMessage(messages.reaction_deck), to: '/reaction_deck' });
menu.push({ text: intl.formatMessage(messages.pins), to: '/pinned' });
menu.push(null);
menu.push({ text: intl.formatMessage(messages.follow_requests), to: '/follow_requests' });

View file

@ -1,11 +1,16 @@
import { Map as ImmutableMap } from 'immutable';
import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { hideRecentEmojis } from 'mastodon/initial_state';
import { useEmoji } from '../../../actions/emojis';
import { changeSetting } from '../../../actions/settings';
import { shortCodes } from '../../emoji/emoji_mart_data_light';
import EmojiPickerDropdown from '../components/emoji_picker_dropdown';
const perLine = 8;
const lines = 2;
@ -28,21 +33,43 @@ const DEFAULTS = [
'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();
const RECENT_SIZE = DEFAULTS.length;
const DECK_SIZE = 16;
if (emojis.length < DEFAULTS.length) {
let uniqueDefaults = DEFAULTS.filter(emoji => !emojis.includes(emoji));
emojis = emojis.concat(uniqueDefaults.slice(0, DEFAULTS.length - emojis.length));
const getFrequentlyUsedEmojis = createSelector([
state => { return {
emojiCounters: state.getIn(['settings', 'frequentlyUsedEmojis'], ImmutableMap()),
reactionDeck: state.get('reaction_deck', ImmutableList()),
}; },
], data => {
const { emojiCounters, reactionDeck } = data;
let deckEmojis = reactionDeck
.toArray()
.map((e) => e.get('emoji'))
.filter((e) => e)
.map((e) => shortCodes[e] || e);
deckEmojis = [...new Set(deckEmojis)];
let emojis;
if (!hideRecentEmojis) {
emojis = emojiCounters
.keySeq()
.filter((ee) => deckEmojis.indexOf(ee) < 0)
.sort((a, b) => emojiCounters.get(a) - emojiCounters.get(b))
.reverse()
.slice(0, perLine * lines)
.toArray();
if (emojis.length < RECENT_SIZE) {
let uniqueDefaults = DEFAULTS.filter(emoji => !emojis.includes(emoji));
emojis = emojis.concat(uniqueDefaults.slice(0, RECENT_SIZE - emojis.length));
}
} else {
emojis = [];
}
emojis = deckEmojis.slice(0, DECK_SIZE).concat(emojis);
return emojis;
});

View file

@ -7,6 +7,7 @@ import { unicodeToUnifiedName } from './unicode_to_unified_name';
const [ shortCodesToEmojiData, skins, categories, short_names ] = emojiCompressed;
const emojis = {};
const shortCodes = {};
// decompress
Object.keys(shortCodesToEmojiData).forEach((shortCode) => {
@ -33,10 +34,12 @@ Object.keys(shortCodesToEmojiData).forEach((shortCode) => {
short_names,
unified,
};
shortCodes[native] = shortCode;
});
export {
emojis,
shortCodes,
skins,
categories,
short_names,

View file

@ -0,0 +1,75 @@
import PropTypes from 'prop-types';
import { injectIntl } from 'react-intl';
import { List as ImmutableList, Map as ImmutableMap } from 'immutable';
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { connect } from 'react-redux';
import { updateReactionDeck } from 'mastodon/actions/reaction_deck';
import EmojiPickerDropdownContainer from 'mastodon/features/compose/containers/emoji_picker_dropdown_container';
import emojify from 'mastodon/features/emoji/emoji';
import { autoPlayGif } from 'mastodon/initial_state';
const MapStateToProps = (state, { emojiId, emojiMap }) => ({
emoji: (state.get('reaction_deck', ImmutableList()).toArray().find(em => em.get('id') === emojiId) || ImmutableMap({ emoji: '' })).get('emoji'),
emojiMap,
});
const mapDispatchToProps = (dispatch, { emojiId }) => ({
onChange: (emoji) => dispatch(updateReactionDeck(emojiId, emoji)),
});
class ReactionEmoji extends ImmutablePureComponent {
static propTypes = {
emoji: PropTypes.string,
emojiMap: ImmutablePropTypes.map.isRequired,
onChange: PropTypes.func.isRequired,
};
static defaultProps = {
emoji: '',
};
render () {
const { emojiMap, emoji, onChange } = this.props;
let content = null;
if (emojiMap.get(emoji)) {
const filename = autoPlayGif ? emojiMap.getIn([emoji, 'url']) : emojiMap.getIn([emoji, 'static_url']);
const shortCode = `:${emoji}:`;
content = (
<img
draggable='false'
className='emojione custom-emoji'
alt={shortCode}
title={shortCode}
src={filename}
/>
);
} else {
const html = { __html: emojify(emoji) };
content = (
<span dangerouslySetInnerHTML={html} />
)
}
return (
<div className='reaction_deck__emoji'>
<div className='reaction_deck__emoji__wrapper'>
<EmojiPickerDropdownContainer onPickEmoji={onChange} />
<div>
{content}
</div>
</div>
</div>
);
}
}
export default connect(MapStateToProps, mapDispatchToProps)(injectIntl(ReactionEmoji));

View file

@ -0,0 +1,86 @@
import PropTypes from 'prop-types';
import { defineMessages, injectIntl } from 'react-intl';
import { Helmet } from 'react-helmet';
import { Map as ImmutableMap } from 'immutable';
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import ColumnHeader from 'mastodon/components/column_header';
import LoadingIndicator from 'mastodon/components/loading_indicator';
import ScrollableList from 'mastodon/components/scrollable_list';
import Column from 'mastodon/features/ui/components/column';
import ReactionEmoji from './components/reaction_emoji';
const DECK_SIZE = 16;
const customEmojiMap = createSelector([state => state.get('custom_emojis')], items => items.reduce((map, emoji) => map.set(emoji.get('shortcode'), emoji), ImmutableMap()));
const messages = defineMessages({
refresh: { id: 'refresh', defaultMessage: 'Refresh' },
heading: { id: 'column.reaction_deck', defaultMessage: 'Reaction deck' },
});
const mapStateToProps = (state, props) => ({
accountIds: state.getIn(['user_lists', 'favourited_by', props.params.statusId]),
deck: state.get('reaction_deck'),
emojiMap: customEmojiMap(state),
});
class ReactionDeck extends ImmutablePureComponent {
static propTypes = {
params: PropTypes.object.isRequired,
dispatch: PropTypes.func.isRequired,
deck: ImmutablePropTypes.list,
emojiMap: ImmutablePropTypes.map,
multiColumn: PropTypes.bool,
intl: PropTypes.object.isRequired,
};
render () {
const { intl, deck, emojiMap, multiColumn } = this.props;
if (!deck) {
return (
<Column>
<LoadingIndicator />
</Column>
);
}
return (
<Column bindToDocument={!multiColumn}>
<ColumnHeader
icon='smile-o'
title={intl.formatMessage(messages.heading)}
multiColumn={multiColumn}
showBackButton
/>
<ScrollableList
scrollKey='reaction_deck'
bindToDocument={!multiColumn}
>
{[...Array(DECK_SIZE).keys()].map(emojiId =>
<ReactionEmoji key={emojiId} emojiMap={emojiMap} emojiId={emojiId} />
)}
</ScrollableList>
<Helmet>
<meta name='robots' content='noindex' />
</Helmet>
</Column>
);
}
}
export default connect(mapStateToProps)(injectIntl(ReactionDeck));

View file

@ -62,6 +62,7 @@ import {
Lists,
Directory,
Explore,
ReactionDeck,
Onboarding,
About,
PrivacyPolicy,
@ -203,6 +204,8 @@ class SwitchingColumnsArea extends PureComponent {
<WrappedRoute path='/bookmarks' component={BookmarkedStatuses} content={children} />
<WrappedRoute path='/pinned' component={PinnedStatuses} content={children} />
<WrappedRoute path='/reaction_deck' component={ReactionDeck} content={children} />
<WrappedRoute path='/start' exact component={Onboarding} content={children} />
<WrappedRoute path='/directory' component={Directory} content={children} />
<WrappedRoute path={['/explore', '/search']} component={Explore} content={children} />

View file

@ -166,6 +166,10 @@ export function Onboarding () {
return import(/* webpackChunkName: "features/onboarding" */'../../onboarding');
}
export function ReactionDeck () {
return import(/* webpackChunkName: "features/reaction_deck" */'../../reaction_deck');
}
export function CompareHistoryModal () {
return import(/*webpackChunkName: "modals/compare_history_modal" */'../components/compare_history_modal');
}