nicolium: restore pinned posts display

Signed-off-by: nicole mikołajczyk <git@mkljczk.pl>
This commit is contained in:
nicole mikołajczyk
2026-03-05 20:55:36 +01:00
parent 5731b975e3
commit 3da955dd7e
3 changed files with 87 additions and 81 deletions

View File

@ -1,6 +1,6 @@
import { Link } from '@tanstack/react-router';
import clsx from 'clsx';
import React, { useRef, useState } from 'react';
import React, { useMemo, useRef, useState } from 'react';
import { defineMessages, FormattedList, FormattedMessage, useIntl } from 'react-intl';
import ScrollTopButton from '@/components/scroll-top-button';
@ -57,6 +57,18 @@ const messages = defineMessages({
},
});
const SkipPinned: React.FC<React.ComponentProps<'button'>> = ({ onClick }) => {
return (
<button className='⁂-skip-pinned' onClick={onClick}>
<Icon src={require('@phosphor-icons/core/regular/arrow-line-down.svg')} />
<p>
<FormattedMessage id='status.skip_pinned' defaultMessage='Skip pinned posts' />
</p>
</button>
);
};
const PlaceholderTimelineStatus = () => (
<div className='⁂-timeline-status relative border-b border-solid border-gray-200 dark:border-gray-800'>
<PlaceholderStatus variant='slim' />
@ -284,6 +296,7 @@ interface ITimelineStatus {
isConnectedBottom?: boolean;
onMoveUp?: (id: string) => void | boolean;
onMoveDown?: (id: string) => void | boolean;
featured?: boolean;
}
/** Status with reply-connector in threads. */
@ -354,14 +367,21 @@ const TimelineStatus: React.FC<ITimelineStatus> = (props): React.JSX.Element =>
type IBaseTimeline = Pick<
IScrollableList,
'emptyMessageIcon' | 'emptyMessageText' | 'onTopItemChanged'
>;
> & {
featuredStatusIds?: Array<string>;
};
interface ITimeline extends IBaseTimeline {
query: ReturnType<typeof useHomeTimeline>;
contextType?: FilterContextType;
}
const Timeline: React.FC<ITimeline> = ({ query, contextType = 'public', ...props }) => {
const Timeline: React.FC<ITimeline> = ({
query,
contextType = 'public',
featuredStatusIds,
...props
}) => {
const node = useRef<VirtuosoHandle | null>(null);
const {
@ -376,16 +396,36 @@ const Timeline: React.FC<ITimeline> = ({ query, contextType = 'public', ...props
hasNextPage,
} = query;
const handleMoveUp = (index: number) =>
const handleMoveUp = (index: number) => {
console.log(index);
selectChild(index - 1, node, document.getElementById('status-list') ?? undefined);
};
const handleMoveDown = (index: number) =>
const handleMoveDown = (index: number) => {
console.log(index);
selectChild(
index + 1,
node,
document.getElementById('status-list') ?? undefined,
entries.length,
);
};
const handleSkipPinned = () => {
const skipPinned = () => {
selectChild(
featuredStatusIds?.length ?? 0,
node,
document.getElementById('status-list') ?? undefined,
(featuredStatusIds?.length ?? 0) + entries.length,
'start',
);
};
skipPinned();
setTimeout(() => skipPinned, 0);
};
const renderEntry = (entry: TimelineEntry, index: number) => {
if (entry.type === 'status') {
@ -417,6 +457,34 @@ const Timeline: React.FC<ITimeline> = ({ query, contextType = 'public', ...props
}
};
const renderedEntries = useMemo(() => {
const rendered = [];
if (featuredStatusIds && featuredStatusIds.length > 0) {
for (const id of featuredStatusIds) {
const index = rendered.length;
rendered.push(
<TimelineStatus
key={id}
id={id}
contextType={contextType}
onMoveUp={() => handleMoveUp(index)}
onMoveDown={() => handleMoveDown(index)}
rebloggedBy={[]}
timelineId={timelineId}
featured
/>,
);
}
}
for (const entry of entries) {
rendered.push(renderEntry(entry, rendered.length));
}
return rendered;
}, [entries, contextType, timelineId, featuredStatusIds]);
return (
<>
<Portal>
@ -427,6 +495,9 @@ const Timeline: React.FC<ITimeline> = ({ query, contextType = 'public', ...props
liveRegionMessage={messages.queueLiveRegion}
/>
</Portal>
{featuredStatusIds && featuredStatusIds.length > 3 && entries?.length > 0 && (
<SkipPinned onClick={handleSkipPinned} />
)}
<ScrollableList
id='status-list'
key='scrollable-list'
@ -440,7 +511,7 @@ const Timeline: React.FC<ITimeline> = ({ query, contextType = 'public', ...props
onLoadMore={fetchNextPage}
{...props}
>
{(entries || []).map(renderEntry)}
{renderedEntries}
</ScrollableList>
</>
);

View File

@ -11,22 +11,8 @@ import PendingStatus from '@/features/ui/components/pending-status';
import { timelineToFilterContextType } from '@/queries/settings/use-filters';
import { selectChild } from '@/utils/scroll-utils';
import Icon from '../ui/icon';
import type { VirtuosoHandle } from 'react-virtuoso';
const SkipPinned: React.FC<React.ComponentProps<'button'>> = ({ onClick }) => {
return (
<button className='⁂-skip-pinned' onClick={onClick}>
<Icon src={require('@phosphor-icons/core/regular/arrow-line-down.svg')} />
<p>
<FormattedMessage id='status.skip_pinned' defaultMessage='Skip pinned posts' />
</p>
</button>
);
};
interface IStatusList extends Omit<IScrollableList, 'onLoadMore' | 'children'> {
/** Unique key to preserve the scroll position when navigating back. */
scrollKey: string;
@ -34,8 +20,6 @@ interface IStatusList extends Omit<IScrollableList, 'onLoadMore' | 'children'> {
statusIds: Array<string>;
/** Last _unfiltered_ status ID (maxId) for pagination. */
lastStatusId?: string;
/** Pinned statuses to show at the top of the feed. */
featuredStatusIds?: Array<string>;
/** Pagination callback when the end of the list is reached. */
onLoadMore?: (lastStatusId: string) => void;
/** Whether the data is currently being fetched. */
@ -54,7 +38,6 @@ interface IStatusList extends Omit<IScrollableList, 'onLoadMore' | 'children'> {
const StatusList: React.FC<IStatusList> = ({
statusIds,
lastStatusId,
featuredStatusIds,
onLoadMore,
timelineId,
isLoading,
@ -67,23 +50,17 @@ const StatusList: React.FC<IStatusList> = ({
const contextType = timelineToFilterContextType(timelineId);
const getFeaturedStatusCount = () => featuredStatusIds?.length ?? 0;
const getCurrentStatusIndex = (id: string, featured: boolean): number => {
if (featured) {
return featuredStatusIds?.findIndex((key) => key === id) ?? 0;
} else {
return statusIds.findIndex((key) => key === id) + getFeaturedStatusCount();
}
const getCurrentStatusIndex = (id: string): number => {
return statusIds.findIndex((key) => key === id);
};
const handleMoveUp = (id: string, featured: boolean = false) => {
const elementIndex = getCurrentStatusIndex(id, featured) - 1;
const handleMoveUp = (id: string) => {
const elementIndex = getCurrentStatusIndex(id) - 1;
selectChild(elementIndex, node, document.getElementById('status-list') ?? undefined);
};
const handleMoveDown = (id: string, featured: boolean = false) => {
const elementIndex = getCurrentStatusIndex(id, featured) + 1;
const handleMoveDown = (id: string) => {
const elementIndex = getCurrentStatusIndex(id) + 1;
selectChild(
elementIndex,
node,
@ -106,22 +83,6 @@ const StatusList: React.FC<IStatusList> = ({
[onLoadMore, lastStatusId, statusIds.at(-1)],
);
const handleSkipPinned = () => {
const skipPinned = () => {
selectChild(
getFeaturedStatusCount(),
node,
document.getElementById('status-list') ?? undefined,
scrollableContent.length,
'start',
);
};
skipPinned();
setTimeout(() => skipPinned, 0);
};
const renderLoadGap = (index: number) => {
const ids = statusIds;
const nextId = ids[index + 1];
@ -155,23 +116,6 @@ const StatusList: React.FC<IStatusList> = ({
};
const scrollableContent = useMemo(() => {
const renderFeaturedStatuses = (): React.ReactNode[] => {
if (!featuredStatusIds) return [];
return featuredStatusIds.map((statusId) => (
<StatusContainer
key={`f-${statusId}`}
id={statusId}
featured
onMoveUp={handleMoveUp}
onMoveDown={handleMoveDown}
contextType={contextType}
showGroup={showGroup}
variant='slim'
/>
));
};
const renderStatuses = (): React.ReactNode[] => {
if (isLoading || statusIds.length > 0) {
return statusIds.reduce((acc, statusId, index) => {
@ -193,15 +137,10 @@ const StatusList: React.FC<IStatusList> = ({
}
};
const featuredStatuses = renderFeaturedStatuses();
const statuses = renderStatuses();
if (featuredStatuses && statuses) {
return featuredStatuses.concat(statuses);
} else {
return statuses;
}
}, [featuredStatusIds, statusIds, isLoading, timelineId, showGroup]);
return statuses;
}, [statusIds, isLoading, timelineId, showGroup]);
if (isPartial) {
return (
@ -226,9 +165,6 @@ const StatusList: React.FC<IStatusList> = ({
return (
<>
{featuredStatusIds && featuredStatusIds.length > 3 && statusIds.length > 0 && (
<SkipPinned onClick={handleSkipPinned} />
)}
<ScrollableList
id='status-list'
key='scrollable-list'

View File

@ -18,8 +18,7 @@ const AccountTimelinePage: React.FC = () => {
const features = useFeatures();
const { data: account, isPending } = useAccountLookup(username);
const { data: _featuredStatusIds } = usePinnedStatuses(account?.id || '');
const { data: featuredStatusIds } = usePinnedStatuses(account?.id || '');
const isBlocked = account?.relationship?.blocked_by && !features.blockersVisible;
@ -51,7 +50,7 @@ const AccountTimelinePage: React.FC = () => {
<AccountTimelineColumn
accountId={account.id}
excludeReplies={!withReplies}
// featuredStatusIds={showPins ? featuredStatusIds : undefined}
featuredStatusIds={!withReplies ? featuredStatusIds : undefined}
emptyMessageText={
<FormattedMessage id='empty_column.account_timeline' defaultMessage='No posts here!' />
}