From cc3ca37cda75f7cafa262117847a0878e2037861 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sun, 28 Jul 2019 14:37:52 +0200 Subject: [PATCH] [Glitch] Add autosuggestions for hashtags Port 450b8a70a510b3f611365d4f28bbf63f8c799af9 to glitch-soc Signed-off-by: Thibaut Girka --- .../flavours/glitch/actions/compose.js | 41 ++++++++++++++++--- .../glitch/components/autosuggest_hashtag.js | 28 +++++++++++++ .../glitch/components/autosuggest_input.js | 9 ++-- .../glitch/components/autosuggest_textarea.js | 9 ++-- .../flavours/glitch/reducers/compose.js | 36 ++++++++++------ .../glitch/styles/components/composer.scss | 8 ++++ 6 files changed, 105 insertions(+), 26 deletions(-) create mode 100644 app/javascript/flavours/glitch/components/autosuggest_hashtag.js diff --git a/app/javascript/flavours/glitch/actions/compose.js b/app/javascript/flavours/glitch/actions/compose.js index 2312bae634..134b698556 100644 --- a/app/javascript/flavours/glitch/actions/compose.js +++ b/app/javascript/flavours/glitch/actions/compose.js @@ -12,7 +12,7 @@ import { showAlertForError } from './alerts'; import { showAlert } from './alerts'; import { defineMessages } from 'react-intl'; -let cancelFetchComposeSuggestionsAccounts; +let cancelFetchComposeSuggestionsAccounts, cancelFetchComposeSuggestionsTags; export const COMPOSE_CHANGE = 'COMPOSE_CHANGE'; export const COMPOSE_CYCLE_ELEFRIEND = 'COMPOSE_CYCLE_ELEFRIEND'; @@ -352,10 +352,12 @@ const fetchComposeSuggestionsAccounts = throttle((dispatch, getState, token) => if (cancelFetchComposeSuggestionsAccounts) { cancelFetchComposeSuggestionsAccounts(); } + api(getState).get('/api/v1/accounts/search', { cancelToken: new CancelToken(cancel => { cancelFetchComposeSuggestionsAccounts = cancel; }), + params: { q: token.slice(1), resolve: false, @@ -376,9 +378,30 @@ const fetchComposeSuggestionsEmojis = (dispatch, getState, token) => { dispatch(readyComposeSuggestionsEmojis(token, results)); }; -const fetchComposeSuggestionsTags = (dispatch, getState, token) => { - dispatch(updateSuggestionTags(token)); -}; +const fetchComposeSuggestionsTags = throttle((dispatch, getState, token) => { + if (cancelFetchComposeSuggestionsTags) { + cancelFetchComposeSuggestionsTags(); + } + + api(getState).get('/api/v2/search', { + cancelToken: new CancelToken(cancel => { + cancelFetchComposeSuggestionsTags = cancel; + }), + + params: { + type: 'hashtags', + q: token.slice(1), + resolve: false, + limit: 4, + }, + }).then(({ data }) => { + dispatch(readyComposeSuggestionsTags(token, data.hashtags)); + }).catch(error => { + if (!isCancel(error)) { + dispatch(showAlertForError(error)); + } + }); +}, 200, { leading: true, trailing: true }); export function fetchComposeSuggestions(token) { return (dispatch, getState) => { @@ -412,14 +435,20 @@ export function readyComposeSuggestionsAccounts(token, accounts) { }; }; +export const readyComposeSuggestionsTags = (token, tags) => ({ + type: COMPOSE_SUGGESTIONS_READY, + token, + tags, +}); + export function selectComposeSuggestion(position, token, suggestion, path) { return (dispatch, getState) => { let completion; if (typeof suggestion === 'object' && suggestion.id) { dispatch(useEmoji(suggestion)); completion = suggestion.native || suggestion.colons; - } else if (suggestion[0] === '#') { - completion = suggestion; + } else if (typeof suggestion === 'object' && suggestion.name) { + completion = `#${suggestion.name}`; } else { completion = '@' + getState().getIn(['accounts', suggestion, 'acct']); } diff --git a/app/javascript/flavours/glitch/components/autosuggest_hashtag.js b/app/javascript/flavours/glitch/components/autosuggest_hashtag.js new file mode 100644 index 0000000000..93dd703835 --- /dev/null +++ b/app/javascript/flavours/glitch/components/autosuggest_hashtag.js @@ -0,0 +1,28 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { shortNumberFormat } from 'flavours/glitch/util/numbers'; +import { FormattedMessage } from 'react-intl'; + +export default class AutosuggestHashtag extends React.PureComponent { + + static propTypes = { + tag: PropTypes.shape({ + name: PropTypes.string.isRequired, + url: PropTypes.string, + history: PropTypes.array.isRequired, + }).isRequired, + }; + + render () { + const { tag } = this.props; + const weeklyUses = shortNumberFormat(tag.history.reduce((total, day) => total + (day.uses * 1), 0)); + + return ( +
+
#{tag.name}
+
+
+ ); + } + +} diff --git a/app/javascript/flavours/glitch/components/autosuggest_input.js b/app/javascript/flavours/glitch/components/autosuggest_input.js index 5fc952d8ea..f931069a9b 100644 --- a/app/javascript/flavours/glitch/components/autosuggest_input.js +++ b/app/javascript/flavours/glitch/components/autosuggest_input.js @@ -1,6 +1,7 @@ import React from 'react'; import AutosuggestAccountContainer from 'flavours/glitch/features/compose/containers/autosuggest_account_container'; import AutosuggestEmoji from './autosuggest_emoji'; +import AutosuggestHashtag from './autosuggest_hashtag'; import ImmutablePropTypes from 'react-immutable-proptypes'; import PropTypes from 'prop-types'; import { isRtl } from 'flavours/glitch/util/rtl'; @@ -167,12 +168,12 @@ export default class AutosuggestInput extends ImmutablePureComponent { const { selectedSuggestion } = this.state; let inner, key; - if (typeof suggestion === 'object') { + if (typeof suggestion === 'object' && suggestion.shortcode) { inner = ; key = suggestion.id; - } else if (suggestion[0] === '#') { - inner = suggestion; - key = suggestion; + } else if (typeof suggestion === 'object' && suggestion.name) { + inner = ; + key = suggestion.name; } else { inner = ; key = suggestion; diff --git a/app/javascript/flavours/glitch/components/autosuggest_textarea.js b/app/javascript/flavours/glitch/components/autosuggest_textarea.js index bbe0ffcbe0..c057f4a5b5 100644 --- a/app/javascript/flavours/glitch/components/autosuggest_textarea.js +++ b/app/javascript/flavours/glitch/components/autosuggest_textarea.js @@ -1,6 +1,7 @@ import React from 'react'; import AutosuggestAccountContainer from 'flavours/glitch/features/compose/containers/autosuggest_account_container'; import AutosuggestEmoji from './autosuggest_emoji'; +import AutosuggestHashtag from './autosuggest_hashtag'; import ImmutablePropTypes from 'react-immutable-proptypes'; import PropTypes from 'prop-types'; import { isRtl } from 'flavours/glitch/util/rtl'; @@ -173,12 +174,12 @@ export default class AutosuggestTextarea extends ImmutablePureComponent { const { selectedSuggestion } = this.state; let inner, key; - if (typeof suggestion === 'object') { + if (typeof suggestion === 'object' && suggestion.shortcode) { inner = ; key = suggestion.id; - } else if (suggestion[0] === '#') { - inner = suggestion; - key = suggestion; + } else if (typeof suggestion === 'object' && suggestion.name) { + inner = ; + key = suggestion.name; } else { inner = ; key = suggestion; diff --git a/app/javascript/flavours/glitch/reducers/compose.js b/app/javascript/flavours/glitch/reducers/compose.js index 5f176b8325..3c769abe3b 100644 --- a/app/javascript/flavours/glitch/reducers/compose.js +++ b/app/javascript/flavours/glitch/reducers/compose.js @@ -18,7 +18,6 @@ import { COMPOSE_SUGGESTIONS_CLEAR, COMPOSE_SUGGESTIONS_READY, COMPOSE_SUGGESTION_SELECT, - COMPOSE_SUGGESTION_TAGS_UPDATE, COMPOSE_TAG_HISTORY_UPDATE, COMPOSE_ADVANCED_OPTIONS_CHANGE, COMPOSE_SENSITIVITY_CHANGE, @@ -231,15 +230,20 @@ const insertSuggestion = (state, position, token, completion, path) => { }); }; -const updateSuggestionTags = (state, token) => { - const prefix = token.slice(1); +const sortHashtagsByUse = (state, tags) => { + const personalHistory = state.get('tagHistory'); - return state.merge({ - suggestions: state.get('tagHistory') - .filter(tag => tag.toLowerCase().startsWith(prefix.toLowerCase())) - .slice(0, 4) - .map(tag => '#' + tag), - suggestion_token: token, + return tags.sort((a, b) => { + const usedA = personalHistory.includes(a.name); + const usedB = personalHistory.includes(b.name); + + if (usedA === usedB) { + return 0; + } else if (usedA && !usedB) { + return 1; + } else { + return -1; + } }); }; @@ -282,6 +286,16 @@ const expiresInFromExpiresAt = expires_at => { return [300, 1800, 3600, 21600, 86400, 259200, 604800].find(expires_in => expires_in >= delta) || 24 * 3600; }; +const normalizeSuggestions = (state, { accounts, emojis, tags }) => { + if (accounts) { + return accounts.map(item => item.id); + } else if (emojis) { + return emojis; + } else { + return sortHashtagsByUse(state, tags); + } +}; + export default function compose(state = initialState, action) { switch(action.type) { case STORE_HYDRATE: @@ -412,11 +426,9 @@ export default function compose(state = initialState, action) { case COMPOSE_SUGGESTIONS_CLEAR: return state.update('suggestions', ImmutableList(), list => list.clear()).set('suggestion_token', null); case COMPOSE_SUGGESTIONS_READY: - return state.set('suggestions', ImmutableList(action.accounts ? action.accounts.map(item => item.id) : action.emojis)).set('suggestion_token', action.token); + return state.set('suggestions', ImmutableList(normalizeSuggestions(state, action))).set('suggestion_token', action.token); case COMPOSE_SUGGESTION_SELECT: return insertSuggestion(state, action.position, action.token, action.completion, action.path); - case COMPOSE_SUGGESTION_TAGS_UPDATE: - return updateSuggestionTags(state, action.token); case COMPOSE_TAG_HISTORY_UPDATE: return state.set('tagHistory', fromJS(action.tags)); case TIMELINE_DELETE: diff --git a/app/javascript/flavours/glitch/styles/components/composer.scss b/app/javascript/flavours/glitch/styles/components/composer.scss index c4fa4f6545..830189a5b2 100644 --- a/app/javascript/flavours/glitch/styles/components/composer.scss +++ b/app/javascript/flavours/glitch/styles/components/composer.scss @@ -352,6 +352,14 @@ } } + .autosuggest-hashtag { + justify-content: space-between; + + strong { + font-weight: 500; + } + } + & > .account.small { .display-name { & > span { color: $lighter-text-color }