@rmdes/indiekit-endpoint-microsub 1.0.18 → 1.0.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/styles.css +1 -0
- package/lib/controllers/reader.js +8 -0
- package/lib/storage/items.js +48 -0
- package/package.json +1 -1
- package/views/item.njk +55 -0
- package/views/partials/item-card.njk +4 -0
package/assets/styles.css
CHANGED
|
@@ -314,9 +314,17 @@ export async function item(request, response) {
|
|
|
314
314
|
return response.status(404).render("404");
|
|
315
315
|
}
|
|
316
316
|
|
|
317
|
+
// Get the channel for this item (needed for mark-read)
|
|
318
|
+
let channel = null;
|
|
319
|
+
if (itemDocument.channelId) {
|
|
320
|
+
const channelsCollection = application.collections.get("microsub_channels");
|
|
321
|
+
channel = await channelsCollection.findOne({ _id: itemDocument.channelId });
|
|
322
|
+
}
|
|
323
|
+
|
|
317
324
|
response.render("item", {
|
|
318
325
|
title: itemDocument.name || "Item",
|
|
319
326
|
item: itemDocument,
|
|
327
|
+
channel,
|
|
320
328
|
baseUrl: request.baseUrl,
|
|
321
329
|
});
|
|
322
330
|
}
|
package/lib/storage/items.js
CHANGED
|
@@ -288,6 +288,46 @@ export async function countReadItems(application, channelId, userId) {
|
|
|
288
288
|
* @param {string} userId - User ID
|
|
289
289
|
* @returns {Promise<number>} Number of items updated
|
|
290
290
|
*/
|
|
291
|
+
// Maximum number of read items to keep per channel
|
|
292
|
+
const MAX_READ_ITEMS = 30;
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Cleanup old read items, keeping only the most recent MAX_READ_ITEMS
|
|
296
|
+
* @param {object} collection - MongoDB collection
|
|
297
|
+
* @param {ObjectId} channelObjectId - Channel ObjectId
|
|
298
|
+
* @param {string} userId - User ID
|
|
299
|
+
*/
|
|
300
|
+
async function cleanupOldReadItems(collection, channelObjectId, userId) {
|
|
301
|
+
// Count read items in this channel
|
|
302
|
+
const readCount = await collection.countDocuments({
|
|
303
|
+
channelId: channelObjectId,
|
|
304
|
+
readBy: userId,
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
if (readCount > MAX_READ_ITEMS) {
|
|
308
|
+
// Find the oldest read items to delete
|
|
309
|
+
const itemsToDelete = await collection
|
|
310
|
+
.find({
|
|
311
|
+
channelId: channelObjectId,
|
|
312
|
+
readBy: userId,
|
|
313
|
+
})
|
|
314
|
+
.sort({ published: -1, _id: -1 }) // Newest first
|
|
315
|
+
.skip(MAX_READ_ITEMS) // Skip the ones we want to keep
|
|
316
|
+
.project({ _id: 1 })
|
|
317
|
+
.toArray();
|
|
318
|
+
|
|
319
|
+
if (itemsToDelete.length > 0) {
|
|
320
|
+
const idsToDelete = itemsToDelete.map((item) => item._id);
|
|
321
|
+
const deleteResult = await collection.deleteMany({
|
|
322
|
+
_id: { $in: idsToDelete },
|
|
323
|
+
});
|
|
324
|
+
console.info(
|
|
325
|
+
`[Microsub] Cleaned up ${deleteResult.deletedCount} old read items (keeping ${MAX_READ_ITEMS})`,
|
|
326
|
+
);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
291
331
|
export async function markItemsRead(application, channelId, entryIds, userId) {
|
|
292
332
|
const collection = getCollection(application);
|
|
293
333
|
const channelObjectId =
|
|
@@ -309,6 +349,10 @@ export async function markItemsRead(application, channelId, entryIds, userId) {
|
|
|
309
349
|
console.info(
|
|
310
350
|
`[Microsub] Marked all items as read: ${result.modifiedCount} updated`,
|
|
311
351
|
);
|
|
352
|
+
|
|
353
|
+
// Cleanup old read items, keeping only the most recent
|
|
354
|
+
await cleanupOldReadItems(collection, channelObjectId, userId);
|
|
355
|
+
|
|
312
356
|
return result.modifiedCount;
|
|
313
357
|
}
|
|
314
358
|
|
|
@@ -339,6 +383,10 @@ export async function markItemsRead(application, channelId, entryIds, userId) {
|
|
|
339
383
|
console.info(
|
|
340
384
|
`[Microsub] markItemsRead result: ${result.modifiedCount} items updated`,
|
|
341
385
|
);
|
|
386
|
+
|
|
387
|
+
// Cleanup old read items, keeping only the most recent
|
|
388
|
+
await cleanupOldReadItems(collection, channelObjectId, userId);
|
|
389
|
+
|
|
342
390
|
return result.modifiedCount;
|
|
343
391
|
}
|
|
344
392
|
|
package/package.json
CHANGED
package/views/item.njk
CHANGED
|
@@ -146,6 +146,61 @@
|
|
|
146
146
|
<a href="{{ baseUrl }}/compose?bookmark={{ item.url | urlencode }}" class="button button--secondary button--small">
|
|
147
147
|
{{ icon("bookmark") }} {{ __("microsub.item.bookmark") }}
|
|
148
148
|
</a>
|
|
149
|
+
{% if not item._is_read %}
|
|
150
|
+
<button type="button"
|
|
151
|
+
class="button button--secondary button--small item__mark-read"
|
|
152
|
+
data-item-id="{{ item._id }}"
|
|
153
|
+
data-channel="{{ channel.uid }}">
|
|
154
|
+
{{ icon("checkboxChecked") }} {{ __("microsub.timeline.markRead") }}
|
|
155
|
+
</button>
|
|
156
|
+
{% endif %}
|
|
149
157
|
</footer>
|
|
150
158
|
</article>
|
|
159
|
+
|
|
160
|
+
<script type="module">
|
|
161
|
+
// Handle mark-read button
|
|
162
|
+
const markReadBtn = document.querySelector('.item__mark-read');
|
|
163
|
+
if (markReadBtn) {
|
|
164
|
+
markReadBtn.addEventListener('click', async () => {
|
|
165
|
+
const itemId = markReadBtn.dataset.itemId;
|
|
166
|
+
const channelUid = markReadBtn.dataset.channel;
|
|
167
|
+
const microsubApiUrl = '{{ baseUrl }}'.replace(/\/reader.*$/, '');
|
|
168
|
+
|
|
169
|
+
markReadBtn.disabled = true;
|
|
170
|
+
markReadBtn.textContent = 'Marking...';
|
|
171
|
+
|
|
172
|
+
try {
|
|
173
|
+
const formData = new URLSearchParams();
|
|
174
|
+
formData.append('action', 'timeline');
|
|
175
|
+
formData.append('method', 'mark_read');
|
|
176
|
+
formData.append('channel', channelUid);
|
|
177
|
+
formData.append('entry', itemId);
|
|
178
|
+
|
|
179
|
+
const response = await fetch(microsubApiUrl, {
|
|
180
|
+
method: 'POST',
|
|
181
|
+
headers: {
|
|
182
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
183
|
+
},
|
|
184
|
+
body: formData.toString(),
|
|
185
|
+
credentials: 'same-origin'
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
if (response.ok) {
|
|
189
|
+
markReadBtn.textContent = 'Marked as read';
|
|
190
|
+
markReadBtn.classList.add('button--success');
|
|
191
|
+
setTimeout(() => {
|
|
192
|
+
markReadBtn.remove();
|
|
193
|
+
}, 1500);
|
|
194
|
+
} else {
|
|
195
|
+
markReadBtn.textContent = 'Failed';
|
|
196
|
+
markReadBtn.disabled = false;
|
|
197
|
+
}
|
|
198
|
+
} catch (error) {
|
|
199
|
+
console.error('Error:', error);
|
|
200
|
+
markReadBtn.textContent = 'Error';
|
|
201
|
+
markReadBtn.disabled = false;
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
</script>
|
|
151
206
|
{% endblock %}
|
|
@@ -165,6 +165,10 @@
|
|
|
165
165
|
{{ icon("repost") }}
|
|
166
166
|
<span class="visually-hidden">Repost</span>
|
|
167
167
|
</a>
|
|
168
|
+
<a href="{{ baseUrl }}/compose?bookmark={{ item.url | urlencode }}" class="item-actions__button" title="Bookmark">
|
|
169
|
+
{{ icon("bookmark") }}
|
|
170
|
+
<span class="visually-hidden">Bookmark</span>
|
|
171
|
+
</a>
|
|
168
172
|
{% if not item._is_read %}
|
|
169
173
|
<button type="button"
|
|
170
174
|
class="item-actions__button item-actions__mark-read"
|