@rmdes/indiekit-endpoint-microsub 1.0.40 → 1.0.42

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
@@ -421,6 +421,12 @@
421
421
  margin-left: auto;
422
422
  }
423
423
 
424
+ /* Save for later button */
425
+ .item-actions__save-later--saved {
426
+ color: var(--color-accent, #4a9eff);
427
+ opacity: 0.6;
428
+ }
429
+
424
430
  /* ==========================================================================
425
431
  Single Item View
426
432
  ========================================================================== */
@@ -6,7 +6,7 @@
6
6
  import { IndiekitError } from "@indiekit/error";
7
7
 
8
8
  import { proxyItemImages } from "../media/proxy.js";
9
- import { getChannel } from "../storage/channels.js";
9
+ import { getChannel, getChannelById } from "../storage/channels.js";
10
10
  import {
11
11
  getTimelineItems,
12
12
  markItemsRead,
@@ -72,8 +72,16 @@ export async function action(request, response) {
72
72
 
73
73
  validateChannel(channel);
74
74
 
75
- // Verify channel exists
76
- const channelDocument = await getChannel(application, channel, userId);
75
+ // Verify channel exists — try by UID first, fall back to ObjectId
76
+ // (timeline view may send ObjectId string for items from orphan channels)
77
+ let channelDocument = await getChannel(application, channel, userId);
78
+ if (!channelDocument) {
79
+ try {
80
+ channelDocument = await getChannelById(application, channel);
81
+ } catch {
82
+ // Invalid ObjectId format — channel string is not a valid ObjectId
83
+ }
84
+ }
77
85
  if (!channelDocument) {
78
86
  throw new IndiekitError("Channel not found", {
79
87
  status: 404,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rmdes/indiekit-endpoint-microsub",
3
- "version": "1.0.40",
3
+ "version": "1.0.42",
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
@@ -173,6 +173,39 @@
173
173
  button.disabled = false;
174
174
  }
175
175
  });
176
+
177
+ // Handle save-for-later buttons
178
+ timeline.addEventListener('click', async (e) => {
179
+ const button = e.target.closest('.item-actions__save-later');
180
+ if (!button) return;
181
+
182
+ e.preventDefault();
183
+ e.stopPropagation();
184
+
185
+ const url = button.dataset.url;
186
+ const title = button.dataset.title;
187
+ if (!url) return;
188
+
189
+ button.disabled = true;
190
+
191
+ try {
192
+ const response = await fetch('/readlater/save', {
193
+ method: 'POST',
194
+ headers: { 'Content-Type': 'application/json' },
195
+ body: JSON.stringify({ url, title: title || url, source: 'microsub' }),
196
+ credentials: 'same-origin'
197
+ });
198
+
199
+ if (response.ok) {
200
+ button.classList.add('item-actions__save-later--saved');
201
+ button.title = 'Saved';
202
+ } else {
203
+ button.disabled = false;
204
+ }
205
+ } catch {
206
+ button.disabled = false;
207
+ }
208
+ });
176
209
  }
177
210
  </script>
178
211
  {% endblock %}
@@ -203,10 +203,22 @@
203
203
  data-action="mark-read"
204
204
  data-item-id="{{ item._id }}"
205
205
  {% if item._channelUid %}data-channel-uid="{{ item._channelUid }}"{% endif %}
206
+ {% if item._channelId %}data-channel-id="{{ item._channelId }}"{% endif %}
206
207
  title="Mark as read">
207
208
  {{ icon("checkboxChecked") }}
208
209
  <span class="visually-hidden">Mark read</span>
209
210
  </button>
210
211
  {% endif %}
212
+ {% if application.readlaterEndpoint %}
213
+ <button type="button"
214
+ class="item-actions__button item-actions__save-later"
215
+ data-action="save-later"
216
+ data-url="{{ item.url }}"
217
+ data-title="{{ item.name or '' }}"
218
+ title="Save for later">
219
+ {{ icon("bookmark") }}
220
+ <span class="visually-hidden">Save for later</span>
221
+ </button>
222
+ {% endif %}
211
223
  </div>
212
224
  </article>
@@ -108,7 +108,8 @@
108
108
 
109
109
  const itemId = button.dataset.itemId;
110
110
  const channelUid = button.dataset.channelUid;
111
- if (!itemId || !channelUid) return;
111
+ const channelId = button.dataset.channelId;
112
+ if (!itemId || (!channelUid && !channelId)) return;
112
113
 
113
114
  button.disabled = true;
114
115
 
@@ -116,7 +117,7 @@
116
117
  const formData = new URLSearchParams();
117
118
  formData.append('action', 'timeline');
118
119
  formData.append('method', 'mark_read');
119
- formData.append('channel', channelUid);
120
+ formData.append('channel', channelUid || channelId);
120
121
  formData.append('entry', itemId);
121
122
 
122
123
  const response = await fetch(microsubApiUrl, {
@@ -150,6 +151,39 @@
150
151
  button.disabled = false;
151
152
  }
152
153
  });
154
+
155
+ // Handle save-for-later buttons
156
+ timeline.addEventListener('click', async (e) => {
157
+ const button = e.target.closest('.item-actions__save-later');
158
+ if (!button) return;
159
+
160
+ e.preventDefault();
161
+ e.stopPropagation();
162
+
163
+ const url = button.dataset.url;
164
+ const title = button.dataset.title;
165
+ if (!url) return;
166
+
167
+ button.disabled = true;
168
+
169
+ try {
170
+ const response = await fetch('/readlater/save', {
171
+ method: 'POST',
172
+ headers: { 'Content-Type': 'application/json' },
173
+ body: JSON.stringify({ url, title: title || url, source: 'microsub' }),
174
+ credentials: 'same-origin'
175
+ });
176
+
177
+ if (response.ok) {
178
+ button.classList.add('item-actions__save-later--saved');
179
+ button.title = 'Saved';
180
+ } else {
181
+ button.disabled = false;
182
+ }
183
+ } catch {
184
+ button.disabled = false;
185
+ }
186
+ });
153
187
  }
154
188
  </script>
155
189
  {% endblock %}