diff --git a/src/components/account-block.jsx b/src/components/account-block.jsx
index 487f7af2..326f415c 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 db013567..7aa4e96e 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 d618f809..83ed51f6 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 && (
+
+ )}
+
+
+ {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,