Add logic to display notification icon based on state

This commit is contained in:
danidfra 2025-02-03 20:52:56 -03:00
parent 63335e9274
commit 3a4c6466b0
4 changed files with 23 additions and 4 deletions

View File

@ -116,6 +116,8 @@ export type Item = {
count?: number;
/** Unique name for this tab. */
name: string;
/** Display a notificationicon over the tab */
notification?: boolean;
}
interface ITabs {
@ -142,7 +144,7 @@ const Tabs = ({ items, activeItem }: ITabs) => {
};
const renderItem = (item: Item, idx: number) => {
const { name, text, title, count } = item;
const { name, text, title, count, notification } = item;
return (
<AnimatedTab
@ -160,7 +162,10 @@ const Tabs = ({ items, activeItem }: ITabs) => {
</span>
) : null}
{text}
<div className='relative flex items-center justify-center gap-1.5'>
{text}
{notification && <div className='absolute -right-4 size-2 animate-pulse rounded-full bg-primary-500' />}
</div>
</div>
</AnimatedTab>
);

View File

@ -6,6 +6,7 @@ import { dequeueTimeline, scrollTopTimeline } from 'soapbox/actions/timelines.ts
import StatusList, { IStatusList } from 'soapbox/components/status-list.tsx';
import { useAppDispatch } from 'soapbox/hooks/useAppDispatch.ts';
import { useAppSelector } from 'soapbox/hooks/useAppSelector.ts';
import { setNotification } from 'soapbox/reducers/notificationsSlice.ts';
import { makeGetStatusIds } from 'soapbox/selectors/index.ts';
interface ITimeline extends Omit<IStatusList, 'statusIds' | 'isLoading' | 'hasMore'> {
@ -30,6 +31,7 @@ const Timeline: React.FC<ITimeline> = ({
const isLoading = useAppSelector(state => (state.timelines.get(timelineId) || { isLoading: true }).isLoading === true);
const isPartial = useAppSelector(state => (state.timelines.get(timelineId)?.isPartial || false) === true);
const hasMore = useAppSelector(state => state.timelines.get(timelineId)?.hasMore === true);
const hasQueuedItems = useAppSelector(state => state.timelines.get(timelineId)?.totalQueuedItemsCount || 0);
const [isInTop, setIsInTop] = useState<boolean>(window.scrollY < 50);
const [intervalId, setIntervalId] = useState<NodeJS.Timeout | null>(null);
@ -48,11 +50,18 @@ const Timeline: React.FC<ITimeline> = ({
dispatch(scrollTopTimeline(timelineId, false));
}, 100), [timelineId]);
useEffect(() => {
if (hasQueuedItems) {
dispatch(setNotification({ timelineId: timelineId, value: hasQueuedItems > 0 }));
}
}, [hasQueuedItems, timelineId]);
useEffect(() => {
if (isInTop) {
handleDequeueTimeline();
const interval = setInterval(handleDequeueTimeline, 2000);
setIntervalId(interval);
dispatch(setNotification({ timelineId: timelineId, value: false }));
} else if (intervalId) {
clearInterval(intervalId);

View File

@ -1,6 +1,7 @@
import clsx from 'clsx';
import { useRef } from 'react';
import { FormattedMessage, useIntl } from 'react-intl';
import { useSelector } from 'react-redux';
import { Link, useLocation } from 'react-router-dom';
import { uploadCompose } from 'soapbox/actions/compose.ts';
@ -30,6 +31,7 @@ import { useInstance } from 'soapbox/hooks/useInstance.ts';
import { useIsMobile } from 'soapbox/hooks/useIsMobile.ts';
import { useOwnAccount } from 'soapbox/hooks/useOwnAccount.ts';
import { useSoapboxConfig } from 'soapbox/hooks/useSoapboxConfig.ts';
import { RootState } from 'soapbox/store.ts';
import ComposeForm from '../features/compose/components/compose-form.tsx';
@ -41,6 +43,7 @@ const HomePage: React.FC<IHomePage> = ({ children }) => {
const intl = useIntl();
const dispatch = useAppDispatch();
const { pathname } = useLocation();
const notifications = useSelector((state: RootState) => state.notificationsTab);
const me = useAppSelector(state => state.me);
const { account } = useOwnAccount();
@ -105,8 +108,8 @@ const HomePage: React.FC<IHomePage> = ({ children }) => {
<div className='sticky top-12 z-20 bg-white/90 backdrop-blur black:bg-black/90 dark:bg-primary-900/90 lg:top-0'>
<Tabs
items={[
{ name: 'home', text: <FormattedMessage id='tabs_bar.home' defaultMessage='Home' />, to: '/' },
{ name: 'local', text: <div className='block max-w-xs truncate'>{instance.domain}</div>, to: '/timeline/local' },
{ name: 'home', text: <FormattedMessage id='tabs_bar.home' defaultMessage='Home' />, to: '/', notification: notifications.home },
{ name: 'local', text: <div className='block max-w-xs truncate'>{instance.domain}</div>, to: '/timeline/local', notification: notifications.instance },
]}
activeItem={pathname === '/timeline/local' ? 'local' : 'home'}
/>

View File

@ -32,6 +32,7 @@ import meta from './meta.ts';
import modals from './modals.ts';
import mutes from './mutes.ts';
import notifications from './notifications.ts';
import notificationsTab from './notificationsSlice.ts';
import onboarding from './onboarding.ts';
import patron from './patron.ts';
import pending_statuses from './pending-statuses.ts';
@ -87,6 +88,7 @@ export default combineReducers({
modals,
mutes,
notifications,
notificationsTab,
onboarding,
patron,
pending_statuses,