218 lines
6.3 KiB
JavaScript
218 lines
6.3 KiB
JavaScript
import { Trans, useLingui } from '@lingui/react/macro';
|
||
import { ControlledMenu, MenuDivider, MenuItem } from '@szhsin/react-menu';
|
||
import { useCallback, useRef, useState } from 'preact/hooks';
|
||
import { useHotkeys } from 'react-hotkeys-hook';
|
||
import { useLongPress } from 'use-long-press';
|
||
import { useSnapshot } from 'valtio';
|
||
|
||
import { api } from '../utils/api';
|
||
import niceDateTime from '../utils/nice-date-time';
|
||
import openCompose from '../utils/open-compose';
|
||
import openOSK from '../utils/open-osk';
|
||
import pmem from '../utils/pmem';
|
||
import safeBoundingBoxPadding from '../utils/safe-bounding-box-padding';
|
||
import showCompose from '../utils/show-compose';
|
||
import states from '../utils/states';
|
||
import statusPeek from '../utils/status-peek';
|
||
import { getCurrentAccountID } from '../utils/store-utils';
|
||
|
||
import Icon from './icon';
|
||
import MenuLink from './menu-link';
|
||
import RelativeTime from './relative-time';
|
||
import SubMenu2 from './submenu2';
|
||
|
||
// Function to fetch the latest posts from the current user
|
||
// Use pmem to memoize fetch results for 1 minute
|
||
const fetchLatestPostsMemoized = pmem(
|
||
async (masto, currentAccountID) => {
|
||
const statusesIterator = masto.v1.accounts
|
||
.$select(currentAccountID)
|
||
.statuses.list({
|
||
limit: 3,
|
||
exclude_replies: true,
|
||
exclude_reblogs: true,
|
||
});
|
||
const { value } = await statusesIterator.next();
|
||
return value || [];
|
||
},
|
||
{ maxAge: 60000 },
|
||
); // 1 minute cache
|
||
|
||
export default function ComposeButton() {
|
||
const { t } = useLingui();
|
||
const snapStates = useSnapshot(states);
|
||
const { masto } = api();
|
||
|
||
// Context menu state
|
||
const [menuOpen, setMenuOpen] = useState(false);
|
||
const [latestPosts, setLatestPosts] = useState([]);
|
||
const [loadingPosts, setLoadingPosts] = useState(false);
|
||
const buttonRef = useRef(null);
|
||
const menuRef = useRef(null);
|
||
|
||
function handleButton(e) {
|
||
if (snapStates.composerState.minimized) {
|
||
states.composerState.minimized = false;
|
||
openOSK();
|
||
return;
|
||
}
|
||
|
||
if (e.shiftKey) {
|
||
const newWin = openCompose();
|
||
|
||
if (!newWin) {
|
||
states.showCompose = true;
|
||
}
|
||
} else {
|
||
openOSK();
|
||
states.showCompose = true;
|
||
}
|
||
}
|
||
|
||
useHotkeys('c, shift+c', handleButton, {
|
||
ignoreEventWhen: (e) => {
|
||
const hasModal = !!document.querySelector('#modal-container > *');
|
||
return hasModal;
|
||
},
|
||
});
|
||
|
||
// Setup longpress handler to open context menu
|
||
const bindLongPress = useLongPress(
|
||
() => {
|
||
setMenuOpen(true);
|
||
},
|
||
{
|
||
threshold: 600,
|
||
},
|
||
);
|
||
|
||
const fetchLatestPosts = useCallback(async () => {
|
||
try {
|
||
setLoadingPosts(true);
|
||
const currentAccountID = getCurrentAccountID();
|
||
if (!currentAccountID) {
|
||
return;
|
||
}
|
||
const posts = await fetchLatestPostsMemoized(masto, currentAccountID);
|
||
setLatestPosts(posts);
|
||
} catch (error) {
|
||
} finally {
|
||
setLoadingPosts(false);
|
||
}
|
||
}, [masto]);
|
||
|
||
// Function to handle opening the compose window to reply to a post
|
||
const handleReplyToPost = useCallback((post) => {
|
||
showCompose({
|
||
replyToStatus: post,
|
||
});
|
||
setMenuOpen(false);
|
||
}, []);
|
||
|
||
return (
|
||
<>
|
||
<button
|
||
ref={buttonRef}
|
||
type="button"
|
||
id="compose-button"
|
||
onClick={handleButton}
|
||
onContextMenu={(e) => {
|
||
e.preventDefault();
|
||
setMenuOpen(true);
|
||
}}
|
||
{...bindLongPress()}
|
||
class={`${snapStates.composerState.minimized ? 'min' : ''} ${
|
||
snapStates.composerState.publishing ? 'loading' : ''
|
||
} ${snapStates.composerState.publishingError ? 'error' : ''}`}
|
||
>
|
||
<Icon icon="quill" size="xl" alt={t`Compose`} />
|
||
</button>
|
||
<ControlledMenu
|
||
ref={menuRef}
|
||
state={menuOpen ? 'open' : undefined}
|
||
anchorRef={buttonRef}
|
||
onClose={() => setMenuOpen(false)}
|
||
direction="top"
|
||
gap={8} // Add gap between menu and button
|
||
unmountOnClose
|
||
portal={{
|
||
target: document.body,
|
||
}}
|
||
boundingBoxPadding={safeBoundingBoxPadding()}
|
||
containerProps={{
|
||
style: {
|
||
zIndex: 1001,
|
||
},
|
||
}}
|
||
>
|
||
<MenuLink to="/sp">
|
||
<Icon icon="schedule" size="l" />{' '}
|
||
<span>
|
||
<Trans>Scheduled Posts</Trans>
|
||
</span>
|
||
</MenuLink>
|
||
<MenuDivider />
|
||
<SubMenu2
|
||
label={
|
||
<>
|
||
<Icon icon="comment" size="l" />{' '}
|
||
<span className="menu-grow">
|
||
<Trans>Add to thread</Trans>
|
||
</span>
|
||
<Icon icon="chevron-right" />
|
||
</>
|
||
}
|
||
onMenuChange={(e) => {
|
||
if (e.open) {
|
||
fetchLatestPosts();
|
||
}
|
||
}}
|
||
>
|
||
{loadingPosts ? (
|
||
<MenuItem disabled>
|
||
<span>
|
||
<Trans>Loading…</Trans>
|
||
</span>
|
||
</MenuItem>
|
||
) : latestPosts.length === 0 ? (
|
||
<MenuItem disabled>
|
||
<small>
|
||
<Trans>No posts found</Trans>
|
||
</small>
|
||
</MenuItem>
|
||
) : (
|
||
latestPosts.map((post) => {
|
||
const createdDate = new Date(post.createdAt);
|
||
const isWithinDay =
|
||
new Date().getTime() - createdDate.getTime() < 86400000;
|
||
|
||
return (
|
||
<MenuItem key={post.id} onClick={() => handleReplyToPost(post)}>
|
||
<small>
|
||
<div class="menu-post-text">{statusPeek(post)}</div>
|
||
<span className="more-insignificant">
|
||
{/* Show relative time if within a day */}
|
||
{isWithinDay && (
|
||
<>
|
||
<RelativeTime datetime={createdDate} format="micro" />{' '}
|
||
‒{' '}
|
||
</>
|
||
)}
|
||
<time
|
||
className="created"
|
||
dateTime={createdDate.toISOString()}
|
||
title={createdDate.toLocaleString()}
|
||
>
|
||
{niceDateTime(post.createdAt)}
|
||
</time>
|
||
</span>
|
||
</small>
|
||
</MenuItem>
|
||
);
|
||
})
|
||
)}
|
||
</SubMenu2>
|
||
</ControlledMenu>
|
||
</>
|
||
);
|
||
}
|