@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 CHANGED
@@ -373,6 +373,7 @@
373
373
  .item-actions {
374
374
  border-top: 1px solid var(--color-offset);
375
375
  display: flex;
376
+ flex-wrap: wrap;
376
377
  gap: var(--space-s);
377
378
  padding-top: var(--space-s);
378
379
  }
@@ -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
  }
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rmdes/indiekit-endpoint-microsub",
3
- "version": "1.0.18",
3
+ "version": "1.0.21",
4
4
  "description": "Microsub endpoint for Indiekit. Enables subscribing to feeds and reading content using the Microsub protocol.",
5
5
  "keywords": [
6
6
  "indiekit",
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"