@rmdes/indiekit-endpoint-activitypub 1.1.15 → 1.1.17
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 +75 -2
- package/index.js +2 -0
- package/lib/controllers/followers.js +1 -1
- package/lib/controllers/following.js +1 -1
- package/lib/controllers/resolve.js +109 -0
- package/lib/storage/notifications.js +5 -4
- package/lib/storage/timeline.js +16 -14
- package/locales/en.json +7 -0
- package/package.json +1 -1
- package/views/activitypub-activities.njk +0 -2
- package/views/activitypub-compose.njk +0 -6
- package/views/activitypub-dashboard.njk +0 -2
- package/views/activitypub-featured-tags.njk +0 -2
- package/views/activitypub-featured.njk +0 -2
- package/views/activitypub-followers.njk +0 -2
- package/views/activitypub-following.njk +0 -2
- package/views/activitypub-migrate.njk +0 -2
- package/views/activitypub-moderation.njk +0 -6
- package/views/activitypub-notifications.njk +0 -6
- package/views/activitypub-post-detail.njk +0 -6
- package/views/activitypub-profile.njk +0 -2
- package/views/activitypub-reader.njk +10 -8
- package/views/activitypub-remote-profile.njk +0 -6
- package/views/partials/ap-item-card.njk +3 -2
- package/views/partials/ap-notification-card.njk +11 -12
package/assets/reader.css
CHANGED
|
@@ -4,6 +4,54 @@
|
|
|
4
4
|
* Uses Indiekit CSS custom properties for automatic dark mode support
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
/* ==========================================================================
|
|
8
|
+
Fediverse Lookup
|
|
9
|
+
========================================================================== */
|
|
10
|
+
|
|
11
|
+
.ap-lookup {
|
|
12
|
+
display: flex;
|
|
13
|
+
gap: var(--space-xs);
|
|
14
|
+
margin-bottom: var(--space-m);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.ap-lookup__input {
|
|
18
|
+
flex: 1;
|
|
19
|
+
padding: var(--space-s) var(--space-m);
|
|
20
|
+
border: var(--border-width-thin) solid var(--color-outline);
|
|
21
|
+
border-radius: var(--border-radius-small);
|
|
22
|
+
background: var(--color-offset);
|
|
23
|
+
color: var(--color-on-background);
|
|
24
|
+
font-size: var(--font-size-m);
|
|
25
|
+
font-family: inherit;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.ap-lookup__input::placeholder {
|
|
29
|
+
color: var(--color-on-offset);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.ap-lookup__input:focus {
|
|
33
|
+
outline: 2px solid var(--color-primary);
|
|
34
|
+
outline-offset: -1px;
|
|
35
|
+
border-color: var(--color-primary);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.ap-lookup__btn {
|
|
39
|
+
padding: var(--space-s) var(--space-m);
|
|
40
|
+
border: var(--border-width-thin) solid var(--color-primary);
|
|
41
|
+
border-radius: var(--border-radius-small);
|
|
42
|
+
background: var(--color-primary);
|
|
43
|
+
color: var(--color-on-primary);
|
|
44
|
+
font-size: var(--font-size-m);
|
|
45
|
+
font-family: inherit;
|
|
46
|
+
font-weight: 600;
|
|
47
|
+
cursor: pointer;
|
|
48
|
+
white-space: nowrap;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.ap-lookup__btn:hover {
|
|
52
|
+
opacity: 0.9;
|
|
53
|
+
}
|
|
54
|
+
|
|
7
55
|
/* ==========================================================================
|
|
8
56
|
Tab Navigation
|
|
9
57
|
========================================================================== */
|
|
@@ -793,9 +841,34 @@
|
|
|
793
841
|
box-shadow: 0 0 8px 0 hsl(var(--tint-yellow) 50% / 0.3);
|
|
794
842
|
}
|
|
795
843
|
|
|
796
|
-
.ap-
|
|
844
|
+
.ap-notification__avatar-wrap {
|
|
797
845
|
flex-shrink: 0;
|
|
798
|
-
|
|
846
|
+
position: relative;
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
.ap-notification__avatar {
|
|
850
|
+
border: var(--border-width-thin) solid var(--color-outline);
|
|
851
|
+
border-radius: 50%;
|
|
852
|
+
height: 40px;
|
|
853
|
+
object-fit: cover;
|
|
854
|
+
width: 40px;
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
.ap-notification__avatar--default {
|
|
858
|
+
align-items: center;
|
|
859
|
+
background: var(--color-offset-variant);
|
|
860
|
+
color: var(--color-on-offset);
|
|
861
|
+
display: inline-flex;
|
|
862
|
+
font-size: 1.1em;
|
|
863
|
+
font-weight: 600;
|
|
864
|
+
justify-content: center;
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
.ap-notification__type-badge {
|
|
868
|
+
bottom: -2px;
|
|
869
|
+
font-size: 0.75em;
|
|
870
|
+
position: absolute;
|
|
871
|
+
right: -4px;
|
|
799
872
|
}
|
|
800
873
|
|
|
801
874
|
.ap-notification__body {
|
package/index.js
CHANGED
|
@@ -57,6 +57,7 @@ import {
|
|
|
57
57
|
featuredTagsAddController,
|
|
58
58
|
featuredTagsRemoveController,
|
|
59
59
|
} from "./lib/controllers/featured-tags.js";
|
|
60
|
+
import { resolveController } from "./lib/controllers/resolve.js";
|
|
60
61
|
import {
|
|
61
62
|
refollowPauseController,
|
|
62
63
|
refollowResumeController,
|
|
@@ -202,6 +203,7 @@ export default class ActivityPubEndpoint {
|
|
|
202
203
|
router.post("/admin/reader/unlike", unlikeController(mp, this));
|
|
203
204
|
router.post("/admin/reader/boost", boostController(mp, this));
|
|
204
205
|
router.post("/admin/reader/unboost", unboostController(mp, this));
|
|
206
|
+
router.get("/admin/reader/resolve", resolveController(mp, this));
|
|
205
207
|
router.get("/admin/reader/profile", remoteProfileController(mp, this));
|
|
206
208
|
router.get("/admin/reader/post", postDetailController(mp, this));
|
|
207
209
|
router.post("/admin/reader/follow", followController(mp, this));
|
|
@@ -32,7 +32,7 @@ export function followersController(mountPath) {
|
|
|
32
32
|
const cursor = buildCursor(page, totalPages, mountPath + "/admin/followers");
|
|
33
33
|
|
|
34
34
|
response.render("activitypub-followers", {
|
|
35
|
-
title: response.locals.__("activitypub.followers")
|
|
35
|
+
title: `${totalCount} ${response.locals.__("activitypub.followers")}`,
|
|
36
36
|
followers,
|
|
37
37
|
followerCount: totalCount,
|
|
38
38
|
mountPath,
|
|
@@ -32,7 +32,7 @@ export function followingController(mountPath) {
|
|
|
32
32
|
const cursor = buildCursor(page, totalPages, mountPath + "/admin/following");
|
|
33
33
|
|
|
34
34
|
response.render("activitypub-following", {
|
|
35
|
-
title: response.locals.__("activitypub.following")
|
|
35
|
+
title: `${totalCount} ${response.locals.__("activitypub.following")}`,
|
|
36
36
|
following,
|
|
37
37
|
followingCount: totalCount,
|
|
38
38
|
mountPath,
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve controller — accepts any fediverse URL or handle, resolves it
|
|
3
|
+
* via lookupObject(), and redirects to the appropriate internal view.
|
|
4
|
+
*/
|
|
5
|
+
import {
|
|
6
|
+
Article,
|
|
7
|
+
Note,
|
|
8
|
+
Person,
|
|
9
|
+
Service,
|
|
10
|
+
Application,
|
|
11
|
+
Organization,
|
|
12
|
+
Group,
|
|
13
|
+
} from "@fedify/fedify";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* GET /admin/reader/resolve?q=<url-or-handle>
|
|
17
|
+
* Resolves a fediverse URL or @user@domain handle and redirects to
|
|
18
|
+
* the post detail or remote profile view.
|
|
19
|
+
*/
|
|
20
|
+
export function resolveController(mountPath, plugin) {
|
|
21
|
+
return async (request, response, next) => {
|
|
22
|
+
try {
|
|
23
|
+
const query = (request.query.q || "").trim();
|
|
24
|
+
|
|
25
|
+
if (!query) {
|
|
26
|
+
return response.redirect(`${mountPath}/admin/reader`);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (!plugin._federation) {
|
|
30
|
+
return response.status(503).render("error", {
|
|
31
|
+
title: "Error",
|
|
32
|
+
content: "Federation not initialized",
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const handle = plugin.options.actor.handle;
|
|
37
|
+
const ctx = plugin._federation.createContext(
|
|
38
|
+
new URL(plugin._publicationUrl),
|
|
39
|
+
{ handle, publicationUrl: plugin._publicationUrl },
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
const documentLoader = await ctx.getDocumentLoader({
|
|
43
|
+
identifier: handle,
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// Determine if input is a URL or a handle
|
|
47
|
+
// lookupObject accepts: URLs, @user@domain, user@domain, acct:user@domain
|
|
48
|
+
let lookupInput;
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
// If it parses as a URL, pass as URL object
|
|
52
|
+
const parsed = new URL(query);
|
|
53
|
+
lookupInput = parsed;
|
|
54
|
+
} catch {
|
|
55
|
+
// Not a URL — treat as handle (strip leading @ if present)
|
|
56
|
+
lookupInput = query;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
let object;
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
object = await ctx.lookupObject(lookupInput, { documentLoader });
|
|
63
|
+
} catch (error) {
|
|
64
|
+
console.warn(
|
|
65
|
+
`[resolve] lookupObject failed for "${query}":`,
|
|
66
|
+
error.message,
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (!object) {
|
|
71
|
+
return response.status(404).render("error", {
|
|
72
|
+
title: response.locals.__("activitypub.reader.resolve.notFoundTitle"),
|
|
73
|
+
content: response.locals.__(
|
|
74
|
+
"activitypub.reader.resolve.notFound",
|
|
75
|
+
),
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Determine object type and redirect accordingly
|
|
80
|
+
const objectUrl =
|
|
81
|
+
object.id?.href || object.url?.href || query;
|
|
82
|
+
|
|
83
|
+
if (
|
|
84
|
+
object instanceof Person ||
|
|
85
|
+
object instanceof Service ||
|
|
86
|
+
object instanceof Application ||
|
|
87
|
+
object instanceof Organization ||
|
|
88
|
+
object instanceof Group
|
|
89
|
+
) {
|
|
90
|
+
return response.redirect(
|
|
91
|
+
`${mountPath}/admin/reader/profile?url=${encodeURIComponent(objectUrl)}`,
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (object instanceof Note || object instanceof Article) {
|
|
96
|
+
return response.redirect(
|
|
97
|
+
`${mountPath}/admin/reader/post?url=${encodeURIComponent(objectUrl)}`,
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Unknown type — try post detail as fallback
|
|
102
|
+
return response.redirect(
|
|
103
|
+
`${mountPath}/admin/reader/post?url=${encodeURIComponent(objectUrl)}`,
|
|
104
|
+
);
|
|
105
|
+
} catch (error) {
|
|
106
|
+
next(error);
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
}
|
|
@@ -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
|
|
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
|
@@ -191,6 +191,13 @@
|
|
|
191
191
|
"loadingThread": "Loading thread...",
|
|
192
192
|
"threadError": "Could not load full thread"
|
|
193
193
|
},
|
|
194
|
+
"resolve": {
|
|
195
|
+
"placeholder": "Paste a fediverse URL or @user@domain handle…",
|
|
196
|
+
"label": "Look up a fediverse post or account",
|
|
197
|
+
"button": "Look up",
|
|
198
|
+
"notFoundTitle": "Not found",
|
|
199
|
+
"notFound": "Could not find this post or account. The URL may be invalid, the server may be unavailable, or the content may have been deleted."
|
|
200
|
+
},
|
|
194
201
|
"linkPreview": {
|
|
195
202
|
"label": "Link preview"
|
|
196
203
|
}
|
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.17",
|
|
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,8 +7,6 @@
|
|
|
7
7
|
{% from "pagination/macro.njk" import pagination with context %}
|
|
8
8
|
|
|
9
9
|
{% block content %}
|
|
10
|
-
{{ heading({ text: __("activitypub.activities"), level: 1, parent: { text: __("activitypub.title"), href: mountPath } }) }}
|
|
11
|
-
|
|
12
10
|
{% if activities.length > 0 %}
|
|
13
11
|
{% for activity in activities %}
|
|
14
12
|
{{ card({
|
|
@@ -3,12 +3,6 @@
|
|
|
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
|
-
}) }}
|
|
11
|
-
|
|
12
6
|
{# Reply context — show the post being replied to #}
|
|
13
7
|
{% if replyContext %}
|
|
14
8
|
<div class="ap-compose__context">
|
|
@@ -6,8 +6,6 @@
|
|
|
6
6
|
{% from "pagination/macro.njk" import pagination with context %}
|
|
7
7
|
|
|
8
8
|
{% block content %}
|
|
9
|
-
{{ heading({ text: followerCount + " " + __("activitypub.followers"), level: 1, parent: { text: __("activitypub.title"), href: mountPath } }) }}
|
|
10
|
-
|
|
11
9
|
{% if followers.length > 0 %}
|
|
12
10
|
{% for follower in followers %}
|
|
13
11
|
{{ card({
|
|
@@ -7,8 +7,6 @@
|
|
|
7
7
|
{% from "pagination/macro.njk" import pagination with context %}
|
|
8
8
|
|
|
9
9
|
{% block content %}
|
|
10
|
-
{{ heading({ text: followingCount + " " + __("activitypub.following"), level: 1, parent: { text: __("activitypub.title"), href: mountPath } }) }}
|
|
11
|
-
|
|
12
10
|
{% if following.length > 0 %}
|
|
13
11
|
{% for account in following %}
|
|
14
12
|
{% if account.source === "import" %}
|
|
@@ -8,8 +8,6 @@
|
|
|
8
8
|
{% from "prose/macro.njk" import prose with context %}
|
|
9
9
|
|
|
10
10
|
{% block content %}
|
|
11
|
-
{{ heading({ text: title, level: 1, parent: { text: __("activitypub.title"), href: mountPath } }) }}
|
|
12
|
-
|
|
13
11
|
{% if result %}
|
|
14
12
|
{{ notificationBanner({ type: result.type, text: result.text }) }}
|
|
15
13
|
{% endif %}
|
|
@@ -4,12 +4,6 @@
|
|
|
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
|
-
}) }}
|
|
12
|
-
|
|
13
7
|
{# Blocked actors #}
|
|
14
8
|
<section class="ap-moderation__section">
|
|
15
9
|
<h2>{{ __("activitypub.moderation.blockedTitle") }}</h2>
|
|
@@ -4,12 +4,6 @@
|
|
|
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
|
-
}) }}
|
|
12
|
-
|
|
13
7
|
{% if items.length > 0 %}
|
|
14
8
|
<div class="ap-notifications__toolbar">
|
|
15
9
|
{% if unreadCount > 0 %}
|
|
@@ -3,12 +3,6 @@
|
|
|
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
|
-
}) }}
|
|
11
|
-
|
|
12
6
|
<div class="ap-post-detail" data-mount-path="{{ mountPath }}">
|
|
13
7
|
{# Back button #}
|
|
14
8
|
<div class="ap-post-detail__back">
|
|
@@ -10,8 +10,6 @@
|
|
|
10
10
|
{% from "prose/macro.njk" import prose with context %}
|
|
11
11
|
|
|
12
12
|
{% block content %}
|
|
13
|
-
{{ heading({ text: title, level: 1, parent: { text: __("activitypub.title"), href: mountPath } }) }}
|
|
14
|
-
|
|
15
13
|
{% if result %}
|
|
16
14
|
{{ notificationBanner({ type: result.type, text: result.text }) }}
|
|
17
15
|
{% endif %}
|
|
@@ -4,17 +4,16 @@
|
|
|
4
4
|
{% from "prose/macro.njk" import prose with context %}
|
|
5
5
|
|
|
6
6
|
{% block readercontent %}
|
|
7
|
-
{
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
7
|
+
{# Fediverse lookup #}
|
|
8
|
+
<form action="{{ mountPath }}/admin/reader/resolve" method="get" class="ap-lookup">
|
|
9
|
+
<input type="text" name="q" class="ap-lookup__input"
|
|
10
|
+
placeholder="{{ __('activitypub.reader.resolve.placeholder') }}"
|
|
11
|
+
aria-label="{{ __('activitypub.reader.resolve.label') }}">
|
|
12
|
+
<button type="submit" class="ap-lookup__btn">{{ __("activitypub.reader.resolve.button") }}</button>
|
|
13
|
+
</form>
|
|
12
14
|
|
|
13
15
|
{# Tab navigation #}
|
|
14
16
|
<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
17
|
<a href="?tab=notes" class="ap-tab{% if tab == 'notes' %} ap-tab--active{% endif %}" role="tab">
|
|
19
18
|
{{ __("activitypub.reader.tabs.notes") }}
|
|
20
19
|
</a>
|
|
@@ -30,6 +29,9 @@
|
|
|
30
29
|
<a href="?tab=media" class="ap-tab{% if tab == 'media' %} ap-tab--active{% endif %}" role="tab">
|
|
31
30
|
{{ __("activitypub.reader.tabs.media") }}
|
|
32
31
|
</a>
|
|
32
|
+
<a href="?tab=all" class="ap-tab{% if tab == 'all' %} ap-tab--active{% endif %}" role="tab">
|
|
33
|
+
{{ __("activitypub.reader.tabs.all") }}
|
|
34
|
+
</a>
|
|
33
35
|
</nav>
|
|
34
36
|
|
|
35
37
|
{# Timeline items #}
|
|
@@ -4,12 +4,6 @@
|
|
|
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
|
-
}) }}
|
|
12
|
-
|
|
13
7
|
<div class="ap-profile"
|
|
14
8
|
x-data="{
|
|
15
9
|
following: {{ 'true' if isFollowing else 'false' }},
|
|
@@ -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 %}
|
|
@@ -8,19 +8,18 @@
|
|
|
8
8
|
<button type="submit" class="ap-notification__dismiss-btn" title="{{ __('activitypub.notifications.dismiss') }}">×</button>
|
|
9
9
|
</form>
|
|
10
10
|
|
|
11
|
-
{#
|
|
12
|
-
<div class="ap-
|
|
13
|
-
{% if item.
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
{%
|
|
18
|
-
|
|
19
|
-
{% elif item.type == "reply" %}
|
|
20
|
-
💬
|
|
21
|
-
{% elif item.type == "mention" %}
|
|
22
|
-
@
|
|
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>
|
|
23
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>
|
|
24
23
|
</div>
|
|
25
24
|
|
|
26
25
|
{# Notification body #}
|