feat: add show/hide read items and fix individual mark-read

- Add countReadItems function to storage/items.js
- Update getTimelineItems to filter out read items by default
- Add showRead query param support to channel controller
- Update channel.njk with show/hide read toggle buttons
- Add "All caught up!" state when all items are read
- Add JavaScript handler for individual mark-read buttons
- Mark-read now hides the item with smooth animation
- Add locale strings: showRead, hideRead, allRead

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Ricardo
2026-02-06 21:24:33 +01:00
parent 8d373dca5f
commit c830ad5df6
5 changed files with 120 additions and 4 deletions

View File

@@ -21,6 +21,7 @@ import {
getTimelineItems,
getItemById,
markItemsRead,
countReadItems,
} from "../storage/items.js";
import { getUserId } from "../utils/auth.js";
import {
@@ -95,24 +96,37 @@ export async function channel(request, response) {
const { application } = request.app.locals;
const userId = getUserId(request);
const { uid } = request.params;
const { before, after } = request.query;
const { before, after, showRead } = request.query;
const channelDocument = await getChannel(application, uid, userId);
if (!channelDocument) {
return response.status(404).render("404");
}
// Check if showing read items
const showReadItems = showRead === "true";
const timeline = await getTimelineItems(application, channelDocument._id, {
before,
after,
userId,
showRead: showReadItems,
});
// Count read items to show "View read items" button
const readCount = await countReadItems(
application,
channelDocument._id,
userId,
);
response.render("channel", {
title: channelDocument.name,
channel: channelDocument,
items: timeline.items,
paging: timeline.paging,
readCount,
showRead: showReadItems,
baseUrl: request.baseUrl,
});
}

View File

@@ -78,6 +78,7 @@ export async function addItem(application, { channelId, feedId, uid, item }) {
* @param {string} [options.after] - After cursor
* @param {number} [options.limit] - Items per page
* @param {string} [options.userId] - User ID for read state
* @param {boolean} [options.showRead] - Whether to show read items (default: false)
* @returns {Promise<object>} Timeline with items and paging
*/
export async function getTimelineItems(application, channelId, options = {}) {
@@ -86,7 +87,12 @@ export async function getTimelineItems(application, channelId, options = {}) {
typeof channelId === "string" ? new ObjectId(channelId) : channelId;
const limit = parseLimit(options.limit);
// Base query - filter out read items unless showRead is true
const baseQuery = { channelId: objectId };
if (options.userId && !options.showRead) {
baseQuery.readBy = { $ne: options.userId };
}
const query = buildPaginationQuery({
before: options.before,
after: options.after,
@@ -256,6 +262,24 @@ export async function getItemsByUids(application, uids, userId) {
return items.map((item) => transformToJf2(item, userId));
}
/**
* Count read items in a channel
* @param {object} application - Indiekit application
* @param {ObjectId|string} channelId - Channel ObjectId
* @param {string} userId - User ID
* @returns {Promise<number>} Number of read items
*/
export async function countReadItems(application, channelId, userId) {
const collection = getCollection(application);
const objectId =
typeof channelId === "string" ? new ObjectId(channelId) : channelId;
return collection.countDocuments({
channelId: objectId,
readBy: userId,
});
}
/**
* Mark items as read
* @param {object} application - Indiekit application