* feat: paginate and merge frequent activities * feat: due date activities * fix: requested changes fixes * refactor: slight reshuffle and improve types * chore: translations --------- Co-authored-by: Henry <henry_ball@hotmail.co.uk>
110 lines
2.8 KiB
TypeScript
110 lines
2.8 KiB
TypeScript
import type { PaginatedActivitiesResult } from "@kan/db/repository/cardActivity.repo";
|
|
import type { ActivityType } from "@kan/db/schema";
|
|
|
|
type ActivityBase = PaginatedActivitiesResult["activities"][number];
|
|
|
|
interface Activity extends ActivityBase {
|
|
mergeCount?: number; // number of activities merged into this one
|
|
mergedLabels?: string[]; // list of label names when merging label activities
|
|
}
|
|
|
|
// types that can be merged with simple count
|
|
const MERGEABLE_COUNT_TYPES: readonly ActivityType[] = [
|
|
"card.updated.description",
|
|
];
|
|
|
|
// types that merge with a list of items
|
|
const MERGEABLE_LIST_TYPES: readonly ActivityType[] = [
|
|
"card.updated.label.added",
|
|
"card.updated.label.removed",
|
|
];
|
|
|
|
const MERGE_TIME_WINDOW_MS = 5 * 60 * 1000; // 5 minutes window for merging activities
|
|
|
|
export function mergeActivities(activities: Activity[]): Activity[] {
|
|
if (activities.length === 0) return [];
|
|
|
|
const merged: Activity[] = [];
|
|
let i = 0;
|
|
|
|
while (i < activities.length) {
|
|
const current = activities[i];
|
|
if (!current) {
|
|
i++;
|
|
continue;
|
|
}
|
|
|
|
const currentType = current.type;
|
|
|
|
const isCountMergeable = MERGEABLE_COUNT_TYPES.includes(currentType);
|
|
const isListMergeable = MERGEABLE_LIST_TYPES.includes(currentType);
|
|
const canMerge = (isCountMergeable || isListMergeable) && current.user?.id;
|
|
|
|
if (!canMerge) {
|
|
merged.push(current);
|
|
i++;
|
|
continue;
|
|
}
|
|
|
|
const group: Activity[] = [current];
|
|
let j = i + 1;
|
|
|
|
while (j < activities.length) {
|
|
const next = activities[j];
|
|
if (!next) break;
|
|
|
|
const timeDiff =
|
|
new Date(next.createdAt).getTime() -
|
|
new Date(current.createdAt).getTime();
|
|
|
|
if (
|
|
next.type === current.type &&
|
|
next.user?.id === current.user?.id &&
|
|
timeDiff >= 0 && // next is newer or same time
|
|
timeDiff <= MERGE_TIME_WINDOW_MS
|
|
) {
|
|
group.push(next);
|
|
j++;
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (group.length > 1) {
|
|
const oldest = group[0];
|
|
const latest = group[group.length - 1];
|
|
|
|
if (oldest && latest) {
|
|
if (isListMergeable) {
|
|
const labelNames = group
|
|
.map((a) => a.label?.name)
|
|
.filter((name): name is string => !!name);
|
|
|
|
merged.push({
|
|
...oldest,
|
|
createdAt: oldest.createdAt,
|
|
mergeCount: group.length,
|
|
mergedLabels: labelNames,
|
|
});
|
|
} else {
|
|
merged.push({
|
|
...oldest,
|
|
createdAt: oldest.createdAt,
|
|
toDescription: latest.toDescription,
|
|
fromDescription: oldest.fromDescription,
|
|
mergeCount: group.length,
|
|
});
|
|
}
|
|
} else {
|
|
merged.push(current);
|
|
}
|
|
} else {
|
|
merged.push(current);
|
|
}
|
|
|
|
i = j;
|
|
}
|
|
|
|
return merged;
|
|
}
|