mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-10-31 11:35:03 +01:00 
			
		
		
		
	Improve emoji and mention matching (#24255)
Prioritize matches that start with the given text, then matches that contain the given text. I wanted to add a heart emoji on a pull request comment so I started writing `:`, `h`, `e`, `a`, `r` (at this point I still couldn't find the heart), `t`... The heart was not on the list, that's weird - it feels like I made a typo or a mistake. This fixes that. This also feels more like GitHub's emoji auto-complete. # Before  # After  --------- Signed-off-by: Yarden Shoham <git@yardenshoham.com> Co-authored-by: silverwind <me@silverwind.io>
This commit is contained in:
		
							parent
							
								
									ce9c1ddc4c
								
							
						
					
					
						commit
						3cc87370c3
					
				| @ -5,11 +5,11 @@ import {attachTribute} from '../tribute.js'; | ||||
| import {hideElem, showElem, autosize} from '../../utils/dom.js'; | ||||
| import {initEasyMDEImagePaste, initTextareaImagePaste} from './ImagePaste.js'; | ||||
| import {handleGlobalEnterQuickSubmit} from './QuickSubmit.js'; | ||||
| import {emojiKeys, emojiString} from '../emoji.js'; | ||||
| import {emojiString} from '../emoji.js'; | ||||
| import {renderPreviewPanelContent} from '../repo-editor.js'; | ||||
| import {matchEmoji, matchMention} from '../../utils/match.js'; | ||||
| 
 | ||||
| let elementIdCounter = 0; | ||||
| const maxExpanderMatches = 6; | ||||
| 
 | ||||
| /** | ||||
|  * validate if the given textarea is non-empty. | ||||
| @ -106,14 +106,7 @@ class ComboMarkdownEditor { | ||||
|     const expander = this.container.querySelector('text-expander'); | ||||
|     expander?.addEventListener('text-expander-change', ({detail: {key, provide, text}}) => { | ||||
|       if (key === ':') { | ||||
|         const matches = []; | ||||
|         const textLowerCase = text.toLowerCase(); | ||||
|         for (const name of emojiKeys) { | ||||
|           if (name.toLowerCase().includes(textLowerCase)) { | ||||
|             matches.push(name); | ||||
|             if (matches.length >= maxExpanderMatches) break; | ||||
|           } | ||||
|         } | ||||
|         const matches = matchEmoji(text); | ||||
|         if (!matches.length) return provide({matched: false}); | ||||
| 
 | ||||
|         const ul = document.createElement('ul'); | ||||
| @ -129,14 +122,7 @@ class ComboMarkdownEditor { | ||||
| 
 | ||||
|         provide({matched: true, fragment: ul}); | ||||
|       } else if (key === '@') { | ||||
|         const matches = []; | ||||
|         const textLowerCase = text.toLowerCase(); | ||||
|         for (const obj of window.config.tributeValues) { | ||||
|           if (obj.key.toLowerCase().includes(textLowerCase)) { | ||||
|             matches.push(obj); | ||||
|             if (matches.length >= maxExpanderMatches) break; | ||||
|           } | ||||
|         } | ||||
|         const matches = matchMention(text); | ||||
|         if (!matches.length) return provide({matched: false}); | ||||
| 
 | ||||
|         const ul = document.createElement('ul'); | ||||
|  | ||||
| @ -3,4 +3,13 @@ window.config = { | ||||
|   pageData: {}, | ||||
|   i18n: {}, | ||||
|   appSubUrl: '', | ||||
|   tributeValues: [ | ||||
|     {key: 'user1 User 1', value: 'user1', name: 'user1', fullname: 'User 1', avatar: 'https://avatar1.com'}, | ||||
|     {key: 'user2 User 2', value: 'user2', name: 'user2', fullname: 'User 2', avatar: 'https://avatar2.com'}, | ||||
|     {key: 'user3 User 3', value: 'user3', name: 'user3', fullname: 'User 3', avatar: 'https://avatar3.com'}, | ||||
|     {key: 'user4 User 4', value: 'user4', name: 'user4', fullname: 'User 4', avatar: 'https://avatar4.com'}, | ||||
|     {key: 'user5 User 5', value: 'user5', name: 'user5', fullname: 'User 5', avatar: 'https://avatar5.com'}, | ||||
|     {key: 'user6 User 6', value: 'user6', name: 'user6', fullname: 'User 6', avatar: 'https://avatar6.com'}, | ||||
|     {key: 'user7 User 7', value: 'user7', name: 'user7', fullname: 'User 7', avatar: 'https://avatar7.com'}, | ||||
|   ], | ||||
| }; | ||||
|  | ||||
							
								
								
									
										43
									
								
								web_src/js/utils/match.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								web_src/js/utils/match.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,43 @@ | ||||
| import emojis from '../../../assets/emoji.json'; | ||||
| 
 | ||||
| const maxMatches = 6; | ||||
| 
 | ||||
| function sortAndReduce(map) { | ||||
|   const sortedMap = new Map([...map.entries()].sort((a, b) => a[1] - b[1])); | ||||
|   return Array.from(sortedMap.keys()).slice(0, maxMatches); | ||||
| } | ||||
| 
 | ||||
| export function matchEmoji(queryText) { | ||||
|   const query = queryText.toLowerCase().replaceAll('_', ' '); | ||||
|   if (!query) return emojis.slice(0, maxMatches).map((e) => e.aliases[0]); | ||||
| 
 | ||||
|   // results is a map of weights, lower is better
 | ||||
|   const results = new Map(); | ||||
|   for (const {aliases} of emojis) { | ||||
|     const mainAlias = aliases[0]; | ||||
|     for (const [aliasIndex, alias] of aliases.entries()) { | ||||
|       const index = alias.replaceAll('_', ' ').indexOf(query); | ||||
|       if (index === -1) continue; | ||||
|       const existing = results.get(mainAlias); | ||||
|       const rankedIndex = index + aliasIndex; | ||||
|       results.set(mainAlias, existing ? existing - rankedIndex : rankedIndex); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   return sortAndReduce(results); | ||||
| } | ||||
| 
 | ||||
| export function matchMention(queryText) { | ||||
|   const query = queryText.toLowerCase(); | ||||
| 
 | ||||
|   // results is a map of weights, lower is better
 | ||||
|   const results = new Map(); | ||||
|   for (const obj of window.config.tributeValues) { | ||||
|     const index = obj.key.toLowerCase().indexOf(query); | ||||
|     if (index === -1) continue; | ||||
|     const existing = results.get(obj); | ||||
|     results.set(obj, existing ? existing - index : index); | ||||
|   } | ||||
| 
 | ||||
|   return sortAndReduce(results); | ||||
| } | ||||
							
								
								
									
										47
									
								
								web_src/js/utils/match.test.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								web_src/js/utils/match.test.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,47 @@ | ||||
| import {test, expect} from 'vitest'; | ||||
| import {matchEmoji, matchMention} from './match.js'; | ||||
| 
 | ||||
| test('matchEmoji', () => { | ||||
|   expect(matchEmoji('')).toEqual([ | ||||
|     '+1', | ||||
|     '-1', | ||||
|     '100', | ||||
|     '1234', | ||||
|     '1st_place_medal', | ||||
|     '2nd_place_medal', | ||||
|   ]); | ||||
| 
 | ||||
|   expect(matchEmoji('hea')).toEqual([ | ||||
|     'headphones', | ||||
|     'headstone', | ||||
|     'health_worker', | ||||
|     'hear_no_evil', | ||||
|     'heard_mcdonald_islands', | ||||
|     'heart', | ||||
|   ]); | ||||
| 
 | ||||
|   expect(matchEmoji('hear')).toEqual([ | ||||
|     'hear_no_evil', | ||||
|     'heard_mcdonald_islands', | ||||
|     'heart', | ||||
|     'heart_decoration', | ||||
|     'heart_eyes', | ||||
|     'heart_eyes_cat', | ||||
|   ]); | ||||
| 
 | ||||
|   expect(matchEmoji('poo')).toEqual([ | ||||
|     'poodle', | ||||
|     'hankey', | ||||
|     'spoon', | ||||
|     'bowl_with_spoon', | ||||
|   ]); | ||||
| 
 | ||||
|   expect(matchEmoji('1st_')).toEqual([ | ||||
|     '1st_place_medal', | ||||
|   ]); | ||||
| }); | ||||
| 
 | ||||
| test('matchMention', () => { | ||||
|   expect(matchMention('')).toEqual(window.config.tributeValues.slice(0, 6)); | ||||
|   expect(matchMention('user4')).toEqual([window.config.tributeValues[3]]); | ||||
| }); | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user