New feature: :shortcode: expander in compose field

Using `innerHTML` because easier to code but the `encodeHTML` function is troublesome
This commit is contained in:
Lim Chee Aun 2022-12-22 19:24:07 +08:00
parent 122f6877c9
commit 263e48d019
2 changed files with 107 additions and 22 deletions

View file

@ -29,7 +29,7 @@ registerRoute(imageRoute);
// Cache /instance because masto.js has to keep calling it while initializing // Cache /instance because masto.js has to keep calling it while initializing
const apiExtendedRoute = new RegExpRoute( const apiExtendedRoute = new RegExpRoute(
/^https?:\/\/[^\/]+\/api\/v\d+\/instance/, /^https?:\/\/[^\/]+\/api\/v\d+\/(instance|custom_emojis)/,
new StaleWhileRevalidate({ new StaleWhileRevalidate({
cacheName: 'api-extended', cacheName: 'api-extended',
plugins: [ plugins: [

View file

@ -36,6 +36,10 @@ const expiresInFromExpiresAt = (expiresAt) => {
return expirySeconds.find((s) => s >= delta) || oneDay; return expirySeconds.find((s) => s >= delta) || oneDay;
}; };
const menu = document.createElement('ul');
menu.role = 'listbox';
menu.className = 'text-expander-menu';
function Compose({ function Compose({
onClose, onClose,
replyToStatus, replyToStatus,
@ -82,6 +86,15 @@ function Compose({
const [mediaAttachments, setMediaAttachments] = useState([]); const [mediaAttachments, setMediaAttachments] = useState([]);
const [poll, setPoll] = useState(null); const [poll, setPoll] = useState(null);
const customEmojis = useRef();
useEffect(() => {
(async () => {
const emojis = await masto.customEmojis.fetchAll();
console.log({ emojis });
customEmojis.current = emojis;
})();
}, []);
useEffect(() => { useEffect(() => {
if (replyToStatus) { if (replyToStatus) {
const { spoilerText, visibility, sensitive } = replyToStatus; const { spoilerText, visibility, sensitive } = replyToStatus;
@ -167,6 +180,7 @@ function Compose({
// console.log('text-expander-change', e); // console.log('text-expander-change', e);
const { key, provide, text } = e.detail; const { key, provide, text } = e.detail;
textExpanderTextRef.current = text; textExpanderTextRef.current = text;
if (text === '') { if (text === '') {
provide( provide(
Promise.resolve({ Promise.resolve({
@ -175,6 +189,34 @@ function Compose({
); );
return; return;
} }
if (key === ':') {
// const emojis = customEmojis.current.filter((emoji) =>
// emoji.shortcode.startsWith(text),
// );
const emojis = filterShortcodes(customEmojis.current, text);
let html = '';
emojis.forEach((emoji) => {
const { shortcode, url } = emoji;
html += `
<li role="option" data-value="${encodeHTML(shortcode)}">
<img src="${encodeHTML(
url,
)}" width="16" height="16" alt="" loading="lazy" />
:${encodeHTML(shortcode)}:
</li>`;
});
// console.log({ emojis, html });
menu.innerHTML = html;
provide(
Promise.resolve({
matched: emojis.length > 0,
fragment: menu,
}),
);
return;
}
const type = { const type = {
'@': 'accounts', '@': 'accounts',
'#': 'hashtags', '#': 'hashtags',
@ -192,9 +234,7 @@ function Compose({
} }
const results = value[type]; const results = value[type];
console.log('RESULTS', value, results); console.log('RESULTS', value, results);
const menu = document.createElement('ul'); let html = '';
menu.role = 'listbox';
menu.className = 'text-expander-menu';
results.forEach((result) => { results.forEach((result) => {
const { const {
name, name,
@ -205,27 +245,29 @@ function Compose({
emojis, emojis,
} = result; } = result;
const displayNameWithEmoji = emojifyText(displayName, emojis); const displayNameWithEmoji = emojifyText(displayName, emojis);
const item = document.createElement('li'); // const item = menuItem.cloneNode();
item.setAttribute('role', 'option');
if (acct) { if (acct) {
item.dataset.value = acct; html += `
// Want to use <Avatar /> here, but will need to render to string 😅 <li role="option" data-value="${encodeHTML(acct)}">
item.innerHTML = `
<span class="avatar"> <span class="avatar">
<img src="${avatarStatic}" width="16" height="16" alt="" loading="lazy" /> <img src="${encodeHTML(
avatarStatic,
)}" width="16" height="16" alt="" loading="lazy" />
</span> </span>
<span> <span>
<b>${displayNameWithEmoji || username}</b> <b>${encodeHTML(displayNameWithEmoji || username)}</b>
<br>@${acct} <br>@${encodeHTML(acct)}
</span> </span>
</li>
`; `;
} else { } else {
item.dataset.value = name; html += `
item.innerHTML = ` <li role="option" data-value="${encodeHTML(name)}">
<span>#<b>${name}</b></span> <span>#<b>${encodeHTML(name)}</b></span>
</li>
`; `;
} }
menu.appendChild(item); menu.innerHTML = html;
}); });
console.log('MENU', results, menu); console.log('MENU', results, menu);
resolve({ resolve({
@ -244,7 +286,11 @@ function Compose({
textExpanderRef.current.addEventListener('text-expander-value', (e) => { textExpanderRef.current.addEventListener('text-expander-value', (e) => {
const { key, item } = e.detail; const { key, item } = e.detail;
e.detail.value = key + item.dataset.value; if (key === ':') {
e.detail.value = `:${item.dataset.value}:`;
} else {
e.detail.value = `${key}${item.dataset.value}`;
}
}); });
} }
}, []); }, []);
@ -664,7 +710,7 @@ function Compose({
</select> </select>
</label>{' '} </label>{' '}
</div> </div>
<text-expander ref={textExpanderRef} keys="@ #"> <text-expander ref={textExpanderRef} keys="@ # :">
<textarea <textarea
class="large" class="large"
ref={textareaRef} ref={textareaRef}
@ -980,4 +1026,43 @@ function Poll({
); );
} }
function filterShortcodes(emojis, searchTerm) {
searchTerm = searchTerm.toLowerCase();
// Return an array of shortcodes that start with or contain the search term, sorted by relevance and limited to the first 5
return emojis
.sort((a, b) => {
let aLower = a.shortcode.toLowerCase();
let bLower = b.shortcode.toLowerCase();
let aStartsWith = aLower.startsWith(searchTerm);
let bStartsWith = bLower.startsWith(searchTerm);
let aContains = aLower.includes(searchTerm);
let bContains = bLower.includes(searchTerm);
let bothStartWith = aStartsWith && bStartsWith;
let bothContain = aContains && bContains;
return bothStartWith
? a.length - b.length
: aStartsWith
? -1
: bStartsWith
? 1
: bothContain
? a.length - b.length
: aContains
? -1
: bContains
? 1
: 0;
})
.slice(0, 5);
}
function encodeHTML(str) {
return str.replace(/[&<>"']/g, function (char) {
return '&#' + char.charCodeAt(0) + ';';
});
}
export default Compose; export default Compose;