@rmdes/indiekit-endpoint-activitypub 1.1.14 → 1.1.16
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/reader.css +82 -2
- package/index.js +16 -0
- package/lib/controllers/reader.js +95 -6
- package/lib/storage/notifications.js +26 -4
- package/lib/storage/timeline.js +16 -14
- package/locales/en.json +5 -1
- package/package.json +1 -1
- package/views/activitypub-activities.njk +1 -1
- package/views/activitypub-compose.njk +1 -5
- package/views/activitypub-featured-tags.njk +1 -1
- package/views/activitypub-featured.njk +1 -1
- package/views/activitypub-followers.njk +1 -1
- package/views/activitypub-following.njk +1 -1
- package/views/activitypub-migrate.njk +1 -1
- package/views/activitypub-moderation.njk +1 -5
- package/views/activitypub-notifications.njk +15 -5
- package/views/activitypub-post-detail.njk +1 -5
- package/views/activitypub-profile.njk +1 -1
- package/views/activitypub-reader.njk +4 -8
- package/views/activitypub-remote-profile.njk +1 -5
- package/views/partials/ap-item-card.njk +3 -2
- package/views/partials/ap-notification-card.njk +18 -12
package/assets/reader.css
CHANGED
|
@@ -746,6 +746,37 @@
|
|
|
746
746
|
Notifications
|
|
747
747
|
========================================================================== */
|
|
748
748
|
|
|
749
|
+
/* Notifications Toolbar */
|
|
750
|
+
.ap-notifications__toolbar {
|
|
751
|
+
display: flex;
|
|
752
|
+
gap: var(--space-s);
|
|
753
|
+
margin-bottom: var(--space-m);
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
.ap-notifications__btn {
|
|
757
|
+
background: var(--color-offset);
|
|
758
|
+
border: var(--border-width-thin) solid var(--color-outline);
|
|
759
|
+
border-radius: var(--border-radius-small);
|
|
760
|
+
color: var(--color-on-background);
|
|
761
|
+
cursor: pointer;
|
|
762
|
+
font-size: var(--font-size-s);
|
|
763
|
+
padding: var(--space-xs) var(--space-m);
|
|
764
|
+
transition: all 0.2s ease;
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
.ap-notifications__btn:hover {
|
|
768
|
+
background: var(--color-offset-variant);
|
|
769
|
+
border-color: var(--color-outline-variant);
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
.ap-notifications__btn--danger {
|
|
773
|
+
color: var(--color-red45);
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
.ap-notifications__btn--danger:hover {
|
|
777
|
+
border-color: var(--color-red45);
|
|
778
|
+
}
|
|
779
|
+
|
|
749
780
|
.ap-notification {
|
|
750
781
|
align-items: flex-start;
|
|
751
782
|
background: var(--color-offset);
|
|
@@ -754,6 +785,7 @@
|
|
|
754
785
|
display: flex;
|
|
755
786
|
gap: var(--space-s);
|
|
756
787
|
padding: var(--space-m);
|
|
788
|
+
position: relative;
|
|
757
789
|
}
|
|
758
790
|
|
|
759
791
|
.ap-notification--unread {
|
|
@@ -761,9 +793,34 @@
|
|
|
761
793
|
box-shadow: 0 0 8px 0 hsl(var(--tint-yellow) 50% / 0.3);
|
|
762
794
|
}
|
|
763
795
|
|
|
764
|
-
.ap-
|
|
796
|
+
.ap-notification__avatar-wrap {
|
|
765
797
|
flex-shrink: 0;
|
|
766
|
-
|
|
798
|
+
position: relative;
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
.ap-notification__avatar {
|
|
802
|
+
border: var(--border-width-thin) solid var(--color-outline);
|
|
803
|
+
border-radius: 50%;
|
|
804
|
+
height: 40px;
|
|
805
|
+
object-fit: cover;
|
|
806
|
+
width: 40px;
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
.ap-notification__avatar--default {
|
|
810
|
+
align-items: center;
|
|
811
|
+
background: var(--color-offset-variant);
|
|
812
|
+
color: var(--color-on-offset);
|
|
813
|
+
display: inline-flex;
|
|
814
|
+
font-size: 1.1em;
|
|
815
|
+
font-weight: 600;
|
|
816
|
+
justify-content: center;
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
.ap-notification__type-badge {
|
|
820
|
+
bottom: -2px;
|
|
821
|
+
font-size: 0.75em;
|
|
822
|
+
position: absolute;
|
|
823
|
+
right: -4px;
|
|
767
824
|
}
|
|
768
825
|
|
|
769
826
|
.ap-notification__body {
|
|
@@ -803,6 +860,29 @@
|
|
|
803
860
|
font-size: var(--font-size-xs);
|
|
804
861
|
}
|
|
805
862
|
|
|
863
|
+
.ap-notification__dismiss {
|
|
864
|
+
position: absolute;
|
|
865
|
+
right: var(--space-xs);
|
|
866
|
+
top: var(--space-xs);
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
.ap-notification__dismiss-btn {
|
|
870
|
+
background: transparent;
|
|
871
|
+
border: 0;
|
|
872
|
+
border-radius: var(--border-radius-small);
|
|
873
|
+
color: var(--color-on-offset);
|
|
874
|
+
cursor: pointer;
|
|
875
|
+
font-size: var(--font-size-m);
|
|
876
|
+
line-height: 1;
|
|
877
|
+
padding: 2px 6px;
|
|
878
|
+
transition: all 0.2s ease;
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
.ap-notification__dismiss-btn:hover {
|
|
882
|
+
background: var(--color-offset-variant);
|
|
883
|
+
color: var(--color-red45);
|
|
884
|
+
}
|
|
885
|
+
|
|
806
886
|
/* ==========================================================================
|
|
807
887
|
Remote Profile
|
|
808
888
|
========================================================================== */
|
package/index.js
CHANGED
|
@@ -12,6 +12,9 @@ import { dashboardController } from "./lib/controllers/dashboard.js";
|
|
|
12
12
|
import {
|
|
13
13
|
readerController,
|
|
14
14
|
notificationsController,
|
|
15
|
+
markAllNotificationsReadController,
|
|
16
|
+
clearAllNotificationsController,
|
|
17
|
+
deleteNotificationController,
|
|
15
18
|
composeController,
|
|
16
19
|
submitComposeController,
|
|
17
20
|
remoteProfileController,
|
|
@@ -79,6 +82,7 @@ const defaults = {
|
|
|
79
82
|
parallelWorkers: 5,
|
|
80
83
|
actorType: "Person",
|
|
81
84
|
timelineRetention: 1000,
|
|
85
|
+
notificationRetentionDays: 30,
|
|
82
86
|
};
|
|
83
87
|
|
|
84
88
|
export default class ActivityPubEndpoint {
|
|
@@ -189,6 +193,9 @@ export default class ActivityPubEndpoint {
|
|
|
189
193
|
router.get("/", dashboardController(mp));
|
|
190
194
|
router.get("/admin/reader", readerController(mp));
|
|
191
195
|
router.get("/admin/reader/notifications", notificationsController(mp));
|
|
196
|
+
router.post("/admin/reader/notifications/mark-read", markAllNotificationsReadController(mp));
|
|
197
|
+
router.post("/admin/reader/notifications/clear", clearAllNotificationsController(mp));
|
|
198
|
+
router.post("/admin/reader/notifications/delete", deleteNotificationController(mp));
|
|
192
199
|
router.get("/admin/reader/compose", composeController(mp, this));
|
|
193
200
|
router.post("/admin/reader/compose", submitComposeController(mp, this));
|
|
194
201
|
router.post("/admin/reader/like", likeController(mp, this));
|
|
@@ -835,6 +842,15 @@ export default class ActivityPubEndpoint {
|
|
|
835
842
|
{ background: true },
|
|
836
843
|
);
|
|
837
844
|
|
|
845
|
+
// TTL index for notification cleanup
|
|
846
|
+
const notifRetention = this.options.notificationRetentionDays;
|
|
847
|
+
if (notifRetention > 0) {
|
|
848
|
+
this._collections.ap_notifications.createIndex(
|
|
849
|
+
{ createdAt: 1 },
|
|
850
|
+
{ expireAfterSeconds: notifRetention * 86_400 },
|
|
851
|
+
);
|
|
852
|
+
}
|
|
853
|
+
|
|
838
854
|
this._collections.ap_muted.createIndex(
|
|
839
855
|
{ url: 1 },
|
|
840
856
|
{ unique: true, sparse: true, background: true },
|
|
@@ -7,8 +7,10 @@ import {
|
|
|
7
7
|
getNotifications,
|
|
8
8
|
getUnreadNotificationCount,
|
|
9
9
|
markAllNotificationsRead,
|
|
10
|
+
clearAllNotifications,
|
|
11
|
+
deleteNotification,
|
|
10
12
|
} from "../storage/notifications.js";
|
|
11
|
-
import { getToken } from "../csrf.js";
|
|
13
|
+
import { getToken, validateToken } from "../csrf.js";
|
|
12
14
|
import {
|
|
13
15
|
getMutedUrls,
|
|
14
16
|
getMutedKeywords,
|
|
@@ -188,19 +190,17 @@ export function notificationsController(mountPath) {
|
|
|
188
190
|
// Get notifications
|
|
189
191
|
const result = await getNotifications(collections, { before, limit });
|
|
190
192
|
|
|
191
|
-
// Get unread count before marking as read
|
|
192
193
|
const unreadCount = await getUnreadNotificationCount(collections);
|
|
193
194
|
|
|
194
|
-
//
|
|
195
|
-
|
|
196
|
-
await markAllNotificationsRead(collections);
|
|
197
|
-
}
|
|
195
|
+
// CSRF token for action forms
|
|
196
|
+
const csrfToken = getToken(request.session);
|
|
198
197
|
|
|
199
198
|
response.render("activitypub-notifications", {
|
|
200
199
|
title: response.locals.__("activitypub.notifications.title"),
|
|
201
200
|
items: result.items,
|
|
202
201
|
before: result.before,
|
|
203
202
|
unreadCount,
|
|
203
|
+
csrfToken,
|
|
204
204
|
mountPath,
|
|
205
205
|
});
|
|
206
206
|
} catch (error) {
|
|
@@ -208,3 +208,92 @@ export function notificationsController(mountPath) {
|
|
|
208
208
|
}
|
|
209
209
|
};
|
|
210
210
|
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* POST /admin/reader/notifications/mark-read — mark all notifications as read.
|
|
214
|
+
*/
|
|
215
|
+
export function markAllNotificationsReadController(mountPath) {
|
|
216
|
+
return async (request, response, next) => {
|
|
217
|
+
try {
|
|
218
|
+
if (!validateToken(request)) {
|
|
219
|
+
return response.status(403).redirect(`${mountPath}/admin/reader/notifications`);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const { application } = request.app.locals;
|
|
223
|
+
const collections = {
|
|
224
|
+
ap_notifications: application?.collections?.get("ap_notifications"),
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
await markAllNotificationsRead(collections);
|
|
228
|
+
|
|
229
|
+
return response.redirect(`${mountPath}/admin/reader/notifications`);
|
|
230
|
+
} catch (error) {
|
|
231
|
+
next(error);
|
|
232
|
+
}
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* POST /admin/reader/notifications/clear — delete all notifications.
|
|
238
|
+
*/
|
|
239
|
+
export function clearAllNotificationsController(mountPath) {
|
|
240
|
+
return async (request, response, next) => {
|
|
241
|
+
try {
|
|
242
|
+
if (!validateToken(request)) {
|
|
243
|
+
return response.status(403).redirect(`${mountPath}/admin/reader/notifications`);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const { application } = request.app.locals;
|
|
247
|
+
const collections = {
|
|
248
|
+
ap_notifications: application?.collections?.get("ap_notifications"),
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
await clearAllNotifications(collections);
|
|
252
|
+
|
|
253
|
+
return response.redirect(`${mountPath}/admin/reader/notifications`);
|
|
254
|
+
} catch (error) {
|
|
255
|
+
next(error);
|
|
256
|
+
}
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* POST /admin/reader/notifications/delete — delete a single notification.
|
|
262
|
+
*/
|
|
263
|
+
export function deleteNotificationController(mountPath) {
|
|
264
|
+
return async (request, response, next) => {
|
|
265
|
+
try {
|
|
266
|
+
if (!validateToken(request)) {
|
|
267
|
+
return response.status(403).json({
|
|
268
|
+
success: false,
|
|
269
|
+
error: "Invalid CSRF token",
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const { uid } = request.body;
|
|
274
|
+
|
|
275
|
+
if (!uid) {
|
|
276
|
+
return response.status(400).json({
|
|
277
|
+
success: false,
|
|
278
|
+
error: "Missing notification UID",
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const { application } = request.app.locals;
|
|
283
|
+
const collections = {
|
|
284
|
+
ap_notifications: application?.collections?.get("ap_notifications"),
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
await deleteNotification(collections, uid);
|
|
288
|
+
|
|
289
|
+
// Support both JSON (fetch) and form redirect
|
|
290
|
+
if (request.headers.accept?.includes("application/json")) {
|
|
291
|
+
return response.json({ success: true, uid });
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
return response.redirect(`${mountPath}/admin/reader/notifications`);
|
|
295
|
+
} catch (error) {
|
|
296
|
+
next(error);
|
|
297
|
+
}
|
|
298
|
+
};
|
|
299
|
+
}
|
|
@@ -66,9 +66,10 @@ export async function getNotifications(collections, options = {}) {
|
|
|
66
66
|
query.read = false;
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
-
// Cursor pagination
|
|
69
|
+
// Cursor pagination — published is stored as ISO string, so compare
|
|
70
|
+
// as strings (lexicographic ISO 8601 comparison is correct for dates)
|
|
70
71
|
if (options.before) {
|
|
71
|
-
query.published = { $lt:
|
|
72
|
+
query.published = { $lt: options.before };
|
|
72
73
|
}
|
|
73
74
|
|
|
74
75
|
const rawItems = await ap_notifications
|
|
@@ -85,9 +86,9 @@ export async function getNotifications(collections, options = {}) {
|
|
|
85
86
|
: item.published,
|
|
86
87
|
}));
|
|
87
88
|
|
|
88
|
-
// Generate cursor for next page
|
|
89
|
+
// Generate cursor for next page (only if full page returned = more may exist)
|
|
89
90
|
const before =
|
|
90
|
-
items.length
|
|
91
|
+
items.length === limit
|
|
91
92
|
? items[items.length - 1].published
|
|
92
93
|
: null;
|
|
93
94
|
|
|
@@ -130,3 +131,24 @@ export async function markAllNotificationsRead(collections) {
|
|
|
130
131
|
const { ap_notifications } = collections;
|
|
131
132
|
return await ap_notifications.updateMany({}, { $set: { read: true } });
|
|
132
133
|
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Delete all notifications
|
|
137
|
+
* @param {object} collections - MongoDB collections
|
|
138
|
+
* @returns {Promise<object>} Delete result
|
|
139
|
+
*/
|
|
140
|
+
export async function clearAllNotifications(collections) {
|
|
141
|
+
const { ap_notifications } = collections;
|
|
142
|
+
return await ap_notifications.deleteMany({});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Delete a single notification by UID
|
|
147
|
+
* @param {object} collections - MongoDB collections
|
|
148
|
+
* @param {string} uid - Notification UID
|
|
149
|
+
* @returns {Promise<object>} Delete result
|
|
150
|
+
*/
|
|
151
|
+
export async function deleteNotification(collections, uid) {
|
|
152
|
+
const { ap_notifications } = collections;
|
|
153
|
+
return await ap_notifications.deleteOne({ uid });
|
|
154
|
+
}
|
package/lib/storage/timeline.js
CHANGED
|
@@ -94,23 +94,20 @@ export async function getTimelineItems(collections, options = {}) {
|
|
|
94
94
|
query["author.url"] = options.authorUrl;
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
-
// Cursor pagination —
|
|
97
|
+
// Cursor pagination — published is stored as ISO string, so compare
|
|
98
|
+
// as strings (lexicographic ISO 8601 comparison is correct for dates)
|
|
98
99
|
if (options.before) {
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
if (Number.isNaN(beforeDate.getTime())) {
|
|
100
|
+
if (Number.isNaN(new Date(options.before).getTime())) {
|
|
102
101
|
throw new Error("Invalid before cursor");
|
|
103
102
|
}
|
|
104
103
|
|
|
105
|
-
query.published = { $lt:
|
|
104
|
+
query.published = { $lt: options.before };
|
|
106
105
|
} else if (options.after) {
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
if (Number.isNaN(afterDate.getTime())) {
|
|
106
|
+
if (Number.isNaN(new Date(options.after).getTime())) {
|
|
110
107
|
throw new Error("Invalid after cursor");
|
|
111
108
|
}
|
|
112
109
|
|
|
113
|
-
query.published = { $gt:
|
|
110
|
+
query.published = { $gt: options.after };
|
|
114
111
|
}
|
|
115
112
|
|
|
116
113
|
const rawItems = await ap_timeline
|
|
@@ -128,13 +125,16 @@ export async function getTimelineItems(collections, options = {}) {
|
|
|
128
125
|
}));
|
|
129
126
|
|
|
130
127
|
// Generate cursors for pagination
|
|
128
|
+
// Items are sorted newest-first, so:
|
|
129
|
+
// - "before" cursor (for "Older" link) = oldest item's date (last in array)
|
|
130
|
+
// - "after" cursor (for "Newer" link) = newest item's date (first in array)
|
|
131
131
|
const before =
|
|
132
|
-
items.length
|
|
133
|
-
? items[
|
|
132
|
+
items.length === limit
|
|
133
|
+
? items[items.length - 1].published
|
|
134
134
|
: null;
|
|
135
135
|
const after =
|
|
136
|
-
items.length > 0
|
|
137
|
-
? items[
|
|
136
|
+
items.length > 0 && (options.before || options.after)
|
|
137
|
+
? items[0].published
|
|
138
138
|
: null;
|
|
139
139
|
|
|
140
140
|
return {
|
|
@@ -190,7 +190,9 @@ export async function updateTimelineItem(collections, uid, updates) {
|
|
|
190
190
|
*/
|
|
191
191
|
export async function deleteOldTimelineItems(collections, cutoffDate) {
|
|
192
192
|
const { ap_timeline } = collections;
|
|
193
|
-
|
|
193
|
+
// published is stored as ISO string — convert cutoff to string for comparison
|
|
194
|
+
const cutoff = cutoffDate instanceof Date ? cutoffDate.toISOString() : cutoffDate;
|
|
195
|
+
const result = await ap_timeline.deleteMany({ published: { $lt: cutoff } });
|
|
194
196
|
return result.deletedCount;
|
|
195
197
|
}
|
|
196
198
|
|
package/locales/en.json
CHANGED
|
@@ -141,7 +141,11 @@
|
|
|
141
141
|
"boostedPost": "boosted your post",
|
|
142
142
|
"followedYou": "followed you",
|
|
143
143
|
"repliedTo": "replied to your post",
|
|
144
|
-
"mentionedYou": "mentioned you"
|
|
144
|
+
"mentionedYou": "mentioned you",
|
|
145
|
+
"markAllRead": "Mark all read",
|
|
146
|
+
"clearAll": "Clear all",
|
|
147
|
+
"clearConfirm": "Delete all notifications? This cannot be undone.",
|
|
148
|
+
"dismiss": "Dismiss"
|
|
145
149
|
},
|
|
146
150
|
"reader": {
|
|
147
151
|
"title": "Reader",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rmdes/indiekit-endpoint-activitypub",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.16",
|
|
4
4
|
"description": "ActivityPub federation endpoint for Indiekit via Fedify. Adds full fediverse support: actor, inbox, outbox, followers, following, syndication, and Mastodon migration.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"indiekit",
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
{% from "pagination/macro.njk" import pagination with context %}
|
|
8
8
|
|
|
9
9
|
{% block content %}
|
|
10
|
-
{{ heading({ text: __("activitypub.activities"), level: 1
|
|
10
|
+
{{ heading({ text: __("activitypub.activities"), level: 1 }) }}
|
|
11
11
|
|
|
12
12
|
{% if activities.length > 0 %}
|
|
13
13
|
{% for activity in activities %}
|
|
@@ -3,11 +3,7 @@
|
|
|
3
3
|
{% from "heading/macro.njk" import heading with context %}
|
|
4
4
|
|
|
5
5
|
{% block readercontent %}
|
|
6
|
-
{{ heading({
|
|
7
|
-
text: title,
|
|
8
|
-
level: 1,
|
|
9
|
-
parent: { text: __("activitypub.reader.title"), href: mountPath + "/admin/reader" }
|
|
10
|
-
}) }}
|
|
6
|
+
{{ heading({ text: title, level: 1 }) }}
|
|
11
7
|
|
|
12
8
|
{# Reply context — show the post being replied to #}
|
|
13
9
|
{% if replyContext %}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
{% from "prose/macro.njk" import prose with context %}
|
|
5
5
|
|
|
6
6
|
{% block content %}
|
|
7
|
-
{{ heading({ text: title, level: 1
|
|
7
|
+
{{ heading({ text: title, level: 1 }) }}
|
|
8
8
|
|
|
9
9
|
{% if tags.length > 0 %}
|
|
10
10
|
<table class="table">
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
{% from "prose/macro.njk" import prose with context %}
|
|
5
5
|
|
|
6
6
|
{% block content %}
|
|
7
|
-
{{ heading({ text: title, level: 1
|
|
7
|
+
{{ heading({ text: title, level: 1 }) }}
|
|
8
8
|
|
|
9
9
|
{% if pinned.length > 0 %}
|
|
10
10
|
<table class="table">
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
{% from "pagination/macro.njk" import pagination with context %}
|
|
7
7
|
|
|
8
8
|
{% block content %}
|
|
9
|
-
{{ heading({ text: followerCount + " " + __("activitypub.followers"), level: 1
|
|
9
|
+
{{ heading({ text: followerCount + " " + __("activitypub.followers"), level: 1 }) }}
|
|
10
10
|
|
|
11
11
|
{% if followers.length > 0 %}
|
|
12
12
|
{% for follower in followers %}
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
{% from "pagination/macro.njk" import pagination with context %}
|
|
8
8
|
|
|
9
9
|
{% block content %}
|
|
10
|
-
{{ heading({ text: followingCount + " " + __("activitypub.following"), level: 1
|
|
10
|
+
{{ heading({ text: followingCount + " " + __("activitypub.following"), level: 1 }) }}
|
|
11
11
|
|
|
12
12
|
{% if following.length > 0 %}
|
|
13
13
|
{% for account in following %}
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
{% from "prose/macro.njk" import prose with context %}
|
|
9
9
|
|
|
10
10
|
{% block content %}
|
|
11
|
-
{{ heading({ text: title, level: 1
|
|
11
|
+
{{ heading({ text: title, level: 1 }) }}
|
|
12
12
|
|
|
13
13
|
{% if result %}
|
|
14
14
|
{{ notificationBanner({ type: result.type, text: result.text }) }}
|
|
@@ -4,11 +4,7 @@
|
|
|
4
4
|
{% from "prose/macro.njk" import prose with context %}
|
|
5
5
|
|
|
6
6
|
{% block readercontent %}
|
|
7
|
-
{{ heading({
|
|
8
|
-
text: title,
|
|
9
|
-
level: 1,
|
|
10
|
-
parent: { text: __("activitypub.reader.title"), href: mountPath + "/admin/reader" }
|
|
11
|
-
}) }}
|
|
7
|
+
{{ heading({ text: title, level: 1 }) }}
|
|
12
8
|
|
|
13
9
|
{# Blocked actors #}
|
|
14
10
|
<section class="ap-moderation__section">
|
|
@@ -4,13 +4,23 @@
|
|
|
4
4
|
{% from "prose/macro.njk" import prose with context %}
|
|
5
5
|
|
|
6
6
|
{% block readercontent %}
|
|
7
|
-
{{ heading({
|
|
8
|
-
text: __("activitypub.notifications.title"),
|
|
9
|
-
level: 1,
|
|
10
|
-
parent: { text: __("activitypub.reader.title"), href: mountPath + "/admin/reader" }
|
|
11
|
-
}) }}
|
|
7
|
+
{{ heading({ text: __("activitypub.notifications.title"), level: 1 }) }}
|
|
12
8
|
|
|
13
9
|
{% if items.length > 0 %}
|
|
10
|
+
<div class="ap-notifications__toolbar">
|
|
11
|
+
{% if unreadCount > 0 %}
|
|
12
|
+
<form method="post" action="{{ mountPath }}/admin/reader/notifications/mark-read">
|
|
13
|
+
<input type="hidden" name="_csrf" value="{{ csrfToken }}">
|
|
14
|
+
<button type="submit" class="ap-notifications__btn">{{ __("activitypub.notifications.markAllRead") }}</button>
|
|
15
|
+
</form>
|
|
16
|
+
{% endif %}
|
|
17
|
+
<form method="post" action="{{ mountPath }}/admin/reader/notifications/clear"
|
|
18
|
+
onsubmit="return confirm('{{ __("activitypub.notifications.clearConfirm") }}')">
|
|
19
|
+
<input type="hidden" name="_csrf" value="{{ csrfToken }}">
|
|
20
|
+
<button type="submit" class="ap-notifications__btn ap-notifications__btn--danger">{{ __("activitypub.notifications.clearAll") }}</button>
|
|
21
|
+
</form>
|
|
22
|
+
</div>
|
|
23
|
+
|
|
14
24
|
<div class="ap-timeline">
|
|
15
25
|
{% for item in items %}
|
|
16
26
|
{% include "partials/ap-notification-card.njk" %}
|
|
@@ -3,11 +3,7 @@
|
|
|
3
3
|
{% from "heading/macro.njk" import heading with context %}
|
|
4
4
|
|
|
5
5
|
{% block readercontent %}
|
|
6
|
-
{{ heading({
|
|
7
|
-
text: title,
|
|
8
|
-
level: 1,
|
|
9
|
-
parent: { text: __("activitypub.reader.title"), href: mountPath + "/admin/reader" }
|
|
10
|
-
}) }}
|
|
6
|
+
{{ heading({ text: title, level: 1 }) }}
|
|
11
7
|
|
|
12
8
|
<div class="ap-post-detail" data-mount-path="{{ mountPath }}">
|
|
13
9
|
{# Back button #}
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
{% from "prose/macro.njk" import prose with context %}
|
|
11
11
|
|
|
12
12
|
{% block content %}
|
|
13
|
-
{{ heading({ text: title, level: 1
|
|
13
|
+
{{ heading({ text: title, level: 1 }) }}
|
|
14
14
|
|
|
15
15
|
{% if result %}
|
|
16
16
|
{{ notificationBanner({ type: result.type, text: result.text }) }}
|
|
@@ -4,17 +4,10 @@
|
|
|
4
4
|
{% from "prose/macro.njk" import prose with context %}
|
|
5
5
|
|
|
6
6
|
{% block readercontent %}
|
|
7
|
-
{{ heading({
|
|
8
|
-
text: __("activitypub.reader.title"),
|
|
9
|
-
level: 1,
|
|
10
|
-
parent: { text: __("activitypub.title"), href: mountPath }
|
|
11
|
-
}) }}
|
|
7
|
+
{{ heading({ text: __("activitypub.reader.title"), level: 1 }) }}
|
|
12
8
|
|
|
13
9
|
{# Tab navigation #}
|
|
14
10
|
<nav class="ap-tabs" role="tablist">
|
|
15
|
-
<a href="?tab=all" class="ap-tab{% if tab == 'all' %} ap-tab--active{% endif %}" role="tab">
|
|
16
|
-
{{ __("activitypub.reader.tabs.all") }}
|
|
17
|
-
</a>
|
|
18
11
|
<a href="?tab=notes" class="ap-tab{% if tab == 'notes' %} ap-tab--active{% endif %}" role="tab">
|
|
19
12
|
{{ __("activitypub.reader.tabs.notes") }}
|
|
20
13
|
</a>
|
|
@@ -30,6 +23,9 @@
|
|
|
30
23
|
<a href="?tab=media" class="ap-tab{% if tab == 'media' %} ap-tab--active{% endif %}" role="tab">
|
|
31
24
|
{{ __("activitypub.reader.tabs.media") }}
|
|
32
25
|
</a>
|
|
26
|
+
<a href="?tab=all" class="ap-tab{% if tab == 'all' %} ap-tab--active{% endif %}" role="tab">
|
|
27
|
+
{{ __("activitypub.reader.tabs.all") }}
|
|
28
|
+
</a>
|
|
33
29
|
</nav>
|
|
34
30
|
|
|
35
31
|
{# Timeline items #}
|
|
@@ -4,11 +4,7 @@
|
|
|
4
4
|
{% from "prose/macro.njk" import prose with context %}
|
|
5
5
|
|
|
6
6
|
{% block readercontent %}
|
|
7
|
-
{{ heading({
|
|
8
|
-
text: title,
|
|
9
|
-
level: 1,
|
|
10
|
-
parent: { text: __("activitypub.reader.title"), href: mountPath + "/admin/reader" }
|
|
11
|
-
}) }}
|
|
7
|
+
{{ heading({ text: title, level: 1 }) }}
|
|
12
8
|
|
|
13
9
|
<div class="ap-profile"
|
|
14
10
|
x-data="{
|
|
@@ -24,8 +24,9 @@
|
|
|
24
24
|
{# Author header #}
|
|
25
25
|
<header class="ap-card__author">
|
|
26
26
|
{% if item.author.photo %}
|
|
27
|
-
<img src="{{ item.author.photo }}" alt="{{ item.author.name }}" class="ap-card__avatar"
|
|
28
|
-
onerror="this.
|
|
27
|
+
<img src="{{ item.author.photo }}" alt="{{ item.author.name }}" class="ap-card__avatar" loading="lazy" crossorigin="anonymous"
|
|
28
|
+
onerror="this.style.display='none';this.nextElementSibling.style.display=''">
|
|
29
|
+
<span class="ap-card__avatar ap-card__avatar--default" style="display:none" aria-hidden="true">{{ item.author.name[0] | upper if item.author.name else "?" }}</span>
|
|
29
30
|
{% else %}
|
|
30
31
|
<span class="ap-card__avatar ap-card__avatar--default" aria-hidden="true">{{ item.author.name[0] | upper if item.author.name else "?" }}</span>
|
|
31
32
|
{% endif %}
|
|
@@ -1,19 +1,25 @@
|
|
|
1
1
|
{# Notification card partial #}
|
|
2
2
|
|
|
3
3
|
<div class="ap-notification{% if not item.read %} ap-notification--unread{% endif %}">
|
|
4
|
-
{#
|
|
5
|
-
<
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
4
|
+
{# Dismiss button #}
|
|
5
|
+
<form method="post" action="{{ mountPath }}/admin/reader/notifications/delete" class="ap-notification__dismiss">
|
|
6
|
+
<input type="hidden" name="_csrf" value="{{ csrfToken }}">
|
|
7
|
+
<input type="hidden" name="uid" value="{{ item.uid }}">
|
|
8
|
+
<button type="submit" class="ap-notification__dismiss-btn" title="{{ __('activitypub.notifications.dismiss') }}">×</button>
|
|
9
|
+
</form>
|
|
10
|
+
|
|
11
|
+
{# Actor avatar with type badge #}
|
|
12
|
+
<div class="ap-notification__avatar-wrap">
|
|
13
|
+
{% if item.actorPhoto %}
|
|
14
|
+
<img src="{{ item.actorPhoto }}" alt="{{ item.actorName }}" class="ap-notification__avatar" loading="lazy" crossorigin="anonymous"
|
|
15
|
+
onerror="this.style.display='none';this.nextElementSibling.style.display=''">
|
|
16
|
+
<span class="ap-notification__avatar ap-notification__avatar--default" style="display:none" aria-hidden="true">{{ item.actorName[0] | upper if item.actorName else "?" }}</span>
|
|
17
|
+
{% else %}
|
|
18
|
+
<span class="ap-notification__avatar ap-notification__avatar--default" aria-hidden="true">{{ item.actorName[0] | upper if item.actorName else "?" }}</span>
|
|
16
19
|
{% endif %}
|
|
20
|
+
<span class="ap-notification__type-badge">
|
|
21
|
+
{% if item.type == "like" %}❤{% elif item.type == "boost" %}🔁{% elif item.type == "follow" %}👤{% elif item.type == "reply" %}💬{% elif item.type == "mention" %}@{% endif %}
|
|
22
|
+
</span>
|
|
17
23
|
</div>
|
|
18
24
|
|
|
19
25
|
{# Notification body #}
|