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;
|
count?: number;
|
||||||
/** Unique name for this tab. */
|
/** Unique name for this tab. */
|
||||||
name: string;
|
name: string;
|
||||||
|
/** Display a notificationicon over the tab */
|
||||||
|
notification?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ITabs {
|
interface ITabs {
|
||||||
|
@ -142,7 +144,7 @@ const Tabs = ({ items, activeItem }: ITabs) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderItem = (item: Item, idx: number) => {
|
const renderItem = (item: Item, idx: number) => {
|
||||||
const { name, text, title, count } = item;
|
const { name, text, title, count, notification } = item;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AnimatedTab
|
<AnimatedTab
|
||||||
|
@ -160,7 +162,10 @@ const Tabs = ({ items, activeItem }: ITabs) => {
|
||||||
</span>
|
</span>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
|
<div className='relative flex items-center justify-center gap-1.5'>
|
||||||
{text}
|
{text}
|
||||||
|
{notification && <div className='absolute -right-4 size-2 animate-pulse rounded-full bg-primary-500' />}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</AnimatedTab>
|
</AnimatedTab>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,20 +1,14 @@
|
||||||
import { debounce } from 'es-toolkit';
|
import { debounce } from 'es-toolkit';
|
||||||
import { OrderedSet as ImmutableOrderedSet } from 'immutable';
|
import { OrderedSet as ImmutableOrderedSet } from 'immutable';
|
||||||
import { useCallback } from 'react';
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
import { defineMessages } from 'react-intl';
|
|
||||||
|
|
||||||
import { dequeueTimeline, scrollTopTimeline } from 'soapbox/actions/timelines.ts';
|
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 StatusList, { IStatusList } from 'soapbox/components/status-list.tsx';
|
||||||
import Portal from 'soapbox/components/ui/portal.tsx';
|
|
||||||
import { useAppDispatch } from 'soapbox/hooks/useAppDispatch.ts';
|
import { useAppDispatch } from 'soapbox/hooks/useAppDispatch.ts';
|
||||||
import { useAppSelector } from 'soapbox/hooks/useAppSelector.ts';
|
import { useAppSelector } from 'soapbox/hooks/useAppSelector.ts';
|
||||||
|
import { setNotification } from 'soapbox/reducers/notificationsSlice.ts';
|
||||||
import { makeGetStatusIds } from 'soapbox/selectors/index.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'> {
|
interface ITimeline extends Omit<IStatusList, 'statusIds' | 'isLoading' | 'hasMore'> {
|
||||||
/** ID of the timeline in Redux. */
|
/** ID of the timeline in Redux. */
|
||||||
timelineId: string;
|
timelineId: string;
|
||||||
|
@ -37,7 +31,11 @@ const Timeline: React.FC<ITimeline> = ({
|
||||||
const isLoading = useAppSelector(state => (state.timelines.get(timelineId) || { isLoading: true }).isLoading === true);
|
const isLoading = useAppSelector(state => (state.timelines.get(timelineId) || { isLoading: true }).isLoading === true);
|
||||||
const isPartial = useAppSelector(state => (state.timelines.get(timelineId)?.isPartial || false) === true);
|
const isPartial = useAppSelector(state => (state.timelines.get(timelineId)?.isPartial || false) === true);
|
||||||
const hasMore = useAppSelector(state => state.timelines.get(timelineId)?.hasMore === 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(() => {
|
const handleDequeueTimeline = useCallback(() => {
|
||||||
dispatch(dequeueTimeline(timelineId, onLoadMore));
|
dispatch(dequeueTimeline(timelineId, onLoadMore));
|
||||||
|
@ -48,20 +46,42 @@ const Timeline: React.FC<ITimeline> = ({
|
||||||
}, 100), [timelineId]);
|
}, 100), [timelineId]);
|
||||||
|
|
||||||
const handleScroll = useCallback(debounce(() => {
|
const handleScroll = useCallback(debounce(() => {
|
||||||
|
setIsInTop(window.scrollY < 50);
|
||||||
dispatch(scrollTopTimeline(timelineId, false));
|
dispatch(scrollTopTimeline(timelineId, false));
|
||||||
}, 100), [timelineId]);
|
}, 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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<Portal>
|
|
||||||
<ScrollTopButton
|
|
||||||
key='timeline-queue-button-header'
|
|
||||||
onClick={handleDequeueTimeline}
|
|
||||||
count={totalQueuedItemsCount}
|
|
||||||
message={messages.queue}
|
|
||||||
/>
|
|
||||||
</Portal>
|
|
||||||
|
|
||||||
<StatusList
|
<StatusList
|
||||||
timelineId={timelineId}
|
timelineId={timelineId}
|
||||||
onScrollToTop={handleScrollToTop}
|
onScrollToTop={handleScrollToTop}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import { useRef } from 'react';
|
import { useRef } from 'react';
|
||||||
import { FormattedMessage, useIntl } from 'react-intl';
|
import { FormattedMessage, useIntl } from 'react-intl';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
import { Link, useLocation } from 'react-router-dom';
|
import { Link, useLocation } from 'react-router-dom';
|
||||||
|
|
||||||
import { uploadCompose } from 'soapbox/actions/compose.ts';
|
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 { useIsMobile } from 'soapbox/hooks/useIsMobile.ts';
|
||||||
import { useOwnAccount } from 'soapbox/hooks/useOwnAccount.ts';
|
import { useOwnAccount } from 'soapbox/hooks/useOwnAccount.ts';
|
||||||
import { useSoapboxConfig } from 'soapbox/hooks/useSoapboxConfig.ts';
|
import { useSoapboxConfig } from 'soapbox/hooks/useSoapboxConfig.ts';
|
||||||
|
import { RootState } from 'soapbox/store.ts';
|
||||||
|
|
||||||
import ComposeForm from '../features/compose/components/compose-form.tsx';
|
import ComposeForm from '../features/compose/components/compose-form.tsx';
|
||||||
|
|
||||||
|
@ -41,6 +43,7 @@ const HomePage: React.FC<IHomePage> = ({ children }) => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const { pathname } = useLocation();
|
const { pathname } = useLocation();
|
||||||
|
const notifications = useSelector((state: RootState) => state.notificationsTab);
|
||||||
|
|
||||||
const me = useAppSelector(state => state.me);
|
const me = useAppSelector(state => state.me);
|
||||||
const { account } = useOwnAccount();
|
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'>
|
<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
|
<Tabs
|
||||||
items={[
|
items={[
|
||||||
{ name: 'home', text: <FormattedMessage id='tabs_bar.home' defaultMessage='Home' />, to: '/' },
|
{ 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' },
|
{ 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'}
|
activeItem={pathname === '/timeline/local' ? 'local' : 'home'}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -32,6 +32,7 @@ import meta from './meta.ts';
|
||||||
import modals from './modals.ts';
|
import modals from './modals.ts';
|
||||||
import mutes from './mutes.ts';
|
import mutes from './mutes.ts';
|
||||||
import notifications from './notifications.ts';
|
import notifications from './notifications.ts';
|
||||||
|
import notificationsTab from './notificationsSlice.ts';
|
||||||
import onboarding from './onboarding.ts';
|
import onboarding from './onboarding.ts';
|
||||||
import patron from './patron.ts';
|
import patron from './patron.ts';
|
||||||
import pending_statuses from './pending-statuses.ts';
|
import pending_statuses from './pending-statuses.ts';
|
||||||
|
@ -87,6 +88,7 @@ export default combineReducers({
|
||||||
modals,
|
modals,
|
||||||
mutes,
|
mutes,
|
||||||
notifications,
|
notifications,
|
||||||
|
notificationsTab,
|
||||||
onboarding,
|
onboarding,
|
||||||
patron,
|
patron,
|
||||||
pending_statuses,
|
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