@rmdes/indiekit-endpoint-webmention-io 1.0.0
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/index.js +113 -0
- package/lib/controllers/api.js +51 -0
- package/lib/controllers/blocklist.js +64 -0
- package/lib/controllers/dashboard.js +227 -0
- package/lib/controllers/sync-controller.js +51 -0
- package/lib/storage/blocklist.js +84 -0
- package/lib/storage/webmentions.js +252 -0
- package/lib/sync.js +289 -0
- package/lib/utils.js +115 -0
- package/locales/en.json +75 -0
- package/package.json +51 -0
- package/views/webmentions-blocklist.njk +193 -0
- package/views/webmentions.njk +222 -0
package/locales/en.json
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
{
|
|
2
|
+
"webmention-io": {
|
|
3
|
+
"title": "Webmentions",
|
|
4
|
+
"webmentions": {
|
|
5
|
+
"none": "No webmentions found. Try running a sync."
|
|
6
|
+
},
|
|
7
|
+
"mention": {
|
|
8
|
+
"bookmark-of": "bookmarked %s",
|
|
9
|
+
"in-reply-to": "replied to %s",
|
|
10
|
+
"like-of": "liked %s",
|
|
11
|
+
"mention-of": "mentioned %s",
|
|
12
|
+
"repost-of": "reposted %s",
|
|
13
|
+
"rsvp": "responded to %s"
|
|
14
|
+
},
|
|
15
|
+
"sync": {
|
|
16
|
+
"now": "Sync Now",
|
|
17
|
+
"full": "Full Re-sync",
|
|
18
|
+
"lastSync": "Last synced",
|
|
19
|
+
"never": "Never synced",
|
|
20
|
+
"synced": "Sync complete",
|
|
21
|
+
"added": "new mentions added",
|
|
22
|
+
"inProgress": "Sync in progress…"
|
|
23
|
+
},
|
|
24
|
+
"filter": {
|
|
25
|
+
"all": "All",
|
|
26
|
+
"visible": "Visible",
|
|
27
|
+
"hidden": "Hidden",
|
|
28
|
+
"show": "Show",
|
|
29
|
+
"type": "Type",
|
|
30
|
+
"likes": "Likes",
|
|
31
|
+
"replies": "Replies",
|
|
32
|
+
"reposts": "Reposts",
|
|
33
|
+
"mentions": "Mentions"
|
|
34
|
+
},
|
|
35
|
+
"actions": {
|
|
36
|
+
"hide": "Hide",
|
|
37
|
+
"unhide": "Unhide",
|
|
38
|
+
"block": "Block",
|
|
39
|
+
"hidden": "Hidden",
|
|
40
|
+
"reasonManual": "manual",
|
|
41
|
+
"reasonBlocklist": "blocklist",
|
|
42
|
+
"reasonPrivacy": "privacy"
|
|
43
|
+
},
|
|
44
|
+
"blocklist": {
|
|
45
|
+
"title": "Webmention Blocklist",
|
|
46
|
+
"description": "Blocked domains will not appear in webmentions. Existing mentions from blocked domains are automatically hidden.",
|
|
47
|
+
"add": "Block a Domain",
|
|
48
|
+
"domainLabel": "Domain",
|
|
49
|
+
"domainPlaceholder": "spam.example.com",
|
|
50
|
+
"reasonLabel": "Reason",
|
|
51
|
+
"reasonSpam": "Spam",
|
|
52
|
+
"reasonManual": "Manual",
|
|
53
|
+
"blockButton": "Block Domain",
|
|
54
|
+
"unblock": "Unblock",
|
|
55
|
+
"empty": "No domains blocked.",
|
|
56
|
+
"privacy": {
|
|
57
|
+
"title": "Privacy Removal",
|
|
58
|
+
"description": "Permanently delete all webmentions from a domain and block future ones. Use for GDPR or personal removal requests.",
|
|
59
|
+
"domainLabel": "Domain to remove",
|
|
60
|
+
"removeButton": "Remove & Block"
|
|
61
|
+
},
|
|
62
|
+
"blocked": "Domain blocked",
|
|
63
|
+
"unblocked": "Domain unblocked",
|
|
64
|
+
"removed": "mentions permanently deleted",
|
|
65
|
+
"unhidden": "mentions restored",
|
|
66
|
+
"mentionsHidden": "mentions hidden",
|
|
67
|
+
"blockedAt": "Blocked"
|
|
68
|
+
},
|
|
69
|
+
"counts": {
|
|
70
|
+
"total": "total",
|
|
71
|
+
"hidden": "hidden",
|
|
72
|
+
"visible": "visible"
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@rmdes/indiekit-endpoint-webmention-io",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Webmention moderation endpoint for Indiekit. Syncs webmentions from webmention.io into MongoDB with delete, block, and privacy removal capabilities.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"indiekit",
|
|
7
|
+
"indiekit-plugin",
|
|
8
|
+
"indieweb",
|
|
9
|
+
"webmention",
|
|
10
|
+
"moderation"
|
|
11
|
+
],
|
|
12
|
+
"homepage": "https://github.com/rmdes/indiekit-endpoint-webmention-io",
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/rmdes/indiekit-endpoint-webmention-io/issues"
|
|
15
|
+
},
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "git+https://github.com/rmdes/indiekit-endpoint-webmention-io.git"
|
|
19
|
+
},
|
|
20
|
+
"author": {
|
|
21
|
+
"name": "Ricardo Mendes",
|
|
22
|
+
"url": "https://rmendes.net"
|
|
23
|
+
},
|
|
24
|
+
"license": "MIT",
|
|
25
|
+
"engines": {
|
|
26
|
+
"node": ">=20"
|
|
27
|
+
},
|
|
28
|
+
"type": "module",
|
|
29
|
+
"main": "index.js",
|
|
30
|
+
"exports": {
|
|
31
|
+
".": "./index.js"
|
|
32
|
+
},
|
|
33
|
+
"files": [
|
|
34
|
+
"lib",
|
|
35
|
+
"locales",
|
|
36
|
+
"views",
|
|
37
|
+
"index.js"
|
|
38
|
+
],
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"@indiekit/error": "^1.0.0-beta.25",
|
|
41
|
+
"@indiekit/frontend": "^1.0.0-beta.25",
|
|
42
|
+
"express": "^5.0.0",
|
|
43
|
+
"sanitize-html": "^2.14.0"
|
|
44
|
+
},
|
|
45
|
+
"peerDependencies": {
|
|
46
|
+
"@indiekit/indiekit": ">=1.0.0-beta.25"
|
|
47
|
+
},
|
|
48
|
+
"publishConfig": {
|
|
49
|
+
"access": "public"
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
{% extends "document.njk" %}
|
|
2
|
+
|
|
3
|
+
{% block content %}
|
|
4
|
+
<style>
|
|
5
|
+
.bl-section {
|
|
6
|
+
background: var(--color-offset, #f5f5f5);
|
|
7
|
+
border-radius: var(--border-radius-small, 0.5rem);
|
|
8
|
+
padding: var(--space-m, 1.5rem);
|
|
9
|
+
margin-block-end: var(--space-m, 1.5rem);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
.bl-section h2 {
|
|
13
|
+
font: var(--font-heading, bold 1.25rem/1.4 sans-serif);
|
|
14
|
+
margin-block-end: var(--space-xs, 0.5rem);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.bl-section__desc {
|
|
18
|
+
color: var(--color-on-offset, #666);
|
|
19
|
+
font: var(--font-body, 0.875rem/1.5 sans-serif);
|
|
20
|
+
margin-block-end: var(--space-s, 0.75rem);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.bl-form {
|
|
24
|
+
display: flex;
|
|
25
|
+
gap: var(--space-xs, 0.5rem);
|
|
26
|
+
align-items: flex-end;
|
|
27
|
+
flex-wrap: wrap;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.bl-form .field {
|
|
31
|
+
flex: 1;
|
|
32
|
+
min-width: 200px;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.bl-form .field__label {
|
|
36
|
+
display: block;
|
|
37
|
+
font: var(--font-caption, 0.75rem/1.4 sans-serif);
|
|
38
|
+
font-weight: 600;
|
|
39
|
+
margin-block-end: 0.25rem;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
.bl-form .field__input {
|
|
43
|
+
width: 100%;
|
|
44
|
+
padding: var(--space-2xs, 0.375rem) var(--space-xs, 0.5rem);
|
|
45
|
+
border: 1px solid var(--color-outline-variant, #ccc);
|
|
46
|
+
border-radius: var(--border-radius-small, 0.25rem);
|
|
47
|
+
font: var(--font-body, 0.875rem/1.5 sans-serif);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.bl-table {
|
|
51
|
+
width: 100%;
|
|
52
|
+
border-collapse: collapse;
|
|
53
|
+
font: var(--font-body, 0.875rem/1.5 sans-serif);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.bl-table th {
|
|
57
|
+
text-align: left;
|
|
58
|
+
font-weight: 600;
|
|
59
|
+
padding: var(--space-xs, 0.5rem);
|
|
60
|
+
border-block-end: 2px solid var(--color-outline-variant, #ddd);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
.bl-table td {
|
|
64
|
+
padding: var(--space-xs, 0.5rem);
|
|
65
|
+
border-block-end: 1px solid var(--color-outline-variant, #eee);
|
|
66
|
+
vertical-align: middle;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.bl-badge {
|
|
70
|
+
display: inline-block;
|
|
71
|
+
font: var(--font-caption, 0.6875rem/1.4 sans-serif);
|
|
72
|
+
padding: 0.125rem 0.375rem;
|
|
73
|
+
border-radius: var(--border-radius-small, 0.25rem);
|
|
74
|
+
background: var(--color-offset, #e9ecef);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.bl-badge--privacy {
|
|
78
|
+
background: var(--color-error-container, #f8d7da);
|
|
79
|
+
color: var(--color-error, #dc3545);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.bl-badge--spam {
|
|
83
|
+
background: var(--color-warning-container, #fff3cd);
|
|
84
|
+
color: var(--color-warning, #856404);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.bl-empty {
|
|
88
|
+
color: var(--color-on-offset, #666);
|
|
89
|
+
text-align: center;
|
|
90
|
+
padding: var(--space-m, 1rem);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.bl-success {
|
|
94
|
+
background: var(--color-success-container, #d4edda);
|
|
95
|
+
border: 1px solid var(--color-success, #28a745);
|
|
96
|
+
border-radius: var(--border-radius-small, 0.25rem);
|
|
97
|
+
padding: var(--space-s, 0.75rem);
|
|
98
|
+
margin-block-end: var(--space-m, 1rem);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
.bl-back {
|
|
102
|
+
margin-block-end: var(--space-m, 1rem);
|
|
103
|
+
}
|
|
104
|
+
</style>
|
|
105
|
+
|
|
106
|
+
<div class="bl-back">
|
|
107
|
+
<a href="{{ wmEndpoint }}">← {{ __("webmention-io.title") }}</a>
|
|
108
|
+
</div>
|
|
109
|
+
|
|
110
|
+
{# Flash messages #}
|
|
111
|
+
{% if request.query.unblocked %}
|
|
112
|
+
<div class="bl-success">
|
|
113
|
+
{{ __("webmention-io.blocklist.unblocked") }} — {{ request.query.unhidden or 0 }} {{ __("webmention-io.blocklist.unhidden") }}
|
|
114
|
+
</div>
|
|
115
|
+
{% endif %}
|
|
116
|
+
{% if request.query.removed %}
|
|
117
|
+
<div class="bl-success">
|
|
118
|
+
{{ request.query.count or 0 }} {{ __("webmention-io.blocklist.removed") }}
|
|
119
|
+
</div>
|
|
120
|
+
{% endif %}
|
|
121
|
+
|
|
122
|
+
{# Add to blocklist #}
|
|
123
|
+
<section class="bl-section">
|
|
124
|
+
<h2>{{ __("webmention-io.blocklist.add") }}</h2>
|
|
125
|
+
<p class="bl-section__desc">{{ __("webmention-io.blocklist.description") }}</p>
|
|
126
|
+
|
|
127
|
+
<form method="post" action="{{ wmEndpoint }}/block" class="bl-form">
|
|
128
|
+
<div class="field">
|
|
129
|
+
<label class="field__label">{{ __("webmention-io.blocklist.domainLabel") }}</label>
|
|
130
|
+
<input type="text" name="domain" class="field__input" placeholder="{{ __("webmention-io.blocklist.domainPlaceholder") }}" required>
|
|
131
|
+
</div>
|
|
132
|
+
<button type="submit" class="button button--primary">{{ __("webmention-io.blocklist.blockButton") }}</button>
|
|
133
|
+
</form>
|
|
134
|
+
</section>
|
|
135
|
+
|
|
136
|
+
{# Privacy removal #}
|
|
137
|
+
<section class="bl-section">
|
|
138
|
+
<h2>{{ __("webmention-io.blocklist.privacy.title") }}</h2>
|
|
139
|
+
<p class="bl-section__desc">{{ __("webmention-io.blocklist.privacy.description") }}</p>
|
|
140
|
+
|
|
141
|
+
<form method="post" action="{{ wmEndpoint }}/privacy-remove" class="bl-form"
|
|
142
|
+
onsubmit="return confirm('This will PERMANENTLY DELETE all webmentions from this domain and block it. This cannot be undone. Continue?')">
|
|
143
|
+
<div class="field">
|
|
144
|
+
<label class="field__label">{{ __("webmention-io.blocklist.privacy.domainLabel") }}</label>
|
|
145
|
+
<input type="text" name="domain" class="field__input" placeholder="{{ __("webmention-io.blocklist.domainPlaceholder") }}" required>
|
|
146
|
+
</div>
|
|
147
|
+
<button type="submit" class="button button--primary">{{ __("webmention-io.blocklist.privacy.removeButton") }}</button>
|
|
148
|
+
</form>
|
|
149
|
+
</section>
|
|
150
|
+
|
|
151
|
+
{# Blocklist table #}
|
|
152
|
+
<section class="bl-section">
|
|
153
|
+
<h2>{{ __("webmention-io.blocklist.title") }} ({{ entries.length }})</h2>
|
|
154
|
+
|
|
155
|
+
{% if entries.length > 0 %}
|
|
156
|
+
<table class="bl-table">
|
|
157
|
+
<thead>
|
|
158
|
+
<tr>
|
|
159
|
+
<th>{{ __("webmention-io.blocklist.domainLabel") }}</th>
|
|
160
|
+
<th>{{ __("webmention-io.blocklist.reasonLabel") }}</th>
|
|
161
|
+
<th>{{ __("webmention-io.blocklist.mentionsHidden") }}</th>
|
|
162
|
+
<th>{{ __("webmention-io.blocklist.blockedAt") }}</th>
|
|
163
|
+
<th></th>
|
|
164
|
+
</tr>
|
|
165
|
+
</thead>
|
|
166
|
+
<tbody>
|
|
167
|
+
{% for entry in entries %}
|
|
168
|
+
<tr>
|
|
169
|
+
<td><strong>{{ entry.domain }}</strong></td>
|
|
170
|
+
<td>
|
|
171
|
+
<span class="bl-badge {% if entry.reason == 'privacy' %}bl-badge--privacy{% elif entry.reason == 'spam' %}bl-badge--spam{% endif %}">
|
|
172
|
+
{{ entry.reason }}
|
|
173
|
+
</span>
|
|
174
|
+
</td>
|
|
175
|
+
<td>{{ entry.mentionsHidden or 0 }}</td>
|
|
176
|
+
<td>{{ entry.blockedAt | date }}</td>
|
|
177
|
+
<td>
|
|
178
|
+
<form method="post" action="{{ wmEndpoint }}/blocklist/{{ entry.domain | urlencode }}/delete" style="display:inline">
|
|
179
|
+
<button type="submit" class="button button--small button--secondary"
|
|
180
|
+
onclick="return confirm('Unblock {{ entry.domain }}? Mentions hidden by this block will be restored.')">
|
|
181
|
+
{{ __("webmention-io.blocklist.unblock") }}
|
|
182
|
+
</button>
|
|
183
|
+
</form>
|
|
184
|
+
</td>
|
|
185
|
+
</tr>
|
|
186
|
+
{% endfor %}
|
|
187
|
+
</tbody>
|
|
188
|
+
</table>
|
|
189
|
+
{% else %}
|
|
190
|
+
<p class="bl-empty">{{ __("webmention-io.blocklist.empty") }}</p>
|
|
191
|
+
{% endif %}
|
|
192
|
+
</section>
|
|
193
|
+
{% endblock %}
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
{% extends "document.njk" %}
|
|
2
|
+
|
|
3
|
+
{% block content %}
|
|
4
|
+
<style>
|
|
5
|
+
.wm-header {
|
|
6
|
+
display: flex;
|
|
7
|
+
justify-content: space-between;
|
|
8
|
+
align-items: flex-start;
|
|
9
|
+
flex-wrap: wrap;
|
|
10
|
+
gap: var(--space-s, 0.75rem);
|
|
11
|
+
margin-block-end: var(--space-m, 1.5rem);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
.wm-stats {
|
|
15
|
+
display: flex;
|
|
16
|
+
gap: var(--space-m, 1rem);
|
|
17
|
+
font: var(--font-body, 0.875rem/1.5 sans-serif);
|
|
18
|
+
color: var(--color-on-offset, #666);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.wm-stats__item {
|
|
22
|
+
display: flex;
|
|
23
|
+
align-items: center;
|
|
24
|
+
gap: 0.25rem;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.wm-stats__count {
|
|
28
|
+
font-weight: 700;
|
|
29
|
+
color: var(--color-on-surface, inherit);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.wm-actions-bar {
|
|
33
|
+
display: flex;
|
|
34
|
+
gap: var(--space-xs, 0.5rem);
|
|
35
|
+
flex-wrap: wrap;
|
|
36
|
+
align-items: center;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.wm-sync-info {
|
|
40
|
+
font: var(--font-caption, 0.75rem/1.4 sans-serif);
|
|
41
|
+
color: var(--color-on-offset, #666);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
.wm-filters {
|
|
45
|
+
display: flex;
|
|
46
|
+
gap: var(--space-m, 1rem);
|
|
47
|
+
margin-block-end: var(--space-m, 1rem);
|
|
48
|
+
flex-wrap: wrap;
|
|
49
|
+
font: var(--font-body, 0.875rem/1.5 sans-serif);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.wm-filters a {
|
|
53
|
+
text-decoration: none;
|
|
54
|
+
padding: 0.25rem 0.5rem;
|
|
55
|
+
border-radius: var(--border-radius-small, 0.25rem);
|
|
56
|
+
color: var(--color-on-offset, #666);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.wm-filters a.active {
|
|
60
|
+
background: var(--color-primary, #0066cc);
|
|
61
|
+
color: var(--color-on-primary, #fff);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
.wm-mention-wrapper {
|
|
65
|
+
margin-block-end: var(--space-xs, 0.5rem);
|
|
66
|
+
position: relative;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.wm-mention-wrapper--hidden {
|
|
70
|
+
opacity: 0.5;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
.wm-mention-actions {
|
|
74
|
+
display: flex;
|
|
75
|
+
gap: var(--space-2xs, 0.25rem);
|
|
76
|
+
margin-block-start: var(--space-2xs, 0.25rem);
|
|
77
|
+
padding-inline-start: 3.5rem;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
.wm-mention-badge {
|
|
81
|
+
display: inline-block;
|
|
82
|
+
font: var(--font-caption, 0.6875rem/1.4 sans-serif);
|
|
83
|
+
padding: 0.125rem 0.375rem;
|
|
84
|
+
border-radius: var(--border-radius-small, 0.25rem);
|
|
85
|
+
background: var(--color-warning-container, #fff3cd);
|
|
86
|
+
color: var(--color-warning, #856404);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
.wm-success {
|
|
90
|
+
background: var(--color-success-container, #d4edda);
|
|
91
|
+
border: 1px solid var(--color-success, #28a745);
|
|
92
|
+
border-radius: var(--border-radius-small, 0.25rem);
|
|
93
|
+
padding: var(--space-s, 0.75rem);
|
|
94
|
+
margin-block-end: var(--space-m, 1rem);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
.wm-error {
|
|
98
|
+
background: var(--color-error-container, #f8d7da);
|
|
99
|
+
border: 1px solid var(--color-error, #dc3545);
|
|
100
|
+
border-radius: var(--border-radius-small, 0.25rem);
|
|
101
|
+
padding: var(--space-s, 0.75rem);
|
|
102
|
+
margin-block-end: var(--space-m, 1rem);
|
|
103
|
+
}
|
|
104
|
+
</style>
|
|
105
|
+
|
|
106
|
+
{# Flash messages #}
|
|
107
|
+
{% if request.query.synced %}
|
|
108
|
+
<div class="wm-success">
|
|
109
|
+
{{ __("webmention-io.sync.synced") }} — {{ request.query.added or 0 }} {{ __("webmention-io.sync.added") }}
|
|
110
|
+
</div>
|
|
111
|
+
{% endif %}
|
|
112
|
+
{% if request.query.hidden %}
|
|
113
|
+
<div class="wm-success">Webmention hidden.</div>
|
|
114
|
+
{% endif %}
|
|
115
|
+
{% if request.query.unhidden %}
|
|
116
|
+
<div class="wm-success">Webmention restored.</div>
|
|
117
|
+
{% endif %}
|
|
118
|
+
{% if request.query.blocked %}
|
|
119
|
+
<div class="wm-success">{{ __("webmention-io.blocklist.blocked") }}: {{ request.query.domain }}</div>
|
|
120
|
+
{% endif %}
|
|
121
|
+
{% if request.query.error %}
|
|
122
|
+
<div class="wm-error">Error: {{ request.query.error }}</div>
|
|
123
|
+
{% endif %}
|
|
124
|
+
|
|
125
|
+
{# Header with stats and sync controls #}
|
|
126
|
+
<div class="wm-header">
|
|
127
|
+
<div class="wm-stats">
|
|
128
|
+
<span class="wm-stats__item">
|
|
129
|
+
<span class="wm-stats__count">{{ counts.visible }}</span> {{ __("webmention-io.counts.visible") }}
|
|
130
|
+
</span>
|
|
131
|
+
<span class="wm-stats__item">
|
|
132
|
+
<span class="wm-stats__count">{{ counts.hidden }}</span> {{ __("webmention-io.counts.hidden") }}
|
|
133
|
+
</span>
|
|
134
|
+
<span class="wm-stats__item">
|
|
135
|
+
<span class="wm-stats__count">{{ counts.total }}</span> {{ __("webmention-io.counts.total") }}
|
|
136
|
+
</span>
|
|
137
|
+
</div>
|
|
138
|
+
|
|
139
|
+
<div class="wm-actions-bar">
|
|
140
|
+
<span class="wm-sync-info">
|
|
141
|
+
{% if syncState.lastSync %}
|
|
142
|
+
{{ __("webmention-io.sync.lastSync") }}: {{ syncState.lastSync | date }}
|
|
143
|
+
{% else %}
|
|
144
|
+
{{ __("webmention-io.sync.never") }}
|
|
145
|
+
{% endif %}
|
|
146
|
+
</span>
|
|
147
|
+
<form method="post" action="{{ wmEndpoint }}/sync" style="display:inline">
|
|
148
|
+
<button type="submit" class="button button--small">{{ __("webmention-io.sync.now") }}</button>
|
|
149
|
+
</form>
|
|
150
|
+
<form method="post" action="{{ wmEndpoint }}/sync/full" style="display:inline"
|
|
151
|
+
onsubmit="return confirm('This will clear all cached webmentions and re-fetch from webmention.io. Continue?')">
|
|
152
|
+
<button type="submit" class="button button--small button--secondary">{{ __("webmention-io.sync.full") }}</button>
|
|
153
|
+
</form>
|
|
154
|
+
<a href="{{ wmEndpoint }}/blocklist" class="button button--small button--secondary">{{ __("webmention-io.blocklist.title") }}</a>
|
|
155
|
+
</div>
|
|
156
|
+
</div>
|
|
157
|
+
|
|
158
|
+
{# Filters #}
|
|
159
|
+
<div class="wm-filters">
|
|
160
|
+
<div>
|
|
161
|
+
{{ __("webmention-io.filter.show") }}:
|
|
162
|
+
<a href="{{ wmEndpoint }}?filter=all&type={{ typeFilter }}" class="{% if filter == 'all' %}active{% endif %}">{{ __("webmention-io.filter.all") }}</a>
|
|
163
|
+
<a href="{{ wmEndpoint }}?filter=visible&type={{ typeFilter }}" class="{% if filter == 'visible' %}active{% endif %}">{{ __("webmention-io.filter.visible") }}</a>
|
|
164
|
+
<a href="{{ wmEndpoint }}?filter=hidden&type={{ typeFilter }}" class="{% if filter == 'hidden' %}active{% endif %}">{{ __("webmention-io.filter.hidden") }}</a>
|
|
165
|
+
</div>
|
|
166
|
+
<div>
|
|
167
|
+
{{ __("webmention-io.filter.type") }}:
|
|
168
|
+
<a href="{{ wmEndpoint }}?filter={{ filter }}&type=all" class="{% if typeFilter == 'all' %}active{% endif %}">{{ __("webmention-io.filter.all") }}</a>
|
|
169
|
+
<a href="{{ wmEndpoint }}?filter={{ filter }}&type=like-of" class="{% if typeFilter == 'like-of' %}active{% endif %}">{{ __("webmention-io.filter.likes") }}</a>
|
|
170
|
+
<a href="{{ wmEndpoint }}?filter={{ filter }}&type=in-reply-to" class="{% if typeFilter == 'in-reply-to' %}active{% endif %}">{{ __("webmention-io.filter.replies") }}</a>
|
|
171
|
+
<a href="{{ wmEndpoint }}?filter={{ filter }}&type=repost-of" class="{% if typeFilter == 'repost-of' %}active{% endif %}">{{ __("webmention-io.filter.reposts") }}</a>
|
|
172
|
+
<a href="{{ wmEndpoint }}?filter={{ filter }}&type=mention-of" class="{% if typeFilter == 'mention-of' %}active{% endif %}">{{ __("webmention-io.filter.mentions") }}</a>
|
|
173
|
+
</div>
|
|
174
|
+
</div>
|
|
175
|
+
|
|
176
|
+
{# Webmention list #}
|
|
177
|
+
{%- if webmentions and webmentions.length > 0 %}
|
|
178
|
+
{% for item in webmentions %}
|
|
179
|
+
<div class="wm-mention-wrapper {% if item.hidden %}wm-mention-wrapper--hidden{% endif %}">
|
|
180
|
+
{{ mention({
|
|
181
|
+
user: {
|
|
182
|
+
avatar: item.user.avatar,
|
|
183
|
+
meta: __("webmention-io.mention." + item["wm-property"], item["wm-target"]) | urlize,
|
|
184
|
+
name: item.user.name
|
|
185
|
+
},
|
|
186
|
+
mention: {
|
|
187
|
+
title: item.title,
|
|
188
|
+
icon: item.icon,
|
|
189
|
+
description: item.description if item.description and item.description.html,
|
|
190
|
+
permalink: item.url,
|
|
191
|
+
published: item.published
|
|
192
|
+
}
|
|
193
|
+
}) | indent(4) }}
|
|
194
|
+
|
|
195
|
+
<div class="wm-mention-actions">
|
|
196
|
+
{% if item.hidden %}
|
|
197
|
+
<span class="wm-mention-badge">{{ __("webmention-io.actions.hidden") }} ({{ item.hiddenReason or "manual" }})</span>
|
|
198
|
+
<form method="post" action="{{ wmEndpoint }}/{{ item.id }}/unhide" style="display:inline">
|
|
199
|
+
<button type="submit" class="button button--small">{{ __("webmention-io.actions.unhide") }}</button>
|
|
200
|
+
</form>
|
|
201
|
+
{% else %}
|
|
202
|
+
<form method="post" action="{{ wmEndpoint }}/{{ item.id }}/hide" style="display:inline">
|
|
203
|
+
<button type="submit" class="button button--small button--secondary">{{ __("webmention-io.actions.hide") }}</button>
|
|
204
|
+
</form>
|
|
205
|
+
{% endif %}
|
|
206
|
+
|
|
207
|
+
{% if item.sourceDomain %}
|
|
208
|
+
<form method="post" action="{{ wmEndpoint }}/block" style="display:inline"
|
|
209
|
+
onsubmit="return confirm('Block all webmentions from {{ item.sourceDomain }}?')">
|
|
210
|
+
<input type="hidden" name="domain" value="{{ item.sourceDomain }}">
|
|
211
|
+
<button type="submit" class="button button--small button--secondary">{{ __("webmention-io.actions.block") }} {{ item.sourceDomain }}</button>
|
|
212
|
+
</form>
|
|
213
|
+
{% endif %}
|
|
214
|
+
</div>
|
|
215
|
+
</div>
|
|
216
|
+
{% endfor %}
|
|
217
|
+
|
|
218
|
+
{{ pagination(cursor) }}
|
|
219
|
+
{%- else -%}
|
|
220
|
+
{{ prose({ text: __("webmention-io.webmentions.none") }) }}
|
|
221
|
+
{%- endif %}
|
|
222
|
+
{% endblock %}
|