kopia lustrzana https://gitlab.com/soapbox-pub/soapbox
Merge branch 'update-notification-icon' into 'main'
Remove "ScrollTopButton" from timeline Closes #1811 See merge request soapbox-pub/soapbox!3321merge-requests/3325/head
commit
18523c4e77
|
@ -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}
|
||||
|
||||
<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>
|
||||
);
|
||||
|
|
|
@ -1,20 +1,14 @@
|
|||
import { debounce } from 'es-toolkit';
|
||||
import { OrderedSet as ImmutableOrderedSet } from 'immutable';
|
||||
import { useCallback } from 'react';
|
||||
import { defineMessages } from 'react-intl';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
import { dequeueTimeline, scrollTopTimeline } from 'soapbox/actions/timelines.ts';
|
||||
import ScrollTopButton from 'soapbox/components/scroll-top-button.tsx';
|
||||
import StatusList, { IStatusList } from 'soapbox/components/status-list.tsx';
|
||||
import Portal from 'soapbox/components/ui/portal.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';
|
||||
|
||||
const messages = defineMessages({
|
||||
queue: { id: 'status_list.queue_label', defaultMessage: 'Click to see {count} new {count, plural, one {post} other {posts}}' },
|
||||
});
|
||||
|
||||
interface ITimeline extends Omit<IStatusList, 'statusIds' | 'isLoading' | 'hasMore'> {
|
||||
/** ID of the timeline in Redux. */
|
||||
timelineId: string;
|
||||
|
@ -37,7 +31,11 @@ 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 totalQueuedItemsCount = useAppSelector(state => state.timelines.get(timelineId)?.totalQueuedItemsCount || 0);
|
||||
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);
|
||||
|
||||
|
||||
const handleDequeueTimeline = useCallback(() => {
|
||||
dispatch(dequeueTimeline(timelineId, onLoadMore));
|
||||
|
@ -48,20 +46,42 @@ const Timeline: React.FC<ITimeline> = ({
|
|||
}, 100), [timelineId]);
|
||||
|
||||
const handleScroll = useCallback(debounce(() => {
|
||||
setIsInTop(window.scrollY < 50);
|
||||
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);
|
||||
setIntervalId(null);
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (intervalId) clearInterval(intervalId);
|
||||
};
|
||||
}, [isInTop, handleDequeueTimeline]);
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener('scroll', handleScroll);
|
||||
return () => {
|
||||
window.removeEventListener('scroll', handleScroll);
|
||||
};
|
||||
}, [handleScroll]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Portal>
|
||||
<ScrollTopButton
|
||||
key='timeline-queue-button-header'
|
||||
onClick={handleDequeueTimeline}
|
||||
count={totalQueuedItemsCount}
|
||||
message={messages.queue}
|
||||
/>
|
||||
</Portal>
|
||||
|
||||
<StatusList
|
||||
timelineId={timelineId}
|
||||
onScrollToTop={handleScrollToTop}
|
||||
|
|
|
@ -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'}
|
||||
/>
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
|
||||
|
||||
interface NotificationState {
|
||||
home: boolean;
|
||||
public: boolean;
|
||||
instance: boolean;
|
||||
}
|
||||
|
||||
const initialState: NotificationState = {
|
||||
home: false,
|
||||
public: false,
|
||||
instance: false,
|
||||
};
|
||||
|
||||
const notificationsTab = createSlice({
|
||||
name: 'notificationsSlice',
|
||||
initialState,
|
||||
reducers: {
|
||||
setNotification: (
|
||||
state,
|
||||
action: PayloadAction<{ timelineId: string; value: boolean }>,
|
||||
) => {
|
||||
if (action.payload.timelineId in state) {
|
||||
state[action.payload.timelineId as keyof NotificationState] = action.payload.value;
|
||||
}
|
||||
},
|
||||
resetNotifications: (state) => {
|
||||
state.home = false;
|
||||
state.public = false;
|
||||
state.instance = false;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const { setNotification, resetNotifications } = notificationsTab.actions;
|
||||
export default notificationsTab.reducer;
|
Ładowanie…
Reference in New Issue