diff --git a/app/javascript/mastodon/actions/compose.js b/app/javascript/mastodon/actions/compose.js
index 1732ff189ed..0fe4800227c 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 { tagHistory } from '../settings';
import { useEmoji } from './emojis';
import {
@@ -27,6 +28,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';
@@ -111,6 +115,7 @@ export function submitCompose() {
'Idempotency-Key': getState().getIn(['compose', 'idempotencyKey']),
},
}).then(function (response) {
+ dispatch(insertIntoTagHistory(response.data.tags));
dispatch(submitComposeSuccess({ ...response.data }));
// To make the app more responsive, immediately get the status into the columns
@@ -273,12 +278,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;
}
};
};
@@ -308,6 +323,9 @@ export function selectComposeSuggestion(position, token, suggestion) {
startPosition = position - 1;
dispatch(useEmoji(suggestion));
+ } else if (suggestion[0] === '#') {
+ completion = suggestion;
+ startPosition = position - 1;
} else {
completion = getState().getIn(['accounts', suggestion, 'acct']);
startPosition = position;
@@ -322,6 +340,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/mastodon/actions/store.js b/app/javascript/mastodon/actions/store.js
index a1db0fdd51c..2dd94a99832 100644
--- a/app/javascript/mastodon/actions/store.js
+++ b/app/javascript/mastodon/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/mastodon/components/autosuggest_textarea.js b/app/javascript/mastodon/components/autosuggest_textarea.js
index 6a16e2fc7ae..34904194f73 100644
--- a/app/javascript/mastodon/components/autosuggest_textarea.js
+++ b/app/javascript/mastodon/components/autosuggest_textarea.js
@@ -20,7 +20,7 @@ const textAtCursorMatchesToken = (str, caretPosition) => {
word = str.slice(left, right + caretPosition);
}
- if (!word || word.trim().length < 3 || ['@', ':'].indexOf(word[0]) === -1) {
+ if (!word || word.trim().length < 3 || ['@', ':', '#'].indexOf(word[0]) === -1) {
return [null, null];
}
@@ -170,6 +170,9 @@ export default class AutosuggestTextarea extends ImmutablePureComponent {
if (typeof suggestion === 'object') {
inner = ;
key = suggestion.id;
+ } else if (suggestion[0] === '#') {
+ inner = suggestion;
+ key = suggestion;
} else {
inner = ;
key = suggestion;
diff --git a/app/javascript/mastodon/reducers/compose.js b/app/javascript/mastodon/reducers/compose.js
index 1358fb4aa87..dc88390dfd4 100644
--- a/app/javascript/mastodon/reducers/compose.js
+++ b/app/javascript/mastodon/reducers/compose.js
@@ -16,6 +16,8 @@ import {
COMPOSE_SUGGESTIONS_CLEAR,
COMPOSE_SUGGESTIONS_READY,
COMPOSE_SUGGESTION_SELECT,
+ COMPOSE_SUGGESTION_TAGS_UPDATE,
+ COMPOSE_TAG_HISTORY_UPDATE,
COMPOSE_SENSITIVITY_CHANGE,
COMPOSE_SPOILERNESS_CHANGE,
COMPOSE_SPOILER_TEXT_CHANGE,
@@ -54,6 +56,7 @@ const initialState = ImmutableMap({
default_sensitive: false,
resetFileKey: Math.floor((Math.random() * 0x10000)),
idempotencyKey: null,
+ tagHistory: ImmutableList(),
});
function statusToTextMentions(state, status) {
@@ -122,6 +125,18 @@ const insertSuggestion = (state, position, token, completion) => {
});
};
+const updateSuggestionTags = (state, token) => {
+ const prefix = token.slice(1);
+
+ return state.merge({
+ suggestions: state.get('tagHistory')
+ .filter(tag => tag.startsWith(prefix))
+ .slice(0, 4)
+ .map(tag => '#' + tag),
+ suggestion_token: token,
+ });
+};
+
const insertEmoji = (state, position, emojiData) => {
const emoji = emojiData.native;
@@ -252,6 +267,10 @@ export default function compose(state = initialState, action) {
return state.set('suggestions', ImmutableList(action.accounts ? action.accounts.map(item => item.id) : action.emojis)).set('suggestion_token', action.token);
case COMPOSE_SUGGESTION_SELECT:
return insertSuggestion(state, action.position, action.token, action.completion);
+ 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:
if (action.id === state.get('in_reply_to')) {
return state.set('in_reply_to', null);
diff --git a/app/javascript/mastodon/settings.js b/app/javascript/mastodon/settings.js
index dbd969cb1bc..7643a508ea0 100644
--- a/app/javascript/mastodon/settings.js
+++ b/app/javascript/mastodon/settings.js
@@ -44,3 +44,4 @@ export default class Settings {
}
export const pushNotificationsSetting = new Settings('mastodon_push_notification_data');
+export const tagHistory = new Settings('mastodon_tag_history');