@rmdes/indiekit-endpoint-activitypub 2.0.35 → 2.1.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/assets/reader-infinite-scroll.js +6 -0
- package/assets/reader-tabs.js +643 -0
- package/assets/reader.css +228 -117
- package/index.js +26 -14
- package/lib/controllers/explore-utils.js +122 -0
- package/lib/controllers/explore.js +28 -142
- package/lib/controllers/hashtag-explore.js +223 -0
- package/lib/controllers/tabs.js +245 -0
- package/locales/en.json +16 -13
- package/package.json +1 -1
- package/views/activitypub-explore.njk +364 -193
- package/views/layouts/ap-reader.njk +2 -2
- package/views/partials/ap-item-card.njk +33 -0
- package/assets/reader-decks.js +0 -212
- package/lib/controllers/decks.js +0 -137
|
@@ -9,210 +9,381 @@
|
|
|
9
9
|
<p class="ap-explore-header__desc">{{ __("activitypub.reader.explore.description") }}</p>
|
|
10
10
|
</header>
|
|
11
11
|
|
|
12
|
-
{#
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
</a>
|
|
18
|
-
<a href="{{ exploreBase }}?tab=decks" class="ap-tab{% if activeTab == 'decks' %} ap-tab--active{% endif %}">
|
|
19
|
-
{{ __("activitypub.reader.explore.tabs.decks") }}
|
|
20
|
-
{% if decks and decks.length > 0 %}
|
|
21
|
-
<span class="ap-tab__count">{{ decks.length }}</span>
|
|
22
|
-
{% endif %}
|
|
23
|
-
</a>
|
|
24
|
-
</nav>
|
|
25
|
-
|
|
26
|
-
{# ── Search tab ────────────────────────────────────────────────── #}
|
|
27
|
-
{% if activeTab != 'decks' %}
|
|
28
|
-
|
|
29
|
-
{# Instance form with autocomplete #}
|
|
30
|
-
<form action="{{ mountPath }}/admin/reader/explore" method="get" class="ap-explore-form"
|
|
31
|
-
x-data="apInstanceSearch('{{ mountPath }}')"
|
|
32
|
-
@submit="onSubmit">
|
|
33
|
-
<div class="ap-explore-form__row">
|
|
34
|
-
<div class="ap-explore-autocomplete">
|
|
35
|
-
<input
|
|
36
|
-
type="text"
|
|
37
|
-
name="instance"
|
|
38
|
-
value="{{ instance }}"
|
|
39
|
-
class="ap-explore-form__input"
|
|
40
|
-
placeholder="{{ __('activitypub.reader.explore.instancePlaceholder') }}"
|
|
41
|
-
aria-label="{{ __('activitypub.reader.explore.instancePlaceholder') }}"
|
|
42
|
-
autocomplete="off"
|
|
43
|
-
required
|
|
44
|
-
x-model="query"
|
|
45
|
-
@input.debounce.300ms="search()"
|
|
46
|
-
@keydown.arrow-down.prevent="highlightNext()"
|
|
47
|
-
@keydown.arrow-up.prevent="highlightPrev()"
|
|
48
|
-
@keydown.enter="selectHighlighted($event)"
|
|
49
|
-
@keydown.escape="close()"
|
|
50
|
-
@focus="showResults && suggestions.length > 0 ? showResults = true : null"
|
|
51
|
-
@click.away="close()"
|
|
52
|
-
x-ref="input">
|
|
53
|
-
|
|
54
|
-
{# Autocomplete dropdown #}
|
|
55
|
-
<div class="ap-explore-autocomplete__dropdown" x-show="showResults && suggestions.length > 0" x-cloak>
|
|
56
|
-
<template x-for="(item, index) in suggestions" :key="item.domain">
|
|
57
|
-
<button type="button"
|
|
58
|
-
class="ap-explore-autocomplete__item"
|
|
59
|
-
:class="{ 'ap-explore-autocomplete__item--highlighted': index === highlighted }"
|
|
60
|
-
@click="selectItem(item)"
|
|
61
|
-
@mouseenter="highlighted = index">
|
|
62
|
-
<span class="ap-explore-autocomplete__domain" x-text="item.domain"></span>
|
|
63
|
-
<span class="ap-explore-autocomplete__meta">
|
|
64
|
-
<span class="ap-explore-autocomplete__software" x-text="item.software"></span>
|
|
65
|
-
<template x-if="item.mau > 0">
|
|
66
|
-
<span class="ap-explore-autocomplete__mau" x-text="item.mau.toLocaleString() + ' {{ __("activitypub.reader.explore.mauLabel") }}'"></span>
|
|
67
|
-
</template>
|
|
68
|
-
</span>
|
|
69
|
-
<span class="ap-explore-autocomplete__status" x-show="item._timelineStatus !== undefined">
|
|
70
|
-
<template x-if="item._timelineStatus === 'checking'">
|
|
71
|
-
<span class="ap-explore-autocomplete__checking">⏳</span>
|
|
72
|
-
</template>
|
|
73
|
-
<template x-if="item._timelineStatus === true">
|
|
74
|
-
<span class="ap-explore-autocomplete__supported" title="{{ __('activitypub.reader.explore.timelineSupported') }}">✅</span>
|
|
75
|
-
</template>
|
|
76
|
-
<template x-if="item._timelineStatus === false">
|
|
77
|
-
<span class="ap-explore-autocomplete__unsupported" title="{{ __('activitypub.reader.explore.timelineUnsupported') }}">❌</span>
|
|
78
|
-
</template>
|
|
79
|
-
</span>
|
|
80
|
-
</button>
|
|
81
|
-
</template>
|
|
82
|
-
</div>
|
|
83
|
-
</div>
|
|
12
|
+
{# ── Tabbed explore container (Alpine.js component) ──────────────────── #}
|
|
13
|
+
<div class="ap-explore-tabs-container"
|
|
14
|
+
data-mount-path="{{ mountPath }}"
|
|
15
|
+
data-csrf="{{ csrfToken }}"
|
|
16
|
+
x-data="apExploreTabs()">
|
|
84
17
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
18
|
+
{# ── Tab bar ──────────────────────────────────────────────────────────── #}
|
|
19
|
+
<nav class="ap-tabs ap-explore-tabs-nav"
|
|
20
|
+
id="ap-explore-tab-bar"
|
|
21
|
+
role="tablist"
|
|
22
|
+
aria-label="{{ __('activitypub.reader.explore.tabs.label') }}">
|
|
23
|
+
|
|
24
|
+
{# Search tab — always first, not removable #}
|
|
25
|
+
<button
|
|
26
|
+
type="button"
|
|
27
|
+
class="ap-tab"
|
|
28
|
+
:class="{ 'ap-tab--active': activeTabId === null }"
|
|
29
|
+
role="tab"
|
|
30
|
+
:aria-selected="activeTabId === null ? 'true' : 'false'"
|
|
31
|
+
aria-controls="ap-tab-panel-search"
|
|
32
|
+
id="ap-tab-btn-search"
|
|
33
|
+
@click="switchToSearch()"
|
|
34
|
+
@keydown="handleTabKeydown($event, 0)">
|
|
35
|
+
{{ __("activitypub.reader.explore.tabs.search") }}
|
|
99
36
|
</button>
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
37
|
+
|
|
38
|
+
{# User-created instance and hashtag tabs #}
|
|
39
|
+
<template x-for="(tab, index) in tabs" :key="tab._id">
|
|
40
|
+
<div class="ap-tab-wrapper">
|
|
41
|
+
{# Tab button #}
|
|
42
|
+
<button
|
|
43
|
+
type="button"
|
|
44
|
+
class="ap-tab ap-tab--user"
|
|
45
|
+
:class="{ 'ap-tab--active': activeTabId === tab._id }"
|
|
46
|
+
role="tab"
|
|
47
|
+
:aria-selected="activeTabId === tab._id ? 'true' : 'false'"
|
|
48
|
+
:aria-controls="'ap-tab-panel-' + tab._id"
|
|
49
|
+
:id="'ap-tab-btn-' + tab._id"
|
|
50
|
+
@click="switchTab(tab._id)"
|
|
51
|
+
@keydown="handleTabKeydown($event, index + 1)">
|
|
52
|
+
<span class="ap-tab__label" :title="tabLabel(tab)" x-text="tabLabel(tab)"></span>
|
|
53
|
+
<span
|
|
54
|
+
x-show="tab.type === 'instance'"
|
|
55
|
+
class="ap-tab__badge"
|
|
56
|
+
:class="tab.scope === 'local' ? 'ap-tab__badge--local' : 'ap-tab__badge--federated'"
|
|
57
|
+
x-text="tab.scope"></span>
|
|
58
|
+
</button>
|
|
59
|
+
|
|
60
|
+
{# Reorder and close controls #}
|
|
61
|
+
<div class="ap-tab-controls">
|
|
62
|
+
<button
|
|
63
|
+
type="button"
|
|
64
|
+
class="ap-tab-control ap-tab-control--up"
|
|
65
|
+
@click.stop="moveUp(tab)"
|
|
66
|
+
:disabled="index === 0"
|
|
67
|
+
:aria-label="'{{ __('activitypub.reader.explore.tabs.moveUp') }}: ' + tabLabel(tab)">↑</button>
|
|
68
|
+
<button
|
|
69
|
+
type="button"
|
|
70
|
+
class="ap-tab-control ap-tab-control--down"
|
|
71
|
+
@click.stop="moveDown(tab)"
|
|
72
|
+
:disabled="index === tabs.length - 1"
|
|
73
|
+
:aria-label="'{{ __('activitypub.reader.explore.tabs.moveDown') }}: ' + tabLabel(tab)">↓</button>
|
|
74
|
+
<button
|
|
75
|
+
type="button"
|
|
76
|
+
class="ap-tab-control ap-tab-control--remove"
|
|
77
|
+
@click.stop="removeTab(tab)"
|
|
78
|
+
:aria-label="'{{ __('activitypub.reader.explore.tabs.remove') }}: ' + tabLabel(tab)">×</button>
|
|
79
|
+
</div>
|
|
80
|
+
</div>
|
|
81
|
+
</template>
|
|
82
|
+
|
|
83
|
+
{# +# button — add a hashtag tab #}
|
|
84
|
+
<div class="ap-tab-add-hashtag">
|
|
114
85
|
<button
|
|
115
86
|
type="button"
|
|
116
|
-
class="ap-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
x-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
87
|
+
class="ap-tab ap-tab--add"
|
|
88
|
+
@click="showHashtagForm = !showHashtagForm"
|
|
89
|
+
:aria-expanded="showHashtagForm ? 'true' : 'false'"
|
|
90
|
+
title="{{ __('activitypub.reader.explore.tabs.addHashtag') }}">+#</button>
|
|
91
|
+
<form
|
|
92
|
+
x-show="showHashtagForm"
|
|
93
|
+
x-cloak
|
|
94
|
+
class="ap-tab-hashtag-form"
|
|
95
|
+
@submit.prevent="submitHashtagTab()"
|
|
96
|
+
@keydown.escape.prevent="showHashtagForm = false; hashtagInput = ''">
|
|
97
|
+
<span class="ap-tab-hashtag-form__prefix">#</span>
|
|
98
|
+
<input
|
|
99
|
+
type="text"
|
|
100
|
+
x-model="hashtagInput"
|
|
101
|
+
class="ap-tab-hashtag-form__input"
|
|
102
|
+
placeholder="{{ __('activitypub.reader.explore.tabs.hashtagTabPlaceholder') }}"
|
|
103
|
+
aria-label="{{ __('activitypub.reader.explore.tabs.addHashtag') }}"
|
|
104
|
+
autocomplete="off"
|
|
105
|
+
maxlength="100">
|
|
106
|
+
<button type="submit" class="ap-tab-hashtag-form__btn">
|
|
107
|
+
{{ __("activitypub.reader.explore.tabs.addTab") }}
|
|
108
|
+
</button>
|
|
109
|
+
</form>
|
|
137
110
|
</div>
|
|
111
|
+
</nav>{# end tab bar nav #}
|
|
112
|
+
|
|
113
|
+
{# Error message (CSRF expiry, network errors) #}
|
|
114
|
+
<p x-show="error" x-cloak class="ap-explore-error" x-text="error"></p>
|
|
115
|
+
|
|
116
|
+
{# ── Search tab panel ─────────────────────────────────────────────────── #}
|
|
117
|
+
<div id="ap-tab-panel-search"
|
|
118
|
+
role="tabpanel"
|
|
119
|
+
aria-labelledby="ap-tab-btn-search"
|
|
120
|
+
x-show="activeTabId === null">
|
|
121
|
+
|
|
122
|
+
{# Instance form with autocomplete #}
|
|
123
|
+
<form action="{{ mountPath }}/admin/reader/explore" method="get" class="ap-explore-form"
|
|
124
|
+
x-data="apInstanceSearch('{{ mountPath }}')"
|
|
125
|
+
@submit="onSubmit">
|
|
126
|
+
<div class="ap-explore-form__row">
|
|
127
|
+
<div class="ap-explore-autocomplete">
|
|
128
|
+
<input
|
|
129
|
+
type="text"
|
|
130
|
+
name="instance"
|
|
131
|
+
value="{{ instance }}"
|
|
132
|
+
class="ap-explore-form__input"
|
|
133
|
+
placeholder="{{ __('activitypub.reader.explore.instancePlaceholder') }}"
|
|
134
|
+
aria-label="{{ __('activitypub.reader.explore.instancePlaceholder') }}"
|
|
135
|
+
autocomplete="off"
|
|
136
|
+
required
|
|
137
|
+
x-model="query"
|
|
138
|
+
@input.debounce.300ms="search()"
|
|
139
|
+
@keydown.arrow-down.prevent="highlightNext()"
|
|
140
|
+
@keydown.arrow-up.prevent="highlightPrev()"
|
|
141
|
+
@keydown.enter="selectHighlighted($event)"
|
|
142
|
+
@keydown.escape="close()"
|
|
143
|
+
@focus="showResults && suggestions.length > 0 ? showResults = true : null"
|
|
144
|
+
@click.away="close()"
|
|
145
|
+
x-ref="input">
|
|
138
146
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
147
|
+
{# Autocomplete dropdown #}
|
|
148
|
+
<div class="ap-explore-autocomplete__dropdown" x-show="showResults && suggestions.length > 0" x-cloak>
|
|
149
|
+
<template x-for="(item, index) in suggestions" :key="item.domain">
|
|
150
|
+
<button type="button"
|
|
151
|
+
class="ap-explore-autocomplete__item"
|
|
152
|
+
:class="{ 'ap-explore-autocomplete__item--highlighted': index === highlighted }"
|
|
153
|
+
@click="selectItem(item)"
|
|
154
|
+
@mouseenter="highlighted = index">
|
|
155
|
+
<span class="ap-explore-autocomplete__domain" x-text="item.domain"></span>
|
|
156
|
+
<span class="ap-explore-autocomplete__meta">
|
|
157
|
+
<span class="ap-explore-autocomplete__software" x-text="item.software"></span>
|
|
158
|
+
<template x-if="item.mau > 0">
|
|
159
|
+
<span class="ap-explore-autocomplete__mau" x-text="item.mau.toLocaleString() + ' {{ __("activitypub.reader.explore.mauLabel") }}'"></span>
|
|
160
|
+
</template>
|
|
161
|
+
</span>
|
|
162
|
+
<span class="ap-explore-autocomplete__status" x-show="item._timelineStatus !== undefined">
|
|
163
|
+
<template x-if="item._timelineStatus === 'checking'">
|
|
164
|
+
<span class="ap-explore-autocomplete__checking">⏳</span>
|
|
165
|
+
</template>
|
|
166
|
+
<template x-if="item._timelineStatus === true">
|
|
167
|
+
<span class="ap-explore-autocomplete__supported" title="{{ __('activitypub.reader.explore.timelineSupported') }}">✅</span>
|
|
168
|
+
</template>
|
|
169
|
+
<template x-if="item._timelineStatus === false">
|
|
170
|
+
<span class="ap-explore-autocomplete__unsupported" title="{{ __('activitypub.reader.explore.timelineUnsupported') }}">❌</span>
|
|
171
|
+
</template>
|
|
172
|
+
</span>
|
|
173
|
+
</button>
|
|
174
|
+
</template>
|
|
175
|
+
</div>
|
|
176
|
+
</div>
|
|
177
|
+
|
|
178
|
+
<div class="ap-explore-form__scope">
|
|
179
|
+
<label class="ap-explore-form__scope-label">
|
|
180
|
+
<input type="radio" name="scope" value="local"
|
|
181
|
+
{% if scope == "local" %}checked{% endif %}>
|
|
182
|
+
{{ __("activitypub.reader.explore.local") }}
|
|
183
|
+
</label>
|
|
184
|
+
<label class="ap-explore-form__scope-label">
|
|
185
|
+
<input type="radio" name="scope" value="federated"
|
|
186
|
+
{% if scope == "federated" %}checked{% endif %}>
|
|
187
|
+
{{ __("activitypub.reader.explore.federated") }}
|
|
188
|
+
</label>
|
|
189
|
+
</div>
|
|
190
|
+
<button type="submit" class="ap-explore-form__btn">
|
|
191
|
+
{{ __("activitypub.reader.explore.browse") }}
|
|
152
192
|
</button>
|
|
153
|
-
<p class="ap-load-more__done" x-show="done" x-cloak>{{ __("activitypub.reader.pagination.noMore") }}</p>
|
|
154
193
|
</div>
|
|
194
|
+
<div class="ap-explore-form__hashtag-row">
|
|
195
|
+
<label class="ap-explore-form__hashtag-label" for="ap-explore-hashtag">
|
|
196
|
+
{{ __("activitypub.reader.explore.hashtagLabel") }}
|
|
197
|
+
</label>
|
|
198
|
+
<span class="ap-explore-form__hashtag-prefix">#</span>
|
|
199
|
+
<input
|
|
200
|
+
type="text"
|
|
201
|
+
id="ap-explore-hashtag"
|
|
202
|
+
name="hashtag"
|
|
203
|
+
value="{{ hashtag }}"
|
|
204
|
+
class="ap-explore-form__input ap-explore-form__input--hashtag"
|
|
205
|
+
placeholder="{{ __('activitypub.reader.explore.hashtagPlaceholder') }}"
|
|
206
|
+
aria-label="{{ __('activitypub.reader.explore.hashtagPlaceholder') }}"
|
|
207
|
+
autocomplete="off"
|
|
208
|
+
pattern="[\w]+"
|
|
209
|
+
maxlength="100">
|
|
210
|
+
<span class="ap-explore-form__hashtag-hint">{{ __("activitypub.reader.explore.hashtagHint") }}</span>
|
|
211
|
+
</div>
|
|
212
|
+
</form>
|
|
213
|
+
|
|
214
|
+
{# Error state #}
|
|
215
|
+
{% if error %}
|
|
216
|
+
<div class="ap-explore-error">{{ error }}</div>
|
|
155
217
|
{% endif %}
|
|
156
|
-
|
|
157
|
-
{
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
218
|
+
|
|
219
|
+
{# Results #}
|
|
220
|
+
{% if instance and not error %}
|
|
221
|
+
{# Pin as tab button — outside form, inside apExploreTabs scope #}
|
|
222
|
+
<div class="ap-explore-pin-bar">
|
|
223
|
+
<button
|
|
224
|
+
type="button"
|
|
225
|
+
class="ap-explore-pin-btn"
|
|
226
|
+
@click="pinInstance('{{ instance }}', '{{ scope }}')"
|
|
227
|
+
:disabled="pinning">
|
|
228
|
+
<span x-show="!pinning">{{ __("activitypub.reader.explore.tabs.pinAsTab") }}</span>
|
|
229
|
+
<span x-show="pinning" x-cloak>…</span>
|
|
230
|
+
</button>
|
|
231
|
+
</div>
|
|
232
|
+
|
|
233
|
+
{% if items.length > 0 %}
|
|
234
|
+
<div class="ap-timeline ap-explore-timeline"
|
|
235
|
+
id="ap-explore-timeline"
|
|
236
|
+
data-instance="{{ instance }}"
|
|
237
|
+
data-scope="{{ scope }}"
|
|
238
|
+
data-hashtag="{{ hashtag }}"
|
|
239
|
+
data-mount-path="{{ mountPath }}"
|
|
240
|
+
data-max-id="{{ maxId if maxId else '' }}">
|
|
241
|
+
{% for item in items %}
|
|
242
|
+
{% include "partials/ap-item-card.njk" %}
|
|
243
|
+
{% endfor %}
|
|
244
|
+
</div>
|
|
245
|
+
|
|
246
|
+
{# Infinite scroll for explore search tab #}
|
|
247
|
+
{% if maxId %}
|
|
248
|
+
<div class="ap-load-more"
|
|
249
|
+
id="ap-explore-load-more"
|
|
250
|
+
data-max-id="{{ maxId }}"
|
|
251
|
+
data-instance="{{ instance }}"
|
|
252
|
+
data-scope="{{ scope }}"
|
|
253
|
+
data-hashtag="{{ hashtag }}"
|
|
254
|
+
x-data="apExploreScroll()"
|
|
255
|
+
x-init="init()">
|
|
256
|
+
<div class="ap-load-more__sentinel" x-ref="sentinel"></div>
|
|
257
|
+
<button class="ap-load-more__btn" @click="loadMore()" :disabled="loading" x-show="!done">
|
|
258
|
+
<span x-show="!loading">{{ __("activitypub.reader.pagination.loadMore") }}</span>
|
|
259
|
+
<span x-show="loading">{{ __("activitypub.reader.pagination.loading") }}</span>
|
|
260
|
+
</button>
|
|
261
|
+
<p class="ap-load-more__done" x-show="done" x-cloak>{{ __("activitypub.reader.pagination.noMore") }}</p>
|
|
262
|
+
</div>
|
|
263
|
+
{% endif %}
|
|
264
|
+
{% elif instance %}
|
|
265
|
+
{{ prose({ text: __("activitypub.reader.explore.noResults") }) }}
|
|
266
|
+
{% endif %}
|
|
267
|
+
{% endif %}
|
|
268
|
+
|
|
269
|
+
</div>{# end Search tab panel #}
|
|
270
|
+
|
|
271
|
+
{# ── Dynamic tab panels (instance + hashtag) ──────────────────────────── #}
|
|
272
|
+
<template x-for="tab in tabs" :key="tab._id + '-panel'">
|
|
273
|
+
<div
|
|
274
|
+
:id="'ap-tab-panel-' + tab._id"
|
|
275
|
+
role="tabpanel"
|
|
276
|
+
:aria-labelledby="'ap-tab-btn-' + tab._id"
|
|
277
|
+
x-show="activeTabId === tab._id"
|
|
278
|
+
x-cloak>
|
|
279
|
+
|
|
280
|
+
{# ── Instance tab panel ───────────────────────────────────────────── #}
|
|
281
|
+
<template x-if="tab.type === 'instance'">
|
|
282
|
+
<div class="ap-explore-instance-panel">
|
|
283
|
+
|
|
284
|
+
{# Loading spinner — first load, no content yet #}
|
|
285
|
+
<div class="ap-explore-tab-loading"
|
|
286
|
+
x-show="tabState[tab._id] && tabState[tab._id].loading && !tabState[tab._id].html">
|
|
287
|
+
<span class="ap-explore-tab-loading__text">{{ __("activitypub.reader.pagination.loading") }}</span>
|
|
288
|
+
</div>
|
|
289
|
+
|
|
290
|
+
{# Error state with retry #}
|
|
291
|
+
<div class="ap-explore-tab-error"
|
|
292
|
+
x-show="tabState[tab._id] && tabState[tab._id].error && !tabState[tab._id].html">
|
|
293
|
+
<p class="ap-explore-tab-error__message" x-text="tabState[tab._id] && tabState[tab._id].error"></p>
|
|
294
|
+
<button type="button" class="ap-explore-tab-error__retry" @click="retryTab(tab)">
|
|
295
|
+
{{ __("activitypub.reader.explore.tabs.retry") }}
|
|
296
|
+
</button>
|
|
297
|
+
</div>
|
|
298
|
+
|
|
299
|
+
{# Timeline content — server-rendered cards injected via x-html #}
|
|
300
|
+
<div class="ap-timeline ap-explore-tab-timeline"
|
|
301
|
+
x-show="tabState[tab._id] && tabState[tab._id].html"
|
|
302
|
+
x-html="tabState[tab._id] ? tabState[tab._id].html : ''">
|
|
204
303
|
</div>
|
|
304
|
+
|
|
305
|
+
{# Inline loading spinner for subsequent pages #}
|
|
306
|
+
<div class="ap-explore-tab-loading ap-explore-tab-loading--more"
|
|
307
|
+
x-show="tabState[tab._id] && tabState[tab._id].loading && tabState[tab._id].html">
|
|
308
|
+
<span class="ap-explore-tab-loading__text">{{ __("activitypub.reader.pagination.loading") }}</span>
|
|
309
|
+
</div>
|
|
310
|
+
|
|
311
|
+
{# Empty state — loaded successfully but no posts #}
|
|
312
|
+
<div class="ap-explore-tab-empty"
|
|
313
|
+
x-show="tabState[tab._id] && tabState[tab._id].done && !tabState[tab._id].html && !tabState[tab._id].loading && !tabState[tab._id].error">
|
|
314
|
+
<p>{{ __("activitypub.reader.explore.noResults") }}</p>
|
|
315
|
+
</div>
|
|
316
|
+
|
|
317
|
+
{# End of feed message #}
|
|
318
|
+
<p class="ap-load-more__done"
|
|
319
|
+
x-show="tabState[tab._id] && tabState[tab._id].done && tabState[tab._id].html"
|
|
320
|
+
x-cloak>
|
|
321
|
+
{{ __("activitypub.reader.pagination.noMore") }}
|
|
322
|
+
</p>
|
|
323
|
+
|
|
324
|
+
{# Infinite scroll sentinel — watched by IntersectionObserver #}
|
|
325
|
+
<div class="ap-tab-sentinel"></div>
|
|
205
326
|
</div>
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
327
|
+
</template>
|
|
328
|
+
|
|
329
|
+
{# ── Hashtag tab panel ────────────────────────────────────────────── #}
|
|
330
|
+
<template x-if="tab.type === 'hashtag'">
|
|
331
|
+
<div class="ap-explore-hashtag-panel">
|
|
332
|
+
|
|
333
|
+
{# Source info line — shows which instances are being searched #}
|
|
334
|
+
<p class="ap-hashtag-sources"
|
|
335
|
+
x-show="tabState[tab._id] && tabState[tab._id].sourceMeta && tabState[tab._id].sourceMeta.instancesQueried > 0"
|
|
336
|
+
x-text="hashtagSourcesLine(tab)"
|
|
337
|
+
x-cloak></p>
|
|
338
|
+
|
|
339
|
+
{# Loading spinner — first load, no content yet #}
|
|
340
|
+
<div class="ap-explore-tab-loading"
|
|
341
|
+
x-show="tabState[tab._id] && tabState[tab._id].loading && !tabState[tab._id].html">
|
|
342
|
+
<span class="ap-explore-tab-loading__text">{{ __("activitypub.reader.pagination.loading") }}</span>
|
|
343
|
+
</div>
|
|
344
|
+
|
|
345
|
+
{# Error state with retry #}
|
|
346
|
+
<div class="ap-explore-tab-error"
|
|
347
|
+
x-show="tabState[tab._id] && tabState[tab._id].error && !tabState[tab._id].html">
|
|
348
|
+
<p class="ap-explore-tab-error__message" x-text="tabState[tab._id] && tabState[tab._id].error"></p>
|
|
349
|
+
<button type="button" class="ap-explore-tab-error__retry" @click="retryTab(tab)">
|
|
350
|
+
{{ __("activitypub.reader.explore.tabs.retry") }}
|
|
351
|
+
</button>
|
|
352
|
+
</div>
|
|
353
|
+
|
|
354
|
+
{# Timeline content #}
|
|
355
|
+
<div class="ap-timeline ap-explore-tab-timeline"
|
|
356
|
+
x-show="tabState[tab._id] && tabState[tab._id].html"
|
|
357
|
+
x-html="tabState[tab._id] ? tabState[tab._id].html : ''">
|
|
358
|
+
</div>
|
|
359
|
+
|
|
360
|
+
{# Inline loading spinner for subsequent pages #}
|
|
361
|
+
<div class="ap-explore-tab-loading ap-explore-tab-loading--more"
|
|
362
|
+
x-show="tabState[tab._id] && tabState[tab._id].loading && tabState[tab._id].html">
|
|
363
|
+
<span class="ap-explore-tab-loading__text">{{ __("activitypub.reader.pagination.loading") }}</span>
|
|
364
|
+
</div>
|
|
365
|
+
|
|
366
|
+
{# Empty state — no instance tabs pinned yet #}
|
|
367
|
+
<div class="ap-explore-tab-empty"
|
|
368
|
+
x-show="tabState[tab._id] && tabState[tab._id].done && !tabState[tab._id].html && !tabState[tab._id].loading && !tabState[tab._id].error">
|
|
369
|
+
<p>{{ __("activitypub.reader.explore.tabs.noInstances") }}</p>
|
|
370
|
+
</div>
|
|
371
|
+
|
|
372
|
+
{# End of feed message #}
|
|
373
|
+
<p class="ap-load-more__done"
|
|
374
|
+
x-show="tabState[tab._id] && tabState[tab._id].done && tabState[tab._id].html"
|
|
375
|
+
x-cloak>
|
|
376
|
+
{{ __("activitypub.reader.pagination.noMore") }}
|
|
377
|
+
</p>
|
|
378
|
+
|
|
379
|
+
{# Infinite scroll sentinel #}
|
|
380
|
+
<div class="ap-tab-sentinel"></div>
|
|
381
|
+
</div>
|
|
382
|
+
</template>
|
|
383
|
+
|
|
214
384
|
</div>
|
|
215
|
-
|
|
216
|
-
|
|
385
|
+
</template>
|
|
386
|
+
|
|
387
|
+
</div>{# end ap-explore-tabs-container #}
|
|
217
388
|
|
|
218
389
|
{% endblock %}
|
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
<script defer src="/assets/@rmdes-indiekit-endpoint-activitypub/reader-infinite-scroll.js"></script>
|
|
6
6
|
{# Autocomplete components for explore + popular accounts #}
|
|
7
7
|
<script defer src="/assets/@rmdes-indiekit-endpoint-activitypub/reader-autocomplete.js"></script>
|
|
8
|
-
{#
|
|
9
|
-
<script defer src="/assets/@rmdes-indiekit-endpoint-activitypub/reader-
|
|
8
|
+
{# Tab components — apExploreTabs #}
|
|
9
|
+
<script defer src="/assets/@rmdes-indiekit-endpoint-activitypub/reader-tabs.js"></script>
|
|
10
10
|
|
|
11
11
|
{# Alpine.js for client-side reactivity (CW toggles, interaction buttons, infinite scroll) #}
|
|
12
12
|
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.14.9/dist/cdn.min.js"></script>
|