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 { 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';
@ -187,7 +187,7 @@ const DropdownMenu = (props: IDropdownMenu) => {
title = 'Menu',
} = props;
const { openDropdownMenu, closeDropdownMenu } = useDropdownMenuStore();
const { openDropdownMenu, closeDropdownMenu } = useUiStore();
const { openModal, closeModal } = useModalsStore();
const [isOpen, setIsOpen] = useState<boolean>(false);

View file

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

View file

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

View file

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

View file

@ -6,13 +6,13 @@ import { Link, NavLink } from 'react-router-dom';
import { fetchOwnAccounts, logOut, switchAccount } from 'pl-fe/actions/auth';
import { getSettings } from 'pl-fe/actions/settings';
import { closeSidebar } from 'pl-fe/actions/sidebar';
import { useAccount } from 'pl-fe/api/hooks';
import Account from 'pl-fe/components/account';
import { Stack, Divider, HStack, Icon, Text } from 'pl-fe/components/ui';
import ProfileStats from 'pl-fe/features/ui/components/profile-stats';
import { useAppDispatch, useAppSelector, useFeatures, useInstance, useRegistrationStatus } from 'pl-fe/hooks';
import { makeGetOtherAccounts } from 'pl-fe/selectors';
import { useUiStore } from 'pl-fe/stores';
import sourceCode from 'pl-fe/utils/code';
import type { List as ImmutableList } from 'immutable';
@ -79,18 +79,19 @@ const SidebarMenu: React.FC = (): JSX.Element | null => {
const intl = useIntl();
const dispatch = useAppDispatch();
const { isSidebarOpen, closeSidebar } = useUiStore();
const getOtherAccounts = useCallback(makeGetOtherAccounts(), []);
const features = useFeatures();
const me = useAppSelector((state) => state.me);
const { account } = useAccount(me || undefined);
const otherAccounts: ImmutableList<AccountEntity> = useAppSelector((state) => getOtherAccounts(state));
const sidebarOpen = useAppSelector((state) => state.sidebar.sidebarOpen);
const settings = useAppSelector((state) => getSettings(state));
const followRequestsCount = useAppSelector((state) => state.user_lists.follow_requests.items.count());
const scheduledStatusCount = useAppSelector((state) => state.scheduled_statuses.size);
const draftCount = useAppSelector((state) => state.draft_statuses.size);
// 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 touchEnd = useRef<number | null>(null);
const { isOpen } = useRegistrationStatus();
@ -102,11 +103,9 @@ const SidebarMenu: React.FC = (): JSX.Element | null => {
const [switcher, setSwitcher] = React.useState(false);
const onClose = () => dispatch(closeSidebar());
const handleClose = () => {
setSwitcher(false);
onClose();
closeSidebar();
};
const handleSwitchAccount = (account: AccountEntity): React.MouseEventHandler => (e) => {
@ -157,17 +156,17 @@ const SidebarMenu: React.FC = (): JSX.Element | null => {
}, []);
useEffect(() => {
if (sidebarOpen) containerRef.current?.querySelector('a')?.focus();
setTimeout(() => setSidebarVisible(sidebarOpen), sidebarOpen ? 0 : 150);
}, [sidebarOpen]);
if (isSidebarOpen) containerRef.current?.querySelector('a')?.focus();
setTimeout(() => setSidebarVisible(isSidebarOpen), isSidebarOpen ? 0 : 150);
}, [isSidebarOpen]);
return (
<div
aria-expanded={sidebarOpen}
aria-expanded={isSidebarOpen}
className={
clsx({
'z-[1000]': sidebarOpen || sidebarVisible,
hidden: !(sidebarOpen || sidebarVisible),
'z-[1000]': isSidebarOpen || sidebarVisible,
hidden: !(isSidebarOpen || sidebarVisible),
})
}
ref={containerRef}
@ -178,8 +177,8 @@ const SidebarMenu: React.FC = (): JSX.Element | null => {
>
<div
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),
'opacity-40': (sidebarVisible && sidebarOpen),
'no-reduce-motion:opacity-0': !(sidebarVisible && isSidebarOpen),
'opacity-40': (sidebarVisible && isSidebarOpen),
})}
role='button'
onClick={handleClose}
@ -188,8 +187,8 @@ const SidebarMenu: React.FC = (): JSX.Element | null => {
<div
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', {
'scale-100': sidebarVisible && sidebarOpen,
'no-reduce-motion:scale-0': !(sidebarVisible && sidebarOpen),
'scale-100': sidebarVisible && isSidebarOpen,
'no-reduce-motion:scale-0': !(sidebarVisible && isSidebarOpen),
})
}
>
@ -197,7 +196,7 @@ const SidebarMenu: React.FC = (): JSX.Element | null => {
<div className='p-4'>
{account ? (
<Stack space={4}>
<Link to={`/@${account.acct}`} onClick={onClose}>
<Link to={`/@${account.acct}`} onClick={closeSidebar}>
<Account account={account} showProfileHoverCard={false} withLinkToProfile={false} />
</Link>
@ -213,7 +212,7 @@ const SidebarMenu: React.FC = (): JSX.Element | null => {
to={`/@${account.acct}`}
icon={require('@tabler/icons/outline/user.svg')}
text={intl.formatMessage(messages.profile)}
onClick={onClose}
onClick={closeSidebar}
/>
{(account.locked || followRequestsCount > 0) && (
@ -221,7 +220,7 @@ const SidebarMenu: React.FC = (): JSX.Element | null => {
to='/follow_requests'
icon={require('@tabler/icons/outline/user-plus.svg')}
text={intl.formatMessage(messages.followRequests)}
onClick={onClose}
onClick={closeSidebar}
/>
)}
@ -230,7 +229,7 @@ const SidebarMenu: React.FC = (): JSX.Element | null => {
to='/conversations'
icon={require('@tabler/icons/outline/mail.svg')}
text={intl.formatMessage(messages.conversations)}
onClick={onClose}
onClick={closeSidebar}
/>
)}
@ -239,7 +238,7 @@ const SidebarMenu: React.FC = (): JSX.Element | null => {
to='/bookmarks'
icon={require('@tabler/icons/outline/bookmark.svg')}
text={intl.formatMessage(messages.bookmarks)}
onClick={onClose}
onClick={closeSidebar}
/>
)}
@ -248,7 +247,7 @@ const SidebarMenu: React.FC = (): JSX.Element | null => {
to='/groups'
icon={require('@tabler/icons/outline/circles.svg')}
text={intl.formatMessage(messages.groups)}
onClick={onClose}
onClick={closeSidebar}
/>
)}
@ -257,7 +256,7 @@ const SidebarMenu: React.FC = (): JSX.Element | null => {
to='/lists'
icon={require('@tabler/icons/outline/list.svg')}
text={intl.formatMessage(messages.lists)}
onClick={onClose}
onClick={closeSidebar}
/>
)}
@ -266,7 +265,7 @@ const SidebarMenu: React.FC = (): JSX.Element | null => {
to='/events'
icon={require('@tabler/icons/outline/calendar-event.svg')}
text={intl.formatMessage(messages.events)}
onClick={onClose}
onClick={closeSidebar}
/>
)}
@ -275,7 +274,7 @@ const SidebarMenu: React.FC = (): JSX.Element | null => {
to='/directory'
icon={require('@tabler/icons/outline/address-book.svg')}
text={intl.formatMessage(messages.profileDirectory)}
onClick={onClose}
onClick={closeSidebar}
/>
)}
@ -284,7 +283,7 @@ const SidebarMenu: React.FC = (): JSX.Element | null => {
to='/scheduled_statuses'
icon={require('@tabler/icons/outline/calendar-stats.svg')}
text={intl.formatMessage(messages.scheduledStatuses)}
onClick={onClose}
onClick={closeSidebar}
/>
)}
@ -293,7 +292,7 @@ const SidebarMenu: React.FC = (): JSX.Element | null => {
to='/draft_statuses'
icon={require('@tabler/icons/outline/notes.svg')}
text={intl.formatMessage(messages.drafts)}
onClick={onClose}
onClick={closeSidebar}
/>
)}
@ -304,7 +303,7 @@ const SidebarMenu: React.FC = (): JSX.Element | null => {
to='/timeline/local'
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' />}
onClick={onClose}
onClick={closeSidebar}
/>
{features.bubbleTimeline && (
@ -312,7 +311,7 @@ const SidebarMenu: React.FC = (): JSX.Element | null => {
to='/timeline/bubble'
icon={require('@tabler/icons/outline/chart-bubble.svg')}
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'
icon={require('@tabler/icons/outline/topology-star-ring-3.svg')}
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'
icon={require('@tabler/icons/outline/settings.svg')}
text={intl.formatMessage(messages.preferences)}
onClick={onClose}
onClick={closeSidebar}
/>
{features.followedHashtagsList && (
@ -340,7 +339,7 @@ const SidebarMenu: React.FC = (): JSX.Element | null => {
to='/followed_tags'
icon={require('@tabler/icons/outline/hash.svg')}
text={intl.formatMessage(messages.followedTags)}
onClick={onClose}
onClick={closeSidebar}
/>
)}
@ -349,7 +348,7 @@ const SidebarMenu: React.FC = (): JSX.Element | null => {
to='/developers'
icon={require('@tabler/icons/outline/code.svg')}
text={intl.formatMessage(messages.developers)}
onClick={onClose}
onClick={closeSidebar}
/>
)}
@ -358,7 +357,7 @@ const SidebarMenu: React.FC = (): JSX.Element | null => {
to='/admin'
icon={require('@tabler/icons/outline/dashboard.svg')}
text={intl.formatMessage(messages.dashboard)}
onClick={onClose}
onClick={closeSidebar}
// count={dashboardCount} WIP
/>
)}
@ -378,7 +377,7 @@ const SidebarMenu: React.FC = (): JSX.Element | null => {
href={sourceCode.url}
icon={require('@tabler/icons/outline/code.svg')}
text={intl.formatMessage(messages.sourceCode)}
onClick={onClose}
onClick={closeSidebar}
/>
<Divider />
@ -419,7 +418,7 @@ const SidebarMenu: React.FC = (): JSX.Element | null => {
to='/timeline/local'
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' />}
onClick={onClose}
onClick={closeSidebar}
/>
{features.bubbleTimeline && !restrictUnauth.timelines.bubble && (
@ -427,7 +426,7 @@ const SidebarMenu: React.FC = (): JSX.Element | null => {
to='/timeline/bubble'
icon={require('@tabler/icons/outline/chart-bubble.svg')}
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'
icon={require('@tabler/icons/outline/topology-star-ring-3.svg')}
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'
icon={require('@tabler/icons/outline/login.svg')}
text={intl.formatMessage(messages.login)}
onClick={onClose}
onClick={closeSidebar}
/>
{isOpen && (
@ -455,7 +454,7 @@ const SidebarMenu: React.FC = (): JSX.Element | null => {
to='/signup'
icon={require('@tabler/icons/outline/user-plus.svg')}
text={intl.formatMessage(messages.register)}
onClick={onClose}
onClick={closeSidebar}
/>
)}
@ -465,7 +464,7 @@ const SidebarMenu: React.FC = (): JSX.Element | null => {
href={sourceCode.url}
icon={require('@tabler/icons/outline/code.svg')}
text={intl.formatMessage(messages.sourceCode)}
onClick={onClose}
onClick={closeSidebar}
/>
</Stack>
)}

View file

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

View file

@ -3,12 +3,11 @@ import { defineMessages, useIntl } from 'react-intl';
import { useRouteMatch } from 'react-router-dom';
import { groupComposeModal } from 'pl-fe/actions/compose';
import { openSidebar } from 'pl-fe/actions/sidebar';
import ThumbNavigationLink from 'pl-fe/components/thumb-navigation-link';
import { useStatContext } from 'pl-fe/contexts/stat-context';
import { Entities } from 'pl-fe/entity-store/entities';
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 { Icon } from './ui';
@ -30,14 +29,13 @@ const ThumbNavigation: React.FC = (): JSX.Element => {
const match = useRouteMatch<{ groupId: string }>('/groups/:groupId');
const { openSidebar } = useUiStore();
const { openModal } = useModalsStore();
const { unreadChatsCount } = useStatContext();
const standalone = useAppSelector(isStandalone);
const notificationCount = useAppSelector((state) => state.notifications.unread);
const handleOpenSidebar = () => dispatch(openSidebar());
const handleOpenComposeModal = () => {
if (match?.params.groupId) {
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'>
<button
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)}
>
<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 SearchLayout from 'pl-fe/layouts/search-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 { isStandalone } from 'pl-fe/utils/state';
@ -352,7 +352,7 @@ const UI: React.FC<IUI> = ({ children }) => {
const features = useFeatures();
const vapidKey = useAppSelector(state => getVapidKey(state));
const { isOpen: dropdownMenuIsOpen } = useDropdownMenuStore();
const { isDropdownMenuOpen } = useUiStore();
const standalone = useAppSelector(isStandalone);
const { isDragging } = useDraggedFiles(node);
@ -445,7 +445,7 @@ const UI: React.FC<IUI> = ({ children }) => {
if (me === null) return null;
const style: React.CSSProperties = {
pointerEvents: dropdownMenuIsOpen ? 'none' : undefined,
pointerEvents: isDropdownMenuOpen ? 'none' : undefined,
};
return (

View file

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