Implement tag auto-completion by history (#6621)
This is a functionality similar to one implemented in Pawoo:
21a3c70f80
			
			
This commit is contained in:
		
				
					committed by
					
						 Eugen Rochko
						Eugen Rochko
					
				
			
			
				
	
			
			
			
						parent
						
							778b37790b
						
					
				
				
					commit
					460e380d38
				
			| @@ -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, | ||||
|   | ||||
| @@ -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()); | ||||
|   }; | ||||
| }; | ||||
|   | ||||
| @@ -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; | ||||
|   | ||||
| @@ -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); | ||||
|   | ||||
| @@ -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'); | ||||
|   | ||||
		Reference in New Issue
	
	Block a user