mirror of
https://github.com/svemagie/indiekit-endpoint-microsub.git
synced 2026-04-02 15:35:00 +02:00
Add ability to mark all items from a specific feed/source as read at once, instead of clicking each item individually. The mark-read button becomes a split button group with a caret that opens a popover offering "Mark [source] as read". Items without a feedId (AP items) keep the simple button. Confab-Link: http://localhost:8080/sessions/a477883d-4aef-4013-983c-ce3d3157cfba
153 lines
3.8 KiB
JavaScript
153 lines
3.8 KiB
JavaScript
/**
|
|
* Timeline controller
|
|
* @module controllers/timeline
|
|
*/
|
|
|
|
import { IndiekitError } from "@indiekit/error";
|
|
|
|
import { proxyItemImages } from "../media/proxy.js";
|
|
import { getChannel, getChannelById } from "../storage/channels.js";
|
|
import {
|
|
getTimelineItems,
|
|
markFeedItemsRead,
|
|
markItemsRead,
|
|
markItemsUnread,
|
|
removeItems,
|
|
} from "../storage/items.js";
|
|
import { getUserId } from "../utils/auth.js";
|
|
import {
|
|
validateChannel,
|
|
validateEntries,
|
|
parseArrayParameter as parseArrayParametereter,
|
|
} from "../utils/validation.js";
|
|
|
|
/**
|
|
* Get timeline items for a channel
|
|
* GET ?action=timeline&channel=<uid>
|
|
* @param {object} request - Express request
|
|
* @param {object} response - Express response
|
|
*/
|
|
export async function get(request, response) {
|
|
const { application } = request.app.locals;
|
|
const userId = getUserId(request);
|
|
const { channel, before, after, limit } = request.query;
|
|
|
|
validateChannel(channel);
|
|
|
|
// Verify channel exists
|
|
const channelDocument = await getChannel(application, channel, userId);
|
|
if (!channelDocument) {
|
|
throw new IndiekitError("Channel not found", {
|
|
status: 404,
|
|
});
|
|
}
|
|
|
|
const timeline = await getTimelineItems(application, channelDocument._id, {
|
|
before,
|
|
after,
|
|
limit,
|
|
userId,
|
|
});
|
|
|
|
// Proxy images if application URL is available
|
|
const baseUrl = application.url;
|
|
if (baseUrl && timeline.items) {
|
|
timeline.items = timeline.items.map((item) =>
|
|
proxyItemImages(item, baseUrl),
|
|
);
|
|
}
|
|
|
|
response.json(timeline);
|
|
}
|
|
|
|
/**
|
|
* Handle timeline actions (mark_read, mark_unread, remove)
|
|
* POST ?action=timeline
|
|
* @param {object} request - Express request
|
|
* @param {object} response - Express response
|
|
*/
|
|
export async function action(request, response) {
|
|
const { application } = request.app.locals;
|
|
const userId = getUserId(request);
|
|
const { method, channel } = request.body;
|
|
|
|
validateChannel(channel);
|
|
|
|
// Verify channel exists — try by UID first, fall back to ObjectId
|
|
// (timeline view may send ObjectId string for items from orphan channels)
|
|
let channelDocument = await getChannel(application, channel, userId);
|
|
if (!channelDocument) {
|
|
try {
|
|
channelDocument = await getChannelById(application, channel);
|
|
} catch {
|
|
// Invalid ObjectId format — channel string is not a valid ObjectId
|
|
}
|
|
}
|
|
if (!channelDocument) {
|
|
throw new IndiekitError("Channel not found", {
|
|
status: 404,
|
|
});
|
|
}
|
|
|
|
// Get entry IDs from request
|
|
const entries = parseArrayParametereter(request.body, "entry");
|
|
|
|
switch (method) {
|
|
case "mark_read": {
|
|
validateEntries(entries);
|
|
const count = await markItemsRead(
|
|
application,
|
|
channelDocument._id,
|
|
entries,
|
|
userId,
|
|
);
|
|
return response.json({ result: "ok", updated: count });
|
|
}
|
|
|
|
case "mark_read_source": {
|
|
const feedId = request.body.feed;
|
|
if (!feedId) {
|
|
throw new IndiekitError("feed parameter required", {
|
|
status: 400,
|
|
});
|
|
}
|
|
const count = await markFeedItemsRead(
|
|
application,
|
|
channelDocument._id,
|
|
feedId,
|
|
userId,
|
|
);
|
|
return response.json({ result: "ok", updated: count });
|
|
}
|
|
|
|
case "mark_unread": {
|
|
validateEntries(entries);
|
|
const count = await markItemsUnread(
|
|
application,
|
|
channelDocument._id,
|
|
entries,
|
|
userId,
|
|
);
|
|
return response.json({ result: "ok", updated: count });
|
|
}
|
|
|
|
case "remove": {
|
|
validateEntries(entries);
|
|
const count = await removeItems(
|
|
application,
|
|
channelDocument._id,
|
|
entries,
|
|
);
|
|
return response.json({ result: "ok", removed: count });
|
|
}
|
|
|
|
default: {
|
|
throw new IndiekitError(`Invalid timeline method: ${method}`, {
|
|
status: 400,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
export const timelineController = { get, action };
|