diff --git a/app/javascript/flavours/glitch/actions/compose.js b/app/javascript/flavours/glitch/actions/compose.js index f6c8086fe1..b9248169f3 100644 --- a/app/javascript/flavours/glitch/actions/compose.js +++ b/app/javascript/flavours/glitch/actions/compose.js @@ -3,6 +3,7 @@ import { CancelToken } from 'axios'; import { throttle } from 'lodash'; import { search as emojiSearch } from 'flavours/glitch/util/emoji/emoji_mart_search_light'; import { useEmoji } from './emojis'; +import { tagHistory } from 'flavours/glitch/util/settings'; import resizeImage from 'flavours/glitch/util/resize_image'; import { updateTimeline } from './timelines'; @@ -28,6 +29,9 @@ export const COMPOSE_UPLOAD_UNDO = 'COMPOSE_UPLOAD_UNDO'; export const COMPOSE_SUGGESTIONS_CLEAR = 'COMPOSE_SUGGESTIONS_CLEAR'; export const COMPOSE_SUGGESTIONS_READY = 'COMPOSE_SUGGESTIONS_READY'; export const COMPOSE_SUGGESTION_SELECT = 'COMPOSE_SUGGESTION_SELECT'; +export const COMPOSE_SUGGESTION_TAGS_UPDATE = 'COMPOSE_SUGGESTION_TAGS_UPDATE'; + +export const COMPOSE_TAG_HISTORY_UPDATE = 'COMPOSE_TAG_HISTORY_UPDATE'; export const COMPOSE_MOUNT = 'COMPOSE_MOUNT'; export const COMPOSE_UNMOUNT = 'COMPOSE_UNMOUNT'; @@ -136,6 +140,7 @@ export function submitCompose() { 'Idempotency-Key': getState().getIn(['compose', 'idempotencyKey']), }, }).then(function (response) { + dispatch(insertIntoTagHistory(response.data.tags)); dispatch(submitComposeSuccess({ ...response.data })); // If the response has no data then we can't do anything else. @@ -315,12 +320,22 @@ const fetchComposeSuggestionsEmojis = (dispatch, getState, token) => { dispatch(readyComposeSuggestionsEmojis(token, results)); }; +const fetchComposeSuggestionsTags = (dispatch, getState, token) => { + dispatch(updateSuggestionTags(token)); +}; + export function fetchComposeSuggestions(token) { return (dispatch, getState) => { - if (token[0] === ':') { + switch (token[0]) { + case ':': fetchComposeSuggestionsEmojis(dispatch, getState, token); - } else { + break; + case '#': + fetchComposeSuggestionsTags(dispatch, getState, token); + break; + default: fetchComposeSuggestionsAccounts(dispatch, getState, token); + break; } }; }; @@ -343,10 +358,15 @@ export function readyComposeSuggestionsAccounts(token, accounts) { export function selectComposeSuggestion(position, token, suggestion) { return (dispatch, getState) => { - const completion = typeof suggestion === 'object' && suggestion.id ? ( - dispatch(useEmoji(suggestion)), - suggestion.native || suggestion.colons - ) : '@' + getState().getIn(['accounts', suggestion, 'acct']); + let completion; + if (typeof suggestion === 'object' && suggestion.id) { + dispatch(useEmoji(suggestion)); + completion = suggestion.native || suggestion.colons; + } else if (suggestion[0] === '#') { + completion = suggestion; + } else { + completion = '@' + getState().getIn(['accounts', suggestion, 'acct']); + } dispatch({ type: COMPOSE_SUGGESTION_SELECT, @@ -357,6 +377,48 @@ export function selectComposeSuggestion(position, token, suggestion) { }; }; +export function updateSuggestionTags(token) { + return { + type: COMPOSE_SUGGESTION_TAGS_UPDATE, + token, + }; +} + +export function updateTagHistory(tags) { + return { + type: COMPOSE_TAG_HISTORY_UPDATE, + tags, + }; +} + +export function hydrateCompose() { + return (dispatch, getState) => { + const me = getState().getIn(['meta', 'me']); + const history = tagHistory.get(me); + + if (history !== null) { + dispatch(updateTagHistory(history)); + } + }; +} + +function insertIntoTagHistory(tags) { + return (dispatch, getState) => { + const state = getState(); + const oldHistory = state.getIn(['compose', 'tagHistory']); + const me = state.getIn(['meta', 'me']); + const names = tags.map(({ name }) => name); + const intersectedOldHistory = oldHistory.filter(name => !names.includes(name)); + + names.push(...intersectedOldHistory.toJS()); + + const newHistory = names.slice(0, 1000); + + tagHistory.set(me, newHistory); + dispatch(updateTagHistory(newHistory)); + }; +} + export function mountCompose() { return { type: COMPOSE_MOUNT, diff --git a/app/javascript/flavours/glitch/actions/store.js b/app/javascript/flavours/glitch/actions/store.js index a1db0fdd51..2dd94a9983 100644 --- a/app/javascript/flavours/glitch/actions/store.js +++ b/app/javascript/flavours/glitch/actions/store.js @@ -1,4 +1,5 @@ import { Iterable, fromJS } from 'immutable'; +import { hydrateCompose } from './compose'; export const STORE_HYDRATE = 'STORE_HYDRATE'; export const STORE_HYDRATE_LAZY = 'STORE_HYDRATE_LAZY'; @@ -8,10 +9,14 @@ const convertState = rawState => Iterable.isIndexed(v) ? v.toList() : v.toMap()); export function hydrateStore(rawState) { - const state = convertState(rawState); + return dispatch => { + const state = convertState(rawState); - return { - type: STORE_HYDRATE, - state, + dispatch({ + type: STORE_HYDRATE, + state, + }); + + dispatch(hydrateCompose()); }; }; diff --git a/app/javascript/flavours/glitch/features/composer/textarea/index.js b/app/javascript/flavours/glitch/features/composer/textarea/index.js index 51d44a83be..50e46fc784 100644 --- a/app/javascript/flavours/glitch/features/composer/textarea/index.js +++ b/app/javascript/flavours/glitch/features/composer/textarea/index.js @@ -58,7 +58,7 @@ const handlers = { const right = value.slice(selectionStart).search(/[\s\u200B]/); const token = function () { switch (true) { - case left < 0 || !/[@:]/.test(value[left]): + case left < 0 || !/[@:#]/.test(value[left]): return null; case right < 0: return value.slice(left); diff --git a/app/javascript/flavours/glitch/features/composer/textarea/suggestions/item/index.js b/app/javascript/flavours/glitch/features/composer/textarea/suggestions/item/index.js index f55640bcf9..3316923986 100644 --- a/app/javascript/flavours/glitch/features/composer/textarea/suggestions/item/index.js +++ b/app/javascript/flavours/glitch/features/composer/textarea/suggestions/item/index.js @@ -57,6 +57,42 @@ export default class ComposerTextareaSuggestionsItem extends React.Component { } = this.props; const computedClass = classNames('composer--textarea--suggestions--item', { selected }); + // If the suggestion is an object, then we render an emoji. + // Otherwise, we render a hashtag if it starts with #, or an account. + let inner; + if (typeof suggestion === 'object') { + let url; + if (suggestion.custom) { + url = suggestion.imageUrl; + } else { + const mapping = unicodeMapping[suggestion.native] || unicodeMapping[suggestion.native.replace(/\uFE0F$/, '')]; + if (mapping) { + url = `${assetHost}/emoji/${mapping.filename}.svg`; + } + } + if (url) { + inner = ( +