this repo has no description
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

New feature: :shortcode: expander in compose field

Using `innerHTML` because easier to code but the `encodeHTML` function is troublesome

+107 -22
+1 -1
public/sw.js
··· 29 29 30 30 // Cache /instance because masto.js has to keep calling it while initializing 31 31 const apiExtendedRoute = new RegExpRoute( 32 - /^https?:\/\/[^\/]+\/api\/v\d+\/instance/, 32 + /^https?:\/\/[^\/]+\/api\/v\d+\/(instance|custom_emojis)/, 33 33 new StaleWhileRevalidate({ 34 34 cacheName: 'api-extended', 35 35 plugins: [
+106 -21
src/components/compose.jsx
··· 36 36 return expirySeconds.find((s) => s >= delta) || oneDay; 37 37 }; 38 38 39 + const menu = document.createElement('ul'); 40 + menu.role = 'listbox'; 41 + menu.className = 'text-expander-menu'; 42 + 39 43 function Compose({ 40 44 onClose, 41 45 replyToStatus, ··· 81 85 const [sensitive, setSensitive] = useState(false); 82 86 const [mediaAttachments, setMediaAttachments] = useState([]); 83 87 const [poll, setPoll] = useState(null); 88 + 89 + const customEmojis = useRef(); 90 + useEffect(() => { 91 + (async () => { 92 + const emojis = await masto.customEmojis.fetchAll(); 93 + console.log({ emojis }); 94 + customEmojis.current = emojis; 95 + })(); 96 + }, []); 84 97 85 98 useEffect(() => { 86 99 if (replyToStatus) { ··· 167 180 // console.log('text-expander-change', e); 168 181 const { key, provide, text } = e.detail; 169 182 textExpanderTextRef.current = text; 183 + 170 184 if (text === '') { 171 185 provide( 172 186 Promise.resolve({ ··· 175 189 ); 176 190 return; 177 191 } 192 + 193 + if (key === ':') { 194 + // const emojis = customEmojis.current.filter((emoji) => 195 + // emoji.shortcode.startsWith(text), 196 + // ); 197 + const emojis = filterShortcodes(customEmojis.current, text); 198 + let html = ''; 199 + emojis.forEach((emoji) => { 200 + const { shortcode, url } = emoji; 201 + html += ` 202 + <li role="option" data-value="${encodeHTML(shortcode)}"> 203 + <img src="${encodeHTML( 204 + url, 205 + )}" width="16" height="16" alt="" loading="lazy" /> 206 + :${encodeHTML(shortcode)}: 207 + </li>`; 208 + }); 209 + // console.log({ emojis, html }); 210 + menu.innerHTML = html; 211 + provide( 212 + Promise.resolve({ 213 + matched: emojis.length > 0, 214 + fragment: menu, 215 + }), 216 + ); 217 + return; 218 + } 219 + 178 220 const type = { 179 221 '@': 'accounts', 180 222 '#': 'hashtags', ··· 192 234 } 193 235 const results = value[type]; 194 236 console.log('RESULTS', value, results); 195 - const menu = document.createElement('ul'); 196 - menu.role = 'listbox'; 197 - menu.className = 'text-expander-menu'; 237 + let html = ''; 198 238 results.forEach((result) => { 199 239 const { 200 240 name, ··· 205 245 emojis, 206 246 } = result; 207 247 const displayNameWithEmoji = emojifyText(displayName, emojis); 208 - const item = document.createElement('li'); 209 - item.setAttribute('role', 'option'); 248 + // const item = menuItem.cloneNode(); 210 249 if (acct) { 211 - item.dataset.value = acct; 212 - // Want to use <Avatar /> here, but will need to render to string 😅 213 - item.innerHTML = ` 214 - <span class="avatar"> 215 - <img src="${avatarStatic}" width="16" height="16" alt="" loading="lazy" /> 216 - </span> 217 - <span> 218 - <b>${displayNameWithEmoji || username}</b> 219 - <br>@${acct} 220 - </span> 250 + html += ` 251 + <li role="option" data-value="${encodeHTML(acct)}"> 252 + <span class="avatar"> 253 + <img src="${encodeHTML( 254 + avatarStatic, 255 + )}" width="16" height="16" alt="" loading="lazy" /> 256 + </span> 257 + <span> 258 + <b>${encodeHTML(displayNameWithEmoji || username)}</b> 259 + <br>@${encodeHTML(acct)} 260 + </span> 261 + </li> 221 262 `; 222 263 } else { 223 - item.dataset.value = name; 224 - item.innerHTML = ` 225 - <span>#<b>${name}</b></span> 264 + html += ` 265 + <li role="option" data-value="${encodeHTML(name)}"> 266 + <span>#<b>${encodeHTML(name)}</b></span> 267 + </li> 226 268 `; 227 269 } 228 - menu.appendChild(item); 270 + menu.innerHTML = html; 229 271 }); 230 272 console.log('MENU', results, menu); 231 273 resolve({ ··· 244 286 245 287 textExpanderRef.current.addEventListener('text-expander-value', (e) => { 246 288 const { key, item } = e.detail; 247 - e.detail.value = key + item.dataset.value; 289 + if (key === ':') { 290 + e.detail.value = `:${item.dataset.value}:`; 291 + } else { 292 + e.detail.value = `${key}${item.dataset.value}`; 293 + } 248 294 }); 249 295 } 250 296 }, []); ··· 664 710 </select> 665 711 </label>{' '} 666 712 </div> 667 - <text-expander ref={textExpanderRef} keys="@ #"> 713 + <text-expander ref={textExpanderRef} keys="@ # :"> 668 714 <textarea 669 715 class="large" 670 716 ref={textareaRef} ··· 978 1024 </div> 979 1025 </div> 980 1026 ); 1027 + } 1028 + 1029 + function filterShortcodes(emojis, searchTerm) { 1030 + searchTerm = searchTerm.toLowerCase(); 1031 + 1032 + // Return an array of shortcodes that start with or contain the search term, sorted by relevance and limited to the first 5 1033 + return emojis 1034 + .sort((a, b) => { 1035 + let aLower = a.shortcode.toLowerCase(); 1036 + let bLower = b.shortcode.toLowerCase(); 1037 + 1038 + let aStartsWith = aLower.startsWith(searchTerm); 1039 + let bStartsWith = bLower.startsWith(searchTerm); 1040 + let aContains = aLower.includes(searchTerm); 1041 + let bContains = bLower.includes(searchTerm); 1042 + let bothStartWith = aStartsWith && bStartsWith; 1043 + let bothContain = aContains && bContains; 1044 + 1045 + return bothStartWith 1046 + ? a.length - b.length 1047 + : aStartsWith 1048 + ? -1 1049 + : bStartsWith 1050 + ? 1 1051 + : bothContain 1052 + ? a.length - b.length 1053 + : aContains 1054 + ? -1 1055 + : bContains 1056 + ? 1 1057 + : 0; 1058 + }) 1059 + .slice(0, 5); 1060 + } 1061 + 1062 + function encodeHTML(str) { 1063 + return str.replace(/[&<>"']/g, function (char) { 1064 + return '&#' + char.charCodeAt(0) + ';'; 1065 + }); 981 1066 } 982 1067 983 1068 export default Compose;