@rmdes/indiekit-endpoint-microsub 1.0.38 → 1.0.40
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 +53 -9
- package/lib/controllers/reader.js +94 -4
- package/package.json +1 -1
- package/views/channel.njk +1 -3
- package/views/layouts/reader.njk +1 -0
- package/views/partials/breadcrumbs.njk +16 -0
- package/views/partials/item-card.njk +1 -0
- package/views/timeline.njk +58 -3
package/assets/styles.css
CHANGED
|
@@ -1015,6 +1015,49 @@
|
|
|
1015
1015
|
color: #7c3aed;
|
|
1016
1016
|
}
|
|
1017
1017
|
|
|
1018
|
+
/* ==========================================================================
|
|
1019
|
+
Breadcrumbs
|
|
1020
|
+
========================================================================== */
|
|
1021
|
+
|
|
1022
|
+
.breadcrumbs {
|
|
1023
|
+
margin-bottom: var(--space-xs);
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
.breadcrumbs__list {
|
|
1027
|
+
align-items: center;
|
|
1028
|
+
display: flex;
|
|
1029
|
+
flex-wrap: wrap;
|
|
1030
|
+
font-size: var(--font-size-small);
|
|
1031
|
+
gap: 0;
|
|
1032
|
+
list-style: none;
|
|
1033
|
+
margin: 0;
|
|
1034
|
+
padding: 0;
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
.breadcrumbs__item::before {
|
|
1038
|
+
color: var(--color-text-muted);
|
|
1039
|
+
content: "/";
|
|
1040
|
+
margin: 0 var(--space-xs);
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
.breadcrumbs__item:first-child::before {
|
|
1044
|
+
content: none;
|
|
1045
|
+
margin: 0;
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
.breadcrumbs__link {
|
|
1049
|
+
color: var(--color-primary);
|
|
1050
|
+
text-decoration: none;
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
.breadcrumbs__link:hover {
|
|
1054
|
+
text-decoration: underline;
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
.breadcrumbs__current {
|
|
1058
|
+
color: var(--color-text-muted);
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1018
1061
|
/* ==========================================================================
|
|
1019
1062
|
View Switcher
|
|
1020
1063
|
========================================================================== */
|
|
@@ -1072,19 +1115,20 @@
|
|
|
1072
1115
|
}
|
|
1073
1116
|
|
|
1074
1117
|
.timeline-view__item {
|
|
1075
|
-
border-radius: var(--border-radius);
|
|
1076
1118
|
position: relative;
|
|
1077
1119
|
}
|
|
1078
1120
|
|
|
1079
|
-
.timeline-
|
|
1080
|
-
border-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
display: block;
|
|
1085
|
-
font-size: 0.75rem;
|
|
1121
|
+
.timeline-view__channel-badge {
|
|
1122
|
+
border-radius: 3px;
|
|
1123
|
+
color: #fff;
|
|
1124
|
+
display: inline-block;
|
|
1125
|
+
font-size: 0.6875rem;
|
|
1086
1126
|
font-weight: 600;
|
|
1087
|
-
|
|
1127
|
+
letter-spacing: 0.02em;
|
|
1128
|
+
line-height: 1;
|
|
1129
|
+
margin-bottom: var(--space-xs);
|
|
1130
|
+
padding: 3px 8px;
|
|
1131
|
+
text-transform: uppercase;
|
|
1088
1132
|
}
|
|
1089
1133
|
|
|
1090
1134
|
.timeline-view__filter {
|
|
@@ -46,9 +46,9 @@ import { getDeckConfig, saveDeckConfig } from "../storage/deck.js";
|
|
|
46
46
|
* @param {object} response - Express response
|
|
47
47
|
*/
|
|
48
48
|
export async function index(request, response) {
|
|
49
|
-
const lastView = request.session?.microsubView || "
|
|
49
|
+
const lastView = request.session?.microsubView || "timeline";
|
|
50
50
|
const validViews = ["channels", "deck", "timeline"];
|
|
51
|
-
const view = validViews.includes(lastView) ? lastView : "
|
|
51
|
+
const view = validViews.includes(lastView) ? lastView : "timeline";
|
|
52
52
|
response.redirect(`${request.baseUrl}/${view}`);
|
|
53
53
|
}
|
|
54
54
|
|
|
@@ -71,6 +71,10 @@ export async function channels(request, response) {
|
|
|
71
71
|
baseUrl: request.baseUrl,
|
|
72
72
|
readerBaseUrl: request.baseUrl,
|
|
73
73
|
activeView: "channels",
|
|
74
|
+
breadcrumbs: [
|
|
75
|
+
{ text: "Reader", href: request.baseUrl },
|
|
76
|
+
{ text: "Channels" },
|
|
77
|
+
],
|
|
74
78
|
});
|
|
75
79
|
}
|
|
76
80
|
|
|
@@ -85,6 +89,11 @@ export async function newChannel(request, response) {
|
|
|
85
89
|
baseUrl: request.baseUrl,
|
|
86
90
|
readerBaseUrl: request.baseUrl,
|
|
87
91
|
activeView: "channels",
|
|
92
|
+
breadcrumbs: [
|
|
93
|
+
{ text: "Reader", href: request.baseUrl },
|
|
94
|
+
{ text: "Channels", href: `${request.baseUrl}/channels` },
|
|
95
|
+
{ text: request.__("microsub.channels.new") },
|
|
96
|
+
],
|
|
88
97
|
});
|
|
89
98
|
}
|
|
90
99
|
|
|
@@ -157,6 +166,11 @@ export async function channel(request, response) {
|
|
|
157
166
|
baseUrl: request.baseUrl,
|
|
158
167
|
readerBaseUrl: request.baseUrl,
|
|
159
168
|
activeView: "channels",
|
|
169
|
+
breadcrumbs: [
|
|
170
|
+
{ text: "Reader", href: request.baseUrl },
|
|
171
|
+
{ text: "Channels", href: `${request.baseUrl}/channels` },
|
|
172
|
+
{ text: channelDocument.name },
|
|
173
|
+
],
|
|
160
174
|
});
|
|
161
175
|
}
|
|
162
176
|
|
|
@@ -184,6 +198,12 @@ export async function settings(request, response) {
|
|
|
184
198
|
baseUrl: request.baseUrl,
|
|
185
199
|
readerBaseUrl: request.baseUrl,
|
|
186
200
|
activeView: "channels",
|
|
201
|
+
breadcrumbs: [
|
|
202
|
+
{ text: "Reader", href: request.baseUrl },
|
|
203
|
+
{ text: "Channels", href: `${request.baseUrl}/channels` },
|
|
204
|
+
{ text: channelDocument.name, href: `${request.baseUrl}/channels/${uid}` },
|
|
205
|
+
{ text: "Settings" },
|
|
206
|
+
],
|
|
187
207
|
});
|
|
188
208
|
}
|
|
189
209
|
|
|
@@ -273,6 +293,12 @@ export async function feeds(request, response) {
|
|
|
273
293
|
baseUrl: request.baseUrl,
|
|
274
294
|
readerBaseUrl: request.baseUrl,
|
|
275
295
|
activeView: "channels",
|
|
296
|
+
breadcrumbs: [
|
|
297
|
+
{ text: "Reader", href: request.baseUrl },
|
|
298
|
+
{ text: "Channels", href: `${request.baseUrl}/channels` },
|
|
299
|
+
{ text: channelDocument.name, href: `${request.baseUrl}/channels/${uid}` },
|
|
300
|
+
{ text: "Feeds" },
|
|
301
|
+
],
|
|
276
302
|
});
|
|
277
303
|
}
|
|
278
304
|
|
|
@@ -354,6 +380,17 @@ export async function item(request, response) {
|
|
|
354
380
|
channel = await channelsCollection.findOne({ _id: itemDocument.channelId });
|
|
355
381
|
}
|
|
356
382
|
|
|
383
|
+
const itemBreadcrumbs = [
|
|
384
|
+
{ text: "Reader", href: request.baseUrl },
|
|
385
|
+
];
|
|
386
|
+
if (channel) {
|
|
387
|
+
itemBreadcrumbs.push(
|
|
388
|
+
{ text: "Channels", href: `${request.baseUrl}/channels` },
|
|
389
|
+
{ text: channel.name, href: `${request.baseUrl}/channels/${channel.uid}` },
|
|
390
|
+
);
|
|
391
|
+
}
|
|
392
|
+
itemBreadcrumbs.push({ text: itemDocument.name || "Item" });
|
|
393
|
+
|
|
357
394
|
response.render("item", {
|
|
358
395
|
title: itemDocument.name || "Item",
|
|
359
396
|
item: itemDocument,
|
|
@@ -361,6 +398,7 @@ export async function item(request, response) {
|
|
|
361
398
|
baseUrl: request.baseUrl,
|
|
362
399
|
readerBaseUrl: request.baseUrl,
|
|
363
400
|
activeView: "channels",
|
|
401
|
+
breadcrumbs: itemBreadcrumbs,
|
|
364
402
|
});
|
|
365
403
|
}
|
|
366
404
|
|
|
@@ -473,6 +511,10 @@ export async function compose(request, response) {
|
|
|
473
511
|
baseUrl: request.baseUrl,
|
|
474
512
|
readerBaseUrl: request.baseUrl,
|
|
475
513
|
activeView: "channels",
|
|
514
|
+
breadcrumbs: [
|
|
515
|
+
{ text: "Reader", href: request.baseUrl },
|
|
516
|
+
{ text: "Compose" },
|
|
517
|
+
],
|
|
476
518
|
});
|
|
477
519
|
}
|
|
478
520
|
|
|
@@ -648,6 +690,10 @@ export async function searchPage(request, response) {
|
|
|
648
690
|
baseUrl: request.baseUrl,
|
|
649
691
|
readerBaseUrl: request.baseUrl,
|
|
650
692
|
activeView: "channels",
|
|
693
|
+
breadcrumbs: [
|
|
694
|
+
{ text: "Reader", href: request.baseUrl },
|
|
695
|
+
{ text: "Search" },
|
|
696
|
+
],
|
|
651
697
|
});
|
|
652
698
|
}
|
|
653
699
|
|
|
@@ -686,6 +732,10 @@ export async function searchFeeds(request, response) {
|
|
|
686
732
|
baseUrl: request.baseUrl,
|
|
687
733
|
readerBaseUrl: request.baseUrl,
|
|
688
734
|
activeView: "channels",
|
|
735
|
+
breadcrumbs: [
|
|
736
|
+
{ text: "Reader", href: request.baseUrl },
|
|
737
|
+
{ text: "Search" },
|
|
738
|
+
],
|
|
689
739
|
});
|
|
690
740
|
}
|
|
691
741
|
|
|
@@ -719,6 +769,10 @@ export async function subscribe(request, response) {
|
|
|
719
769
|
baseUrl: request.baseUrl,
|
|
720
770
|
readerBaseUrl: request.baseUrl,
|
|
721
771
|
activeView: "channels",
|
|
772
|
+
breadcrumbs: [
|
|
773
|
+
{ text: "Reader", href: request.baseUrl },
|
|
774
|
+
{ text: "Search" },
|
|
775
|
+
],
|
|
722
776
|
});
|
|
723
777
|
}
|
|
724
778
|
|
|
@@ -811,6 +865,13 @@ export async function editFeedForm(request, response) {
|
|
|
811
865
|
baseUrl: request.baseUrl,
|
|
812
866
|
readerBaseUrl: request.baseUrl,
|
|
813
867
|
activeView: "channels",
|
|
868
|
+
breadcrumbs: [
|
|
869
|
+
{ text: "Reader", href: request.baseUrl },
|
|
870
|
+
{ text: "Channels", href: `${request.baseUrl}/channels` },
|
|
871
|
+
{ text: channelDocument.name, href: `${request.baseUrl}/channels/${uid}` },
|
|
872
|
+
{ text: "Feeds", href: `${request.baseUrl}/channels/${uid}/feeds` },
|
|
873
|
+
{ text: "Edit" },
|
|
874
|
+
],
|
|
814
875
|
});
|
|
815
876
|
}
|
|
816
877
|
|
|
@@ -848,6 +909,13 @@ export async function updateFeedUrl(request, response) {
|
|
|
848
909
|
baseUrl: request.baseUrl,
|
|
849
910
|
readerBaseUrl: request.baseUrl,
|
|
850
911
|
activeView: "channels",
|
|
912
|
+
breadcrumbs: [
|
|
913
|
+
{ text: "Reader", href: request.baseUrl },
|
|
914
|
+
{ text: "Channels", href: `${request.baseUrl}/channels` },
|
|
915
|
+
{ text: channelDocument.name, href: `${request.baseUrl}/channels/${uid}` },
|
|
916
|
+
{ text: "Feeds", href: `${request.baseUrl}/channels/${uid}/feeds` },
|
|
917
|
+
{ text: "Edit" },
|
|
918
|
+
],
|
|
851
919
|
});
|
|
852
920
|
}
|
|
853
921
|
|
|
@@ -1029,6 +1097,10 @@ export async function actorProfile(request, response) {
|
|
|
1029
1097
|
baseUrl: request.baseUrl,
|
|
1030
1098
|
readerBaseUrl: request.baseUrl,
|
|
1031
1099
|
activeView: "channels",
|
|
1100
|
+
breadcrumbs: [
|
|
1101
|
+
{ text: "Reader", href: request.baseUrl },
|
|
1102
|
+
{ text: actor.name || "Actor" },
|
|
1103
|
+
],
|
|
1032
1104
|
});
|
|
1033
1105
|
} catch (error) {
|
|
1034
1106
|
console.error(`[Microsub] Actor profile fetch failed: ${error.message}`);
|
|
@@ -1043,6 +1115,10 @@ export async function actorProfile(request, response) {
|
|
|
1043
1115
|
readerBaseUrl: request.baseUrl,
|
|
1044
1116
|
activeView: "channels",
|
|
1045
1117
|
error: "Could not fetch this actor's profile. They may have restricted access.",
|
|
1118
|
+
breadcrumbs: [
|
|
1119
|
+
{ text: "Reader", href: request.baseUrl },
|
|
1120
|
+
{ text: "Actor" },
|
|
1121
|
+
],
|
|
1046
1122
|
});
|
|
1047
1123
|
}
|
|
1048
1124
|
}
|
|
@@ -1108,10 +1184,10 @@ export async function timeline(request, response) {
|
|
|
1108
1184
|
// Get channels with colors for filtering UI and item decoration
|
|
1109
1185
|
const channelList = await getChannelsWithColors(application, userId);
|
|
1110
1186
|
|
|
1111
|
-
// Build channel lookup map (ObjectId string -> { name, color })
|
|
1187
|
+
// Build channel lookup map (ObjectId string -> { name, color, uid })
|
|
1112
1188
|
const channelMap = new Map();
|
|
1113
1189
|
for (const ch of channelList) {
|
|
1114
|
-
channelMap.set(ch._id.toString(), { name: ch.name, color: ch.color });
|
|
1190
|
+
channelMap.set(ch._id.toString(), { name: ch.name, color: ch.color, uid: ch.uid });
|
|
1115
1191
|
}
|
|
1116
1192
|
|
|
1117
1193
|
// Parse excluded channel IDs from query params
|
|
@@ -1147,6 +1223,7 @@ export async function timeline(request, response) {
|
|
|
1147
1223
|
if (info) {
|
|
1148
1224
|
item._channelName = info.name;
|
|
1149
1225
|
item._channelColor = info.color;
|
|
1226
|
+
item._channelUid = info.uid;
|
|
1150
1227
|
}
|
|
1151
1228
|
}
|
|
1152
1229
|
}
|
|
@@ -1163,6 +1240,10 @@ export async function timeline(request, response) {
|
|
|
1163
1240
|
baseUrl: request.baseUrl,
|
|
1164
1241
|
readerBaseUrl: request.baseUrl,
|
|
1165
1242
|
activeView: "timeline",
|
|
1243
|
+
breadcrumbs: [
|
|
1244
|
+
{ text: "Reader", href: request.baseUrl },
|
|
1245
|
+
{ text: "Timeline" },
|
|
1246
|
+
],
|
|
1166
1247
|
});
|
|
1167
1248
|
}
|
|
1168
1249
|
|
|
@@ -1223,6 +1304,10 @@ export async function deck(request, response) {
|
|
|
1223
1304
|
baseUrl: request.baseUrl,
|
|
1224
1305
|
readerBaseUrl: request.baseUrl,
|
|
1225
1306
|
activeView: "deck",
|
|
1307
|
+
breadcrumbs: [
|
|
1308
|
+
{ text: "Reader", href: request.baseUrl },
|
|
1309
|
+
{ text: "Deck" },
|
|
1310
|
+
],
|
|
1226
1311
|
});
|
|
1227
1312
|
}
|
|
1228
1313
|
|
|
@@ -1249,6 +1334,11 @@ export async function deckSettings(request, response) {
|
|
|
1249
1334
|
baseUrl: request.baseUrl,
|
|
1250
1335
|
readerBaseUrl: request.baseUrl,
|
|
1251
1336
|
activeView: "deck",
|
|
1337
|
+
breadcrumbs: [
|
|
1338
|
+
{ text: "Reader", href: request.baseUrl },
|
|
1339
|
+
{ text: "Deck", href: `${request.baseUrl}/deck` },
|
|
1340
|
+
{ text: "Settings" },
|
|
1341
|
+
],
|
|
1252
1342
|
});
|
|
1253
1343
|
}
|
|
1254
1344
|
|
package/package.json
CHANGED
package/views/channel.njk
CHANGED
|
@@ -3,9 +3,7 @@
|
|
|
3
3
|
{% block reader %}
|
|
4
4
|
<div class="channel">
|
|
5
5
|
<header class="channel__header">
|
|
6
|
-
<
|
|
7
|
-
{{ icon("previous") }} {{ __("microsub.channels.title") }}
|
|
8
|
-
</a>
|
|
6
|
+
<h1>{{ channel.name }}</h1>
|
|
9
7
|
<div class="channel__actions">
|
|
10
8
|
{% if not showRead and items.length > 0 %}
|
|
11
9
|
<form action="{{ baseUrl }}/api/mark-read" method="POST" style="display: inline;">
|
package/views/layouts/reader.njk
CHANGED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{# Breadcrumb navigation #}
|
|
2
|
+
{% if breadcrumbs and breadcrumbs.length > 0 %}
|
|
3
|
+
<nav class="breadcrumbs" aria-label="Breadcrumb">
|
|
4
|
+
<ol class="breadcrumbs__list">
|
|
5
|
+
{% for crumb in breadcrumbs %}
|
|
6
|
+
<li class="breadcrumbs__item">
|
|
7
|
+
{% if crumb.href %}
|
|
8
|
+
<a href="{{ crumb.href }}" class="breadcrumbs__link">{{ crumb.text }}</a>
|
|
9
|
+
{% else %}
|
|
10
|
+
<span class="breadcrumbs__current" aria-current="page">{{ crumb.text }}</span>
|
|
11
|
+
{% endif %}
|
|
12
|
+
</li>
|
|
13
|
+
{% endfor %}
|
|
14
|
+
</ol>
|
|
15
|
+
</nav>
|
|
16
|
+
{% endif %}
|
|
@@ -202,6 +202,7 @@
|
|
|
202
202
|
class="item-actions__button item-actions__mark-read"
|
|
203
203
|
data-action="mark-read"
|
|
204
204
|
data-item-id="{{ item._id }}"
|
|
205
|
+
{% if item._channelUid %}data-channel-uid="{{ item._channelUid }}"{% endif %}
|
|
205
206
|
title="Mark as read">
|
|
206
207
|
{{ icon("checkboxChecked") }}
|
|
207
208
|
<span class="visually-hidden">Mark read</span>
|
package/views/timeline.njk
CHANGED
|
@@ -31,13 +31,13 @@
|
|
|
31
31
|
{% if items.length > 0 %}
|
|
32
32
|
<div class="timeline" id="timeline">
|
|
33
33
|
{% for item in items %}
|
|
34
|
-
<div class="timeline-view__item"
|
|
35
|
-
{% include "partials/item-card.njk" %}
|
|
34
|
+
<div class="timeline-view__item">
|
|
36
35
|
{% if item._channelName %}
|
|
37
|
-
<span class="timeline-view__channel-
|
|
36
|
+
<span class="timeline-view__channel-badge" style="background: {{ item._channelColor or '#888' }}">
|
|
38
37
|
{{ item._channelName }}
|
|
39
38
|
</span>
|
|
40
39
|
{% endif %}
|
|
40
|
+
{% include "partials/item-card.njk" %}
|
|
41
41
|
</div>
|
|
42
42
|
{% endfor %}
|
|
43
43
|
</div>
|
|
@@ -95,6 +95,61 @@
|
|
|
95
95
|
break;
|
|
96
96
|
}
|
|
97
97
|
});
|
|
98
|
+
|
|
99
|
+
// Handle individual mark-read buttons
|
|
100
|
+
const microsubApiUrl = '{{ baseUrl }}'.replace(/\/reader$/, '');
|
|
101
|
+
|
|
102
|
+
timeline.addEventListener('click', async (e) => {
|
|
103
|
+
const button = e.target.closest('.item-actions__mark-read');
|
|
104
|
+
if (!button) return;
|
|
105
|
+
|
|
106
|
+
e.preventDefault();
|
|
107
|
+
e.stopPropagation();
|
|
108
|
+
|
|
109
|
+
const itemId = button.dataset.itemId;
|
|
110
|
+
const channelUid = button.dataset.channelUid;
|
|
111
|
+
if (!itemId || !channelUid) return;
|
|
112
|
+
|
|
113
|
+
button.disabled = true;
|
|
114
|
+
|
|
115
|
+
try {
|
|
116
|
+
const formData = new URLSearchParams();
|
|
117
|
+
formData.append('action', 'timeline');
|
|
118
|
+
formData.append('method', 'mark_read');
|
|
119
|
+
formData.append('channel', channelUid);
|
|
120
|
+
formData.append('entry', itemId);
|
|
121
|
+
|
|
122
|
+
const response = await fetch(microsubApiUrl, {
|
|
123
|
+
method: 'POST',
|
|
124
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
125
|
+
body: formData.toString(),
|
|
126
|
+
credentials: 'same-origin'
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
if (response.ok) {
|
|
130
|
+
const card = button.closest('.item-card');
|
|
131
|
+
if (card) {
|
|
132
|
+
card.style.transition = 'opacity 0.3s ease, transform 0.3s ease';
|
|
133
|
+
card.style.opacity = '0';
|
|
134
|
+
card.style.transform = 'translateX(-20px)';
|
|
135
|
+
setTimeout(() => {
|
|
136
|
+
const wrapper = card.closest('.timeline-view__item');
|
|
137
|
+
if (wrapper) wrapper.remove();
|
|
138
|
+
else card.remove();
|
|
139
|
+
if (timeline.querySelectorAll('.item-card').length === 0) {
|
|
140
|
+
location.reload();
|
|
141
|
+
}
|
|
142
|
+
}, 300);
|
|
143
|
+
}
|
|
144
|
+
} else {
|
|
145
|
+
console.error('Failed to mark item as read');
|
|
146
|
+
button.disabled = false;
|
|
147
|
+
}
|
|
148
|
+
} catch (error) {
|
|
149
|
+
console.error('Error marking item as read:', error);
|
|
150
|
+
button.disabled = false;
|
|
151
|
+
}
|
|
152
|
+
});
|
|
98
153
|
}
|
|
99
154
|
</script>
|
|
100
155
|
{% endblock %}
|