From 488584bfc15ace3a097947f5190b73354aaa19e9 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sun, 8 Oct 2017 21:47:15 +0200 Subject: 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 +++++++++++---- .../compose/components/emoji_picker_dropdown.js | 10 ++++++-- .../containers/emoji_picker_dropdown_container.js | 27 +++++++++++++++++++++- app/javascript/mastodon/reducers/settings.js | 27 ++++++++++++++++++---- 6 files changed, 86 insertions(+), 13 deletions(-) create mode 100644 app/javascript/mastodon/actions/emojis.js (limited to 'app') diff --git a/app/javascript/mastodon/actions/compose.js b/app/javascript/mastodon/actions/compose.js index 560c00720..8a35049b3 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 000000000..7cd9d4b7b --- /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 f9d304c96..79adca18c 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 9be8909d8..dffa04ff0 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 56cc6c3b1..4fa93f6b0 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 3063ddadd..a9f3f9529 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; } -- cgit