Emoji and Hashtag autocomplete
This commit is contained in:
		
							parent
							
								
									b0487488a7
								
							
						
					
					
						commit
						3783062450
					
				| @ -1,4 +1,5 @@ | ||||
| import api from '../api'; | ||||
| import emojione from 'emojione'; | ||||
| 
 | ||||
| import { | ||||
|   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_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_MOUNT   = 'COMPOSE_MOUNT'; | ||||
| @ -212,10 +214,14 @@ export function clearComposeSuggestions() { | ||||
| }; | ||||
| 
 | ||||
| export function fetchComposeSuggestions(token) { | ||||
|   let leading = token[0]; | ||||
| 
 | ||||
|   if (leading === '@') { | ||||
|     // handle search
 | ||||
|     return (dispatch, getState) => { | ||||
|       api(getState).get('/api/v1/accounts/search', { | ||||
|         params: { | ||||
|         q: token, | ||||
|           q: token.slice(1), // remove the '@'
 | ||||
|           resolve: false, | ||||
|           limit: 4, | ||||
|         }, | ||||
| @ -223,6 +229,28 @@ export function fetchComposeSuggestions(token) { | ||||
|         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) { | ||||
| @ -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) { | ||||
|   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({ | ||||
|       type: COMPOSE_SUGGESTION_SELECT, | ||||
|  | ||||
| @ -1,5 +1,6 @@ | ||||
| import React from 'react'; | ||||
| import AutosuggestAccountContainer from '../features/compose/containers/autosuggest_account_container'; | ||||
| import AutosuggestShortcode from '../features/compose/components/autosuggest_shortcode'; | ||||
| import ImmutablePropTypes from 'react-immutable-proptypes'; | ||||
| import PropTypes from 'prop-types'; | ||||
| import { isRtl } from '../rtl'; | ||||
| @ -18,11 +19,12 @@ const textAtCursorMatchesToken = (str, 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]; | ||||
|   } | ||||
| 
 | ||||
|   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) { | ||||
|     return [left + 1, word]; | ||||
| @ -128,7 +130,9 @@ export default class AutosuggestTextarea extends ImmutablePureComponent { | ||||
|   } | ||||
| 
 | ||||
|   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(); | ||||
|     this.props.onSuggestionSelected(this.state.tokenStart, this.state.lastToken, suggestion); | ||||
|     this.textarea.focus(); | ||||
| @ -160,6 +164,14 @@ export default class AutosuggestTextarea extends ImmutablePureComponent { | ||||
|       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 ( | ||||
|       <div className='autosuggest-textarea'> | ||||
|         <label> | ||||
| @ -190,7 +202,7 @@ export default class AutosuggestTextarea extends ImmutablePureComponent { | ||||
|               className={`autosuggest-textarea__suggestions__item ${i === selectedSuggestion ? 'selected' : ''}`} | ||||
|               onMouseDown={this.onSuggestionClick} | ||||
|             > | ||||
|               <AutosuggestAccountContainer id={suggestion} /> | ||||
|               {makeItem(suggestion)} | ||||
|             </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_SUGGESTIONS_CLEAR, | ||||
|   COMPOSE_SUGGESTIONS_READY, | ||||
|   COMPOSE_SUGGESTIONS_READY_TXT, | ||||
|   COMPOSE_SUGGESTION_SELECT, | ||||
|   COMPOSE_ADVANCED_OPTIONS_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); | ||||
|   case COMPOSE_SUGGESTIONS_READY: | ||||
|     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: | ||||
|     return insertSuggestion(state, action.position, action.token, action.completion); | ||||
|   case TIMELINE_DELETE: | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user