mirror of
https://github.com/svemagie/indiekit-endpoint-activitypub.git
synced 2026-04-02 15:44:58 +02:00
Extract shared item-processing.js module with postProcessItems(), applyModerationFilters(), buildInteractionMap(), applyTabFilter(), renderItemCards(), and loadModerationData(). All controllers (reader, api-timeline, explore, hashtag-explore, tag-timeline) now flow through the same pipeline. Unify Alpine.js infinite scroll into single parameterized apInfiniteScroll component configured via data attributes, replacing the separate apExploreScroll component. Also adds fetchAndStoreQuote() for quote enrichment and on-demand quote fetching in post-detail controller. Bump version to 2.5.0. Confab-Link: http://localhost:8080/sessions/e9d666ac-3c90-4298-9e92-9ac9d142bc06
138 lines
4.2 KiB
JavaScript
138 lines
4.2 KiB
JavaScript
/**
|
|
* JSON API timeline endpoint — returns pre-rendered HTML cards for infinite scroll AJAX loads.
|
|
*/
|
|
|
|
import { getTimelineItems, countNewItems, markItemsRead } from "../storage/timeline.js";
|
|
import { getToken, validateToken } from "../csrf.js";
|
|
import { postProcessItems, applyTabFilter, loadModerationData, renderItemCards } from "../item-processing.js";
|
|
|
|
export function apiTimelineController(mountPath) {
|
|
return async (request, response, next) => {
|
|
try {
|
|
const { application } = request.app.locals;
|
|
const collections = {
|
|
ap_timeline: application?.collections?.get("ap_timeline"),
|
|
};
|
|
|
|
// Query parameters
|
|
const tab = request.query.tab || "notes";
|
|
const tag = typeof request.query.tag === "string" ? request.query.tag.trim() : "";
|
|
const before = request.query.before;
|
|
const limit = 20;
|
|
|
|
// Build storage query options
|
|
const unread = request.query.unread === "1";
|
|
const options = { before, limit, unread };
|
|
|
|
if (tag) {
|
|
options.tag = tag;
|
|
} else {
|
|
if (tab === "notes") {
|
|
options.type = "note";
|
|
options.excludeReplies = true;
|
|
} else if (tab === "articles") {
|
|
options.type = "article";
|
|
} else if (tab === "boosts") {
|
|
options.type = "boost";
|
|
}
|
|
}
|
|
|
|
const result = await getTimelineItems(collections, options);
|
|
|
|
// Tab filtering for types not supported by storage layer
|
|
const tabFiltered = tag ? result.items : applyTabFilter(result.items, tab);
|
|
|
|
// Shared processing pipeline: moderation, quote stripping, interactions
|
|
const modCollections = {
|
|
ap_muted: application?.collections?.get("ap_muted"),
|
|
ap_blocked: application?.collections?.get("ap_blocked"),
|
|
ap_profile: application?.collections?.get("ap_profile"),
|
|
};
|
|
const moderation = await loadModerationData(modCollections);
|
|
|
|
const { items, interactionMap } = await postProcessItems(tabFiltered, {
|
|
moderation,
|
|
interactionsCol: application?.collections?.get("ap_interactions"),
|
|
});
|
|
|
|
const csrfToken = getToken(request.session);
|
|
const html = await renderItemCards(items, request, {
|
|
...response.locals,
|
|
mountPath,
|
|
csrfToken,
|
|
interactionMap,
|
|
});
|
|
|
|
response.json({
|
|
html,
|
|
before: result.before,
|
|
});
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
};
|
|
}
|
|
|
|
/**
|
|
* GET /admin/reader/api/timeline/count-new — count items newer than a given date.
|
|
*/
|
|
export function countNewController() {
|
|
return async (request, response, next) => {
|
|
try {
|
|
const { application } = request.app.locals;
|
|
const collections = {
|
|
ap_timeline: application?.collections?.get("ap_timeline"),
|
|
};
|
|
|
|
const after = request.query.after;
|
|
const tab = request.query.tab || "notes";
|
|
|
|
const options = {};
|
|
if (tab === "notes") {
|
|
options.type = "note";
|
|
options.excludeReplies = true;
|
|
} else if (tab === "articles") {
|
|
options.type = "article";
|
|
} else if (tab === "boosts") {
|
|
options.type = "boost";
|
|
}
|
|
|
|
const count = await countNewItems(collections, after, options);
|
|
response.json({ count });
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
};
|
|
}
|
|
|
|
/**
|
|
* POST /admin/reader/api/timeline/mark-read — mark items as read by UID array.
|
|
*/
|
|
export function markReadController() {
|
|
return async (request, response, next) => {
|
|
try {
|
|
if (!validateToken(request)) {
|
|
return response.status(403).json({ success: false, error: "Invalid CSRF token" });
|
|
}
|
|
|
|
const { uids } = request.body;
|
|
if (!Array.isArray(uids) || uids.length === 0) {
|
|
return response.status(400).json({ success: false, error: "Missing uids array" });
|
|
}
|
|
|
|
// Cap batch size to prevent abuse
|
|
const batch = uids.slice(0, 100).filter((uid) => typeof uid === "string");
|
|
|
|
const { application } = request.app.locals;
|
|
const collections = {
|
|
ap_timeline: application?.collections?.get("ap_timeline"),
|
|
};
|
|
|
|
const updated = await markItemsRead(collections, batch);
|
|
response.json({ success: true, updated });
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
};
|
|
}
|