nicolium: remove legacy timeline

Signed-off-by: nicole mikołajczyk <git@mkljczk.pl>
This commit is contained in:
nicole mikołajczyk
2026-03-05 15:40:53 +01:00
parent 0bbcf18325
commit 19ea9208e2
17 changed files with 164 additions and 659 deletions

View File

@ -887,29 +887,6 @@ const Preferences = () => {
</ListItem>
</List>
)}
<List>
<ListItem
label={
<FormattedMessage
id='preferences.fields.experimental_timeline_label'
defaultMessage='Enable experimental timeline'
/>
}
hint={
<FormattedMessage
id='preferences.fields.experimental_timeline_hint'
defaultMessage='It replaces the stable timeline experience and might not offer all features.'
/>
}
>
<SettingToggle
settings={settings}
settingPath={['experimentalTimeline']}
onChange={onToggleChange}
/>
</ListItem>
</List>
</Form>
);
};

View File

@ -1568,8 +1568,6 @@
"preferences.fields.display_media.default": "Hide posts marked as sensitive",
"preferences.fields.display_media.hide_all": "Always hide media posts",
"preferences.fields.display_media.show_all": "Always show posts",
"preferences.fields.experimental_timeline_hint": "It replaces the stable timeline experience and might not offer all features.",
"preferences.fields.experimental_timeline_label": "Enable experimental timeline",
"preferences.fields.implicit_addressing_label": "Include mentions in post content when replying",
"preferences.fields.interface_size": "Interface size",
"preferences.fields.known_languages_label": "Languages you know",

View File

@ -4,20 +4,14 @@ import { FormattedMessage } from 'react-intl';
import { fetchAccountTimeline } from '@/actions/timelines';
import { AccountTimelineColumn } from '@/columns/timeline';
import MissingIndicator from '@/components/missing-indicator';
import StatusList from '@/components/statuses/status-list';
import Card, { CardBody } from '@/components/ui/card';
import Spinner from '@/components/ui/spinner';
import Text from '@/components/ui/text';
import { profileRoute } from '@/features/ui/router';
import { useAppDispatch } from '@/hooks/use-app-dispatch';
import { useAppSelector } from '@/hooks/use-app-selector';
import { useFeatures } from '@/hooks/use-features';
import { useAccountLookup } from '@/queries/accounts/use-account-lookup';
import { usePinnedStatuses } from '@/queries/status-lists/use-pinned-statuses';
import { makeGetStatusIds } from '@/selectors';
import { useSettings } from '@/stores/settings';
const getStatusIds = makeGetStatusIds();
const AccountTimelinePage: React.FC = () => {
const { username } = profileRoute.useParams();
@ -25,20 +19,12 @@ const AccountTimelinePage: React.FC = () => {
const dispatch = useAppDispatch();
const features = useFeatures();
const settings = useSettings();
const { data: account, isPending } = useAccountLookup(username);
const path = withReplies ? `${account?.id}:with_replies` : account?.id;
const showPins = settings.account_timeline.shows.pinned && !withReplies;
const statusIds = useAppSelector((state) =>
getStatusIds(state, { type: `account:${path}`, prefix: 'account_timeline' }),
);
const { data: featuredStatusIds } = usePinnedStatuses(account?.id || '');
const { data: _featuredStatusIds } = usePinnedStatuses(account?.id || '');
const isBlocked = account?.relationship?.blocked_by && !features.blockersVisible;
const isLoading = useAppSelector((state) => state.timelines[`account:${path}`]?.isLoading);
const hasMore = useAppSelector((state) => state.timelines[`account:${path}`]?.hasMore);
const accountUsername = account?.username ?? username;
@ -52,12 +38,6 @@ const AccountTimelinePage: React.FC = () => {
}
}, [account?.id, withReplies]);
const handleLoadMore = () => {
if (account) {
dispatch(fetchAccountTimeline(account.id, { exclude_replies: !withReplies }, true));
}
};
if (!account && isPending) {
return <Spinner />;
} else if (!account) {
@ -80,21 +60,11 @@ const AccountTimelinePage: React.FC = () => {
);
}
return settings.experimentalTimeline ? (
return (
<AccountTimelineColumn
accountId={account.id}
excludeReplies={!withReplies}
// featuredStatusIds={showPins ? featuredStatusIds : undefined}
/>
) : (
<StatusList
timelineId={`account:${path}`}
scrollKey='account_timeline'
statusIds={statusIds}
featuredStatusIds={showPins ? featuredStatusIds : undefined}
isLoading={isLoading}
hasMore={hasMore}
onLoadMore={handleLoadMore}
emptyMessageText={
<FormattedMessage id='empty_column.account_timeline' defaultMessage='No posts here!' />
}

View File

@ -1,20 +1,16 @@
import { useNavigate } from '@tanstack/react-router';
import React, { useEffect } from 'react';
import React from 'react';
import { FormattedMessage, defineMessages, useIntl } from 'react-intl';
import { fetchAntennaTimeline } from '@/actions/timelines';
import { AntennaTimelineColumn } from '@/columns/timeline';
import DropdownMenu from '@/components/dropdown-menu';
import MissingIndicator from '@/components/missing-indicator';
// import Button from '@/components/ui/button';
import Column from '@/components/ui/column';
import Spinner from '@/components/ui/spinner';
import Timeline from '@/features/ui/components/timeline';
import { antennaTimelineRoute } from '@/features/ui/router';
import { useAppDispatch } from '@/hooks/use-app-dispatch';
import { useAntenna, useDeleteAntenna } from '@/queries/accounts/use-antennas';
import { useModalsActions } from '@/stores/modals';
import { useSettings } from '@/stores/settings';
const messages = defineMessages({
deleteHeading: { id: 'confirmations.delete_antenna.heading', defaultMessage: 'Delete antenna' },
@ -27,47 +23,10 @@ const messages = defineMessages({
deleteAntenna: { id: 'antennas.delete', defaultMessage: 'Delete antenna' },
});
interface IAntennaTimeline {
antennaId: string;
}
const AntennaTimeline: React.FC<IAntennaTimeline> = ({ antennaId }) => {
const dispatch = useAppDispatch();
useEffect(() => {
dispatch(fetchAntennaTimeline(antennaId));
}, [antennaId]);
const handleLoadMore = () => {
dispatch(fetchAntennaTimeline(antennaId, true));
};
const emptyMessage = (
<div>
<FormattedMessage
id='empty_column.antenna'
defaultMessage='There is nothing in this antenna yet. When posts matching the criteria will be created, they will appear here.'
/>
</div>
);
return (
<Timeline
loadMoreClassName='sm:pb-4 black:sm:pb-0 black:sm:mx-4'
scrollKey='antenna_timeline'
timelineId={`antenna:${antennaId}`}
onLoadMore={handleLoadMore}
emptyMessageText={emptyMessage}
emptyMessageIcon={require('@phosphor-icons/core/regular/chat-centered-text.svg')}
/>
);
};
const AntennaTimelinePage: React.FC = () => {
const { antennaId } = antennaTimelineRoute.useParams();
const intl = useIntl();
const { experimentalTimeline } = useSettings();
const { openModal } = useModalsActions();
const navigate = useNavigate();
@ -132,11 +91,18 @@ const AntennaTimelinePage: React.FC = () => {
/>
}
>
{experimentalTimeline ? (
<AntennaTimelineColumn antennaId={antennaId} />
) : (
<AntennaTimeline antennaId={antennaId} />
)}
<AntennaTimelineColumn
antennaId={antennaId}
emptyMessageText={
<div>
<FormattedMessage
id='empty_column.antenna'
defaultMessage='There is nothing in this antenna yet. When posts matching the criteria will be created, they will appear here.'
/>
</div>
}
emptyMessageIcon={require('@phosphor-icons/core/regular/chat-centered-text.svg')}
/>
</Column>
);
};

View File

@ -1,49 +1,19 @@
import React, { useEffect } from 'react';
import React from 'react';
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
import { fetchBubbleTimeline } from '@/actions/timelines';
import { useBubbleStream } from '@/api/hooks/streaming/use-bubble-stream';
import { BubbleTimelineColumn } from '@/columns/timeline';
import PullToRefresh from '@/components/pull-to-refresh';
import Column from '@/components/ui/column';
import Timeline from '@/features/ui/components/timeline';
import { useAppDispatch } from '@/hooks/use-app-dispatch';
import { useFeatures } from '@/hooks/use-features';
import { useSettings } from '@/stores/settings';
const messages = defineMessages({
title: { id: 'column.bubble', defaultMessage: 'Bubble timeline' },
});
const BubbleTimeline = () => {
const dispatch = useAppDispatch();
const features = useFeatures();
const settings = useSettings();
const onlyMedia = settings.timelines.bubble?.other.onlyMedia ?? false;
const timelineId = 'bubble';
const handleLoadMore = () => {
dispatch(fetchBubbleTimeline({ onlyMedia }, true));
};
const handleRefresh = () => dispatch(fetchBubbleTimeline({ onlyMedia }, true));
useBubbleStream({ onlyMedia, enabled: features.bubbleTimelineStreaming });
useEffect(() => {
dispatch(fetchBubbleTimeline({ onlyMedia }));
}, [onlyMedia]);
const BubbleTimelinePage = () => {
const intl = useIntl();
return (
<PullToRefresh onRefresh={handleRefresh}>
<Timeline
loadMoreClassName='sm:pb-4 black:sm:pb-0 black:sm:mx-4'
scrollKey={`${timelineId}_timeline`}
timelineId={`${timelineId}${onlyMedia ? ':media' : ''}`}
prefix='home'
onLoadMore={handleLoadMore}
<Column className='-mt-3 sm:mt-0' label={intl.formatMessage(messages.title)}>
<BubbleTimelineColumn
emptyMessageText={
<FormattedMessage
id='empty_column.bubble'
@ -52,19 +22,6 @@ const BubbleTimeline = () => {
}
emptyMessageIcon={require('@phosphor-icons/core/regular/chat-centered-text.svg')}
/>
</PullToRefresh>
);
};
const BubbleTimelinePage = () => {
const intl = useIntl();
const settings = useSettings();
const { experimentalTimeline } = settings;
return (
<Column className='-mt-3 sm:mt-0' label={intl.formatMessage(messages.title)}>
{experimentalTimeline ? <BubbleTimelineColumn /> : <BubbleTimeline />}
</Column>
);
};

View File

@ -9,12 +9,10 @@ import MissingIndicator from '@/components/missing-indicator';
import Button from '@/components/ui/button';
import Column from '@/components/ui/column';
import Spinner from '@/components/ui/spinner';
import Timeline from '@/features/ui/components/timeline';
import { circleTimelineRoute } from '@/features/ui/router';
import { useAppDispatch } from '@/hooks/use-app-dispatch';
import { useCircle, useDeleteCircle } from '@/queries/accounts/use-circles';
import { useModalsActions } from '@/stores/modals';
import { useSettings } from '@/stores/settings';
const messages = defineMessages({
deleteHeading: { id: 'confirmations.delete_circle.heading', defaultMessage: 'Delete circle' },
@ -27,48 +25,6 @@ const messages = defineMessages({
deleteCircle: { id: 'circles.delete', defaultMessage: 'Delete circle' },
});
interface ICircleTimeline {
circleId: string;
}
const CircleTimeline: React.FC<ICircleTimeline> = ({ circleId }) => {
const dispatch = useAppDispatch();
const { openModal } = useModalsActions();
const handleLoadMore = () => {
dispatch(fetchCircleTimeline(circleId, true));
};
const handleEditClick = () => {
openModal('CIRCLE_EDITOR', { circleId });
};
const emptyMessage = (
<div>
<FormattedMessage
id='empty_column.circle'
defaultMessage='There is nothing in this circle yet. When members of this circle create new posts, they will appear here.'
/>
<br />
<br />
<Button onClick={handleEditClick}>
<FormattedMessage id='circle.click_to_add' defaultMessage='Click here to add people' />
</Button>
</div>
);
return (
<Timeline
loadMoreClassName='sm:pb-4 black:sm:pb-0 black:sm:mx-4'
scrollKey='circle_timeline'
timelineId={`circle:${circleId}`}
onLoadMore={handleLoadMore}
emptyMessageText={emptyMessage}
emptyMessageIcon={require('@phosphor-icons/core/regular/chat-centered-text.svg')}
/>
);
};
const CircleTimelinePage: React.FC = () => {
const { circleId } = circleTimelineRoute.useParams();
@ -76,7 +32,6 @@ const CircleTimelinePage: React.FC = () => {
const dispatch = useAppDispatch();
const { openModal } = useModalsActions();
const navigate = useNavigate();
const { experimentalTimeline } = useSettings();
const { data: circle, isFetching } = useCircle(circleId);
const { mutate: deleteCircle } = useDeleteCircle();
@ -143,11 +98,26 @@ const CircleTimelinePage: React.FC = () => {
/>
}
>
{experimentalTimeline ? (
<CircleTimelineColumn circleId={circleId} />
) : (
<CircleTimeline circleId={circleId} />
)}
<CircleTimelineColumn
circleId={circleId}
emptyMessageText={
<div>
<FormattedMessage
id='empty_column.circle'
defaultMessage='There is nothing in this circle yet. When members of this circle create new posts, they will appear here.'
/>
<br />
<br />
<Button onClick={handleEditClick}>
<FormattedMessage
id='circle.click_to_add'
defaultMessage='Click here to add people'
/>
</Button>
</div>
}
emptyMessageIcon={require('@phosphor-icons/core/regular/chat-centered-text.svg')}
/>
</Column>
);
};

View File

@ -1,55 +1,20 @@
import React, { useEffect } from 'react';
import React from 'react';
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
import { fetchPublicTimeline } from '@/actions/timelines';
import { useCommunityStream } from '@/api/hooks/streaming/use-community-stream';
import { PublicTimelineColumn } from '@/columns/timeline';
import PullToRefresh from '@/components/pull-to-refresh';
import Column from '@/components/ui/column';
import Timeline from '@/features/ui/components/timeline';
import { useAppDispatch } from '@/hooks/use-app-dispatch';
import { useSettings } from '@/stores/settings';
const messages = defineMessages({
title: { id: 'column.community', defaultMessage: 'Local timeline' },
});
interface ICommunityTimeline {
onTimelineFailed?: () => void;
}
const CommunityTimeline: React.FC<ICommunityTimeline> = ({ onTimelineFailed }) => {
const dispatch = useAppDispatch();
const settings = useSettings();
const onlyMedia = settings.timelines['public:local']?.other.onlyMedia ?? false;
const timelineId = 'public:local';
const handleLoadMore = () => {
dispatch(
fetchPublicTimeline({ onlyMedia, local: true }, true, undefined, () => {
onTimelineFailed?.();
}),
);
};
const handleRefresh = () => dispatch(fetchPublicTimeline({ onlyMedia, local: true }, true));
useCommunityStream({ onlyMedia });
useEffect(() => {
dispatch(fetchPublicTimeline({ onlyMedia, local: true }));
}, [onlyMedia]);
const CommunityTimelinePage = () => {
const intl = useIntl();
return (
<PullToRefresh onRefresh={handleRefresh}>
<Timeline
loadMoreClassName='sm:pb-4 black:sm:pb-0 black:sm:mx-4'
scrollKey={`${timelineId}_timeline`}
timelineId={`${timelineId}${onlyMedia ? ':media' : ''}`}
prefix='home'
onLoadMore={handleLoadMore}
<Column className='-mt-3 sm:mt-0' label={intl.formatMessage(messages.title)}>
<PublicTimelineColumn
local
emptyMessageText={
<FormattedMessage
id='empty_column.community'
@ -58,20 +23,8 @@ const CommunityTimeline: React.FC<ICommunityTimeline> = ({ onTimelineFailed }) =
}
emptyMessageIcon={require('@phosphor-icons/core/regular/chat-centered-text.svg')}
/>
</PullToRefresh>
);
};
const CommunityTimelinePage = () => {
const intl = useIntl();
const { experimentalTimeline } = useSettings();
return (
<Column className='-mt-3 sm:mt-0' label={intl.formatMessage(messages.title)}>
{experimentalTimeline ? <PublicTimelineColumn local /> : <CommunityTimeline />}
</Column>
);
};
export { CommunityTimeline, CommunityTimelinePage as default };
export { CommunityTimelinePage as default };

View File

@ -3,55 +3,16 @@ import clsx from 'clsx';
import React, { useEffect, useRef } from 'react';
import { FormattedMessage } from 'react-intl';
import { fetchGroupTimeline } from '@/actions/timelines';
import { useGroupStream } from '@/api/hooks/streaming/use-group-stream';
import { GroupTimelineColumn } from '@/columns/timeline';
import Avatar from '@/components/ui/avatar';
import HStack from '@/components/ui/hstack';
import Stack from '@/components/ui/stack';
import Timeline from '@/features/ui/components/timeline';
import { groupTimelineRoute } from '@/features/ui/router';
import { ComposeForm } from '@/features/ui/util/async-components';
import { useAppDispatch } from '@/hooks/use-app-dispatch';
import { useDraggedFiles } from '@/hooks/use-dragged-files';
import { useOwnAccount } from '@/hooks/use-own-account';
import { useGroupQuery } from '@/queries/groups/use-group';
import { useComposeActions, useUploadCompose } from '@/stores/compose';
import { useSettings } from '@/stores/settings';
interface IGroupTimeline {
groupId: string;
}
const GroupTimeline: React.FC<IGroupTimeline> = ({ groupId }) => {
const dispatch = useAppDispatch();
const handleLoadMore = () => {
dispatch(fetchGroupTimeline(groupId, {}, true));
};
useGroupStream(groupId);
useEffect(() => {
dispatch(fetchGroupTimeline(groupId, {}));
}, [groupId]);
return (
<Timeline
scrollKey='group_timeline'
timelineId={`group:${groupId}`}
onLoadMore={handleLoadMore}
emptyMessageText={
<FormattedMessage
id='empty_column.group'
defaultMessage='There are no posts in this group yet.'
/>
}
emptyMessageIcon={require('@phosphor-icons/core/regular/chat-centered-text.svg')}
showGroup={false}
/>
);
};
const GroupTimelinePage: React.FC = () => {
const { groupId } = groupTimelineRoute.useParams();
@ -59,7 +20,6 @@ const GroupTimelinePage: React.FC = () => {
const composeId = `group:${groupId}`;
const { data: account } = useOwnAccount();
const { experimentalTimeline } = useSettings();
const uploadCompose = useUploadCompose(composeId);
const { updateCompose } = useComposeActions();
const composer = useRef<HTMLDivElement>(null);
@ -111,11 +71,17 @@ const GroupTimelinePage: React.FC = () => {
</div>
)}
{experimentalTimeline ? (
<GroupTimelineColumn groupId={groupId} />
) : (
<GroupTimeline groupId={groupId} />
)}
<GroupTimelineColumn
groupId={groupId}
emptyMessageText={
<FormattedMessage
id='empty_column.group'
defaultMessage='There are no posts in this group yet.'
/>
}
emptyMessageIcon={require('@phosphor-icons/core/regular/chat-centered-text.svg')}
// showGroup={falsse}
/>
</Stack>
);
};

View File

@ -1,15 +1,11 @@
import React, { useEffect } from 'react';
import React from 'react';
import { FormattedMessage } from 'react-intl';
import { fetchHashtagTimeline, clearTimeline } from '@/actions/timelines';
import { useHashtagStream } from '@/api/hooks/streaming/use-hashtag-stream';
import { HashtagTimelineColumn } from '@/columns/timeline';
import List, { ListItem } from '@/components/list';
import Column from '@/components/ui/column';
import Toggle from '@/components/ui/toggle';
import Timeline from '@/features/ui/components/timeline';
import { hashtagTimelineRoute } from '@/features/ui/router';
import { useAppDispatch } from '@/hooks/use-app-dispatch';
import { useFeatures } from '@/hooks/use-features';
import { useLoggedIn } from '@/hooks/use-logged-in';
import {
@ -17,47 +13,11 @@ import {
useUnfollowHashtagMutation,
} from '@/queries/hashtags/use-followed-tags';
import { useHashtag } from '@/queries/hashtags/use-hashtag';
import { useSettings } from '@/stores/settings';
interface IHashtagTimeline {
hashtag: string;
}
const HashtagTimeline: React.FC<IHashtagTimeline> = ({ hashtag }) => {
const dispatch = useAppDispatch();
const handleLoadMore = () => {
dispatch(fetchHashtagTimeline(hashtag, {}, true));
};
useHashtagStream(hashtag);
useEffect(() => {
dispatch(clearTimeline(`hashtag:${hashtag}`));
dispatch(fetchHashtagTimeline(hashtag));
}, [hashtag]);
return (
<Timeline
loadMoreClassName='sm:pb-4 black:sm:pb-0 black:sm:mx-4'
scrollKey='hashtag_timeline'
timelineId={`hashtag:${hashtag}`}
onLoadMore={handleLoadMore}
emptyMessageText={
<FormattedMessage
id='empty_column.hashtag'
defaultMessage='There is nothing in this hashtag yet.'
/>
}
/>
);
};
const HashtagTimelinePage: React.FC = () => {
const { hashtag } = hashtagTimelineRoute.useParams();
const features = useFeatures();
const { experimentalTimeline } = useSettings();
const { data: tag } = useHashtag(hashtag);
const { isLoggedIn } = useLoggedIn();
@ -84,11 +44,15 @@ const HashtagTimelinePage: React.FC = () => {
</ListItem>
</List>
)}
{experimentalTimeline ? (
<HashtagTimelineColumn hashtag={hashtag} />
) : (
<HashtagTimeline hashtag={hashtag} />
)}
<HashtagTimelineColumn
hashtag={hashtag}
emptyMessageText={
<FormattedMessage
id='empty_column.hashtag'
defaultMessage='There is nothing in this hashtag yet.'
/>
}
/>
</Column>
);
};

View File

@ -1,66 +1,49 @@
import React, { useCallback, useEffect, useRef } from 'react';
import React from 'react';
import { defineMessages, useIntl, FormattedMessage } from 'react-intl';
import { fetchHomeTimeline } from '@/actions/timelines';
import { HomeTimelineColumn } from '@/columns/timeline';
import { Link } from '@/components/link';
import PullToRefresh from '@/components/pull-to-refresh';
import Column from '@/components/ui/column';
import Stack from '@/components/ui/stack';
import Text from '@/components/ui/text';
import Timeline from '@/features/ui/components/timeline';
import { useAppDispatch } from '@/hooks/use-app-dispatch';
import { useAppSelector } from '@/hooks/use-app-selector';
import { useFeatures } from '@/hooks/use-features';
import { useInstance } from '@/hooks/use-instance';
import { useSettings } from '@/stores/settings';
const messages = defineMessages({
title: { id: 'column.home', defaultMessage: 'Home' },
});
const HomeTimeline: React.FC = () => {
const dispatch = useAppDispatch();
// TODO restore this
// const isPartial = useAppSelector((state) => state.timelines.home?.isPartial);
// // Mastodon generates the feed in Redis, and can return a partial timeline
// // (HTTP 206) for new users. Poll until we get a full page of results.
// const checkIfReloadNeeded = useCallback((isPartial: boolean) => {
// if (isPartial) {
// polling.current = setInterval(() => {
// dispatch(fetchHomeTimeline());
// }, 3000);
// } else if (polling.current) {
// clearInterval(polling.current);
// polling.current = null;
// }
// return () => {
// if (polling.current) {
// clearInterval(polling.current);
// polling.current = null;
// }
// };
// }, []);
const HomeTimelinePage: React.FC = () => {
const intl = useIntl();
const features = useFeatures();
const instance = useInstance();
const polling = useRef<NodeJS.Timeout | null>(null);
const isPartial = useAppSelector((state) => state.timelines.home?.isPartial);
// Mastodon generates the feed in Redis, and can return a partial timeline
// (HTTP 206) for new users. Poll until we get a full page of results.
const checkIfReloadNeeded = useCallback((isPartial: boolean) => {
if (isPartial) {
polling.current = setInterval(() => {
dispatch(fetchHomeTimeline());
}, 3000);
} else if (polling.current) {
clearInterval(polling.current);
polling.current = null;
}
return () => {
if (polling.current) {
clearInterval(polling.current);
polling.current = null;
}
};
}, []);
const handleLoadMore = useCallback(() => dispatch(fetchHomeTimeline(true)), []);
const handleRefresh = useCallback(() => dispatch(fetchHomeTimeline(false)), []);
useEffect(() => checkIfReloadNeeded(isPartial), [isPartial]);
return (
<PullToRefresh onRefresh={handleRefresh}>
<Timeline
loadMoreClassName='sm:pb-4 black:sm:pb-0 black:sm:mx-4'
scrollKey='home_timeline'
onLoadMore={handleLoadMore}
timelineId='home'
<Column className='py-0' label={intl.formatMessage(messages.title)} withHeader={false}>
<HomeTimelineColumn
emptyMessageText={
<Stack space={1}>
<Text size='xl' weight='medium' align='center'>
@ -100,17 +83,6 @@ const HomeTimeline: React.FC = () => {
}
emptyMessageIcon={require('@phosphor-icons/core/regular/chat-centered-text.svg')}
/>
</PullToRefresh>
);
};
const HomeTimelinePage: React.FC = () => {
const intl = useIntl();
const { experimentalTimeline } = useSettings();
return (
<Column className='py-0' label={intl.formatMessage(messages.title)} withHeader={false}>
{experimentalTimeline ? <HomeTimelineColumn /> : <HomeTimeline />}
</Column>
);
};

View File

@ -12,11 +12,8 @@ import Stack from '@/components/ui/stack';
import { useInstance } from '@/hooks/use-instance';
import { useRegistrationStatus } from '@/hooks/use-registration-status';
import { About } from '@/pages/utils/about';
import { useSettings } from '@/stores/settings';
import { getTextDirection } from '@/utils/rtl';
import { CommunityTimeline } from './community-timeline';
interface ILogoText extends Pick<React.HTMLAttributes<HTMLHeadingElement>, 'className' | 'dir'> {
children: React.ReactNode;
}
@ -55,9 +52,9 @@ const SiteBanner: React.FC = () => {
const LandingTimelinePage = () => {
const instance = useInstance();
const { isOpen } = useRegistrationStatus();
const { experimentalTimeline } = useSettings();
const [timelineFailed, setTimelineFailed] = useState(false);
// todo fix this
const [timelineFailed, _setTimelineFailed] = useState(false);
const timelineEnabled = !instance.pleroma.metadata.restrict_unauthenticated.timelines.local;
@ -79,11 +76,16 @@ const LandingTimelinePage = () => {
</HStack>
{timelineEnabled && !timelineFailed ? (
experimentalTimeline ? (
<PublicTimelineColumn local />
) : (
<CommunityTimeline onTimelineFailed={() => setTimelineFailed(true)} />
)
<PublicTimelineColumn
local
emptyMessageText={
<FormattedMessage
id='empty_column.community'
defaultMessage='The local timeline is empty. Write something publicly to get the ball rolling!'
/>
}
emptyMessageIcon={require('@phosphor-icons/core/regular/chat-centered-text.svg')}
/>
) : (
<About slug='index' />
)}

View File

@ -1,67 +1,34 @@
import React, { useEffect } from 'react';
import React from 'react';
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
import { clearTimeline, fetchLinkTimeline } from '@/actions/timelines';
import { LinkTimelineColumn } from '@/columns/timeline';
import Column from '@/components/ui/column';
import Timeline from '@/features/ui/components/timeline';
import { linkTimelineRoute } from '@/features/ui/router';
import { useAppDispatch } from '@/hooks/use-app-dispatch';
import { useSettings } from '@/stores/settings';
const messages = defineMessages({
header: { id: 'column.link_timeline', defaultMessage: 'Posts linking to {url}' },
});
interface ILinkTimeline {
url: string;
}
const LinkTimeline: React.FC<ILinkTimeline> = ({ url }) => {
const dispatch = useAppDispatch();
const handleLoadMore = () => {
dispatch(fetchLinkTimeline(url, true));
};
useEffect(() => {
dispatch(clearTimeline(`link:${url}`));
dispatch(fetchLinkTimeline(url));
}, [url]);
return (
<Timeline
loadMoreClassName='sm:pb-4 black:sm:pb-0 black:sm:mx-4'
scrollKey='link_timeline'
timelineId={`link:${url}`}
onLoadMore={handleLoadMore}
emptyMessageText={
<FormattedMessage
id='empty_column.link_timeline'
defaultMessage='There are no posts with this link yet.'
/>
}
emptyMessageIcon={require('@phosphor-icons/core/regular/chat-centered-text.svg')}
/>
);
};
const LinkTimelinePage: React.FC = () => {
const { url } = linkTimelineRoute.useParams();
const decodedUrl = decodeURIComponent(url || '');
const intl = useIntl();
const { experimentalTimeline } = useSettings();
return (
<Column
label={intl.formatMessage(messages.header, { url: decodedUrl.replace(/^https?:\/\//, '') })}
>
{experimentalTimeline ? (
<LinkTimelineColumn url={decodedUrl} />
) : (
<LinkTimeline url={decodedUrl} />
)}
<LinkTimelineColumn
url={decodedUrl}
emptyMessageText={
<FormattedMessage
id='empty_column.link_timeline'
defaultMessage='There are no posts with this link yet.'
/>
}
emptyMessageIcon={require('@phosphor-icons/core/regular/chat-centered-text.svg')}
/>
</Column>
);
};

View File

@ -1,21 +1,16 @@
import { useNavigate } from '@tanstack/react-router';
import React, { useEffect } from 'react';
import React from 'react';
import { FormattedMessage, defineMessages, useIntl } from 'react-intl';
import { fetchListTimeline } from '@/actions/timelines';
import { useListStream } from '@/api/hooks/streaming/use-list-stream';
import { ListTimelineColumn } from '@/columns/timeline';
import DropdownMenu from '@/components/dropdown-menu';
import MissingIndicator from '@/components/missing-indicator';
import Button from '@/components/ui/button';
import Column from '@/components/ui/column';
import Spinner from '@/components/ui/spinner';
import Timeline from '@/features/ui/components/timeline';
import { listTimelineRoute } from '@/features/ui/router';
import { useAppDispatch } from '@/hooks/use-app-dispatch';
import { useDeleteList, useList } from '@/queries/accounts/use-lists';
import { useModalsActions } from '@/stores/modals';
import { useSettings } from '@/stores/settings';
const messages = defineMessages({
deleteHeading: { id: 'confirmations.delete_list.heading', defaultMessage: 'Delete list' },
@ -28,60 +23,10 @@ const messages = defineMessages({
deleteList: { id: 'lists.delete', defaultMessage: 'Delete list' },
});
interface IListTimeline {
listId: string;
}
const ListTimeline: React.FC<IListTimeline> = ({ listId }) => {
const dispatch = useAppDispatch();
const settings = useSettings();
const { openModal } = useModalsActions();
const onlyMedia = settings.timelines[`list:${listId}`]?.other.onlyMedia ?? false;
const handleLoadMore = () => {
dispatch(fetchListTimeline(listId, true));
};
useListStream(listId);
useEffect(() => {
dispatch(fetchListTimeline(listId, false));
}, [listId, onlyMedia]);
const handleEditClick = () => {
openModal('LIST_EDITOR', { listId });
};
return (
<Timeline
loadMoreClassName='sm:pb-4 black:sm:pb-0 black:sm:mx-4'
scrollKey={`list_${listId}_timeline`}
timelineId={`list:${listId}${onlyMedia ? ':media' : ''}`}
onLoadMore={handleLoadMore}
emptyMessageText={
<div>
<FormattedMessage
id='empty_column.list'
defaultMessage='There is nothing in this list yet. When members of this list create new posts, they will appear here.'
/>
<br />
<br />
<Button onClick={handleEditClick}>
<FormattedMessage id='list.click_to_add' defaultMessage='Click here to add people' />
</Button>
</div>
}
emptyMessageIcon={require('@phosphor-icons/core/regular/list-bullets.svg')}
/>
);
};
const ListTimelinePage: React.FC = () => {
const { listId } = listTimelineRoute.useParams();
const intl = useIntl();
const { experimentalTimeline } = useSettings();
const { openModal } = useModalsActions();
const navigate = useNavigate();
@ -146,11 +91,23 @@ const ListTimelinePage: React.FC = () => {
/>
}
>
{experimentalTimeline ? (
<ListTimelineColumn listId={listId} />
) : (
<ListTimeline listId={listId} />
)}
<ListTimelineColumn
listId={listId}
emptyMessageText={
<div>
<FormattedMessage
id='empty_column.list'
defaultMessage='There is nothing in this list yet. When members of this list create new posts, they will appear here.'
/>
<br />
<br />
<Button onClick={handleEditClick}>
<FormattedMessage id='list.click_to_add' defaultMessage='Click here to add people' />
</Button>
</div>
}
emptyMessageIcon={require('@phosphor-icons/core/regular/list-bullets.svg')}
/>
</Column>
);
};

View File

@ -1,16 +1,12 @@
import { Link } from '@tanstack/react-router';
import React, { useEffect } from 'react';
import React from 'react';
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
import { changeSetting } from '@/actions/settings';
import { fetchPublicTimeline } from '@/actions/timelines';
import { usePublicStream } from '@/api/hooks/streaming/use-public-stream';
import { PublicTimelineColumn } from '@/columns/timeline';
import PullToRefresh from '@/components/pull-to-refresh';
import Accordion from '@/components/ui/accordion';
import Column from '@/components/ui/column';
import PinnedHostsPicker from '@/features/remote-timeline/components/pinned-hosts-picker';
import Timeline from '@/features/ui/components/timeline';
import { useAppDispatch } from '@/hooks/use-app-dispatch';
import { useInstance } from '@/hooks/use-instance';
import { useSettings } from '@/stores/settings';
@ -20,52 +16,12 @@ const messages = defineMessages({
dismiss: { id: 'fediverse_tab.explanation_box.dismiss', defaultMessage: "Don't show again" },
});
const PublicTimeline = () => {
const dispatch = useAppDispatch();
const settings = useSettings();
const onlyMedia = settings.timelines.public?.other.onlyMedia ?? false;
const timelineId = 'public';
const handleLoadMore = () => {
dispatch(fetchPublicTimeline({ onlyMedia }, true));
};
const handleRefresh = () => dispatch(fetchPublicTimeline({ onlyMedia }));
usePublicStream({ onlyMedia });
useEffect(() => {
dispatch(fetchPublicTimeline({ onlyMedia }, true));
}, [onlyMedia]);
return (
<PullToRefresh onRefresh={handleRefresh}>
<Timeline
loadMoreClassName='sm:pb-4 black:sm:pb-0 black:sm:mx-4'
scrollKey={`${timelineId}_timeline`}
timelineId={`${timelineId}${onlyMedia ? ':media' : ''}`}
prefix='home'
onLoadMore={handleLoadMore}
emptyMessageText={
<FormattedMessage
id='empty_column.public'
defaultMessage='There is nothing here! Write something publicly, or manually follow users from other servers to fill it up'
/>
}
emptyMessageIcon={require('@phosphor-icons/core/regular/chat-centered-text.svg')}
/>
</PullToRefresh>
);
};
const PublicTimelinePage = () => {
const dispatch = useAppDispatch();
const intl = useIntl();
const instance = useInstance();
const settings = useSettings();
const { experimentalTimeline } = settings;
const explanationBoxExpanded = settings.explanationBox;
const showExplanationBox = settings.showExplanationBox;
@ -115,7 +71,15 @@ const PublicTimelinePage = () => {
/>
</Accordion>
)}
{experimentalTimeline ? <PublicTimelineColumn /> : <PublicTimeline />}
<PublicTimelineColumn
emptyMessageText={
<FormattedMessage
id='empty_column.public'
defaultMessage='There is nothing here! Write something publicly, or manually follow users from other servers to fill it up'
/>
}
emptyMessageIcon={require('@phosphor-icons/core/regular/chat-centered-text.svg')}
/>
</Column>
);
};

View File

@ -1,63 +1,20 @@
import { useNavigate } from '@tanstack/react-router';
import React, { useEffect } from 'react';
import React from 'react';
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
import { fetchPublicTimeline } from '@/actions/timelines';
import { useRemoteStream } from '@/api/hooks/streaming/use-remote-stream';
import { PublicTimelineColumn } from '@/columns/timeline';
import Column from '@/components/ui/column';
import HStack from '@/components/ui/hstack';
import IconButton from '@/components/ui/icon-button';
import Text from '@/components/ui/text';
import PinnedHostsPicker from '@/features/remote-timeline/components/pinned-hosts-picker';
import Timeline from '@/features/ui/components/timeline';
import { remoteTimelineRoute } from '@/features/ui/router';
import { useAppDispatch } from '@/hooks/use-app-dispatch';
import { useSettings } from '@/stores/settings';
const messages = defineMessages({
close: { id: 'remote_timeline.close', defaultMessage: 'Close remote timeline' },
});
interface IRemoteTimeline {
instance: string;
}
const RemoteTimeline: React.FC<IRemoteTimeline> = ({ instance }) => {
const dispatch = useAppDispatch();
const settings = useSettings();
const timelineId = 'remote';
const onlyMedia = settings.timelines.remote?.other.onlyMedia ?? false;
const handleLoadMore = () => {
dispatch(fetchPublicTimeline({ onlyMedia, instance }, true));
};
useRemoteStream({ instance, onlyMedia });
useEffect(() => {
dispatch(fetchPublicTimeline({ onlyMedia, instance }));
}, [onlyMedia, instance]);
return (
<Timeline
loadMoreClassName='sm:pb-4 black:sm:pb-0 black:sm:mx-4'
scrollKey={`${timelineId}_${instance}_timeline`}
timelineId={`${timelineId}${onlyMedia ? ':media' : ''}:${instance}`}
onLoadMore={handleLoadMore}
emptyMessageText={
<FormattedMessage
id='empty_column.remote'
defaultMessage='There is nothing here! Manually follow users from {instance} to fill it up.'
values={{ instance }}
/>
}
emptyMessageIcon={require('@phosphor-icons/core/regular/chat-centered-text.svg')}
/>
);
};
/** View statuses from a remote instance. */
const RemoteTimelinePage: React.FC = () => {
const { instance } = remoteTimelineRoute.useParams();
@ -66,7 +23,6 @@ const RemoteTimelinePage: React.FC = () => {
const navigate = useNavigate();
const settings = useSettings();
const { experimentalTimeline } = settings;
const pinned = settings.remote_timeline.pinnedHosts.includes(instance);
@ -97,11 +53,17 @@ const RemoteTimelinePage: React.FC = () => {
</HStack>
)}
{experimentalTimeline ? (
<PublicTimelineColumn instance={instance} />
) : (
<RemoteTimeline instance={instance} />
)}
<PublicTimelineColumn
emptyMessageText={
<FormattedMessage
id='empty_column.remote'
defaultMessage='There is nothing here! Manually follow users from {instance} to fill it up.'
values={{ instance }}
/>
}
emptyMessageIcon={require('@phosphor-icons/core/regular/chat-centered-text.svg')}
instance={instance}
/>
</Column>
);
};

View File

@ -1,44 +1,19 @@
import React, { useEffect } from 'react';
import React from 'react';
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
import { fetchWrenchedTimeline } from '@/actions/timelines';
import { WrenchedTimelineColumn } from '@/columns/timeline';
import PullToRefresh from '@/components/pull-to-refresh';
import Column from '@/components/ui/column';
import Timeline from '@/features/ui/components/timeline';
import { useAppDispatch } from '@/hooks/use-app-dispatch';
import { useSettings } from '@/stores/settings';
const messages = defineMessages({
title: { id: 'column.wrenched', defaultMessage: 'Recent wrenches timeline' },
});
const WrenchedTimeline = () => {
const dispatch = useAppDispatch();
const settings = useSettings();
const onlyMedia = settings.timelines.wrenched?.other.onlyMedia ?? false;
const timelineId = 'wrenched';
const handleLoadMore = () => {
dispatch(fetchWrenchedTimeline({ onlyMedia }, true));
};
const handleRefresh = () => dispatch(fetchWrenchedTimeline({ onlyMedia }, true));
useEffect(() => {
dispatch(fetchWrenchedTimeline({ onlyMedia }));
}, [onlyMedia]);
const WrenchedTimelinePage = () => {
const intl = useIntl();
return (
<PullToRefresh onRefresh={handleRefresh}>
<Timeline
loadMoreClassName='sm:pb-4 black:sm:pb-0 black:sm:mx-4'
scrollKey={`${timelineId}_timeline`}
timelineId={`${timelineId}${onlyMedia ? ':media' : ''}`}
prefix='home'
onLoadMore={handleLoadMore}
<Column className='-mt-3 sm:mt-0' label={intl.formatMessage(messages.title)}>
<WrenchedTimelineColumn
emptyMessageText={
<FormattedMessage
id='empty_column.wrenched'
@ -47,19 +22,6 @@ const WrenchedTimeline = () => {
}
emptyMessageIcon={require('@phosphor-icons/core/regular/wrench.svg')}
/>
</PullToRefresh>
);
};
const WrenchedTimelinePage = () => {
const intl = useIntl();
const settings = useSettings();
const { experimentalTimeline } = settings;
return (
<Column className='-mt-3 sm:mt-0' label={intl.formatMessage(messages.title)}>
{experimentalTimeline ? <WrenchedTimelineColumn /> : <WrenchedTimeline />}
</Column>
);
};

View File

@ -122,8 +122,6 @@ const settingsSchema = v.object({
saved: v.fallback(v.boolean(), true),
demo: v.fallback(v.boolean(), false),
experimentalTimeline: v.fallback(v.boolean(), false),
});
type Settings = v.InferOutput<typeof settingsSchema>;