* 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 emojismaster
@@ -1,6 +1,7 @@ | |||||
import api from '../api'; | import api from '../api'; | ||||
import { throttle } from 'lodash'; | import { throttle } from 'lodash'; | ||||
import { search as emojiSearch } from '../features/emoji/emoji_mart_search_light'; | import { search as emojiSearch } from '../features/emoji/emoji_mart_search_light'; | ||||
import { useEmoji } from './emojis'; | |||||
import { | import { | ||||
updateTimeline, | updateTimeline, | ||||
@@ -305,6 +306,8 @@ export function selectComposeSuggestion(position, token, suggestion) { | |||||
if (typeof suggestion === 'object' && suggestion.id) { | if (typeof suggestion === 'object' && suggestion.id) { | ||||
completion = suggestion.native || suggestion.colons; | completion = suggestion.native || suggestion.colons; | ||||
startPosition = position - 1; | startPosition = position - 1; | ||||
dispatch(useEmoji(suggestion)); | |||||
} else { | } else { | ||||
completion = getState().getIn(['accounts', suggestion, 'acct']); | completion = getState().getIn(['accounts', suggestion, 'acct']); | ||||
startPosition = position; | startPosition = position; | ||||
@@ -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()); | |||||
}; | |||||
}; |
@@ -1,6 +1,8 @@ | |||||
import axios from 'axios'; | import axios from 'axios'; | ||||
import { debounce } from 'lodash'; | |||||
export const SETTING_CHANGE = 'SETTING_CHANGE'; | export const SETTING_CHANGE = 'SETTING_CHANGE'; | ||||
export const SETTING_SAVE = 'SETTING_SAVE'; | |||||
export function changeSetting(key, value) { | export function changeSetting(key, value) { | ||||
return dispatch => { | 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() { | export function saveSettings() { | ||||
return (_, getState) => { | |||||
axios.put('/api/web/settings', { | |||||
data: getState().get('settings').toJS(), | |||||
}); | |||||
}; | |||||
return (dispatch, getState) => debouncedSave(dispatch, getState); | |||||
}; | }; |
@@ -146,6 +146,7 @@ class EmojiPickerMenu extends React.PureComponent { | |||||
static propTypes = { | static propTypes = { | ||||
custom_emojis: ImmutablePropTypes.list, | custom_emojis: ImmutablePropTypes.list, | ||||
frequentlyUsedEmojis: PropTypes.arrayOf(PropTypes.string), | |||||
loading: PropTypes.bool, | loading: PropTypes.bool, | ||||
onClose: PropTypes.func.isRequired, | onClose: PropTypes.func.isRequired, | ||||
onPick: PropTypes.func.isRequired, | onPick: PropTypes.func.isRequired, | ||||
@@ -163,6 +164,7 @@ class EmojiPickerMenu extends React.PureComponent { | |||||
style: {}, | style: {}, | ||||
loading: true, | loading: true, | ||||
placement: 'bottom', | placement: 'bottom', | ||||
frequentlyUsedEmojis: [], | |||||
}; | }; | ||||
state = { | state = { | ||||
@@ -233,7 +235,7 @@ class EmojiPickerMenu extends React.PureComponent { | |||||
} | } | ||||
render () { | render () { | ||||
const { loading, style, intl, custom_emojis, autoPlay, skinTone } = this.props; | |||||
const { loading, style, intl, custom_emojis, autoPlay, skinTone, frequentlyUsedEmojis } = this.props; | |||||
if (loading) { | if (loading) { | ||||
return <div style={{ width: 299 }} />; | return <div style={{ width: 299 }} />; | ||||
@@ -256,9 +258,11 @@ class EmojiPickerMenu extends React.PureComponent { | |||||
i18n={this.getI18n()} | i18n={this.getI18n()} | ||||
onClick={this.handleClick} | onClick={this.handleClick} | ||||
include={categoriesSort} | include={categoriesSort} | ||||
recent={frequentlyUsedEmojis} | |||||
skin={skinTone} | skin={skinTone} | ||||
showPreview={false} | showPreview={false} | ||||
backgroundImageFn={backgroundImageFn} | backgroundImageFn={backgroundImageFn} | ||||
emojiTooltip | |||||
/> | /> | ||||
<ModifierPicker | <ModifierPicker | ||||
@@ -279,6 +283,7 @@ export default class EmojiPickerDropdown extends React.PureComponent { | |||||
static propTypes = { | static propTypes = { | ||||
custom_emojis: ImmutablePropTypes.list, | custom_emojis: ImmutablePropTypes.list, | ||||
frequentlyUsedEmojis: PropTypes.arrayOf(PropTypes.string), | |||||
autoPlay: PropTypes.bool, | autoPlay: PropTypes.bool, | ||||
intl: PropTypes.object.isRequired, | intl: PropTypes.object.isRequired, | ||||
onPickEmoji: PropTypes.func.isRequired, | onPickEmoji: PropTypes.func.isRequired, | ||||
@@ -341,7 +346,7 @@ export default class EmojiPickerDropdown extends React.PureComponent { | |||||
} | } | ||||
render () { | render () { | ||||
const { intl, onPickEmoji, autoPlay, onSkinTone, skinTone } = this.props; | |||||
const { intl, onPickEmoji, autoPlay, onSkinTone, skinTone, frequentlyUsedEmojis } = this.props; | |||||
const title = intl.formatMessage(messages.emoji); | const title = intl.formatMessage(messages.emoji); | ||||
const { active, loading } = this.state; | const { active, loading } = this.state; | ||||
@@ -364,6 +369,7 @@ export default class EmojiPickerDropdown extends React.PureComponent { | |||||
autoPlay={autoPlay} | autoPlay={autoPlay} | ||||
onSkinTone={onSkinTone} | onSkinTone={onSkinTone} | ||||
skinTone={skinTone} | skinTone={skinTone} | ||||
frequentlyUsedEmojis={frequentlyUsedEmojis} | |||||
/> | /> | ||||
</Overlay> | </Overlay> | ||||
</div> | </div> | ||||
@@ -1,17 +1,42 @@ | |||||
import { connect } from 'react-redux'; | import { connect } from 'react-redux'; | ||||
import EmojiPickerDropdown from '../components/emoji_picker_dropdown'; | import EmojiPickerDropdown from '../components/emoji_picker_dropdown'; | ||||
import { changeSetting } from '../../../actions/settings'; | 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 => ({ | const mapStateToProps = state => ({ | ||||
custom_emojis: state.get('custom_emojis'), | custom_emojis: state.get('custom_emojis'), | ||||
autoPlay: state.getIn(['meta', 'auto_play_gif']), | autoPlay: state.getIn(['meta', 'auto_play_gif']), | ||||
skinTone: state.getIn(['settings', 'skinTone']), | skinTone: state.getIn(['settings', 'skinTone']), | ||||
frequentlyUsedEmojis: getFrequentlyUsedEmojis(state), | |||||
}); | }); | ||||
const mapDispatchToProps = dispatch => ({ | |||||
const mapDispatchToProps = (dispatch, { onPickEmoji }) => ({ | |||||
onSkinTone: skinTone => { | onSkinTone: skinTone => { | ||||
dispatch(changeSetting(['skinTone'], skinTone)); | dispatch(changeSetting(['skinTone'], skinTone)); | ||||
}, | }, | ||||
onPickEmoji: emoji => { | |||||
dispatch(useEmoji(emoji)); | |||||
if (onPickEmoji) { | |||||
onPickEmoji(emoji); | |||||
} | |||||
}, | |||||
}); | }); | ||||
export default connect(mapStateToProps, mapDispatchToProps)(EmojiPickerDropdown); | export default connect(mapStateToProps, mapDispatchToProps)(EmojiPickerDropdown); |
@@ -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 { COLUMN_ADD, COLUMN_REMOVE, COLUMN_MOVE } from '../actions/columns'; | ||||
import { STORE_HYDRATE } from '../actions/store'; | import { STORE_HYDRATE } from '../actions/store'; | ||||
import { EMOJI_USE } from '../actions/emojis'; | |||||
import { Map as ImmutableMap, fromJS } from 'immutable'; | import { Map as ImmutableMap, fromJS } from 'immutable'; | ||||
import uuid from '../uuid'; | import uuid from '../uuid'; | ||||
const initialState = ImmutableMap({ | const initialState = ImmutableMap({ | ||||
saved: true, | |||||
onboarded: false, | onboarded: false, | ||||
skinTone: 1, | skinTone: 1, | ||||
@@ -74,21 +77,35 @@ const moveColumn = (state, uuid, direction) => { | |||||
newColumns = columns.splice(index, 1); | newColumns = columns.splice(index, 1); | ||||
newColumns = newColumns.splice(newIndex, 0, columns.get(index)); | 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) { | export default function settings(state = initialState, action) { | ||||
switch(action.type) { | switch(action.type) { | ||||
case STORE_HYDRATE: | case STORE_HYDRATE: | ||||
return hydrate(state, action.state.get('settings')); | return hydrate(state, action.state.get('settings')); | ||||
case SETTING_CHANGE: | case SETTING_CHANGE: | ||||
return state.setIn(action.key, action.value); | |||||
return state | |||||
.setIn(action.key, action.value) | |||||
.set('saved', false); | |||||
case COLUMN_ADD: | 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: | 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: | case COLUMN_MOVE: | ||||
return moveColumn(state, action.uuid, action.direction); | return moveColumn(state, action.uuid, action.direction); | ||||
case EMOJI_USE: | |||||
return updateFrequentEmojis(state, action.emoji); | |||||
case SETTING_SAVE: | |||||
return state.set('saved', true); | |||||
default: | default: | ||||
return state; | return state; | ||||
} | } | ||||
@@ -45,7 +45,7 @@ | |||||
"css-loader": "^0.28.4", | "css-loader": "^0.28.4", | ||||
"detect-passive-events": "^1.0.2", | "detect-passive-events": "^1.0.2", | ||||
"dotenv": "^4.0.0", | "dotenv": "^4.0.0", | ||||
"emoji-mart": "^2.1.1", | |||||
"emoji-mart": "Gargron/emoji-mart#build", | |||||
"es6-symbol": "^3.1.1", | "es6-symbol": "^3.1.1", | ||||
"escape-html": "^1.0.3", | "escape-html": "^1.0.3", | ||||
"express": "^4.15.2", | "express": "^4.15.2", | ||||
@@ -2191,9 +2191,9 @@ elliptic@^6.0.0: | |||||
minimalistic-assert "^1.0.0" | minimalistic-assert "^1.0.0" | ||||
minimalistic-crypto-utils "^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: | emoji-regex@^6.1.0: | ||||
version "6.4.3" | version "6.4.3" | ||||