Emoji and Hashtag autocomplete
This commit is contained in:
		
							parent
							
								
									b0487488a7
								
							
						
					
					
						commit
						3783062450
					
				| @ -1,4 +1,5 @@ | |||||||
| import api from '../api'; | import api from '../api'; | ||||||
|  | import emojione from 'emojione'; | ||||||
| 
 | 
 | ||||||
| import { | import { | ||||||
|   updateTimeline, |   updateTimeline, | ||||||
| @ -22,6 +23,7 @@ export const COMPOSE_UPLOAD_UNDO     = 'COMPOSE_UPLOAD_UNDO'; | |||||||
| 
 | 
 | ||||||
| export const COMPOSE_SUGGESTIONS_CLEAR = 'COMPOSE_SUGGESTIONS_CLEAR'; | export const COMPOSE_SUGGESTIONS_CLEAR = 'COMPOSE_SUGGESTIONS_CLEAR'; | ||||||
| export const COMPOSE_SUGGESTIONS_READY = 'COMPOSE_SUGGESTIONS_READY'; | export const COMPOSE_SUGGESTIONS_READY = 'COMPOSE_SUGGESTIONS_READY'; | ||||||
|  | export const COMPOSE_SUGGESTIONS_READY_TXT = 'COMPOSE_SUGGESTIONS_READY_TXT'; | ||||||
| export const COMPOSE_SUGGESTION_SELECT = 'COMPOSE_SUGGESTION_SELECT'; | export const COMPOSE_SUGGESTION_SELECT = 'COMPOSE_SUGGESTION_SELECT'; | ||||||
| 
 | 
 | ||||||
| export const COMPOSE_MOUNT   = 'COMPOSE_MOUNT'; | export const COMPOSE_MOUNT   = 'COMPOSE_MOUNT'; | ||||||
| @ -212,17 +214,43 @@ export function clearComposeSuggestions() { | |||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| export function fetchComposeSuggestions(token) { | export function fetchComposeSuggestions(token) { | ||||||
|   return (dispatch, getState) => { |   let leading = token[0]; | ||||||
|     api(getState).get('/api/v1/accounts/search', { | 
 | ||||||
|       params: { |   if (leading === '@') { | ||||||
|         q: token, |     // handle search
 | ||||||
|         resolve: false, |     return (dispatch, getState) => { | ||||||
|         limit: 4, |       api(getState).get('/api/v1/accounts/search', { | ||||||
|       }, |         params: { | ||||||
|     }).then(response => { |           q: token.slice(1), // remove the '@'
 | ||||||
|       dispatch(readyComposeSuggestions(token, response.data)); |           resolve: false, | ||||||
|     }); |           limit: 4, | ||||||
|   }; |         }, | ||||||
|  |       }).then(response => { | ||||||
|  |         dispatch(readyComposeSuggestions(token, response.data)); | ||||||
|  |       }); | ||||||
|  |     }; | ||||||
|  |   } else if (leading === ':') { | ||||||
|  |     // mojos
 | ||||||
|  |     let allShortcodes = Object.keys(emojione.emojioneList); | ||||||
|  |     // TODO when we have custom emojons merged, add theme to this shortcode list
 | ||||||
|  |     return (dispatch) => { | ||||||
|  |       dispatch(readyComposeSuggestionsTxt(token, allShortcodes.filter((sc) => { | ||||||
|  |         return sc.indexOf(token) === 0; | ||||||
|  |       }))); | ||||||
|  |     }; | ||||||
|  |   } else { | ||||||
|  |     // hashtag
 | ||||||
|  |     return (dispatch, getState) => { | ||||||
|  |       api(getState).get('/api/v1/search', { | ||||||
|  |         params: { | ||||||
|  |           q: token, | ||||||
|  |           resolve: true, | ||||||
|  |         }, | ||||||
|  |       }).then(response => { | ||||||
|  |         dispatch(readyComposeSuggestionsTxt(token, response.data.hashtags.map((ht) => `#${ht}`))); | ||||||
|  |       }); | ||||||
|  |     }; | ||||||
|  |   } | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| export function readyComposeSuggestions(token, accounts) { | export function readyComposeSuggestions(token, accounts) { | ||||||
| @ -233,9 +261,19 @@ export function readyComposeSuggestions(token, accounts) { | |||||||
|   }; |   }; | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
|  | export function readyComposeSuggestionsTxt(token, items) { | ||||||
|  |   return { | ||||||
|  |     type: COMPOSE_SUGGESTIONS_READY_TXT, | ||||||
|  |     token, | ||||||
|  |     items, | ||||||
|  |   }; | ||||||
|  | }; | ||||||
|  | 
 | ||||||
| export function selectComposeSuggestion(position, token, accountId) { | export function selectComposeSuggestion(position, token, accountId) { | ||||||
|   return (dispatch, getState) => { |   return (dispatch, getState) => { | ||||||
|     const completion = getState().getIn(['accounts', accountId, 'acct']); |     const completion = (typeof accountId === 'string') ? | ||||||
|  |       accountId.slice(1) : // text suggestion: discard the leading : or # - the replacing code replaces only what follows
 | ||||||
|  |       getState().getIn(['accounts', accountId, 'acct']); | ||||||
| 
 | 
 | ||||||
|     dispatch({ |     dispatch({ | ||||||
|       type: COMPOSE_SUGGESTION_SELECT, |       type: COMPOSE_SUGGESTION_SELECT, | ||||||
|  | |||||||
| @ -1,5 +1,6 @@ | |||||||
| import React from 'react'; | import React from 'react'; | ||||||
| import AutosuggestAccountContainer from '../features/compose/containers/autosuggest_account_container'; | import AutosuggestAccountContainer from '../features/compose/containers/autosuggest_account_container'; | ||||||
|  | import AutosuggestShortcode from '../features/compose/components/autosuggest_shortcode'; | ||||||
| import ImmutablePropTypes from 'react-immutable-proptypes'; | import ImmutablePropTypes from 'react-immutable-proptypes'; | ||||||
| import PropTypes from 'prop-types'; | import PropTypes from 'prop-types'; | ||||||
| import { isRtl } from '../rtl'; | import { isRtl } from '../rtl'; | ||||||
| @ -18,11 +19,12 @@ const textAtCursorMatchesToken = (str, caretPosition) => { | |||||||
|     word = str.slice(left, right + caretPosition); |     word = str.slice(left, right + caretPosition); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   if (!word || word.trim().length < 2 || word[0] !== '@') { |   if (!word || word.trim().length < 2 || ['@', ':', '#'].indexOf(word[0]) === -1) { | ||||||
|     return [null, null]; |     return [null, null]; | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   word = word.trim().toLowerCase().slice(1); |   word = word.trim().toLowerCase(); | ||||||
|  |   // was: .slice(1); - we leave the leading char there, handler can decide what to do based on it
 | ||||||
| 
 | 
 | ||||||
|   if (word.length > 0) { |   if (word.length > 0) { | ||||||
|     return [left + 1, word]; |     return [left + 1, word]; | ||||||
| @ -128,7 +130,9 @@ export default class AutosuggestTextarea extends ImmutablePureComponent { | |||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   onSuggestionClick = (e) => { |   onSuggestionClick = (e) => { | ||||||
|     const suggestion = Number(e.currentTarget.getAttribute('data-index')); |     // leave suggestion string unchanged if it's a hash / shortcode suggestion. convert account number to int.
 | ||||||
|  |     const suggestionStr = e.currentTarget.getAttribute('data-index'); | ||||||
|  |     const suggestion = [':', '#'].indexOf(suggestionStr[0]) !== -1 ? suggestionStr : Number(suggestionStr); | ||||||
|     e.preventDefault(); |     e.preventDefault(); | ||||||
|     this.props.onSuggestionSelected(this.state.tokenStart, this.state.lastToken, suggestion); |     this.props.onSuggestionSelected(this.state.tokenStart, this.state.lastToken, suggestion); | ||||||
|     this.textarea.focus(); |     this.textarea.focus(); | ||||||
| @ -160,6 +164,14 @@ export default class AutosuggestTextarea extends ImmutablePureComponent { | |||||||
|       style.direction = 'rtl'; |       style.direction = 'rtl'; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     let makeItem = (suggestion) => { | ||||||
|  |       if (suggestion[0] === ':') return <AutosuggestShortcode shortcode={suggestion} />; | ||||||
|  |       if (suggestion[0] === '#') return suggestion; // hashtag
 | ||||||
|  | 
 | ||||||
|  |       // else - accounts are always returned as IDs with no prefix
 | ||||||
|  |       return <AutosuggestAccountContainer id={suggestion} />; | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|     return ( |     return ( | ||||||
|       <div className='autosuggest-textarea'> |       <div className='autosuggest-textarea'> | ||||||
|         <label> |         <label> | ||||||
| @ -190,7 +202,7 @@ export default class AutosuggestTextarea extends ImmutablePureComponent { | |||||||
|               className={`autosuggest-textarea__suggestions__item ${i === selectedSuggestion ? 'selected' : ''}`} |               className={`autosuggest-textarea__suggestions__item ${i === selectedSuggestion ? 'selected' : ''}`} | ||||||
|               onMouseDown={this.onSuggestionClick} |               onMouseDown={this.onSuggestionClick} | ||||||
|             > |             > | ||||||
|               <AutosuggestAccountContainer id={suggestion} /> |               {makeItem(suggestion)} | ||||||
|             </div> |             </div> | ||||||
|           ))} |           ))} | ||||||
|         </div> |         </div> | ||||||
|  | |||||||
| @ -0,0 +1,38 @@ | |||||||
|  | import React from 'react'; | ||||||
|  | import ImmutablePureComponent from 'react-immutable-pure-component'; | ||||||
|  | import PropTypes from 'prop-types'; | ||||||
|  | import emojione from 'emojione'; | ||||||
|  | 
 | ||||||
|  | // This is bad, but I don't know how to make it work without importing the entirety of emojione.
 | ||||||
|  | // taken from some old version of mastodon before they gutted emojione to "emojione_light"
 | ||||||
|  | const shortnameToImage = str => str.replace(emojione.regShortNames, shortname => { | ||||||
|  |   if (typeof shortname === 'undefined' || shortname === '' || !(shortname in emojione.emojioneList)) { | ||||||
|  |     return shortname; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   const unicode = emojione.emojioneList[shortname].unicode[emojione.emojioneList[shortname].unicode.length - 1]; | ||||||
|  |   const alt     = emojione.convert(unicode.toUpperCase()); | ||||||
|  | 
 | ||||||
|  |   return `<img draggable="false" class="emojione" alt="${alt}" src="/emoji/${unicode}.svg" />`; | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | export default class AutosuggestShortcode extends ImmutablePureComponent { | ||||||
|  | 
 | ||||||
|  |   static propTypes = { | ||||||
|  |     shortcode: PropTypes.string.isRequired, | ||||||
|  |   }; | ||||||
|  | 
 | ||||||
|  |   render () { | ||||||
|  |     const { shortcode } = this.props; | ||||||
|  | 
 | ||||||
|  |     let emoji = shortnameToImage(shortcode); | ||||||
|  | 
 | ||||||
|  |     return ( | ||||||
|  |       <div className='autosuggest-account'> | ||||||
|  |         <div className='autosuggest-account-icon' dangerouslySetInnerHTML={{ __html: emoji }} /> | ||||||
|  |         {shortcode} | ||||||
|  |       </div> | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  | } | ||||||
| @ -15,6 +15,7 @@ import { | |||||||
|   COMPOSE_UPLOAD_PROGRESS, |   COMPOSE_UPLOAD_PROGRESS, | ||||||
|   COMPOSE_SUGGESTIONS_CLEAR, |   COMPOSE_SUGGESTIONS_CLEAR, | ||||||
|   COMPOSE_SUGGESTIONS_READY, |   COMPOSE_SUGGESTIONS_READY, | ||||||
|  |   COMPOSE_SUGGESTIONS_READY_TXT, | ||||||
|   COMPOSE_SUGGESTION_SELECT, |   COMPOSE_SUGGESTION_SELECT, | ||||||
|   COMPOSE_ADVANCED_OPTIONS_CHANGE, |   COMPOSE_ADVANCED_OPTIONS_CHANGE, | ||||||
|   COMPOSE_SENSITIVITY_CHANGE, |   COMPOSE_SENSITIVITY_CHANGE, | ||||||
| @ -263,6 +264,9 @@ export default function compose(state = initialState, action) { | |||||||
|     return state.update('suggestions', ImmutableList(), list => list.clear()).set('suggestion_token', null); |     return state.update('suggestions', ImmutableList(), list => list.clear()).set('suggestion_token', null); | ||||||
|   case COMPOSE_SUGGESTIONS_READY: |   case COMPOSE_SUGGESTIONS_READY: | ||||||
|     return state.set('suggestions', ImmutableList(action.accounts.map(item => item.id))).set('suggestion_token', action.token); |     return state.set('suggestions', ImmutableList(action.accounts.map(item => item.id))).set('suggestion_token', action.token); | ||||||
|  |   case COMPOSE_SUGGESTIONS_READY_TXT: | ||||||
|  |     // suggestion received that is not an account - hashtag or emojo
 | ||||||
|  |     return state.set('suggestions', ImmutableList(action.items.map(item => item))).set('suggestion_token', action.token); | ||||||
|   case COMPOSE_SUGGESTION_SELECT: |   case COMPOSE_SUGGESTION_SELECT: | ||||||
|     return insertSuggestion(state, action.position, action.token, action.completion); |     return insertSuggestion(state, action.position, action.token, action.completion); | ||||||
|   case TIMELINE_DELETE: |   case TIMELINE_DELETE: | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user