pl-fe: Replace some redux stores with zustand

Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
This commit is contained in:
marcin mikołajczak 2024-09-29 00:26:51 +02:00
parent b801acffe9
commit afdaa61a06
22 changed files with 167 additions and 365 deletions

View file

@ -1,27 +0,0 @@
const PROFILE_HOVER_CARD_OPEN = 'PROFILE_HOVER_CARD_OPEN';
const PROFILE_HOVER_CARD_UPDATE = 'PROFILE_HOVER_CARD_UPDATE';
const PROFILE_HOVER_CARD_CLOSE = 'PROFILE_HOVER_CARD_CLOSE';
const openProfileHoverCard = (ref: React.MutableRefObject<HTMLDivElement>, accountId: string) => ({
type: PROFILE_HOVER_CARD_OPEN,
ref,
accountId,
});
const updateProfileHoverCard = () => ({
type: PROFILE_HOVER_CARD_UPDATE,
});
const closeProfileHoverCard = (force = false) => ({
type: PROFILE_HOVER_CARD_CLOSE,
force,
});
export {
PROFILE_HOVER_CARD_OPEN,
PROFILE_HOVER_CARD_UPDATE,
PROFILE_HOVER_CARD_CLOSE,
openProfileHoverCard,
updateProfileHoverCard,
closeProfileHoverCard,
};

View file

@ -1,17 +0,0 @@
const SIDEBAR_OPEN = 'SIDEBAR_OPEN';
const SIDEBAR_CLOSE = 'SIDEBAR_CLOSE';
const openSidebar = () => ({
type: SIDEBAR_OPEN,
});
const closeSidebar = () => ({
type: SIDEBAR_CLOSE,
});
export {
SIDEBAR_OPEN,
SIDEBAR_CLOSE,
openSidebar,
closeSidebar,
};

View file

@ -1,27 +0,0 @@
const STATUS_HOVER_CARD_OPEN = 'STATUS_HOVER_CARD_OPEN';
const STATUS_HOVER_CARD_UPDATE = 'STATUS_HOVER_CARD_UPDATE';
const STATUS_HOVER_CARD_CLOSE = 'STATUS_HOVER_CARD_CLOSE';
const openStatusHoverCard = (ref: React.MutableRefObject<HTMLDivElement>, statusId: string) => ({
type: STATUS_HOVER_CARD_OPEN,
ref,
statusId,
});
const updateStatusHoverCard = () => ({
type: STATUS_HOVER_CARD_UPDATE,
});
const closeStatusHoverCard = (force = false) => ({
type: STATUS_HOVER_CARD_CLOSE,
force,
});
export {
STATUS_HOVER_CARD_OPEN,
STATUS_HOVER_CARD_UPDATE,
STATUS_HOVER_CARD_CLOSE,
openStatusHoverCard,
updateStatusHoverCard,
closeStatusHoverCard,
};

View file

@ -5,7 +5,7 @@ import React, { useEffect, useMemo, useRef, useState } from 'react';
import ReactSwipeableViews from 'react-swipeable-views'; import ReactSwipeableViews from 'react-swipeable-views';
import { userTouching } from 'pl-fe/is-mobile'; import { userTouching } from 'pl-fe/is-mobile';
import { useDropdownMenuStore, useModalsStore } from 'pl-fe/stores'; import { useUiStore, useModalsStore } from 'pl-fe/stores';
import { HStack, IconButton, Portal } from '../ui'; import { HStack, IconButton, Portal } from '../ui';
@ -187,7 +187,7 @@ const DropdownMenu = (props: IDropdownMenu) => {
title = 'Menu', title = 'Menu',
} = props; } = props;
const { openDropdownMenu, closeDropdownMenu } = useDropdownMenuStore(); const { openDropdownMenu, closeDropdownMenu } = useUiStore();
const { openModal, closeModal } = useModalsStore(); const { openModal, closeModal } = useModalsStore();
const [isOpen, setIsOpen] = useState<boolean>(false); const [isOpen, setIsOpen] = useState<boolean>(false);

View file

@ -3,12 +3,12 @@ import debounce from 'lodash/debounce';
import React, { useRef } from 'react'; import React, { useRef } from 'react';
import { fetchAccount } from 'pl-fe/actions/accounts'; import { fetchAccount } from 'pl-fe/actions/accounts';
import { openProfileHoverCard, closeProfileHoverCard } from 'pl-fe/actions/profile-hover-card';
import { useAppDispatch } from 'pl-fe/hooks'; import { useAppDispatch } from 'pl-fe/hooks';
import { isMobile } from 'pl-fe/is-mobile'; import { isMobile } from 'pl-fe/is-mobile';
import { useAccountHoverCardStore } from 'pl-fe/stores';
const showProfileHoverCard = debounce((dispatch, ref, accountId) => { const showProfileHoverCard = debounce((openAccountHoverCard, ref, accountId) => {
dispatch(openProfileHoverCard(ref, accountId)); openAccountHoverCard(ref, accountId);
}, 600); }, 600);
interface IHoverRefWrapper { interface IHoverRefWrapper {
@ -21,24 +21,27 @@ interface IHoverRefWrapper {
/** Makes a profile hover card appear when the wrapped element is hovered. */ /** Makes a profile hover card appear when the wrapped element is hovered. */
const HoverRefWrapper: React.FC<IHoverRefWrapper> = ({ accountId, children, inline = false, className }) => { const HoverRefWrapper: React.FC<IHoverRefWrapper> = ({ accountId, children, inline = false, className }) => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const { openAccountHoverCard, closeAccountHoverCard } = useAccountHoverCardStore();
const ref = useRef<HTMLDivElement>(null); const ref = useRef<HTMLDivElement>(null);
const Elem: keyof JSX.IntrinsicElements = inline ? 'span' : 'div'; const Elem: keyof JSX.IntrinsicElements = inline ? 'span' : 'div';
const handleMouseEnter = () => { const handleMouseEnter = () => {
if (!isMobile(window.innerWidth)) { if (!isMobile(window.innerWidth)) {
dispatch(fetchAccount(accountId)); dispatch(fetchAccount(accountId));
showProfileHoverCard(dispatch, ref, accountId); showProfileHoverCard(openAccountHoverCard, ref, accountId);
} }
}; };
const handleMouseLeave = () => { const handleMouseLeave = () => {
showProfileHoverCard.cancel(); showProfileHoverCard.cancel();
setTimeout(() => dispatch(closeProfileHoverCard()), 300); setTimeout(() => closeAccountHoverCard(), 300);
}; };
const handleClick = () => { const handleClick = () => {
showProfileHoverCard.cancel(); showProfileHoverCard.cancel();
dispatch(closeProfileHoverCard(true)); closeAccountHoverCard(true);
}; };
return ( return (

View file

@ -1,13 +1,12 @@
import clsx from 'clsx'; import clsx from 'clsx';
import debounce from 'lodash/debounce'; import debounce from 'lodash/debounce';
import React, { useRef } from 'react'; import React, { useRef } from 'react';
import { useDispatch } from 'react-redux';
import { openStatusHoverCard, closeStatusHoverCard } from 'pl-fe/actions/status-hover-card';
import { isMobile } from 'pl-fe/is-mobile'; import { isMobile } from 'pl-fe/is-mobile';
import { useStatusHoverCardStore } from 'pl-fe/stores';
const showStatusHoverCard = debounce((dispatch, ref, statusId) => { const showStatusHoverCard = debounce((openStatusHoverCard, ref, statusId) => {
dispatch(openStatusHoverCard(ref, statusId)); openStatusHoverCard(ref, statusId);
}, 300); }, 300);
interface IHoverStatusWrapper { interface IHoverStatusWrapper {
@ -19,24 +18,25 @@ interface IHoverStatusWrapper {
/** Makes a status hover card appear when the wrapped element is hovered. */ /** Makes a status hover card appear when the wrapped element is hovered. */
const HoverStatusWrapper: React.FC<IHoverStatusWrapper> = ({ statusId, children, inline = false, className }) => { const HoverStatusWrapper: React.FC<IHoverStatusWrapper> = ({ statusId, children, inline = false, className }) => {
const dispatch = useDispatch(); const { openStatusHoverCard, closeStatusHoverCard } = useStatusHoverCardStore();
const ref = useRef<HTMLDivElement>(null); const ref = useRef<HTMLDivElement>(null);
const Elem: keyof JSX.IntrinsicElements = inline ? 'span' : 'div'; const Elem: keyof JSX.IntrinsicElements = inline ? 'span' : 'div';
const handleMouseEnter = () => { const handleMouseEnter = () => {
if (!isMobile(window.innerWidth)) { if (!isMobile(window.innerWidth)) {
showStatusHoverCard(dispatch, ref, statusId); showStatusHoverCard(openStatusHoverCard, ref, statusId);
} }
}; };
const handleMouseLeave = () => { const handleMouseLeave = () => {
showStatusHoverCard.cancel(); showStatusHoverCard.cancel();
setTimeout(() => dispatch(closeStatusHoverCard()), 200); setTimeout(() => closeStatusHoverCard(), 200);
}; };
const handleClick = () => { const handleClick = () => {
showStatusHoverCard.cancel(); showStatusHoverCard.cancel();
dispatch(closeStatusHoverCard(true)); closeStatusHoverCard(true);
}; };
return ( return (

View file

@ -5,12 +5,12 @@ import { useIntl, FormattedMessage } from 'react-intl';
import { useHistory } from 'react-router-dom'; import { useHistory } from 'react-router-dom';
import { fetchRelationships } from 'pl-fe/actions/accounts'; import { fetchRelationships } from 'pl-fe/actions/accounts';
import { closeProfileHoverCard, updateProfileHoverCard } from 'pl-fe/actions/profile-hover-card';
import { useAccount } from 'pl-fe/api/hooks'; import { useAccount } from 'pl-fe/api/hooks';
import Badge from 'pl-fe/components/badge'; import Badge from 'pl-fe/components/badge';
import ActionButton from 'pl-fe/features/ui/components/action-button'; import ActionButton from 'pl-fe/features/ui/components/action-button';
import { UserPanel } from 'pl-fe/features/ui/util/async-components'; import { UserPanel } from 'pl-fe/features/ui/util/async-components';
import { useAppSelector, useAppDispatch } from 'pl-fe/hooks'; import { useAppSelector, useAppDispatch } from 'pl-fe/hooks';
import { useAccountHoverCardStore } from 'pl-fe/stores';
import { showProfileHoverCard } from './hover-ref-wrapper'; import { showProfileHoverCard } from './hover-ref-wrapper';
import { dateFormatOptions } from './relative-timestamp'; import { dateFormatOptions } from './relative-timestamp';
@ -18,7 +18,6 @@ import Scrobble from './scrobble';
import { Card, CardBody, HStack, Icon, Stack, Text } from './ui'; import { Card, CardBody, HStack, Icon, Stack, Text } from './ui';
import type { Account } from 'pl-fe/normalizers'; import type { Account } from 'pl-fe/normalizers';
import type { AppDispatch } from 'pl-fe/store';
const getBadges = ( const getBadges = (
account?: Pick<Account, 'is_admin' | 'is_moderator'>, account?: Pick<Account, 'is_admin' | 'is_moderator'>,
@ -34,14 +33,6 @@ const getBadges = (
return badges; return badges;
}; };
const handleMouseEnter = (dispatch: AppDispatch): React.MouseEventHandler => () => {
dispatch(updateProfileHoverCard());
};
const handleMouseLeave = (dispatch: AppDispatch): React.MouseEventHandler => () => {
dispatch(closeProfileHoverCard(true));
};
interface IProfileHoverCard { interface IProfileHoverCard {
visible?: boolean; visible?: boolean;
} }
@ -52,10 +43,10 @@ const ProfileHoverCard: React.FC<IProfileHoverCard> = ({ visible = true }) => {
const history = useHistory(); const history = useHistory();
const intl = useIntl(); const intl = useIntl();
const { accountId, ref, updateAccountHoverCard, closeAccountHoverCard } = useAccountHoverCardStore();
const me = useAppSelector(state => state.me); const me = useAppSelector(state => state.me);
const accountId: string | undefined = useAppSelector(state => state.profile_hover_card.accountId || undefined); const { account } = useAccount(accountId || undefined, { withRelationship: true, withScrobble: true });
const { account } = useAccount(accountId, { withRelationship: true, withScrobble: true });
const targetRef = useAppSelector(state => state.profile_hover_card.ref?.current);
const badges = getBadges(account); const badges = getBadges(account);
useEffect(() => { useEffect(() => {
@ -65,7 +56,7 @@ const ProfileHoverCard: React.FC<IProfileHoverCard> = ({ visible = true }) => {
useEffect(() => { useEffect(() => {
const unlisten = history.listen(() => { const unlisten = history.listen(() => {
showProfileHoverCard.cancel(); showProfileHoverCard.cancel();
dispatch(closeProfileHoverCard()); closeAccountHoverCard();
}); });
return () => { return () => {
@ -76,7 +67,7 @@ const ProfileHoverCard: React.FC<IProfileHoverCard> = ({ visible = true }) => {
const { x, y, strategy, refs, context, placement } = useFloating({ const { x, y, strategy, refs, context, placement } = useFloating({
open: !!account, open: !!account,
elements: { elements: {
reference: targetRef, reference: ref?.current,
}, },
middleware: [ middleware: [
shift({ shift({
@ -116,8 +107,8 @@ const ProfileHoverCard: React.FC<IProfileHoverCard> = ({ visible = true }) => {
left: x ?? 0, left: x ?? 0,
...styles, ...styles,
}} }}
onMouseEnter={handleMouseEnter(dispatch)} onMouseEnter={() => updateAccountHoverCard()}
onMouseLeave={handleMouseLeave(dispatch)} onMouseLeave={() => closeAccountHoverCard()}
> >
<Card variant='rounded' className='relative isolate overflow-hidden black:rounded-xl black:border black:border-gray-800'> <Card variant='rounded' className='relative isolate overflow-hidden black:rounded-xl black:border black:border-gray-800'>
<CardBody> <CardBody>

View file

@ -6,13 +6,13 @@ import { Link, NavLink } from 'react-router-dom';
import { fetchOwnAccounts, logOut, switchAccount } from 'pl-fe/actions/auth'; import { fetchOwnAccounts, logOut, switchAccount } from 'pl-fe/actions/auth';
import { getSettings } from 'pl-fe/actions/settings'; import { getSettings } from 'pl-fe/actions/settings';
import { closeSidebar } from 'pl-fe/actions/sidebar';
import { useAccount } from 'pl-fe/api/hooks'; import { useAccount } from 'pl-fe/api/hooks';
import Account from 'pl-fe/components/account'; import Account from 'pl-fe/components/account';
import { Stack, Divider, HStack, Icon, Text } from 'pl-fe/components/ui'; import { Stack, Divider, HStack, Icon, Text } from 'pl-fe/components/ui';
import ProfileStats from 'pl-fe/features/ui/components/profile-stats'; import ProfileStats from 'pl-fe/features/ui/components/profile-stats';
import { useAppDispatch, useAppSelector, useFeatures, useInstance, useRegistrationStatus } from 'pl-fe/hooks'; import { useAppDispatch, useAppSelector, useFeatures, useInstance, useRegistrationStatus } from 'pl-fe/hooks';
import { makeGetOtherAccounts } from 'pl-fe/selectors'; import { makeGetOtherAccounts } from 'pl-fe/selectors';
import { useUiStore } from 'pl-fe/stores';
import sourceCode from 'pl-fe/utils/code'; import sourceCode from 'pl-fe/utils/code';
import type { List as ImmutableList } from 'immutable'; import type { List as ImmutableList } from 'immutable';
@ -79,18 +79,19 @@ const SidebarMenu: React.FC = (): JSX.Element | null => {
const intl = useIntl(); const intl = useIntl();
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const { isSidebarOpen, closeSidebar } = useUiStore();
const getOtherAccounts = useCallback(makeGetOtherAccounts(), []); const getOtherAccounts = useCallback(makeGetOtherAccounts(), []);
const features = useFeatures(); const features = useFeatures();
const me = useAppSelector((state) => state.me); const me = useAppSelector((state) => state.me);
const { account } = useAccount(me || undefined); const { account } = useAccount(me || undefined);
const otherAccounts: ImmutableList<AccountEntity> = useAppSelector((state) => getOtherAccounts(state)); const otherAccounts: ImmutableList<AccountEntity> = useAppSelector((state) => getOtherAccounts(state));
const sidebarOpen = useAppSelector((state) => state.sidebar.sidebarOpen);
const settings = useAppSelector((state) => getSettings(state)); const settings = useAppSelector((state) => getSettings(state));
const followRequestsCount = useAppSelector((state) => state.user_lists.follow_requests.items.count()); const followRequestsCount = useAppSelector((state) => state.user_lists.follow_requests.items.count());
const scheduledStatusCount = useAppSelector((state) => state.scheduled_statuses.size); const scheduledStatusCount = useAppSelector((state) => state.scheduled_statuses.size);
const draftCount = useAppSelector((state) => state.draft_statuses.size); const draftCount = useAppSelector((state) => state.draft_statuses.size);
// const dashboardCount = useAppSelector((state) => state.admin.openReports.count() + state.admin.awaitingApproval.count()); // const dashboardCount = useAppSelector((state) => state.admin.openReports.count() + state.admin.awaitingApproval.count());
const [sidebarVisible, setSidebarVisible] = useState(sidebarOpen); const [sidebarVisible, setSidebarVisible] = useState(isSidebarOpen);
const touchStart = useRef(0); const touchStart = useRef(0);
const touchEnd = useRef<number | null>(null); const touchEnd = useRef<number | null>(null);
const { isOpen } = useRegistrationStatus(); const { isOpen } = useRegistrationStatus();
@ -102,11 +103,9 @@ const SidebarMenu: React.FC = (): JSX.Element | null => {
const [switcher, setSwitcher] = React.useState(false); const [switcher, setSwitcher] = React.useState(false);
const onClose = () => dispatch(closeSidebar());
const handleClose = () => { const handleClose = () => {
setSwitcher(false); setSwitcher(false);
onClose(); closeSidebar();
}; };
const handleSwitchAccount = (account: AccountEntity): React.MouseEventHandler => (e) => { const handleSwitchAccount = (account: AccountEntity): React.MouseEventHandler => (e) => {
@ -157,17 +156,17 @@ const SidebarMenu: React.FC = (): JSX.Element | null => {
}, []); }, []);
useEffect(() => { useEffect(() => {
if (sidebarOpen) containerRef.current?.querySelector('a')?.focus(); if (isSidebarOpen) containerRef.current?.querySelector('a')?.focus();
setTimeout(() => setSidebarVisible(sidebarOpen), sidebarOpen ? 0 : 150); setTimeout(() => setSidebarVisible(isSidebarOpen), isSidebarOpen ? 0 : 150);
}, [sidebarOpen]); }, [isSidebarOpen]);
return ( return (
<div <div
aria-expanded={sidebarOpen} aria-expanded={isSidebarOpen}
className={ className={
clsx({ clsx({
'z-[1000]': sidebarOpen || sidebarVisible, 'z-[1000]': isSidebarOpen || sidebarVisible,
hidden: !(sidebarOpen || sidebarVisible), hidden: !(isSidebarOpen || sidebarVisible),
}) })
} }
ref={containerRef} ref={containerRef}
@ -178,8 +177,8 @@ const SidebarMenu: React.FC = (): JSX.Element | null => {
> >
<div <div
className={clsx('fixed inset-0 cursor-default bg-gray-500 black:bg-gray-900 no-reduce-motion:transition-opacity dark:bg-gray-700', { className={clsx('fixed inset-0 cursor-default bg-gray-500 black:bg-gray-900 no-reduce-motion:transition-opacity dark:bg-gray-700', {
'no-reduce-motion:opacity-0': !(sidebarVisible && sidebarOpen), 'no-reduce-motion:opacity-0': !(sidebarVisible && isSidebarOpen),
'opacity-40': (sidebarVisible && sidebarOpen), 'opacity-40': (sidebarVisible && isSidebarOpen),
})} })}
role='button' role='button'
onClick={handleClose} onClick={handleClose}
@ -188,8 +187,8 @@ const SidebarMenu: React.FC = (): JSX.Element | null => {
<div <div
className={ className={
clsx('fixed bottom-[60px] left-2 z-[1000] flex max-h-[calc(100dvh-68px)] w-full max-w-xs flex-1 origin-bottom-left flex-col overflow-hidden rounded-xl bg-white shadow-lg ease-in-out black:bg-black no-reduce-motion:transition-transform dark:border dark:border-gray-800 dark:bg-primary-900 dark:shadow-none rtl:right-2 rtl:origin-bottom-right', { clsx('fixed bottom-[60px] left-2 z-[1000] flex max-h-[calc(100dvh-68px)] w-full max-w-xs flex-1 origin-bottom-left flex-col overflow-hidden rounded-xl bg-white shadow-lg ease-in-out black:bg-black no-reduce-motion:transition-transform dark:border dark:border-gray-800 dark:bg-primary-900 dark:shadow-none rtl:right-2 rtl:origin-bottom-right', {
'scale-100': sidebarVisible && sidebarOpen, 'scale-100': sidebarVisible && isSidebarOpen,
'no-reduce-motion:scale-0': !(sidebarVisible && sidebarOpen), 'no-reduce-motion:scale-0': !(sidebarVisible && isSidebarOpen),
}) })
} }
> >
@ -197,7 +196,7 @@ const SidebarMenu: React.FC = (): JSX.Element | null => {
<div className='p-4'> <div className='p-4'>
{account ? ( {account ? (
<Stack space={4}> <Stack space={4}>
<Link to={`/@${account.acct}`} onClick={onClose}> <Link to={`/@${account.acct}`} onClick={closeSidebar}>
<Account account={account} showProfileHoverCard={false} withLinkToProfile={false} /> <Account account={account} showProfileHoverCard={false} withLinkToProfile={false} />
</Link> </Link>
@ -213,7 +212,7 @@ const SidebarMenu: React.FC = (): JSX.Element | null => {
to={`/@${account.acct}`} to={`/@${account.acct}`}
icon={require('@tabler/icons/outline/user.svg')} icon={require('@tabler/icons/outline/user.svg')}
text={intl.formatMessage(messages.profile)} text={intl.formatMessage(messages.profile)}
onClick={onClose} onClick={closeSidebar}
/> />
{(account.locked || followRequestsCount > 0) && ( {(account.locked || followRequestsCount > 0) && (
@ -221,7 +220,7 @@ const SidebarMenu: React.FC = (): JSX.Element | null => {
to='/follow_requests' to='/follow_requests'
icon={require('@tabler/icons/outline/user-plus.svg')} icon={require('@tabler/icons/outline/user-plus.svg')}
text={intl.formatMessage(messages.followRequests)} text={intl.formatMessage(messages.followRequests)}
onClick={onClose} onClick={closeSidebar}
/> />
)} )}
@ -230,7 +229,7 @@ const SidebarMenu: React.FC = (): JSX.Element | null => {
to='/conversations' to='/conversations'
icon={require('@tabler/icons/outline/mail.svg')} icon={require('@tabler/icons/outline/mail.svg')}
text={intl.formatMessage(messages.conversations)} text={intl.formatMessage(messages.conversations)}
onClick={onClose} onClick={closeSidebar}
/> />
)} )}
@ -239,7 +238,7 @@ const SidebarMenu: React.FC = (): JSX.Element | null => {
to='/bookmarks' to='/bookmarks'
icon={require('@tabler/icons/outline/bookmark.svg')} icon={require('@tabler/icons/outline/bookmark.svg')}
text={intl.formatMessage(messages.bookmarks)} text={intl.formatMessage(messages.bookmarks)}
onClick={onClose} onClick={closeSidebar}
/> />
)} )}
@ -248,7 +247,7 @@ const SidebarMenu: React.FC = (): JSX.Element | null => {
to='/groups' to='/groups'
icon={require('@tabler/icons/outline/circles.svg')} icon={require('@tabler/icons/outline/circles.svg')}
text={intl.formatMessage(messages.groups)} text={intl.formatMessage(messages.groups)}
onClick={onClose} onClick={closeSidebar}
/> />
)} )}
@ -257,7 +256,7 @@ const SidebarMenu: React.FC = (): JSX.Element | null => {
to='/lists' to='/lists'
icon={require('@tabler/icons/outline/list.svg')} icon={require('@tabler/icons/outline/list.svg')}
text={intl.formatMessage(messages.lists)} text={intl.formatMessage(messages.lists)}
onClick={onClose} onClick={closeSidebar}
/> />
)} )}
@ -266,7 +265,7 @@ const SidebarMenu: React.FC = (): JSX.Element | null => {
to='/events' to='/events'
icon={require('@tabler/icons/outline/calendar-event.svg')} icon={require('@tabler/icons/outline/calendar-event.svg')}
text={intl.formatMessage(messages.events)} text={intl.formatMessage(messages.events)}
onClick={onClose} onClick={closeSidebar}
/> />
)} )}
@ -275,7 +274,7 @@ const SidebarMenu: React.FC = (): JSX.Element | null => {
to='/directory' to='/directory'
icon={require('@tabler/icons/outline/address-book.svg')} icon={require('@tabler/icons/outline/address-book.svg')}
text={intl.formatMessage(messages.profileDirectory)} text={intl.formatMessage(messages.profileDirectory)}
onClick={onClose} onClick={closeSidebar}
/> />
)} )}
@ -284,7 +283,7 @@ const SidebarMenu: React.FC = (): JSX.Element | null => {
to='/scheduled_statuses' to='/scheduled_statuses'
icon={require('@tabler/icons/outline/calendar-stats.svg')} icon={require('@tabler/icons/outline/calendar-stats.svg')}
text={intl.formatMessage(messages.scheduledStatuses)} text={intl.formatMessage(messages.scheduledStatuses)}
onClick={onClose} onClick={closeSidebar}
/> />
)} )}
@ -293,7 +292,7 @@ const SidebarMenu: React.FC = (): JSX.Element | null => {
to='/draft_statuses' to='/draft_statuses'
icon={require('@tabler/icons/outline/notes.svg')} icon={require('@tabler/icons/outline/notes.svg')}
text={intl.formatMessage(messages.drafts)} text={intl.formatMessage(messages.drafts)}
onClick={onClose} onClick={closeSidebar}
/> />
)} )}
@ -304,7 +303,7 @@ const SidebarMenu: React.FC = (): JSX.Element | null => {
to='/timeline/local' to='/timeline/local'
icon={features.federating ? require('@tabler/icons/outline/affiliate.svg') : require('@tabler/icons/outline/world.svg')} icon={features.federating ? require('@tabler/icons/outline/affiliate.svg') : require('@tabler/icons/outline/world.svg')}
text={features.federating ? <FormattedMessage id='tabs_bar.local' defaultMessage='Local' /> : <FormattedMessage id='tabs_bar.all' defaultMessage='All' />} text={features.federating ? <FormattedMessage id='tabs_bar.local' defaultMessage='Local' /> : <FormattedMessage id='tabs_bar.all' defaultMessage='All' />}
onClick={onClose} onClick={closeSidebar}
/> />
{features.bubbleTimeline && ( {features.bubbleTimeline && (
@ -312,7 +311,7 @@ const SidebarMenu: React.FC = (): JSX.Element | null => {
to='/timeline/bubble' to='/timeline/bubble'
icon={require('@tabler/icons/outline/chart-bubble.svg')} icon={require('@tabler/icons/outline/chart-bubble.svg')}
text={<FormattedMessage id='tabs_bar.bubble' defaultMessage='Bubble' />} text={<FormattedMessage id='tabs_bar.bubble' defaultMessage='Bubble' />}
onClick={onClose} onClick={closeSidebar}
/> />
)} )}
@ -321,7 +320,7 @@ const SidebarMenu: React.FC = (): JSX.Element | null => {
to='/timeline/fediverse' to='/timeline/fediverse'
icon={require('@tabler/icons/outline/topology-star-ring-3.svg')} icon={require('@tabler/icons/outline/topology-star-ring-3.svg')}
text={<FormattedMessage id='tabs_bar.fediverse' defaultMessage='Fediverse' />} text={<FormattedMessage id='tabs_bar.fediverse' defaultMessage='Fediverse' />}
onClick={onClose} onClick={closeSidebar}
/> />
)} )}
</>} </>}
@ -332,7 +331,7 @@ const SidebarMenu: React.FC = (): JSX.Element | null => {
to='/settings/preferences' to='/settings/preferences'
icon={require('@tabler/icons/outline/settings.svg')} icon={require('@tabler/icons/outline/settings.svg')}
text={intl.formatMessage(messages.preferences)} text={intl.formatMessage(messages.preferences)}
onClick={onClose} onClick={closeSidebar}
/> />
{features.followedHashtagsList && ( {features.followedHashtagsList && (
@ -340,7 +339,7 @@ const SidebarMenu: React.FC = (): JSX.Element | null => {
to='/followed_tags' to='/followed_tags'
icon={require('@tabler/icons/outline/hash.svg')} icon={require('@tabler/icons/outline/hash.svg')}
text={intl.formatMessage(messages.followedTags)} text={intl.formatMessage(messages.followedTags)}
onClick={onClose} onClick={closeSidebar}
/> />
)} )}
@ -349,7 +348,7 @@ const SidebarMenu: React.FC = (): JSX.Element | null => {
to='/developers' to='/developers'
icon={require('@tabler/icons/outline/code.svg')} icon={require('@tabler/icons/outline/code.svg')}
text={intl.formatMessage(messages.developers)} text={intl.formatMessage(messages.developers)}
onClick={onClose} onClick={closeSidebar}
/> />
)} )}
@ -358,7 +357,7 @@ const SidebarMenu: React.FC = (): JSX.Element | null => {
to='/admin' to='/admin'
icon={require('@tabler/icons/outline/dashboard.svg')} icon={require('@tabler/icons/outline/dashboard.svg')}
text={intl.formatMessage(messages.dashboard)} text={intl.formatMessage(messages.dashboard)}
onClick={onClose} onClick={closeSidebar}
// count={dashboardCount} WIP // count={dashboardCount} WIP
/> />
)} )}
@ -378,7 +377,7 @@ const SidebarMenu: React.FC = (): JSX.Element | null => {
href={sourceCode.url} href={sourceCode.url}
icon={require('@tabler/icons/outline/code.svg')} icon={require('@tabler/icons/outline/code.svg')}
text={intl.formatMessage(messages.sourceCode)} text={intl.formatMessage(messages.sourceCode)}
onClick={onClose} onClick={closeSidebar}
/> />
<Divider /> <Divider />
@ -419,7 +418,7 @@ const SidebarMenu: React.FC = (): JSX.Element | null => {
to='/timeline/local' to='/timeline/local'
icon={features.federating ? require('@tabler/icons/outline/affiliate.svg') : require('@tabler/icons/outline/world.svg')} icon={features.federating ? require('@tabler/icons/outline/affiliate.svg') : require('@tabler/icons/outline/world.svg')}
text={features.federating ? <FormattedMessage id='tabs_bar.local' defaultMessage='Local' /> : <FormattedMessage id='tabs_bar.all' defaultMessage='All' />} text={features.federating ? <FormattedMessage id='tabs_bar.local' defaultMessage='Local' /> : <FormattedMessage id='tabs_bar.all' defaultMessage='All' />}
onClick={onClose} onClick={closeSidebar}
/> />
{features.bubbleTimeline && !restrictUnauth.timelines.bubble && ( {features.bubbleTimeline && !restrictUnauth.timelines.bubble && (
@ -427,7 +426,7 @@ const SidebarMenu: React.FC = (): JSX.Element | null => {
to='/timeline/bubble' to='/timeline/bubble'
icon={require('@tabler/icons/outline/chart-bubble.svg')} icon={require('@tabler/icons/outline/chart-bubble.svg')}
text={<FormattedMessage id='tabs_bar.bubble' defaultMessage='Bubble' />} text={<FormattedMessage id='tabs_bar.bubble' defaultMessage='Bubble' />}
onClick={onClose} onClick={closeSidebar}
/> />
)} )}
@ -436,7 +435,7 @@ const SidebarMenu: React.FC = (): JSX.Element | null => {
to='/timeline/fediverse' to='/timeline/fediverse'
icon={require('@tabler/icons/outline/topology-star-ring-3.svg')} icon={require('@tabler/icons/outline/topology-star-ring-3.svg')}
text={<FormattedMessage id='tabs_bar.fediverse' defaultMessage='Fediverse' />} text={<FormattedMessage id='tabs_bar.fediverse' defaultMessage='Fediverse' />}
onClick={onClose} onClick={closeSidebar}
/> />
)} )}
@ -447,7 +446,7 @@ const SidebarMenu: React.FC = (): JSX.Element | null => {
to='/login' to='/login'
icon={require('@tabler/icons/outline/login.svg')} icon={require('@tabler/icons/outline/login.svg')}
text={intl.formatMessage(messages.login)} text={intl.formatMessage(messages.login)}
onClick={onClose} onClick={closeSidebar}
/> />
{isOpen && ( {isOpen && (
@ -455,7 +454,7 @@ const SidebarMenu: React.FC = (): JSX.Element | null => {
to='/signup' to='/signup'
icon={require('@tabler/icons/outline/user-plus.svg')} icon={require('@tabler/icons/outline/user-plus.svg')}
text={intl.formatMessage(messages.register)} text={intl.formatMessage(messages.register)}
onClick={onClose} onClick={closeSidebar}
/> />
)} )}
@ -465,7 +464,7 @@ const SidebarMenu: React.FC = (): JSX.Element | null => {
href={sourceCode.url} href={sourceCode.url}
icon={require('@tabler/icons/outline/code.svg')} icon={require('@tabler/icons/outline/code.svg')}
text={intl.formatMessage(messages.sourceCode)} text={intl.formatMessage(messages.sourceCode)}
onClick={onClose} onClick={closeSidebar}
/> />
</Stack> </Stack>
)} )}

View file

@ -4,10 +4,10 @@ import React, { useEffect, useCallback } from 'react';
import { useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
import { useHistory } from 'react-router-dom'; import { useHistory } from 'react-router-dom';
import { closeStatusHoverCard, updateStatusHoverCard } from 'pl-fe/actions/status-hover-card';
import { fetchStatus } from 'pl-fe/actions/statuses'; import { fetchStatus } from 'pl-fe/actions/statuses';
import StatusContainer from 'pl-fe/containers/status-container'; import StatusContainer from 'pl-fe/containers/status-container';
import { useAppSelector, useAppDispatch } from 'pl-fe/hooks'; import { useAppSelector, useAppDispatch } from 'pl-fe/hooks';
import { useStatusHoverCardStore } from 'pl-fe/stores';
import { showStatusHoverCard } from './hover-status-wrapper'; import { showStatusHoverCard } from './hover-status-wrapper';
import { Card, CardBody } from './ui'; import { Card, CardBody } from './ui';
@ -22,9 +22,9 @@ const StatusHoverCard: React.FC<IStatusHoverCard> = ({ visible = true }) => {
const intl = useIntl(); const intl = useIntl();
const history = useHistory(); const history = useHistory();
const statusId: string | undefined = useAppSelector(state => state.status_hover_card.statusId || undefined); const { statusId, ref, closeStatusHoverCard, updateStatusHoverCard } = useStatusHoverCardStore();
const status = useAppSelector(state => state.statuses.get(statusId!)); const status = useAppSelector(state => state.statuses.get(statusId!));
const targetRef = useAppSelector(state => state.status_hover_card.ref?.current);
useEffect(() => { useEffect(() => {
if (statusId && !status) { if (statusId && !status) {
@ -35,7 +35,7 @@ const StatusHoverCard: React.FC<IStatusHoverCard> = ({ visible = true }) => {
useEffect(() => { useEffect(() => {
const unlisten = history.listen(() => { const unlisten = history.listen(() => {
showStatusHoverCard.cancel(); showStatusHoverCard.cancel();
dispatch(closeStatusHoverCard()); closeStatusHoverCard();
}); });
return () => { return () => {
@ -46,7 +46,7 @@ const StatusHoverCard: React.FC<IStatusHoverCard> = ({ visible = true }) => {
const { x, y, strategy, refs, context, placement } = useFloating({ const { x, y, strategy, refs, context, placement } = useFloating({
open: !!statusId, open: !!statusId,
elements: { elements: {
reference: targetRef, reference: ref?.current,
}, },
placement: 'top', placement: 'top',
middleware: [ middleware: [
@ -69,11 +69,11 @@ const StatusHoverCard: React.FC<IStatusHoverCard> = ({ visible = true }) => {
}); });
const handleMouseEnter = useCallback((): React.MouseEventHandler => () => { const handleMouseEnter = useCallback((): React.MouseEventHandler => () => {
dispatch(updateStatusHoverCard()); updateStatusHoverCard();
}, []); }, []);
const handleMouseLeave = useCallback((): React.MouseEventHandler => () => { const handleMouseLeave = useCallback((): React.MouseEventHandler => () => {
dispatch(closeStatusHoverCard(true)); closeStatusHoverCard(true);
}, []); }, []);
if (!statusId) return null; if (!statusId) return null;

View file

@ -3,12 +3,11 @@ import { defineMessages, useIntl } from 'react-intl';
import { useRouteMatch } from 'react-router-dom'; import { useRouteMatch } from 'react-router-dom';
import { groupComposeModal } from 'pl-fe/actions/compose'; import { groupComposeModal } from 'pl-fe/actions/compose';
import { openSidebar } from 'pl-fe/actions/sidebar';
import ThumbNavigationLink from 'pl-fe/components/thumb-navigation-link'; import ThumbNavigationLink from 'pl-fe/components/thumb-navigation-link';
import { useStatContext } from 'pl-fe/contexts/stat-context'; import { useStatContext } from 'pl-fe/contexts/stat-context';
import { Entities } from 'pl-fe/entity-store/entities'; import { Entities } from 'pl-fe/entity-store/entities';
import { useAppDispatch, useAppSelector, useFeatures, useOwnAccount } from 'pl-fe/hooks'; import { useAppDispatch, useAppSelector, useFeatures, useOwnAccount } from 'pl-fe/hooks';
import { useModalsStore } from 'pl-fe/stores'; import { useModalsStore, useUiStore } from 'pl-fe/stores';
import { isStandalone } from 'pl-fe/utils/state'; import { isStandalone } from 'pl-fe/utils/state';
import { Icon } from './ui'; import { Icon } from './ui';
@ -30,14 +29,13 @@ const ThumbNavigation: React.FC = (): JSX.Element => {
const match = useRouteMatch<{ groupId: string }>('/groups/:groupId'); const match = useRouteMatch<{ groupId: string }>('/groups/:groupId');
const { openSidebar } = useUiStore();
const { openModal } = useModalsStore(); const { openModal } = useModalsStore();
const { unreadChatsCount } = useStatContext(); const { unreadChatsCount } = useStatContext();
const standalone = useAppSelector(isStandalone); const standalone = useAppSelector(isStandalone);
const notificationCount = useAppSelector((state) => state.notifications.unread); const notificationCount = useAppSelector((state) => state.notifications.unread);
const handleOpenSidebar = () => dispatch(openSidebar());
const handleOpenComposeModal = () => { const handleOpenComposeModal = () => {
if (match?.params.groupId) { if (match?.params.groupId) {
dispatch((_, getState) => { dispatch((_, getState) => {
@ -66,7 +64,7 @@ const ThumbNavigation: React.FC = (): JSX.Element => {
<div className='fixed inset-x-0 bottom-0 z-50 flex w-full overflow-x-auto border-t border-solid border-gray-200 bg-white/90 shadow-2xl backdrop-blur-md black:bg-black/80 dark:border-gray-800 dark:bg-primary-900/90 lg:hidden'> <div className='fixed inset-x-0 bottom-0 z-50 flex w-full overflow-x-auto border-t border-solid border-gray-200 bg-white/90 shadow-2xl backdrop-blur-md black:bg-black/80 dark:border-gray-800 dark:bg-primary-900/90 lg:hidden'>
<button <button
className='flex flex-1 flex-col items-center px-2 py-4 text-lg text-gray-600' className='flex flex-1 flex-col items-center px-2 py-4 text-lg text-gray-600'
onClick={handleOpenSidebar} onClick={openSidebar}
title={intl.formatMessage(messages.sidebar)} title={intl.formatMessage(messages.sidebar)}
> >
<Icon <Icon

View file

@ -34,7 +34,7 @@ import ProfileLayout from 'pl-fe/layouts/profile-layout';
import RemoteInstanceLayout from 'pl-fe/layouts/remote-instance-layout'; import RemoteInstanceLayout from 'pl-fe/layouts/remote-instance-layout';
import SearchLayout from 'pl-fe/layouts/search-layout'; import SearchLayout from 'pl-fe/layouts/search-layout';
import StatusLayout from 'pl-fe/layouts/status-layout'; import StatusLayout from 'pl-fe/layouts/status-layout';
import { useDropdownMenuStore } from 'pl-fe/stores'; import { useUiStore } from 'pl-fe/stores';
import { getVapidKey } from 'pl-fe/utils/auth'; import { getVapidKey } from 'pl-fe/utils/auth';
import { isStandalone } from 'pl-fe/utils/state'; import { isStandalone } from 'pl-fe/utils/state';
@ -352,7 +352,7 @@ const UI: React.FC<IUI> = ({ children }) => {
const features = useFeatures(); const features = useFeatures();
const vapidKey = useAppSelector(state => getVapidKey(state)); const vapidKey = useAppSelector(state => getVapidKey(state));
const { isOpen: dropdownMenuIsOpen } = useDropdownMenuStore(); const { isDropdownMenuOpen } = useUiStore();
const standalone = useAppSelector(isStandalone); const standalone = useAppSelector(isStandalone);
const { isDragging } = useDraggedFiles(node); const { isDragging } = useDraggedFiles(node);
@ -445,7 +445,7 @@ const UI: React.FC<IUI> = ({ children }) => {
if (me === null) return null; if (me === null) return null;
const style: React.CSSProperties = { const style: React.CSSProperties = {
pointerEvents: dropdownMenuIsOpen ? 'none' : undefined, pointerEvents: isDropdownMenuOpen ? 'none' : undefined,
}; };
return ( return (

View file

@ -33,14 +33,11 @@ import onboarding from './onboarding';
import pending_statuses from './pending-statuses'; import pending_statuses from './pending-statuses';
import plfe from './pl-fe'; import plfe from './pl-fe';
import polls from './polls'; import polls from './polls';
import profile_hover_card from './profile-hover-card';
import push_notifications from './push-notifications'; import push_notifications from './push-notifications';
import scheduled_statuses from './scheduled-statuses'; import scheduled_statuses from './scheduled-statuses';
import search from './search'; import search from './search';
import security from './security'; import security from './security';
import settings from './settings'; import settings from './settings';
import sidebar from './sidebar';
import status_hover_card from './status-hover-card';
import status_lists from './status-lists'; import status_lists from './status-lists';
import statuses from './statuses'; import statuses from './statuses';
import suggestions from './suggestions'; import suggestions from './suggestions';
@ -80,14 +77,11 @@ const reducers = {
pending_statuses, pending_statuses,
plfe, plfe,
polls, polls,
profile_hover_card,
push_notifications, push_notifications,
scheduled_statuses, scheduled_statuses,
search, search,
security, security,
settings, settings,
sidebar,
status_hover_card,
status_lists, status_lists,
statuses, statuses,
suggestions, suggestions,

View file

@ -1,38 +0,0 @@
import { Record as ImmutableRecord } from 'immutable';
import {
PROFILE_HOVER_CARD_OPEN,
PROFILE_HOVER_CARD_CLOSE,
PROFILE_HOVER_CARD_UPDATE,
} from 'pl-fe/actions/profile-hover-card';
import type { AnyAction } from 'redux';
const ReducerRecord = ImmutableRecord({
ref: null as React.MutableRefObject<HTMLDivElement> | null,
accountId: '',
hovered: false,
});
type State = ReturnType<typeof ReducerRecord>;
const profileHoverCard = (state: State = ReducerRecord(), action: AnyAction) => {
switch (action.type) {
case PROFILE_HOVER_CARD_OPEN:
return state.withMutations((state) => {
state.set('ref', action.ref);
state.set('accountId', action.accountId);
});
case PROFILE_HOVER_CARD_UPDATE:
return state.set('hovered', true);
case PROFILE_HOVER_CARD_CLOSE:
if (state.get('hovered') === true && !action.force)
return state;
else
return ReducerRecord();
default:
return state;
}
};
export { profileHoverCard as default };

View file

@ -1,7 +0,0 @@
import reducer from './sidebar';
describe('sidebar reducer', () => {
it('should return the initial state', () => {
expect(reducer(undefined, {} as any)).toEqual({ sidebarOpen: false });
});
});

View file

@ -1,24 +0,0 @@
import { SIDEBAR_OPEN, SIDEBAR_CLOSE } from '../actions/sidebar';
import type { AnyAction } from 'redux';
type State = {
sidebarOpen: boolean;
};
const initialState: State = {
sidebarOpen: false,
};
const sidebar = (state: State = initialState, action: AnyAction): State => {
switch (action.type) {
case SIDEBAR_OPEN:
return { sidebarOpen: true };
case SIDEBAR_CLOSE:
return { sidebarOpen: false };
default:
return state;
}
};
export { sidebar as default };

View file

@ -1,72 +0,0 @@
import {
STATUS_HOVER_CARD_OPEN,
STATUS_HOVER_CARD_CLOSE,
STATUS_HOVER_CARD_UPDATE,
} from 'pl-fe/actions/status-hover-card';
import reducer, { ReducerRecord } from './status-hover-card';
describe(STATUS_HOVER_CARD_OPEN, () => {
it('sets the ref and statusId', () => {
const ref = { current: document.createElement('div') };
const action = {
type: STATUS_HOVER_CARD_OPEN,
ref,
statusId: '1234',
};
const result = reducer(undefined, action);
expect(result.ref).toBe(ref);
expect(result.statusId).toBe('1234');
});
});
describe(STATUS_HOVER_CARD_CLOSE, () => {
it('flushes the state', () => {
const state = ReducerRecord({
ref: { current: document.createElement('div') },
statusId: '1234',
});
const action = { type: STATUS_HOVER_CARD_CLOSE };
const result = reducer(state, action);
expect(result.ref).toBe(null);
expect(result.statusId).toBe('');
});
it('leaves the state alone if hovered', () => {
const state = ReducerRecord({
ref: { current: document.createElement('div') },
statusId: '1234',
hovered: true,
});
const action = { type: STATUS_HOVER_CARD_CLOSE };
const result = reducer(state, action);
expect(result).toEqual(state);
});
it('action.force flushes the state even if hovered', () => {
const state = ReducerRecord({
ref: { current: document.createElement('div') },
statusId: '1234',
hovered: true,
});
const action = { type: STATUS_HOVER_CARD_CLOSE, force: true };
const result = reducer(state, action);
expect(result.ref).toBe(null);
expect(result.statusId).toBe('');
});
});
describe(STATUS_HOVER_CARD_UPDATE, () => {
it('sets hovered', () => {
const state = ReducerRecord();
const action = { type: STATUS_HOVER_CARD_UPDATE };
const result = reducer(state, action);
expect(result.hovered).toBe(true);
});
});

View file

@ -1,41 +0,0 @@
import { Record as ImmutableRecord } from 'immutable';
import {
STATUS_HOVER_CARD_OPEN,
STATUS_HOVER_CARD_CLOSE,
STATUS_HOVER_CARD_UPDATE,
} from 'pl-fe/actions/status-hover-card';
import type { AnyAction } from 'redux';
const ReducerRecord = ImmutableRecord({
ref: null as React.MutableRefObject<HTMLDivElement> | null,
statusId: '',
hovered: false,
});
type State = ReturnType<typeof ReducerRecord>;
const statusHoverCard = (state: State = ReducerRecord(), action: AnyAction) => {
switch (action.type) {
case STATUS_HOVER_CARD_OPEN:
return state.withMutations((state) => {
state.set('ref', action.ref);
state.set('statusId', action.statusId);
});
case STATUS_HOVER_CARD_UPDATE:
return state.set('hovered', true);
case STATUS_HOVER_CARD_CLOSE:
if (state.hovered === true && !action.force)
return state;
else
return ReducerRecord();
default:
return state;
}
};
export {
ReducerRecord,
statusHoverCard as default,
};

View file

@ -0,0 +1,31 @@
import { create } from 'zustand';
type State = {
ref: React.MutableRefObject<HTMLDivElement> | null;
accountId: string | null;
hovered: boolean;
openAccountHoverCard: (ref: React.MutableRefObject<HTMLDivElement>, accountId: string) => void;
updateAccountHoverCard: () => void;
closeAccountHoverCard: (force?: boolean) => void;
}
const useAccountHoverCardStore = create<State>((set) => ({
ref: null,
accountId: null,
hovered: false,
openAccountHoverCard: (ref, accountId) => set({
ref,
accountId,
}),
updateAccountHoverCard: () => set({
hovered: true,
}),
closeAccountHoverCard: (force = false) => set((state) => state.hovered && !force ? {} : {
ref: null,
accountId: null,
hovered: false,
}),
}));
export { useAccountHoverCardStore };

View file

@ -1,16 +0,0 @@
import { create } from 'zustand';
type State = {
isOpen: boolean;
openDropdownMenu: () => void;
closeDropdownMenu: () => void;
}
const useDropdownMenuStore = create<State>((set) => ({
isOpen: false,
openDropdownMenu: () => set({ isOpen: true }),
closeDropdownMenu: () => set({ isOpen: false }),
}));
export { useDropdownMenuStore };

View file

@ -1,2 +1,4 @@
export { useDropdownMenuStore } from './dropdown-menu'; export { useAccountHoverCardStore } from './account-hover-card';
export { useModalsStore } from './modals'; export { useModalsStore } from './modals';
export { useStatusHoverCardStore } from './status-hover-card';
export { useUiStore } from './ui';

View file

@ -0,0 +1,31 @@
import { create } from 'zustand';
type State = {
ref: React.MutableRefObject<HTMLDivElement> | null;
statusId: string | null;
hovered: boolean;
openStatusHoverCard: (ref: React.MutableRefObject<HTMLDivElement>, statusId: string) => void;
updateStatusHoverCard: () => void;
closeStatusHoverCard: (force?: boolean) => void;
}
const useStatusHoverCardStore = create<State>((set) => ({
ref: null,
statusId: null,
hovered: false,
openStatusHoverCard: (ref, statusId) => set({
ref,
statusId,
}),
updateStatusHoverCard: () => set({
hovered: true,
}),
closeStatusHoverCard: (force = false) => set((state) => state.hovered && !force ? {} : {
ref: null,
statusId: null,
hovered: false,
}),
}));
export { useStatusHoverCardStore };

View file

@ -0,0 +1,22 @@
import { create } from 'zustand';
type State = {
isDropdownMenuOpen: boolean;
openDropdownMenu: () => void;
closeDropdownMenu: () => void;
isSidebarOpen: boolean;
openSidebar: () => void;
closeSidebar: () => void;
}
const useUiStore = create<State>((set) => ({
isDropdownMenuOpen: false,
openDropdownMenu: () => set({ isDropdownMenuOpen: true }),
closeDropdownMenu: () => set({ isDropdownMenuOpen: false }),
isSidebarOpen: false,
openSidebar: () => set({ isSidebarOpen: true }),
closeSidebar: () => set({ isSidebarOpen: false }),
}));
export { useUiStore };