From 2e0ef6494b92edde7430e84337740d7070ace9d7 Mon Sep 17 00:00:00 2001 From: Lim Chee Aun Date: Sat, 25 May 2024 11:06:58 +0800 Subject: [PATCH] Extend at-mentions with dedicated UI --- src/components/account-block.jsx | 27 ++- src/components/compose.css | 69 +++++++ src/components/compose.jsx | 299 ++++++++++++++++++++++++++++++- 3 files changed, 379 insertions(+), 16 deletions(-) diff --git a/src/components/account-block.jsx b/src/components/account-block.jsx index 487f7af..326f415 100644 --- a/src/components/account-block.jsx +++ b/src/components/account-block.jsx @@ -133,21 +133,18 @@ function AccountBlock({ )} {showActivity && ( - <> -
- - Posts: {statusesCount} - {!!lastStatusAt && ( - <> - {' '} - · Last posted:{' '} - {niceDateTime(lastStatusAt, { - hideTime: true, - })} - - )} - - +
+ Posts: {shortenNumber(statusesCount)} + {!!lastStatusAt && ( + <> + {' '} + · Last posted:{' '} + {niceDateTime(lastStatusAt, { + hideTime: true, + })} + + )} +
)} {showStats && (
diff --git a/src/components/compose.css b/src/components/compose.css index db01356..7aa4e96 100644 --- a/src/components/compose.css +++ b/src/components/compose.css @@ -600,6 +600,75 @@ } */ } +#mention-sheet { + height: 50vh; + + .accounts-list { + --list-gap: 1px; + list-style: none; + margin: 0; + padding: 8px 0; + display: flex; + flex-direction: column; + row-gap: var(--list-gap); + + &.loading { + opacity: 0.5; + } + + li { + display: flex; + flex-grow: 1; + /* align-items: center; */ + margin: 0 -8px; + padding: 8px; + gap: 8px; + position: relative; + justify-content: space-between; + border-radius: 8px; + /* align-items: center; */ + + &:hover { + background-image: linear-gradient( + to right, + transparent 75%, + var(--link-bg-color) + ); + } + + &.selected { + background-image: linear-gradient( + to right, + var(--bg-faded-color) 75%, + var(--link-bg-color) + ); + } + + &:before { + content: ''; + display: block; + border-top: var(--hairline-width) solid var(--divider-color); + position: absolute; + bottom: 0; + left: 58px; + right: 0; + } + + &:has(+ li:is(.selected, :hover)):before, + &:is(.selected, :hover):before { + opacity: 0; + } + + > button { + border-radius: 4px; + &:hover { + outline: 2px solid var(--button-bg-blur-color); + } + } + } + } +} + #custom-emojis-sheet { max-height: 50vh; max-height: 50dvh; diff --git a/src/components/compose.jsx b/src/components/compose.jsx index d618f80..83ed51f 100644 --- a/src/components/compose.jsx +++ b/src/components/compose.jsx @@ -31,6 +31,7 @@ import localeMatch from '../utils/locale-match'; import localeCode2Text from '../utils/localeCode2Text'; import openCompose from '../utils/open-compose'; import pmem from '../utils/pmem'; +import { fetchRelationships } from '../utils/relationships'; import shortenNumber from '../utils/shorten-number'; import showToast from '../utils/show-toast'; import states, { saveStatus } from '../utils/states'; @@ -630,6 +631,7 @@ function Compose({ }; }, [mediaAttachments]); + const [showMentionPicker, setShowMentionPicker] = useState(false); const [showEmoji2Picker, setShowEmoji2Picker] = useState(false); const [showGIFPicker, setShowGIFPicker] = useState(false); @@ -1166,6 +1168,10 @@ function Compose({ setShowEmoji2Picker({ defaultSearchTerm: action?.defaultSearchTerm || null, }); + } else if (action?.name === 'mention') { + setShowMentionPicker({ + defaultSearchTerm: action?.defaultSearchTerm || null, + }); } }} /> @@ -1304,6 +1310,16 @@ function Compose({ {' '} ))} + {/* */}
+ {showMentionPicker && ( + { + if (e.target === e.currentTarget) { + setShowMentionPicker(false); + } + }} + > + { + setShowMentionPicker(false); + }} + defaultSearchTerm={showMentionPicker?.defaultSearchTerm} + onSelect={(socialAddress) => { + const textarea = textareaRef.current; + if (!textarea) return; + const { selectionStart, selectionEnd } = textarea; + const text = textarea.value; + const textBeforeMention = text.slice(0, selectionStart); + const spaceBeforeMention = textBeforeMention + ? /[\s\t\n\r]$/.test(textBeforeMention) + ? '' + : ' ' + : ''; + const textAfterMention = text.slice(selectionEnd); + const spaceAfterMention = /^[\s\t\n\r]/.test(textAfterMention) + ? '' + : ' '; + const newText = + textBeforeMention + + spaceBeforeMention + + '@' + + socialAddress + + spaceAfterMention + + textAfterMention; + textarea.value = newText; + textarea.selectionStart = textarea.selectionEnd = + selectionEnd + + 1 + + socialAddress.length + + spaceAfterMention.length; + textarea.focus(); + textarea.dispatchEvent(new Event('input')); + }} + /> + + )} {showEmoji2Picker && ( { @@ -1648,8 +1713,9 @@ const Textarea = forwardRef((props, ref) => { `; } - menu.innerHTML = html; }); + html += `
  • More…
  • `; + menu.innerHTML = html; console.log('MENU', results, menu); resolve({ matched: results.length > 0, @@ -1681,6 +1747,17 @@ const Textarea = forwardRef((props, ref) => { }); }, 300); } + } else if (key === '@') { + e.detail.value = value ? `@${value} ` : '​'; // zero-width space + if (more) { + e.detail.continue = true; + setTimeout(() => { + onTrigger?.({ + name: 'mention', + defaultSearchTerm: more, + }); + }, 300); + } } else { e.detail.value = `${key}${value}`; } @@ -2345,6 +2422,226 @@ function removeNullUndefined(obj) { return obj; } +function MentionModal({ + onClose = () => {}, + onSelect = () => {}, + defaultSearchTerm, +}) { + const { masto } = api(); + const [uiState, setUIState] = useState('default'); + const [accounts, setAccounts] = useState([]); + const [relationshipsMap, setRelationshipsMap] = useState({}); + + const [selectedIndex, setSelectedIndex] = useState(0); + + const loadRelationships = async (accounts) => { + if (!accounts?.length) return; + const relationships = await fetchRelationships(accounts, relationshipsMap); + if (relationships) { + setRelationshipsMap({ + ...relationshipsMap, + ...relationships, + }); + } + }; + + const loadAccounts = (term) => { + if (!term) return; + setUIState('loading'); + (async () => { + try { + const accounts = await masto.v1.accounts.search.list({ + q: term, + limit: 40, + resolve: false, + }); + setAccounts(accounts); + loadRelationships(accounts); + setUIState('default'); + } catch (e) { + setUIState('error'); + console.error(e); + } + })(); + }; + + const debouncedLoadAccounts = useDebouncedCallback(loadAccounts, 1000); + + useEffect(() => { + loadAccounts(); + }, [loadAccounts]); + + const inputRef = useRef(); + useEffect(() => { + if (inputRef.current) { + inputRef.current.focus(); + // Put cursor at the end + if (inputRef.current.value) { + inputRef.current.selectionStart = inputRef.current.value.length; + inputRef.current.selectionEnd = inputRef.current.value.length; + } + } + }, []); + + useEffect(() => { + if (defaultSearchTerm) { + loadAccounts(defaultSearchTerm); + } + }, [defaultSearchTerm]); + + const selectAccount = (account) => { + const socialAddress = account.acct; + onSelect(socialAddress); + onClose(); + }; + + useHotkeys( + 'enter', + () => { + const selectedAccount = accounts[selectedIndex]; + if (selectedAccount) { + selectAccount(selectedAccount); + } + }, + { + preventDefault: true, + enableOnFormTags: ['input'], + }, + ); + + const listRef = useRef(); + useHotkeys( + 'down', + () => { + if (selectedIndex < accounts.length - 1) { + setSelectedIndex(selectedIndex + 1); + } else { + setSelectedIndex(0); + } + setTimeout(() => { + const selectedItem = listRef.current.querySelector('.selected'); + if (selectedItem) { + selectedItem.scrollIntoView({ + behavior: 'smooth', + block: 'center', + inline: 'center', + }); + } + }, 1); + }, + { + preventDefault: true, + enableOnFormTags: ['input'], + }, + ); + + useHotkeys( + 'up', + () => { + if (selectedIndex > 0) { + setSelectedIndex(selectedIndex - 1); + } else { + setSelectedIndex(accounts.length - 1); + } + setTimeout(() => { + const selectedItem = listRef.current.querySelector('.selected'); + if (selectedItem) { + selectedItem.scrollIntoView({ + behavior: 'smooth', + block: 'center', + inline: 'center', + }); + } + }, 1); + }, + { + preventDefault: true, + enableOnFormTags: ['input'], + }, + ); + + return ( +
    + {!!onClose && ( + + )} +
    +
    { + e.preventDefault(); + debouncedLoadAccounts.flush?.(); + // const searchTerm = inputRef.current.value; + // debouncedLoadAccounts(searchTerm); + }} + > + { + const { value } = e.target; + debouncedLoadAccounts(value); + }} + autocomplete="off" + autocorrect="off" + autocapitalize="off" + spellCheck="false" + dir="auto" + defaultValue={defaultSearchTerm || ''} + /> +
    +
    +
    + {accounts?.length > 0 ? ( +
      + {accounts.map((account, i) => { + const relationship = relationshipsMap[account.id]; + return ( +
    • + + +
    • + ); + })} +
    + ) : uiState === 'loading' ? ( +
    + +
    + ) : uiState === 'error' ? ( +
    +

    Error loading accounts

    +
    + ) : null} +
    +
    + ); +} + function CustomEmojisModal({ masto, instance,