nicolium: fix sorting of items in filtered timelines

Signed-off-by: nicole mikołajczyk <git@mkljczk.pl>
This commit is contained in:
nicole mikołajczyk
2026-03-12 23:12:38 +01:00
parent 04a4a144cf
commit b4f7db3092
3 changed files with 110 additions and 39 deletions

View File

@ -33,9 +33,10 @@ import {
} from '@/queries/timelines/use-timelines';
import { useSettings } from '@/stores/settings';
import { selectChild } from '@/utils/scroll-utils';
import { hasActiveFilters, sortFilteredTimeline } from '@/utils/timeline-filter';
import type { FilterContextType } from '@/queries/settings/use-filters';
import type { Settings } from '@/schemas/frontend-settings';
import type { TimelineFilters } from '@/schemas/frontend-settings';
import type { TimelineEntry } from '@/stores/timelines';
import type { VirtuosoHandle } from 'react-virtuoso';
@ -361,7 +362,7 @@ type IBaseTimeline = Pick<
'emptyMessageIcon' | 'emptyMessageText' | 'onTopItemChanged'
> & {
featuredStatusIds?: Array<string>;
filters?: Settings['timelines'][string];
filters?: TimelineFilters;
};
interface ITimeline extends IBaseTimeline {
@ -478,45 +479,32 @@ const Timeline: React.FC<ITimeline> = ({
}
}
entries
.map((entry) => {
if (entry.type === 'status') {
return {
...entry,
filtered:
(filters?.showDirect === false && entry.isDirect) ||
(filters?.showReblogs === false && entry.isReblog) ||
(filters?.showReplies === false && entry.isReply) ||
(filters?.showQuotes === false && entry.isQuote) ||
(filters?.showNonMedia === false && !entry.hasMedia),
};
}
return entry;
})
.forEach((entry, entryIndex, entries) => {
if (entry.type === 'status' && entry.filtered) {
return;
}
const processedEntries = hasActiveFilters(filters)
? sortFilteredTimeline(entries, filters)
: entries;
if (entry.type === 'status') {
const previousEntry = entries[entryIndex - 1];
const nextEntry = entries[entryIndex + 1];
processedEntries.forEach((entry, entryIndex, arr) => {
if (entry.type === 'status') {
const previousEntry = arr[entryIndex - 1];
const nextEntry = arr[entryIndex + 1];
rendered.push(
renderEntry(entry, rendered.length, {
isConnectedTop:
!!entry.isConnectedTop &&
previousEntry?.type === 'status' &&
!previousEntry.filtered,
isConnectedBottom:
!!entry.isConnectedBottom && nextEntry?.type === 'status' && !nextEntry.filtered,
}),
);
return;
}
rendered.push(
renderEntry(entry, rendered.length, {
isConnectedTop:
!!entry.isConnectedTop &&
previousEntry?.type === 'status' &&
!!previousEntry.isConnectedBottom,
isConnectedBottom:
!!entry.isConnectedBottom &&
nextEntry?.type === 'status' &&
!!nextEntry.isConnectedTop,
}),
);
return;
}
rendered.push(renderEntry(entry, rendered.length));
});
rendered.push(renderEntry(entry, rendered.length));
});
return rendered;
}, [entries, contextType, timelineId, featuredStatusIds, filters]);

View File

@ -125,5 +125,6 @@ const settingsSchema = v.object({
});
type Settings = v.InferOutput<typeof settingsSchema>;
type TimelineFilters = Settings['timelines'][string];
export { settingsSchema, type Settings };
export { settingsSchema, type Settings, type TimelineFilters };

View File

@ -0,0 +1,82 @@
import { compareId } from '@/utils/comparators';
import type { TimelineFilters } from '@/schemas/frontend-settings';
import type { TimelineEntry } from '@/stores/timelines';
type StatusEntry = Extract<TimelineEntry, { type: 'status' }>;
const isEntryFiltered = (entry: StatusEntry, filters: TimelineFilters): boolean =>
(filters?.showDirect === false && entry.isDirect) ||
(filters?.showReblogs === false && entry.isReblog) ||
(filters?.showReplies === false && entry.isReply) ||
(filters?.showQuotes === false && entry.isQuote) ||
(filters?.showNonMedia === false && !entry.hasMedia);
const hasActiveFilters = (filters: TimelineFilters | undefined): filters is TimelineFilters =>
!!filters &&
(filters.showDirect === false ||
filters.showReblogs === false ||
filters.showReplies === false ||
filters.showQuotes === false ||
filters.showNonMedia === false);
const sortFilteredTimeline = (
entries: Array<TimelineEntry>,
filters: TimelineFilters,
): Array<TimelineEntry> => {
const result: Array<TimelineEntry> = [];
let collectedGroups: Array<{ sortKey: string; entries: Array<StatusEntry> }> = [];
let currentGroup: Array<StatusEntry> = [];
const endGroup = () => {
if (currentGroup.length === 0) return;
let sortKey = currentGroup[0].originalId;
for (let i = 1; i < currentGroup.length; i++) {
if (compareId(currentGroup[i].originalId, sortKey) > 0) {
sortKey = currentGroup[i].originalId;
}
}
collectedGroups.push({ sortKey, entries: currentGroup });
currentGroup = [];
};
const endSection = () => {
endGroup();
if (collectedGroups.length > 1) {
collectedGroups.sort((a, b) => compareId(b.sortKey, a.sortKey));
}
for (const group of collectedGroups) {
for (const entry of group.entries) {
result.push(entry);
}
}
collectedGroups = [];
};
for (const entry of entries) {
if (entry.type === 'status' && isEntryFiltered(entry, filters)) continue;
if (entry.type !== 'status') {
endSection();
result.push(entry);
continue;
}
if (entry.isConnectedTop && currentGroup.length > 0) {
currentGroup.push(entry);
} else {
endGroup();
currentGroup = [entry];
}
}
endSection();
return result;
};
export { sortFilteredTimeline, hasActiveFilters };