2023-02-06 14:33:08 -08:00
|
|
|
import { useFloating } from '@floating-ui/react';
|
|
|
|
import clsx from 'clsx';
|
2022-03-21 11:09:01 -07:00
|
|
|
import throttle from 'lodash/throttle';
|
2023-02-06 14:33:08 -08:00
|
|
|
import React, { useEffect, useMemo, useState } from 'react';
|
2022-03-21 11:09:01 -07:00
|
|
|
import { defineMessages, useIntl } from 'react-intl';
|
2022-03-22 05:28:18 -07:00
|
|
|
import { Link } from 'react-router-dom';
|
2022-03-21 11:09:01 -07:00
|
|
|
|
2022-06-07 13:21:18 -07:00
|
|
|
import { fetchOwnAccounts, logOut, switchAccount } from 'soapbox/actions/auth';
|
2022-05-30 11:23:55 -07:00
|
|
|
import Account from 'soapbox/components/account';
|
2023-02-06 14:33:08 -08:00
|
|
|
import { MenuDivider } from 'soapbox/components/ui';
|
2023-02-06 15:17:12 -08:00
|
|
|
import { useAppDispatch, useAppSelector, useClickOutside, useFeatures } from 'soapbox/hooks';
|
2022-03-21 11:09:01 -07:00
|
|
|
import { makeGetAccount } from 'soapbox/selectors';
|
|
|
|
|
2022-05-04 06:09:10 -07:00
|
|
|
import ThemeToggle from './theme-toggle';
|
|
|
|
|
2022-03-21 11:09:01 -07:00
|
|
|
import type { Account as AccountEntity } from 'soapbox/types/entities';
|
|
|
|
|
|
|
|
const messages = defineMessages({
|
|
|
|
add: { id: 'profile_dropdown.add_account', defaultMessage: 'Add an existing account' },
|
2022-05-04 06:40:04 -07:00
|
|
|
theme: { id: 'profile_dropdown.theme', defaultMessage: 'Theme' },
|
2022-03-21 11:09:01 -07:00
|
|
|
logout: { id: 'profile_dropdown.logout', defaultMessage: 'Log out @{acct}' },
|
|
|
|
});
|
|
|
|
|
|
|
|
interface IProfileDropdown {
|
|
|
|
account: AccountEntity
|
2023-01-10 15:03:15 -08:00
|
|
|
children: React.ReactNode
|
2022-03-21 11:09:01 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
type IMenuItem = {
|
2022-05-04 06:09:10 -07:00
|
|
|
text: string | React.ReactElement | null
|
|
|
|
to?: string
|
|
|
|
toggle?: JSX.Element
|
|
|
|
icon?: string
|
2022-03-21 11:09:01 -07:00
|
|
|
action?: (event: React.MouseEvent) => void
|
|
|
|
}
|
|
|
|
|
2022-04-01 17:35:57 -07:00
|
|
|
const getAccount = makeGetAccount();
|
2022-03-21 11:09:01 -07:00
|
|
|
|
|
|
|
const ProfileDropdown: React.FC<IProfileDropdown> = ({ account, children }) => {
|
2023-01-09 14:13:12 -08:00
|
|
|
const dispatch = useAppDispatch();
|
2022-05-04 06:09:10 -07:00
|
|
|
const features = useFeatures();
|
2022-03-21 11:09:01 -07:00
|
|
|
const intl = useIntl();
|
|
|
|
|
2023-02-06 14:33:08 -08:00
|
|
|
const [visible, setVisible] = useState(false);
|
2023-02-06 14:53:56 -08:00
|
|
|
const { x, y, strategy, refs } = useFloating<HTMLButtonElement>({ placement: 'bottom-end' });
|
2022-12-25 15:31:07 -08:00
|
|
|
const authUsers = useAppSelector((state) => state.auth.users);
|
|
|
|
const otherAccounts = useAppSelector((state) => authUsers.map((authUser: any) => getAccount(state, authUser.id)!));
|
2022-03-21 11:09:01 -07:00
|
|
|
|
2022-03-22 08:41:44 -07:00
|
|
|
const handleLogOut = () => {
|
2022-06-10 10:56:22 -07:00
|
|
|
dispatch(logOut());
|
2022-03-21 11:09:01 -07:00
|
|
|
};
|
|
|
|
|
|
|
|
const handleSwitchAccount = (account: AccountEntity) => {
|
2022-03-22 08:41:44 -07:00
|
|
|
return () => {
|
2022-03-21 11:09:01 -07:00
|
|
|
dispatch(switchAccount(account.id));
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
const fetchOwnAccountThrottled = throttle(() => {
|
|
|
|
dispatch(fetchOwnAccounts());
|
|
|
|
}, 2000);
|
|
|
|
|
|
|
|
const renderAccount = (account: AccountEntity) => {
|
|
|
|
return (
|
2022-07-01 13:07:01 -07:00
|
|
|
<Account account={account} showProfileHoverCard={false} withLinkToProfile={false} hideActions />
|
2022-03-21 11:09:01 -07:00
|
|
|
);
|
|
|
|
};
|
|
|
|
|
2023-02-06 14:33:08 -08:00
|
|
|
const menu: IMenuItem[] = useMemo(() => {
|
2022-03-21 11:09:01 -07:00
|
|
|
const menu: IMenuItem[] = [];
|
|
|
|
|
|
|
|
menu.push({ text: renderAccount(account), to: `/@${account.acct}` });
|
|
|
|
|
|
|
|
otherAccounts.forEach((otherAccount: AccountEntity) => {
|
|
|
|
if (otherAccount && otherAccount.id !== account.id) {
|
|
|
|
menu.push({
|
|
|
|
text: renderAccount(otherAccount),
|
|
|
|
action: handleSwitchAccount(otherAccount),
|
|
|
|
});
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2022-05-04 09:05:55 -07:00
|
|
|
menu.push({ text: null });
|
|
|
|
menu.push({ text: intl.formatMessage(messages.theme), toggle: <ThemeToggle /> });
|
2022-03-21 11:09:01 -07:00
|
|
|
menu.push({ text: null });
|
|
|
|
|
2022-04-25 16:37:20 -07:00
|
|
|
menu.push({
|
|
|
|
text: intl.formatMessage(messages.add),
|
2022-06-29 11:32:59 -07:00
|
|
|
to: '/login/add',
|
2022-07-09 09:20:02 -07:00
|
|
|
icon: require('@tabler/icons/plus.svg'),
|
2022-04-25 16:37:20 -07:00
|
|
|
});
|
2022-03-21 11:09:01 -07:00
|
|
|
|
|
|
|
menu.push({
|
|
|
|
text: intl.formatMessage(messages.logout, { acct: account.acct }),
|
2022-04-19 12:37:48 -07:00
|
|
|
to: '/logout',
|
2022-03-21 11:09:01 -07:00
|
|
|
action: handleLogOut,
|
2022-07-09 09:20:02 -07:00
|
|
|
icon: require('@tabler/icons/logout.svg'),
|
2022-03-21 11:09:01 -07:00
|
|
|
});
|
|
|
|
|
|
|
|
return menu;
|
2022-05-04 06:09:10 -07:00
|
|
|
}, [account, authUsers, features]);
|
2022-03-21 11:09:01 -07:00
|
|
|
|
2023-02-06 14:33:08 -08:00
|
|
|
const toggleVisible = () => setVisible(!visible);
|
|
|
|
|
|
|
|
useEffect(() => {
|
2022-03-21 11:09:01 -07:00
|
|
|
fetchOwnAccountThrottled();
|
|
|
|
}, [account, authUsers]);
|
|
|
|
|
2023-02-06 15:17:12 -08:00
|
|
|
useClickOutside(refs, () => {
|
|
|
|
setVisible(false);
|
|
|
|
});
|
2023-02-06 14:53:56 -08:00
|
|
|
|
2022-03-21 11:09:01 -07:00
|
|
|
return (
|
2023-02-06 14:33:08 -08:00
|
|
|
<>
|
|
|
|
<button type='button' ref={refs.setReference} onClick={toggleVisible}>
|
2022-03-21 11:09:01 -07:00
|
|
|
{children}
|
2023-02-06 14:33:08 -08:00
|
|
|
</button>
|
|
|
|
|
|
|
|
{visible && (
|
|
|
|
<div
|
|
|
|
ref={refs.setFloating}
|
2023-02-07 10:02:37 -08:00
|
|
|
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'
|
2023-02-06 14:33:08 -08:00
|
|
|
style={{
|
|
|
|
position: strategy,
|
|
|
|
top: y ?? 0,
|
|
|
|
left: x ?? 0,
|
|
|
|
width: 'max-content',
|
|
|
|
}}
|
|
|
|
>
|
|
|
|
{menu.map((menuItem, i) => (
|
|
|
|
<MenuItem key={i} menuItem={menuItem} />
|
|
|
|
))}
|
|
|
|
</div>
|
|
|
|
)}
|
|
|
|
</>
|
2022-03-21 11:09:01 -07:00
|
|
|
);
|
|
|
|
};
|
|
|
|
|
2023-02-06 14:33:08 -08:00
|
|
|
interface MenuItemProps {
|
|
|
|
className?: string
|
|
|
|
menuItem: IMenuItem
|
|
|
|
}
|
|
|
|
|
|
|
|
const MenuItem: React.FC<MenuItemProps> = ({ className, menuItem }) => {
|
2023-02-06 15:00:25 -08:00
|
|
|
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');
|
2023-02-06 14:33:08 -08:00
|
|
|
|
|
|
|
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}
|
2023-02-06 15:00:25 -08:00
|
|
|
className={baseClassName}
|
2023-02-06 14:33:08 -08:00
|
|
|
>
|
|
|
|
{menuItem.text}
|
|
|
|
</Link>
|
|
|
|
);
|
|
|
|
} else {
|
|
|
|
throw menuItem;
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2022-03-21 11:09:01 -07:00
|
|
|
export default ProfileDropdown;
|