@rmdes/indiekit-endpoint-microsub 1.0.9 → 1.0.10

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.
@@ -21,6 +21,7 @@ import {
21
21
  getTimelineItems,
22
22
  getItemById,
23
23
  markItemsRead,
24
+ countReadItems,
24
25
  } from "../storage/items.js";
25
26
  import { getUserId } from "../utils/auth.js";
26
27
  import {
@@ -95,24 +96,37 @@ export async function channel(request, response) {
95
96
  const { application } = request.app.locals;
96
97
  const userId = getUserId(request);
97
98
  const { uid } = request.params;
98
- const { before, after } = request.query;
99
+ const { before, after, showRead } = request.query;
99
100
 
100
101
  const channelDocument = await getChannel(application, uid, userId);
101
102
  if (!channelDocument) {
102
103
  return response.status(404).render("404");
103
104
  }
104
105
 
106
+ // Check if showing read items
107
+ const showReadItems = showRead === "true";
108
+
105
109
  const timeline = await getTimelineItems(application, channelDocument._id, {
106
110
  before,
107
111
  after,
108
112
  userId,
113
+ showRead: showReadItems,
109
114
  });
110
115
 
116
+ // Count read items to show "View read items" button
117
+ const readCount = await countReadItems(
118
+ application,
119
+ channelDocument._id,
120
+ userId,
121
+ );
122
+
111
123
  response.render("channel", {
112
124
  title: channelDocument.name,
113
125
  channel: channelDocument,
114
126
  items: timeline.items,
115
127
  paging: timeline.paging,
128
+ readCount,
129
+ showRead: showReadItems,
116
130
  baseUrl: request.baseUrl,
117
131
  });
118
132
  }
@@ -78,6 +78,7 @@ export async function addItem(application, { channelId, feedId, uid, item }) {
78
78
  * @param {string} [options.after] - After cursor
79
79
  * @param {number} [options.limit] - Items per page
80
80
  * @param {string} [options.userId] - User ID for read state
81
+ * @param {boolean} [options.showRead] - Whether to show read items (default: false)
81
82
  * @returns {Promise<object>} Timeline with items and paging
82
83
  */
83
84
  export async function getTimelineItems(application, channelId, options = {}) {
@@ -86,7 +87,12 @@ export async function getTimelineItems(application, channelId, options = {}) {
86
87
  typeof channelId === "string" ? new ObjectId(channelId) : channelId;
87
88
  const limit = parseLimit(options.limit);
88
89
 
90
+ // Base query - filter out read items unless showRead is true
89
91
  const baseQuery = { channelId: objectId };
92
+ if (options.userId && !options.showRead) {
93
+ baseQuery.readBy = { $ne: options.userId };
94
+ }
95
+
90
96
  const query = buildPaginationQuery({
91
97
  before: options.before,
92
98
  after: options.after,
@@ -256,6 +262,24 @@ export async function getItemsByUids(application, uids, userId) {
256
262
  return items.map((item) => transformToJf2(item, userId));
257
263
  }
258
264
 
265
+ /**
266
+ * Count read items in a channel for a user
267
+ * @param {object} application - Indiekit application
268
+ * @param {ObjectId|string} channelId - Channel ObjectId
269
+ * @param {string} userId - User ID
270
+ * @returns {Promise<number>} Count of read items
271
+ */
272
+ export async function countReadItems(application, channelId, userId) {
273
+ const collection = getCollection(application);
274
+ const objectId =
275
+ typeof channelId === "string" ? new ObjectId(channelId) : channelId;
276
+
277
+ return collection.countDocuments({
278
+ channelId: objectId,
279
+ readBy: userId,
280
+ });
281
+ }
282
+
259
283
  /**
260
284
  * Mark items as read
261
285
  * @param {object} application - Indiekit application
package/locales/en.json CHANGED
@@ -4,6 +4,9 @@
4
4
  "title": "Reader",
5
5
  "empty": "No items to display",
6
6
  "markAllRead": "Mark all as read",
7
+ "showRead": "Show read ({{count}})",
8
+ "hideRead": "Hide read items",
9
+ "allRead": "All caught up!",
7
10
  "newer": "Newer",
8
11
  "older": "Older"
9
12
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rmdes/indiekit-endpoint-microsub",
3
- "version": "1.0.9",
3
+ "version": "1.0.10",
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/channel.njk CHANGED
@@ -7,6 +7,7 @@
7
7
  {{ icon("previous") }} {{ __("microsub.channels.title") }}
8
8
  </a>
9
9
  <div class="channel__actions">
10
+ {% if not showRead and items.length > 0 %}
10
11
  <form action="{{ baseUrl }}/api/mark-read" method="POST" style="display: inline;">
11
12
  <input type="hidden" name="channel" value="{{ channel.uid }}">
12
13
  <input type="hidden" name="entry" value="last-read-entry">
@@ -14,6 +15,16 @@
14
15
  {{ icon("checkboxChecked") }} {{ __("microsub.reader.markAllRead") }}
15
16
  </button>
16
17
  </form>
18
+ {% endif %}
19
+ {% if showRead %}
20
+ <a href="{{ baseUrl }}/channels/{{ channel.uid }}" class="button button--secondary button--small">
21
+ {{ icon("hide") }} {{ __("microsub.reader.hideRead") }}
22
+ </a>
23
+ {% elif readCount > 0 %}
24
+ <a href="{{ baseUrl }}/channels/{{ channel.uid }}?showRead=true" class="button button--secondary button--small">
25
+ {{ icon("show") }} {{ __("microsub.reader.showRead", { count: readCount }) }}
26
+ </a>
27
+ {% endif %}
17
28
  <a href="{{ baseUrl }}/channels/{{ channel.uid }}/feeds" class="button button--secondary button--small">
18
29
  {{ icon("syndicate") }} {{ __("microsub.feeds.title") }}
19
30
  </a>
@@ -48,11 +59,19 @@
48
59
  {% endif %}
49
60
  {% else %}
50
61
  <div class="reader__empty">
62
+ {% if readCount > 0 and not showRead %}
63
+ {{ icon("checkboxChecked") }}
64
+ <p>{{ __("microsub.reader.allRead") }}</p>
65
+ <a href="{{ baseUrl }}/channels/{{ channel.uid }}?showRead=true" class="button button--secondary">
66
+ {{ icon("show") }} {{ __("microsub.reader.showRead", { count: readCount }) }}
67
+ </a>
68
+ {% else %}
51
69
  {{ icon("syndicate") }}
52
70
  <p>{{ __("microsub.timeline.empty") }}</p>
53
71
  <a href="{{ baseUrl }}/channels/{{ channel.uid }}/feeds" class="button button--primary">
54
72
  {{ __("microsub.feeds.subscribe") }}
55
73
  </a>
74
+ {% endif %}
56
75
  </div>
57
76
  {% endif %}
58
77
  </div>