Add reaction deck
This commit is contained in:
parent
db5c358f4f
commit
f1625fe101
24 changed files with 404 additions and 13 deletions
79
app/javascript/mastodon/actions/reaction_deck.js
Normal file
79
app/javascript/mastodon/actions/reaction_deck.js
Normal file
|
@ -0,0 +1,79 @@
|
|||
import api from '../api';
|
||||
|
||||
export const REACTION_DECK_FETCH_REQUEST = 'REACTION_DECK_FETCH_REQUEST';
|
||||
export const REACTION_DECK_FETCH_SUCCESS = 'REACTION_DECK_FETCH_SUCCESS';
|
||||
export const REACTION_DECK_FETCH_FAIL = 'REACTION_DECK_FETCH_FAIL';
|
||||
|
||||
export const REACTION_DECK_UPDATE_REQUEST = 'REACTION_DECK_UPDATE_REQUEST';
|
||||
export const REACTION_DECK_UPDATE_SUCCESS = 'REACTION_DECK_UPDATE_SUCCESS';
|
||||
export const REACTION_DECK_UPDATE_FAIL = 'REACTION_DECK_UPDATE_FAIL';
|
||||
|
||||
export function fetchReactionDeck() {
|
||||
return (dispatch, getState) => {
|
||||
dispatch(fetchReactionDeckRequest());
|
||||
|
||||
api(getState).get('/api/v1/reaction_deck').then(response => {
|
||||
dispatch(fetchReactionDeckSuccess(response.data));
|
||||
}).catch(error => {
|
||||
dispatch(fetchReactionDeckFail(error));
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function fetchReactionDeckRequest() {
|
||||
return {
|
||||
type: REACTION_DECK_FETCH_REQUEST,
|
||||
skipLoading: true,
|
||||
};
|
||||
}
|
||||
|
||||
export function fetchReactionDeckSuccess(emojis) {
|
||||
return {
|
||||
type: REACTION_DECK_FETCH_SUCCESS,
|
||||
emojis,
|
||||
skipLoading: true,
|
||||
};
|
||||
}
|
||||
|
||||
export function fetchReactionDeckFail(error) {
|
||||
return {
|
||||
type: REACTION_DECK_FETCH_FAIL,
|
||||
error,
|
||||
skipLoading: true,
|
||||
};
|
||||
}
|
||||
|
||||
export function updateReactionDeck(id, emoji) {
|
||||
return (dispatch, getState) => {
|
||||
dispatch(updateReactionDeckRequest());
|
||||
|
||||
api(getState).post('/api/v1/reaction_deck', { emojis: [{ id, emoji: emoji.native || emoji.id }] }).then(response => {
|
||||
dispatch(updateReactionDeckSuccess(response.data));
|
||||
}).catch(error => {
|
||||
dispatch(updateReactionDeckFail(error));
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function updateReactionDeckRequest() {
|
||||
return {
|
||||
type: REACTION_DECK_UPDATE_REQUEST,
|
||||
skipLoading: true,
|
||||
};
|
||||
}
|
||||
|
||||
export function updateReactionDeckSuccess(emojis) {
|
||||
return {
|
||||
type: REACTION_DECK_UPDATE_SUCCESS,
|
||||
emojis,
|
||||
skipLoading: true,
|
||||
};
|
||||
}
|
||||
|
||||
export function updateReactionDeckFail(error) {
|
||||
return {
|
||||
type: REACTION_DECK_UPDATE_FAIL,
|
||||
error,
|
||||
skipLoading: true,
|
||||
};
|
||||
}
|
|
@ -11,6 +11,7 @@ import { Provider as ReduxProvider } from 'react-redux';
|
|||
import { ScrollContext } from 'react-router-scroll-4';
|
||||
|
||||
import { fetchCustomEmojis } from 'mastodon/actions/custom_emojis';
|
||||
import { fetchReactionDeck } from 'mastodon/actions/reaction_deck';
|
||||
import { hydrateStore } from 'mastodon/actions/store';
|
||||
import { connectUserStream } from 'mastodon/actions/streaming';
|
||||
import ErrorBoundary from 'mastodon/components/error_boundary';
|
||||
|
@ -29,6 +30,7 @@ const hydrateAction = hydrateStore(initialState);
|
|||
store.dispatch(hydrateAction);
|
||||
if (initialState.meta.me) {
|
||||
store.dispatch(fetchCustomEmojis());
|
||||
store.dispatch(fetchReactionDeck());
|
||||
}
|
||||
|
||||
const createIdentityContext = state => ({
|
||||
|
|
|
@ -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' });
|
||||
|
|
|
@ -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;
|
||||
});
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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));
|
86
app/javascript/mastodon/features/reaction_deck/index.jsx
Normal file
86
app/javascript/mastodon/features/reaction_deck/index.jsx
Normal 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));
|
|
@ -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} />
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
|
|
|
@ -60,6 +60,7 @@
|
|||
* @property {string} domain
|
||||
* @property {boolean} enable_login_privacy
|
||||
* @property {boolean=} expand_spoilers
|
||||
* @property {boolean} hide_recent_emojis
|
||||
* @property {boolean} limited_federation_mode
|
||||
* @property {string} locale
|
||||
* @property {string | null} mascot
|
||||
|
@ -115,6 +116,7 @@ export const domain = getMeta('domain');
|
|||
export const enableLoginPrivacy = getMeta('enable_login_privacy');
|
||||
export const expandSpoilers = getMeta('expand_spoilers');
|
||||
export const forceSingleColumn = !getMeta('advanced_layout');
|
||||
export const hideRecentEmojis = getMeta('hide_recent_emojis');
|
||||
export const limitedFederationMode = getMeta('limited_federation_mode');
|
||||
export const mascot = getMeta('mascot');
|
||||
export const me = getMeta('me');
|
||||
|
|
|
@ -34,6 +34,7 @@ import notifications from './notifications';
|
|||
import picture_in_picture from './picture_in_picture';
|
||||
import polls from './polls';
|
||||
import push_notifications from './push_notifications';
|
||||
import reaction_deck from './reaction_deck';
|
||||
import relationships from './relationships';
|
||||
import search from './search';
|
||||
import server from './server';
|
||||
|
@ -92,6 +93,7 @@ const reducers = {
|
|||
history,
|
||||
tags,
|
||||
followed_tags,
|
||||
reaction_deck,
|
||||
};
|
||||
|
||||
const rootReducer = combineReducers(reducers);
|
||||
|
|
13
app/javascript/mastodon/reducers/reaction_deck.js
Normal file
13
app/javascript/mastodon/reducers/reaction_deck.js
Normal file
|
@ -0,0 +1,13 @@
|
|||
import { List as ImmutableList, fromJS as ConvertToImmutable } from 'immutable';
|
||||
|
||||
import { REACTION_DECK_FETCH_SUCCESS, REACTION_DECK_UPDATE_SUCCESS } from '../actions/reaction_deck';
|
||||
|
||||
const initialState = ImmutableList([]);
|
||||
|
||||
export default function reaction_deck(state = initialState, action) {
|
||||
if(action.type === REACTION_DECK_FETCH_SUCCESS || action.type === REACTION_DECK_UPDATE_SUCCESS) {
|
||||
state = ConvertToImmutable(action.emojis);
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
|
@ -7544,6 +7544,26 @@ noscript {
|
|||
}
|
||||
}
|
||||
|
||||
.reaction_deck__emoji {
|
||||
&__wrapper {
|
||||
display: flex;
|
||||
|
||||
margin: 8px 4px;
|
||||
height: 32px;
|
||||
|
||||
.emojione {
|
||||
min-width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.emoji-button {
|
||||
margin-left: 20px;
|
||||
margin-right: 24px;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.focal-point {
|
||||
position: relative;
|
||||
cursor: move;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue