[Glitch] Implement tag auto-completion by history
Port 460e380d38 to glitch-soc
			
			
This commit is contained in:
		| @@ -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, | ||||
|   | ||||
| @@ -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()); | ||||
|   }; | ||||
| }; | ||||
|   | ||||
| @@ -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); | ||||
|   | ||||
| @@ -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 = ( | ||||
|           <div className='emoji'> | ||||
|             <img | ||||
|               alt={suggestion.native || suggestion.colons} | ||||
|               className='emojione' | ||||
|               src={url} | ||||
|             /> | ||||
|             {suggestion.colons} | ||||
|           </div> | ||||
|         ); | ||||
|       } | ||||
|     } else if (suggestion[0] === '#') { | ||||
|       inner = suggestion; | ||||
|     } else { | ||||
|       inner = ( | ||||
|         <AccountContainer | ||||
|           id={suggestion} | ||||
|           small | ||||
|         /> | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     //  The result. | ||||
|     return ( | ||||
|       <div | ||||
| @@ -66,37 +102,7 @@ export default class ComposerTextareaSuggestionsItem extends React.Component { | ||||
|         role='button' | ||||
|         tabIndex='0' | ||||
|       > | ||||
|         { //  If the suggestion is an object, then we render an emoji. | ||||
|           //  Otherwise, we render an account. | ||||
|           typeof suggestion === 'object' ? function () { | ||||
|             const url = function () { | ||||
|               if (suggestion.custom) { | ||||
|                 return suggestion.imageUrl; | ||||
|               } else { | ||||
|                 const mapping = unicodeMapping[suggestion.native] || unicodeMapping[suggestion.native.replace(/\uFE0F$/, '')]; | ||||
|                 if (!mapping) { | ||||
|                   return null; | ||||
|                 } | ||||
|                 return `${assetHost}/emoji/${mapping.filename}.svg`; | ||||
|               } | ||||
|             }(); | ||||
|             return url ? ( | ||||
|               <div className='emoji'> | ||||
|                 <img | ||||
|                   alt={suggestion.native || suggestion.colons} | ||||
|                   className='emojione' | ||||
|                   src={url} | ||||
|                 /> | ||||
|                 {suggestion.colons} | ||||
|               </div> | ||||
|             ) : null; | ||||
|           }() : ( | ||||
|             <AccountContainer | ||||
|               id={suggestion} | ||||
|               small | ||||
|             /> | ||||
|           ) | ||||
|         } | ||||
|         { inner } | ||||
|       </div> | ||||
|     ); | ||||
|   } | ||||
|   | ||||
| @@ -18,6 +18,8 @@ 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, | ||||
|   COMPOSE_SPOILERNESS_CHANGE, | ||||
| @@ -77,6 +79,7 @@ const initialState = ImmutableMap({ | ||||
|   default_sensitive: false, | ||||
|   resetFileKey: Math.floor((Math.random() * 0x10000)), | ||||
|   idempotencyKey: null, | ||||
|   tagHistory: ImmutableList(), | ||||
|   doodle: ImmutableMap({ | ||||
|     fg: 'rgb(  0,    0,    0)', | ||||
|     bg: 'rgb(255,  255,  255)', | ||||
| @@ -206,6 +209,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; | ||||
|  | ||||
| @@ -360,6 +375,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