soapbox/app/soapbox/features/ui/components/profile-dropdown.tsx

182 wiersze
5.0 KiB
TypeScript

import { useFloating } from '@floating-ui/react';
import clsx from 'clsx';
import throttle from 'lodash/throttle';
import React, { useEffect, useMemo, useState } from 'react';
import { defineMessages, useIntl } from 'react-intl';
import { Link } from 'react-router-dom';
import { fetchOwnAccounts, logOut, switchAccount } from 'soapbox/actions/auth';
import Account from 'soapbox/components/account';
import { MenuDivider } from 'soapbox/components/ui';
import { useAppDispatch, useAppSelector, useClickOutside, useFeatures } from 'soapbox/hooks';
import { makeGetAccount } from 'soapbox/selectors';
import ThemeToggle from './theme-toggle';
import type { Account as AccountEntity } from 'soapbox/schemas';
const messages = defineMessages({
add: { id: 'profile_dropdown.add_account', defaultMessage: 'Add an existing account' },
theme: { id: 'profile_dropdown.theme', defaultMessage: 'Theme' },
logout: { id: 'profile_dropdown.logout', defaultMessage: 'Log out @{acct}' },
});
interface IProfileDropdown {
account: AccountEntity
children: React.ReactNode
}
type IMenuItem = {
text: string | React.ReactElement | null
to?: string
toggle?: JSX.Element
icon?: string
action?: (event: React.MouseEvent) => void
}
const getAccount = makeGetAccount();
const ProfileDropdown: React.FC<IProfileDropdown> = ({ account, children }) => {
const dispatch = useAppDispatch();
const features = useFeatures();
const intl = useIntl();
const [visible, setVisible] = useState(false);
const { x, y, strategy, refs } = useFloating<HTMLButtonElement>({ placement: 'bottom-end' });
const authUsers = useAppSelector((state) => state.auth.users);
const otherAccounts = useAppSelector((state) => authUsers.map((authUser: any) => getAccount(state, authUser.id)!));
const handleLogOut = () => {
dispatch(logOut());
};
const handleSwitchAccount = (account: AccountEntity) => {
return () => {
dispatch(switchAccount(account.id));
};
};
const fetchOwnAccountThrottled = throttle(() => {
dispatch(fetchOwnAccounts());
}, 2000);
const renderAccount = (account: AccountEntity) => {
return (
<Account account={account} showProfileHoverCard={false} withLinkToProfile={false} hideActions />
);
};
const menu: IMenuItem[] = useMemo(() => {
const menu: IMenuItem[] = [];
menu.push({ text: renderAccount(account), to: `/@${account.acct}` });
otherAccounts.forEach((otherAccount) => {
if (otherAccount && otherAccount.id !== account.id) {
menu.push({
text: renderAccount(otherAccount),
action: handleSwitchAccount(otherAccount),
});
}
});
menu.push({ text: null });
menu.push({ text: intl.formatMessage(messages.theme), toggle: <ThemeToggle /> });
menu.push({ text: null });
menu.push({
text: intl.formatMessage(messages.add),
to: '/login/add',
icon: require('@tabler/icons/plus.svg'),
});
menu.push({
text: intl.formatMessage(messages.logout, { acct: account.acct }),
to: '/logout',
action: handleLogOut,
icon: require('@tabler/icons/logout.svg'),
});
return menu;
}, [account, authUsers, features]);
const toggleVisible = () => setVisible(!visible);
useEffect(() => {
fetchOwnAccountThrottled();
}, [account, authUsers]);
useClickOutside(refs, () => {
setVisible(false);
});
return (
<>
<button type='button' ref={refs.setReference} onClick={toggleVisible}>
{children}
</button>
{visible && (
<div
ref={refs.setFloating}
className='z-[1003] mt-2 max-w-xs rounded-md bg-white shadow-lg focus:outline-none dark:bg-gray-900 dark:ring-2 dark:ring-primary-700'
style={{
position: strategy,
top: y ?? 0,
left: x ?? 0,
width: 'max-content',
}}
>
{menu.map((menuItem, i) => (
<MenuItem key={i} menuItem={menuItem} />
))}
</div>
)}
</>
);
};
interface MenuItemProps {
className?: string
menuItem: IMenuItem
}
const MenuItem: React.FC<MenuItemProps> = ({ className, menuItem }) => {
const baseClassName = clsx(className, 'block w-full cursor-pointer truncate px-4 py-2.5 text-left text-sm text-gray-700 hover:bg-gray-100 rtl:text-right dark:text-gray-500 dark:hover:bg-gray-800');
if (menuItem.toggle) {
return (
<div className='flex flex-row items-center justify-between space-x-4 px-4 py-1 text-sm text-gray-700 dark:text-gray-400'>
<span>{menuItem.text}</span>
{menuItem.toggle}
</div>
);
} else if (!menuItem.text) {
return <MenuDivider />;
} else if (menuItem.action) {
return (
<button
type='button'
onClick={menuItem.action}
className={baseClassName}
>
{menuItem.text}
</button>
);
} else if (menuItem.to) {
return (
<Link
to={menuItem.to}
className={baseClassName}
>
{menuItem.text}
</Link>
);
} else {
throw menuItem;
}
};
export default ProfileDropdown;