about summary refs log tree commit diff
path: root/app
diff options
context:
space:
mode:
authorAkihiko Odaki <akihiko.odaki.4i@stu.hosei.ac.jp>2018-03-05 04:27:25 +0900
committerEugen Rochko <eugen@zeonfederated.com>2018-03-04 20:27:25 +0100
commit460e380d386367b6809d319859e13d17a6a2acea (patch)
treefe568a5b9e9767393c331bd4b2d96a9e2b950919 /app
parent778b37790b425debd16b73c21f0805ff2e9a4ce8 (diff)
Implement tag auto-completion by history (#6621)
This is a functionality similar to one implemented in Pawoo:
https://github.com/pixiv/mastodon/commit/21a3c70f8083b1347d2b8420ed7001b78c2c9620
Diffstat (limited to 'app')
-rw-r--r--app/javascript/mastodon/actions/compose.js64
-rw-r--r--app/javascript/mastodon/actions/store.js13
-rw-r--r--app/javascript/mastodon/components/autosuggest_textarea.js5
-rw-r--r--app/javascript/mastodon/reducers/compose.js19
-rw-r--r--app/javascript/mastodon/settings.js1
5 files changed, 95 insertions, 7 deletions
diff --git a/app/javascript/mastodon/actions/compose.js b/app/javascript/mastodon/actions/compose.js
index 1732ff189..0fe480022 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 a1db0fdd5..2dd94a998 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 6a16e2fc7..34904194f 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 = <AutosuggestEmoji emoji={suggestion} />;
       key   = suggestion.id;
+    } else if (suggestion[0] === '#') {
+      inner = suggestion;
+      key   = suggestion;
     } else {
       inner = <AutosuggestAccountContainer id={suggestion} />;
       key   = suggestion;
diff --git a/app/javascript/mastodon/reducers/compose.js b/app/javascript/mastodon/reducers/compose.js
index 1358fb4aa..dc88390df 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 dbd969cb1..7643a508e 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');