@rmdes/indiekit-endpoint-activitypub 1.0.28 → 1.1.1

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.
@@ -0,0 +1,9 @@
1
+ {% extends "document.njk" %}
2
+
3
+ {% block head %}
4
+ {# Alpine.js for client-side reactivity #}
5
+ <script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.14/dist/cdn.min.js"></script>
6
+
7
+ {# Reader stylesheet #}
8
+ <link rel="stylesheet" href="/assets/@rmdes-indiekit-endpoint-activitypub/reader.css">
9
+ {% endblock %}
@@ -0,0 +1,157 @@
1
+ {# Timeline item card partial - reusable across timeline and profile views #}
2
+
3
+ <article class="ap-card">
4
+ {# Boost header if this is a boosted post #}
5
+ {% if item.type == "boost" and item.boostedBy %}
6
+ <div class="ap-card__boost">
7
+ 🔁 <a href="{{ item.boostedBy.url }}">{{ item.boostedBy.name }}</a> {{ __("activitypub.reader.boosted") }}
8
+ </div>
9
+ {% endif %}
10
+
11
+ {# Reply context if this is a reply #}
12
+ {% if item.inReplyTo %}
13
+ <div class="ap-card__reply-to">
14
+ ↩ {{ __("activitypub.reader.replyingTo") }} <a href="{{ item.inReplyTo }}">{{ item.inReplyTo }}</a>
15
+ </div>
16
+ {% endif %}
17
+
18
+ {# Author header #}
19
+ <header class="ap-card__author">
20
+ <img src="{{ item.author.photo }}" alt="{{ item.author.name }}" class="ap-card__avatar">
21
+ <div class="ap-card__author-info">
22
+ <div class="ap-card__author-name">
23
+ <a href="{{ item.author.url }}">{{ item.author.name }}</a>
24
+ </div>
25
+ <div class="ap-card__author-handle">{{ item.author.handle }}</div>
26
+ </div>
27
+ <time datetime="{{ item.published }}" class="ap-card__timestamp">
28
+ {{ item.published | date("PPp") }}
29
+ </time>
30
+ </header>
31
+
32
+ {# Post title (articles only) #}
33
+ {% if item.name %}
34
+ <h2 class="ap-card__title">
35
+ <a href="{{ item.url }}">{{ item.name }}</a>
36
+ </h2>
37
+ {% endif %}
38
+
39
+ {# Determine if content should be hidden behind CW #}
40
+ {% set hasCW = item.summary or item.sensitive %}
41
+ {% set cwLabel = item.summary if item.summary else __("activitypub.reader.sensitiveContent") %}
42
+
43
+ {% if hasCW %}
44
+ <div class="ap-card__cw" x-data="{ shown: false }">
45
+ <button @click="shown = !shown" class="ap-card__cw-toggle">
46
+ <span x-show="!shown">⚠️ {{ cwLabel }} — {{ __("activitypub.reader.showContent") }}</span>
47
+ <span x-show="shown" x-cloak>{{ __("activitypub.reader.hideContent") }}</span>
48
+ </button>
49
+ <div x-show="shown" x-cloak>
50
+ {% if item.content and item.content.html %}
51
+ <div class="ap-card__content">
52
+ {{ item.content.html | safe }}
53
+ </div>
54
+ {% endif %}
55
+
56
+ {# Media hidden behind CW #}
57
+ {% include "partials/ap-item-media.njk" %}
58
+ </div>
59
+ </div>
60
+ {% else %}
61
+ {# Regular content (no CW) #}
62
+ {% if item.content and item.content.html %}
63
+ <div class="ap-card__content">
64
+ {{ item.content.html | safe }}
65
+ </div>
66
+ {% endif %}
67
+
68
+ {# Media visible directly #}
69
+ {% include "partials/ap-item-media.njk" %}
70
+ {% endif %}
71
+
72
+ {# Tags/categories #}
73
+ {% if item.category and item.category.length > 0 %}
74
+ <div class="ap-card__tags">
75
+ {% for tag in item.category %}
76
+ <a href="?tag={{ tag }}" class="ap-card__tag">#{{ tag }}</a>
77
+ {% endfor %}
78
+ </div>
79
+ {% endif %}
80
+
81
+ {# Interaction buttons — Alpine.js for optimistic updates #}
82
+ {# Dynamic data moved to data-* attributes to prevent XSS from inline interpolation #}
83
+ {% set itemUrl = item.url or item.originalUrl %}
84
+ {% set isLiked = interactionMap[itemUrl].like if interactionMap[itemUrl] else false %}
85
+ {% set isBoosted = interactionMap[itemUrl].boost if interactionMap[itemUrl] else false %}
86
+ <footer class="ap-card__actions"
87
+ data-item-url="{{ itemUrl }}"
88
+ data-csrf-token="{{ csrfToken }}"
89
+ data-mount-path="{{ mountPath }}"
90
+ x-data="{
91
+ liked: {{ 'true' if isLiked else 'false' }},
92
+ boosted: {{ 'true' if isBoosted else 'false' }},
93
+ loading: false,
94
+ error: '',
95
+ async interact(action) {
96
+ if (this.loading) return;
97
+ this.loading = true;
98
+ this.error = '';
99
+ const el = this.$root;
100
+ const itemUrl = el.dataset.itemUrl;
101
+ const csrfToken = el.dataset.csrfToken;
102
+ const basePath = el.dataset.mountPath;
103
+ const prev = { liked: this.liked, boosted: this.boosted };
104
+ if (action === 'like') this.liked = true;
105
+ else if (action === 'unlike') this.liked = false;
106
+ else if (action === 'boost') this.boosted = true;
107
+ else if (action === 'unboost') this.boosted = false;
108
+ try {
109
+ const res = await fetch(basePath + '/admin/reader/' + action, {
110
+ method: 'POST',
111
+ headers: {
112
+ 'Content-Type': 'application/json',
113
+ 'X-CSRF-Token': csrfToken
114
+ },
115
+ body: JSON.stringify({ url: itemUrl })
116
+ });
117
+ const data = await res.json();
118
+ if (!data.success) {
119
+ this.liked = prev.liked;
120
+ this.boosted = prev.boosted;
121
+ this.error = data.error || 'Failed';
122
+ }
123
+ } catch (e) {
124
+ this.liked = prev.liked;
125
+ this.boosted = prev.boosted;
126
+ this.error = e.message;
127
+ }
128
+ this.loading = false;
129
+ if (this.error) setTimeout(() => this.error = '', 3000);
130
+ }
131
+ }">
132
+ <a href="{{ mountPath }}/admin/reader/compose?replyTo={{ itemUrl | urlencode }}"
133
+ class="ap-card__action ap-card__action--reply"
134
+ title="{{ __('activitypub.reader.actions.reply') }}">
135
+ ↩ {{ __("activitypub.reader.actions.reply") }}
136
+ </a>
137
+ <button class="ap-card__action ap-card__action--boost"
138
+ :class="{ 'ap-card__action--active': boosted }"
139
+ :title="boosted ? '{{ __('activitypub.reader.actions.unboost') }}' : '{{ __('activitypub.reader.actions.boost') }}'"
140
+ :disabled="loading"
141
+ @click="interact(boosted ? 'unboost' : 'boost')">
142
+ 🔁 <span x-text="boosted ? '{{ __('activitypub.reader.actions.boosted') }}' : '{{ __('activitypub.reader.actions.boost') }}'"></span>
143
+ </button>
144
+ <button class="ap-card__action ap-card__action--like"
145
+ :class="{ 'ap-card__action--active': liked }"
146
+ :title="liked ? '{{ __('activitypub.reader.actions.unlike') }}' : '{{ __('activitypub.reader.actions.like') }}'"
147
+ :disabled="loading"
148
+ @click="interact(liked ? 'unlike' : 'like')">
149
+ <span x-text="liked ? '❤️' : '♥'"></span>
150
+ <span x-text="liked ? '{{ __('activitypub.reader.actions.liked') }}' : '{{ __('activitypub.reader.actions.like') }}'"></span>
151
+ </button>
152
+ <a href="{{ itemUrl }}" class="ap-card__action ap-card__action--link" target="_blank" rel="noopener">
153
+ 🔗 {{ __("activitypub.reader.actions.viewOriginal") }}
154
+ </a>
155
+ <div x-show="error" x-text="error" class="ap-card__action-error" x-transition></div>
156
+ </footer>
157
+ </article>
@@ -0,0 +1,37 @@
1
+ {# Media attachments partial — included from ap-item-card.njk #}
2
+
3
+ {# Photo gallery #}
4
+ {% if item.photo and item.photo.length > 0 %}
5
+ {% set displayCount = [item.photo.length, 4] | min %}
6
+ {% set extraCount = item.photo.length - 4 %}
7
+ <div class="ap-card__gallery ap-card__gallery--{{ displayCount }}">
8
+ {% for photoUrl in item.photo %}
9
+ {% if loop.index0 < 4 %}
10
+ <a href="{{ photoUrl }}" target="_blank" rel="noopener" class="ap-card__gallery-link{% if loop.index0 == 3 and extraCount > 0 %} ap-card__gallery-link--more{% endif %}">
11
+ <img src="{{ photoUrl }}" alt="" loading="lazy">
12
+ {% if loop.index0 == 3 and extraCount > 0 %}
13
+ <span class="ap-card__gallery-more">+{{ extraCount }}</span>
14
+ {% endif %}
15
+ </a>
16
+ {% endif %}
17
+ {% endfor %}
18
+ </div>
19
+ {% endif %}
20
+
21
+ {# Video embed #}
22
+ {% if item.video and item.video.length > 0 %}
23
+ <div class="ap-card__video">
24
+ <video controls preload="metadata" src="{{ item.video[0] }}">
25
+ {{ __("activitypub.reader.videoNotSupported") }}
26
+ </video>
27
+ </div>
28
+ {% endif %}
29
+
30
+ {# Audio player #}
31
+ {% if item.audio and item.audio.length > 0 %}
32
+ <div class="ap-card__audio">
33
+ <audio controls preload="metadata" src="{{ item.audio[0] }}">
34
+ {{ __("activitypub.reader.audioNotSupported") }}
35
+ </audio>
36
+ </div>
37
+ {% endif %}
@@ -0,0 +1,58 @@
1
+ {# Notification card partial #}
2
+
3
+ <div class="ap-notification{% if not item.read %} ap-notification--unread{% endif %}">
4
+ {# Type icon #}
5
+ <div class="ap-notification__icon">
6
+ {% if item.type == "like" %}
7
+
8
+ {% elif item.type == "boost" %}
9
+ 🔁
10
+ {% elif item.type == "follow" %}
11
+ 👤
12
+ {% elif item.type == "reply" %}
13
+ 💬
14
+ {% elif item.type == "mention" %}
15
+ @
16
+ {% endif %}
17
+ </div>
18
+
19
+ {# Notification body #}
20
+ <div class="ap-notification__body">
21
+ <span class="ap-notification__actor">
22
+ <a href="{{ item.actorUrl }}">{{ item.actorName }}</a>
23
+ </span>
24
+
25
+ <span class="ap-notification__action">
26
+ {% if item.type == "like" %}
27
+ {{ __("activitypub.notifications.liked") }}
28
+ {% elif item.type == "boost" %}
29
+ {{ __("activitypub.notifications.boostedPost") }}
30
+ {% elif item.type == "follow" %}
31
+ {{ __("activitypub.notifications.followedYou") }}
32
+ {% elif item.type == "reply" %}
33
+ {{ __("activitypub.notifications.repliedTo") }}
34
+ {% elif item.type == "mention" %}
35
+ {{ __("activitypub.notifications.mentionedYou") }}
36
+ {% endif %}
37
+ </span>
38
+
39
+ {% if item.targetUrl %}
40
+ <a href="{{ item.targetUrl }}" class="ap-notification__target">
41
+ {{ item.targetName or item.targetUrl }}
42
+ </a>
43
+ {% endif %}
44
+
45
+ {% if item.content and item.content.text %}
46
+ <div class="ap-notification__excerpt">
47
+ {{ item.content.text | truncate(200) }}
48
+ </div>
49
+ {% endif %}
50
+ </div>
51
+
52
+ {# Timestamp #}
53
+ {% if item.published %}
54
+ <time datetime="{{ item.published }}" class="ap-notification__time">
55
+ {{ item.published | date("PPp") }}
56
+ </time>
57
+ {% endif %}
58
+ </div>