From 488584bfc15ace3a097947f5190b73354aaa19e9 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sun, 8 Oct 2017 21:47:15 +0200 Subject: [PATCH] Track frequently used emojis in web UI (#5275) * Track frequently used emojis in web UI * Persist emoji usage, but debounce commits to the settings API * Fix #5144 - Add tooltips to picker * Display only 2 lines of frequently used emojis --- app/javascript/mastodon/actions/compose.js | 3 +++ app/javascript/mastodon/actions/emojis.js | 14 ++++++++++ app/javascript/mastodon/actions/settings.js | 18 +++++++++---- .../components/emoji_picker_dropdown.js | 10 +++++-- .../emoji_picker_dropdown_container.js | 27 ++++++++++++++++++- app/javascript/mastodon/reducers/settings.js | 27 +++++++++++++++---- package.json | 2 +- yarn.lock | 6 ++--- 8 files changed, 90 insertions(+), 17 deletions(-) create mode 100644 app/javascript/mastodon/actions/emojis.js diff --git a/app/javascript/mastodon/actions/compose.js b/app/javascript/mastodon/actions/compose.js index 560c0072063..8a35049b32c 100644 --- a/app/javascript/mastodon/actions/compose.js +++ b/app/javascript/mastodon/actions/compose.js @@ -1,6 +1,7 @@ import api from '../api'; import { throttle } from 'lodash'; import { search as emojiSearch } from '../features/emoji/emoji_mart_search_light'; +import { useEmoji } from './emojis'; import { updateTimeline, @@ -305,6 +306,8 @@ export function selectComposeSuggestion(position, token, suggestion) { if (typeof suggestion === 'object' && suggestion.id) { completion = suggestion.native || suggestion.colons; startPosition = position - 1; + + dispatch(useEmoji(suggestion)); } else { completion = getState().getIn(['accounts', suggestion, 'acct']); startPosition = position; diff --git a/app/javascript/mastodon/actions/emojis.js b/app/javascript/mastodon/actions/emojis.js new file mode 100644 index 00000000000..7cd9d4b7b35 --- /dev/null +++ b/app/javascript/mastodon/actions/emojis.js @@ -0,0 +1,14 @@ +import { saveSettings } from './settings'; + +export const EMOJI_USE = 'EMOJI_USE'; + +export function useEmoji(emoji) { + return dispatch => { + dispatch({ + type: EMOJI_USE, + emoji, + }); + + dispatch(saveSettings()); + }; +}; diff --git a/app/javascript/mastodon/actions/settings.js b/app/javascript/mastodon/actions/settings.js index f9d304c96a3..79adca18c6e 100644 --- a/app/javascript/mastodon/actions/settings.js +++ b/app/javascript/mastodon/actions/settings.js @@ -1,6 +1,8 @@ import axios from 'axios'; +import { debounce } from 'lodash'; export const SETTING_CHANGE = 'SETTING_CHANGE'; +export const SETTING_SAVE = 'SETTING_SAVE'; export function changeSetting(key, value) { return dispatch => { @@ -14,10 +16,16 @@ export function changeSetting(key, value) { }; }; +const debouncedSave = debounce((dispatch, getState) => { + if (getState().getIn(['settings', 'saved'])) { + return; + } + + const data = getState().get('settings').filter((_, key) => key !== 'saved').toJS(); + + axios.put('/api/web/settings', { data }).then(() => dispatch({ type: SETTING_SAVE })); +}, 5000, { trailing: true }); + export function saveSettings() { - return (_, getState) => { - axios.put('/api/web/settings', { - data: getState().get('settings').toJS(), - }); - }; + return (dispatch, getState) => debouncedSave(dispatch, getState); }; diff --git a/app/javascript/mastodon/features/compose/components/emoji_picker_dropdown.js b/app/javascript/mastodon/features/compose/components/emoji_picker_dropdown.js index 9be8909d8b4..dffa04ff032 100644 --- a/app/javascript/mastodon/features/compose/components/emoji_picker_dropdown.js +++ b/app/javascript/mastodon/features/compose/components/emoji_picker_dropdown.js @@ -146,6 +146,7 @@ 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, @@ -163,6 +164,7 @@ class EmojiPickerMenu extends React.PureComponent { style: {}, loading: true, placement: 'bottom', + frequentlyUsedEmojis: [], }; state = { @@ -233,7 +235,7 @@ class EmojiPickerMenu extends React.PureComponent { } render () { - const { loading, style, intl, custom_emojis, autoPlay, skinTone } = this.props; + const { loading, style, intl, custom_emojis, autoPlay, skinTone, frequentlyUsedEmojis } = this.props; if (loading) { return
; @@ -256,9 +258,11 @@ class EmojiPickerMenu extends React.PureComponent { i18n={this.getI18n()} onClick={this.handleClick} include={categoriesSort} + recent={frequentlyUsedEmojis} skin={skinTone} showPreview={false} backgroundImageFn={backgroundImageFn} + emojiTooltip />
diff --git a/app/javascript/mastodon/features/compose/containers/emoji_picker_dropdown_container.js b/app/javascript/mastodon/features/compose/containers/emoji_picker_dropdown_container.js index 56cc6c3b10a..4fa93f6b033 100644 --- a/app/javascript/mastodon/features/compose/containers/emoji_picker_dropdown_container.js +++ b/app/javascript/mastodon/features/compose/containers/emoji_picker_dropdown_container.js @@ -1,17 +1,42 @@ import { connect } from 'react-redux'; import EmojiPickerDropdown from '../components/emoji_picker_dropdown'; import { changeSetting } from '../../../actions/settings'; +import { createSelector } from 'reselect'; +import { Map as ImmutableMap } from 'immutable'; +import { useEmoji } from '../../../actions/emojis'; + +const perLine = 8; +const lines = 2; + +const getFrequentlyUsedEmojis = createSelector([ + state => state.getIn(['settings', 'frequentlyUsedEmojis'], ImmutableMap()), +], emojiCounters => emojiCounters + .keySeq() + .sort((a, b) => emojiCounters.get(a) - emojiCounters.get(b)) + .reverse() + .slice(0, perLine * lines) + .toArray() +); const mapStateToProps = state => ({ custom_emojis: state.get('custom_emojis'), autoPlay: state.getIn(['meta', 'auto_play_gif']), skinTone: state.getIn(['settings', 'skinTone']), + frequentlyUsedEmojis: getFrequentlyUsedEmojis(state), }); -const mapDispatchToProps = dispatch => ({ +const mapDispatchToProps = (dispatch, { onPickEmoji }) => ({ onSkinTone: skinTone => { dispatch(changeSetting(['skinTone'], skinTone)); }, + + onPickEmoji: emoji => { + dispatch(useEmoji(emoji)); + + if (onPickEmoji) { + onPickEmoji(emoji); + } + }, }); export default connect(mapStateToProps, mapDispatchToProps)(EmojiPickerDropdown); diff --git a/app/javascript/mastodon/reducers/settings.js b/app/javascript/mastodon/reducers/settings.js index 3063ddadd45..a9f3f95296f 100644 --- a/app/javascript/mastodon/reducers/settings.js +++ b/app/javascript/mastodon/reducers/settings.js @@ -1,10 +1,13 @@ -import { SETTING_CHANGE } from '../actions/settings'; +import { SETTING_CHANGE, SETTING_SAVE } from '../actions/settings'; import { COLUMN_ADD, COLUMN_REMOVE, COLUMN_MOVE } from '../actions/columns'; import { STORE_HYDRATE } from '../actions/store'; +import { EMOJI_USE } from '../actions/emojis'; import { Map as ImmutableMap, fromJS } from 'immutable'; import uuid from '../uuid'; const initialState = ImmutableMap({ + saved: true, + onboarded: false, skinTone: 1, @@ -74,21 +77,35 @@ const moveColumn = (state, uuid, direction) => { newColumns = columns.splice(index, 1); newColumns = newColumns.splice(newIndex, 0, columns.get(index)); - return state.set('columns', newColumns); + return state + .set('columns', newColumns) + .set('saved', false); }; +const updateFrequentEmojis = (state, emoji) => state.update('frequentlyUsedEmojis', ImmutableMap(), map => map.update(emoji.id, 0, count => count + 1)).set('saved', false); + export default function settings(state = initialState, action) { switch(action.type) { case STORE_HYDRATE: return hydrate(state, action.state.get('settings')); case SETTING_CHANGE: - return state.setIn(action.key, action.value); + return state + .setIn(action.key, action.value) + .set('saved', false); case COLUMN_ADD: - return state.update('columns', list => list.push(fromJS({ id: action.id, uuid: uuid(), params: action.params }))); + return state + .update('columns', list => list.push(fromJS({ id: action.id, uuid: uuid(), params: action.params }))) + .set('saved', false); case COLUMN_REMOVE: - return state.update('columns', list => list.filterNot(item => item.get('uuid') === action.uuid)); + return state + .update('columns', list => list.filterNot(item => item.get('uuid') === action.uuid)) + .set('saved', false); case COLUMN_MOVE: return moveColumn(state, action.uuid, action.direction); + case EMOJI_USE: + return updateFrequentEmojis(state, action.emoji); + case SETTING_SAVE: + return state.set('saved', true); default: return state; } diff --git a/package.json b/package.json index 3d085690248..93e254abc41 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,7 @@ "css-loader": "^0.28.4", "detect-passive-events": "^1.0.2", "dotenv": "^4.0.0", - "emoji-mart": "^2.1.1", + "emoji-mart": "Gargron/emoji-mart#build", "es6-symbol": "^3.1.1", "escape-html": "^1.0.3", "express": "^4.15.2", diff --git a/yarn.lock b/yarn.lock index f0d2f5c232b..46daac160ce 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2191,9 +2191,9 @@ elliptic@^6.0.0: minimalistic-assert "^1.0.0" minimalistic-crypto-utils "^1.0.0" -emoji-mart@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/emoji-mart/-/emoji-mart-2.1.1.tgz#4bce8ec9d9fd0d8adfd2517e7e296871c40762ac" +emoji-mart@Gargron/emoji-mart#build: + version "2.1.2" + resolved "https://codeload.github.com/Gargron/emoji-mart/tar.gz/c28a721169d95eb40031a4dae5a79fa8a12a66c7" emoji-regex@^6.1.0: version "6.4.3"