nicolium: add a way to use the new timeline

Signed-off-by: nicole mikołajczyk <git@mkljczk.pl>
This commit is contained in:
nicole mikołajczyk
2026-03-01 21:07:48 +01:00
parent 335910611c
commit c9c3d50814
5 changed files with 203 additions and 43 deletions

View File

@ -0,0 +1,123 @@
import clsx from 'clsx';
import React from 'react';
import LoadMore from '@/components/load-more';
import ScrollableList from '@/components/scrollable-list';
import Status from '@/components/statuses/status';
import Tombstone from '@/components/statuses/tombstone';
import PlaceholderStatus from '@/features/placeholder/components/placeholder-status';
import { useStatus } from '@/queries/statuses/use-status';
import { type TimelineEntry, useHomeTimeline } from '@/queries/timelines/use-home-timeline';
import type { FilterContextType } from '@/queries/settings/use-filters';
interface ITimelineStatus {
id: string;
contextType?: FilterContextType;
isConnectedTop?: boolean;
isConnectedBottom?: boolean;
onMoveUp?: (id: string) => void;
onMoveDown?: (id: string) => void;
}
/** Status with reply-connector in threads. */
const TimelineStatus: React.FC<ITimelineStatus> = (props): React.JSX.Element => {
const { id, isConnectedTop, isConnectedBottom } = props;
const statusQuery = useStatus(id, { withFilteredResults: true });
if (statusQuery.data?.deleted) {
return (
<div className='py-4 pb-8'>
<Tombstone id={id} onMoveUp={props.onMoveUp} onMoveDown={props.onMoveDown} deleted />
</div>
);
}
const renderConnector = (): React.JSX.Element | null => {
const isConnected = isConnectedTop || isConnectedBottom;
if (!isConnected) return null;
return (
<div
className={clsx(
'absolute left-10 z-[1] hidden w-0.5 bg-gray-200 black:bg-gray-800 dark:bg-primary-800 rtl:left-auto rtl:right-5',
{
'top-20 !block h-[calc(100%-42px-8px-1rem)]': isConnectedBottom,
},
)}
/>
);
};
return (
<div
className={clsx('relative', {
'timeline-status-connected': isConnectedBottom,
'border-b border-solid border-gray-200 dark:border-gray-800': !isConnectedBottom,
})}
>
{renderConnector()}
{statusQuery.isPending ? (
<PlaceholderStatus variant='default' />
) : statusQuery.data ? (
<Status status={statusQuery.data!} {...props} />
) : null}
</div>
);
};
const NewTimelineColumn = () => {
const { data, handleLoadMore, isLoading } = useHomeTimeline();
const renderEntry = (entry: TimelineEntry) => {
if (entry.type === 'status') {
return (
<TimelineStatus
key={entry.id}
id={entry.id}
isConnectedTop={entry.isConnectedTop}
isConnectedBottom={entry.isConnectedBottom}
contextType='home'
// onMoveUp={handleMoveUp}
// onMoveDown={handleMoveDown}
// contextType={timelineId}
// showGroup={showGroup}
// variant={divideType === 'border' ? 'slim' : 'rounded'}
// fromBookmarks={other.scrollKey === 'bookmarked_statuses'}
/>
);
}
if (entry.type === 'page-end' || entry.type === 'page-start') {
return (
<div className='m-4'>
<LoadMore key='load-more' onClick={() => handleLoadMore(entry)} disabled={isLoading} />
</div>
);
}
};
return (
<ScrollableList
id='status-list'
key='scrollable-list'
isLoading={isLoading}
showLoading={isLoading && !data}
placeholderComponent={() => <PlaceholderStatus variant={'slim'} />}
placeholderCount={20}
// className={className}
// listClassName={clsx('divide-y divide-solid divide-gray-200 dark:divide-gray-800', {
// 'divide-none': divideType !== 'border',
// })}
// itemClassName={clsx({
// 'pb-3': divideType !== 'border',
// })}
// {...other}
>
{(data || []).map(renderEntry)}
</ScrollableList>
);
};
export { NewTimelineColumn };

View File

@ -902,6 +902,29 @@ 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

@ -1556,6 +1556,8 @@
"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

@ -2,6 +2,7 @@ import React, { useCallback, useEffect, useRef } from 'react';
import { defineMessages, useIntl, FormattedMessage } from 'react-intl';
import { fetchHomeTimeline } from '@/actions/timelines';
import { NewTimelineColumn } from '@/columns/timeline';
import { Link } from '@/components/link';
import PullToRefresh from '@/components/pull-to-refresh';
import Column from '@/components/ui/column';
@ -12,13 +13,13 @@ 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 HomeTimelinePage: React.FC = () => {
const intl = useIntl();
const HomeTimeline: React.FC = () => {
const dispatch = useAppDispatch();
const features = useFeatures();
const instance = useInstance();
@ -54,53 +55,62 @@ const HomeTimelinePage: React.FC = () => {
useEffect(() => checkIfReloadNeeded(isPartial), [isPartial]);
return (
<Column className='py-0' label={intl.formatMessage(messages.title)} withHeader={false}>
<PullToRefresh onRefresh={handleRefresh}>
<Timeline
loadMoreClassName='sm:pb-4 black:sm:pb-0 black:sm:mx-4'
scrollKey='home_timeline'
onLoadMore={handleLoadMore}
timelineId='home'
emptyMessageText={
<Stack space={1}>
<Text size='xl' weight='medium' align='center'>
<FormattedMessage
id='empty_column.home.title'
defaultMessage="You're not following anyone yet"
/>
</Text>
<PullToRefresh onRefresh={handleRefresh}>
<Timeline
loadMoreClassName='sm:pb-4 black:sm:pb-0 black:sm:mx-4'
scrollKey='home_timeline'
onLoadMore={handleLoadMore}
timelineId='home'
emptyMessageText={
<Stack space={1}>
<Text size='xl' weight='medium' align='center'>
<FormattedMessage
id='empty_column.home.title'
defaultMessage="You're not following anyone yet"
/>
</Text>
<Text theme='muted' align='center'>
<FormattedMessage
id='empty_column.home.subtitle'
defaultMessage='{siteTitle} gets more interesting once you follow other users.'
values={{ siteTitle: instance.title }}
/>
</Text>
{features.federating && (
<Text theme='muted' align='center'>
<FormattedMessage
id='empty_column.home.subtitle'
defaultMessage='{siteTitle} gets more interesting once you follow other users.'
values={{ siteTitle: instance.title }}
id='empty_column.home'
defaultMessage='Or you can visit {public} to get started and meet other users.'
values={{
public: (
<Link to='/timeline/local'>
<FormattedMessage
id='empty_column.home.local_tab'
defaultMessage='the Local tab'
/>
</Link>
),
}}
/>
</Text>
)}
</Stack>
}
emptyMessageIcon={require('@phosphor-icons/core/regular/chat-centered-text.svg')}
/>
</PullToRefresh>
);
};
{features.federating && (
<Text theme='muted' align='center'>
<FormattedMessage
id='empty_column.home'
defaultMessage='Or you can visit {public} to get started and meet other users.'
values={{
public: (
<Link to='/timeline/local'>
<FormattedMessage
id='empty_column.home.local_tab'
defaultMessage='the Local tab'
/>
</Link>
),
}}
/>
</Text>
)}
</Stack>
}
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 ? <NewTimelineColumn /> : <HomeTimeline />}
</Column>
);
};

View File

@ -123,6 +123,8 @@ 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>;