@rmdes/indiekit-endpoint-microsub 1.0.2 → 1.0.4

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/locales/en.json CHANGED
@@ -27,6 +27,7 @@
27
27
  "feeds": {
28
28
  "title": "Feeds",
29
29
  "follow": "Follow",
30
+ "subscribe": "Subscribe to a feed",
30
31
  "unfollow": "Unfollow",
31
32
  "empty": "No feeds followed in this channel",
32
33
  "url": "Feed URL",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rmdes/indiekit-endpoint-microsub",
3
- "version": "1.0.2",
3
+ "version": "1.0.4",
4
4
  "description": "Microsub endpoint for Indiekit. Enables subscribing to feeds and reading content using the Microsub protocol.",
5
5
  "keywords": [
6
6
  "indiekit",
@@ -25,6 +25,7 @@
25
25
  "test": "node --test test/unit/*.js"
26
26
  },
27
27
  "files": [
28
+ "assets",
28
29
  "lib",
29
30
  "locales",
30
31
  "views",
@@ -1,11 +1,13 @@
1
- {% extends "document.njk" %}
1
+ {% extends "layouts/reader.njk" %}
2
2
 
3
- {% block content %}
3
+ {% block reader %}
4
4
  <div class="channel-new">
5
5
  <a href="{{ baseUrl }}/channels" class="back-link">
6
6
  {{ icon("previous") }} {{ __("microsub.channels.title") }}
7
7
  </a>
8
8
 
9
+ <h2>{{ __("microsub.channels.new") }}</h2>
10
+
9
11
  <form method="post" action="{{ baseUrl }}/channels/new">
10
12
  {{ input({
11
13
  id: "name",
package/views/channel.njk CHANGED
@@ -1,12 +1,19 @@
1
- {% extends "document.njk" %}
1
+ {% extends "layouts/reader.njk" %}
2
2
 
3
- {% block content %}
3
+ {% block reader %}
4
4
  <div class="channel">
5
5
  <header class="channel__header">
6
6
  <a href="{{ baseUrl }}/channels" class="back-link">
7
7
  {{ icon("previous") }} {{ __("microsub.channels.title") }}
8
8
  </a>
9
9
  <div class="channel__actions">
10
+ <form action="{{ baseUrl }}/api/mark-read" method="POST" style="display: inline;">
11
+ <input type="hidden" name="channel" value="{{ channel.uid }}">
12
+ <input type="hidden" name="entry" value="last-read-entry">
13
+ <button type="submit" class="button button--secondary button--small">
14
+ {{ icon("checkboxChecked") }} {{ __("microsub.reader.markAllRead") }}
15
+ </button>
16
+ </form>
10
17
  <a href="{{ baseUrl }}/channels/{{ channel.uid }}/feeds" class="button button--secondary button--small">
11
18
  {{ icon("syndicate") }} {{ __("microsub.feeds.title") }}
12
19
  </a>
@@ -17,7 +24,7 @@
17
24
  </header>
18
25
 
19
26
  {% if items.length > 0 %}
20
- <div class="timeline">
27
+ <div class="timeline" id="timeline" data-channel="{{ channel.uid }}">
21
28
  {% for item in items %}
22
29
  {% include "partials/item-card.njk" %}
23
30
  {% endfor %}
@@ -29,6 +36,8 @@
29
36
  <a href="?before={{ paging.before }}" class="button button--secondary">
30
37
  {{ icon("previous") }} {{ __("microsub.reader.newer") }}
31
38
  </a>
39
+ {% else %}
40
+ <span></span>
32
41
  {% endif %}
33
42
  {% if paging.after %}
34
43
  <a href="?after={{ paging.after }}" class="button button--secondary">
@@ -38,7 +47,56 @@
38
47
  </nav>
39
48
  {% endif %}
40
49
  {% else %}
41
- {{ prose({ text: __("microsub.timeline.empty") }) }}
50
+ <div class="reader__empty">
51
+ {{ icon("syndicate") }}
52
+ <p>{{ __("microsub.timeline.empty") }}</p>
53
+ <a href="{{ baseUrl }}/channels/{{ channel.uid }}/feeds" class="button button--primary">
54
+ {{ __("microsub.feeds.subscribe") }}
55
+ </a>
56
+ </div>
42
57
  {% endif %}
43
58
  </div>
59
+
60
+ <script type="module">
61
+ // Keyboard navigation (j/k for items, o to open)
62
+ const timeline = document.getElementById('timeline');
63
+ if (timeline) {
64
+ const items = Array.from(timeline.querySelectorAll('.item-card'));
65
+ let currentIndex = -1;
66
+
67
+ function focusItem(index) {
68
+ if (items[currentIndex]) {
69
+ items[currentIndex].classList.remove('item-card--focused');
70
+ }
71
+ currentIndex = Math.max(0, Math.min(index, items.length - 1));
72
+ if (items[currentIndex]) {
73
+ items[currentIndex].classList.add('item-card--focused');
74
+ items[currentIndex].scrollIntoView({ behavior: 'smooth', block: 'center' });
75
+ }
76
+ }
77
+
78
+ document.addEventListener('keydown', (e) => {
79
+ if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
80
+
81
+ switch(e.key) {
82
+ case 'j':
83
+ e.preventDefault();
84
+ focusItem(currentIndex + 1);
85
+ break;
86
+ case 'k':
87
+ e.preventDefault();
88
+ focusItem(currentIndex - 1);
89
+ break;
90
+ case 'o':
91
+ case 'Enter':
92
+ e.preventDefault();
93
+ if (items[currentIndex]) {
94
+ const link = items[currentIndex].querySelector('.item-card__link');
95
+ if (link) link.click();
96
+ }
97
+ break;
98
+ }
99
+ });
100
+ }
101
+ </script>
44
102
  {% endblock %}
package/views/compose.njk CHANGED
@@ -1,50 +1,66 @@
1
- {% extends "document.njk" %}
1
+ {% extends "layouts/reader.njk" %}
2
2
 
3
- {% block content %}
3
+ {% block reader %}
4
4
  <div class="compose">
5
- <a href="{{ baseUrl }}/channels" class="back-link">
5
+ <a href="{{ backUrl or (baseUrl + '/channels') }}" class="back-link">
6
6
  {{ icon("previous") }} {{ __("Back") }}
7
7
  </a>
8
8
 
9
- {% if replyTo %}
9
+ <h2>{{ __("microsub.compose.title") }}</h2>
10
+
11
+ {% if replyTo or reply %}
10
12
  <div class="compose__context">
11
- {{ icon("reply") }} {{ __("microsub.compose.replyTo") }}: <a href="{{ replyTo }}">{{ replyTo }}</a>
13
+ {{ icon("reply") }} {{ __("microsub.compose.replyTo") }}:
14
+ <a href="{{ replyTo or reply }}" target="_blank" rel="noopener">
15
+ {{ (replyTo or reply) | replace("https://", "") | replace("http://", "") }}
16
+ </a>
12
17
  </div>
13
18
  {% endif %}
14
19
 
15
- {% if likeOf %}
20
+ {% if likeOf or like %}
16
21
  <div class="compose__context">
17
- {{ icon("like") }} {{ __("microsub.compose.likeOf") }}: <a href="{{ likeOf }}">{{ likeOf }}</a>
22
+ {{ icon("like") }} {{ __("microsub.compose.likeOf") }}:
23
+ <a href="{{ likeOf or like }}" target="_blank" rel="noopener">
24
+ {{ (likeOf or like) | replace("https://", "") | replace("http://", "") }}
25
+ </a>
18
26
  </div>
19
27
  {% endif %}
20
28
 
21
- {% if repostOf %}
29
+ {% if repostOf or repost %}
22
30
  <div class="compose__context">
23
- {{ icon("repost") }} {{ __("microsub.compose.repostOf") }}: <a href="{{ repostOf }}">{{ repostOf }}</a>
31
+ {{ icon("repost") }} {{ __("microsub.compose.repostOf") }}:
32
+ <a href="{{ repostOf or repost }}" target="_blank" rel="noopener">
33
+ {{ (repostOf or repost) | replace("https://", "") | replace("http://", "") }}
34
+ </a>
24
35
  </div>
25
36
  {% endif %}
26
37
 
27
- {% if bookmarkOf %}
38
+ {% if bookmarkOf or bookmark %}
28
39
  <div class="compose__context">
29
- {{ icon("bookmark") }} {{ __("microsub.compose.bookmarkOf") }}: <a href="{{ bookmarkOf }}">{{ bookmarkOf }}</a>
40
+ {{ icon("bookmark") }} {{ __("microsub.compose.bookmarkOf") }}:
41
+ <a href="{{ bookmarkOf or bookmark }}" target="_blank" rel="noopener">
42
+ {{ (bookmarkOf or bookmark) | replace("https://", "") | replace("http://", "") }}
43
+ </a>
30
44
  </div>
31
45
  {% endif %}
32
46
 
33
47
  <form method="post" action="{{ baseUrl }}/compose">
34
- {% if replyTo %}
35
- <input type="hidden" name="in-reply-to" value="{{ replyTo }}">
48
+ {% if replyTo or reply %}
49
+ <input type="hidden" name="in-reply-to" value="{{ replyTo or reply }}">
36
50
  {% endif %}
37
- {% if likeOf %}
38
- <input type="hidden" name="like-of" value="{{ likeOf }}">
51
+ {% if likeOf or like %}
52
+ <input type="hidden" name="like-of" value="{{ likeOf or like }}">
39
53
  {% endif %}
40
- {% if repostOf %}
41
- <input type="hidden" name="repost-of" value="{{ repostOf }}">
54
+ {% if repostOf or repost %}
55
+ <input type="hidden" name="repost-of" value="{{ repostOf or repost }}">
42
56
  {% endif %}
43
- {% if bookmarkOf %}
44
- <input type="hidden" name="bookmark-of" value="{{ bookmarkOf }}">
57
+ {% if bookmarkOf or bookmark %}
58
+ <input type="hidden" name="bookmark-of" value="{{ bookmarkOf or bookmark }}">
45
59
  {% endif %}
46
60
 
47
- {% if not likeOf and not repostOf and not bookmarkOf %}
61
+ {% set isAction = (likeOf or like) or (repostOf or repost) or (bookmarkOf or bookmark) %}
62
+
63
+ {% if not isAction %}
48
64
  {{ textarea({
49
65
  label: __("microsub.compose.content"),
50
66
  id: "content",
@@ -52,16 +68,31 @@
52
68
  rows: 5,
53
69
  attributes: { autofocus: true }
54
70
  }) }}
71
+ <div class="compose__counter">
72
+ <span id="char-count">0</span> characters
73
+ </div>
55
74
  {% endif %}
56
75
 
57
76
  <div class="button-group">
58
77
  {{ button({
59
78
  text: __("microsub.compose.submit")
60
79
  }) }}
61
- <a href="{{ baseUrl }}/channels" class="button button--secondary">
80
+ <a href="{{ backUrl or (baseUrl + '/channels') }}" class="button button--secondary">
62
81
  {{ __("microsub.compose.cancel") }}
63
82
  </a>
64
83
  </div>
65
84
  </form>
66
85
  </div>
86
+
87
+ {% if not isAction %}
88
+ <script type="module">
89
+ const textarea = document.getElementById('content');
90
+ const counter = document.getElementById('char-count');
91
+ if (textarea && counter) {
92
+ textarea.addEventListener('input', () => {
93
+ counter.textContent = textarea.value.length;
94
+ });
95
+ }
96
+ </script>
97
+ {% endif %}
67
98
  {% endblock %}
package/views/feeds.njk CHANGED
@@ -1,6 +1,6 @@
1
- {% extends "document.njk" %}
1
+ {% extends "layouts/reader.njk" %}
2
2
 
3
- {% block content %}
3
+ {% block reader %}
4
4
  <div class="feeds">
5
5
  <header class="feeds__header">
6
6
  <a href="{{ baseUrl }}/channels/{{ channel.uid }}" class="back-link">
@@ -11,16 +11,24 @@
11
11
  <h2>{{ __("microsub.feeds.title") }}</h2>
12
12
 
13
13
  {% if feeds.length > 0 %}
14
- <ul class="feeds__list">
14
+ <div class="feeds__list">
15
15
  {% for feed in feeds %}
16
- <li class="feeds__item">
16
+ <div class="feeds__item">
17
17
  <div class="feeds__info">
18
18
  {% if feed.photo %}
19
- <img src="{{ feed.photo }}" alt="" class="feeds__photo" width="32" height="32" loading="lazy">
19
+ <img src="{{ feed.photo }}"
20
+ alt=""
21
+ class="feeds__photo"
22
+ width="48"
23
+ height="48"
24
+ loading="lazy"
25
+ onerror="this.style.display='none'">
20
26
  {% endif %}
21
27
  <div class="feeds__details">
22
28
  <span class="feeds__name">{{ feed.title or feed.url }}</span>
23
- <a href="{{ feed.url }}" class="feeds__url" target="_blank" rel="noopener">{{ feed.url }}</a>
29
+ <a href="{{ feed.url }}" class="feeds__url" target="_blank" rel="noopener">
30
+ {{ feed.url | replace("https://", "") | replace("http://", "") }}
31
+ </a>
24
32
  </div>
25
33
  </div>
26
34
  <form method="post" action="{{ baseUrl }}/channels/{{ channel.uid }}/feeds/remove" class="feeds__actions">
@@ -30,11 +38,14 @@
30
38
  classes: "button--secondary button--small"
31
39
  }) }}
32
40
  </form>
33
- </li>
41
+ </div>
34
42
  {% endfor %}
35
- </ul>
43
+ </div>
36
44
  {% else %}
37
- {{ prose({ text: __("microsub.feeds.empty") }) }}
45
+ <div class="reader__empty">
46
+ {{ icon("syndicate") }}
47
+ <p>{{ __("microsub.feeds.empty") }}</p>
48
+ </div>
38
49
  {% endif %}
39
50
 
40
51
  <div class="feeds__add">
package/views/item.njk CHANGED
@@ -1,20 +1,26 @@
1
- {% extends "document.njk" %}
1
+ {% extends "layouts/reader.njk" %}
2
2
 
3
- {% block content %}
3
+ {% block reader %}
4
4
  <article class="item">
5
- <a href="{{ baseUrl }}/channels" class="back-link">
5
+ <a href="{{ backUrl or (baseUrl + '/channels') }}" class="back-link">
6
6
  {{ icon("previous") }} {{ __("Back") }}
7
7
  </a>
8
8
 
9
9
  {% if item.author %}
10
10
  <header class="item__author">
11
11
  {% if item.author.photo %}
12
- <img src="{{ item.author.photo }}" alt="" class="item__author-photo" width="48" height="48" loading="lazy">
12
+ <img src="{{ item.author.photo }}"
13
+ alt=""
14
+ class="item__author-photo"
15
+ width="48"
16
+ height="48"
17
+ loading="lazy"
18
+ onerror="this.style.display='none'">
13
19
  {% endif %}
14
20
  <div class="item__author-info">
15
21
  <span class="item__author-name">
16
22
  {% if item.author.url %}
17
- <a href="{{ item.author.url }}">{{ item.author.name or item.author.url }}</a>
23
+ <a href="{{ item.author.url }}" target="_blank" rel="noopener">{{ item.author.name or item.author.url }}</a>
18
24
  {% else %}
19
25
  {{ item.author.name or "Unknown" }}
20
26
  {% endif %}
@@ -28,6 +34,44 @@
28
34
  </header>
29
35
  {% endif %}
30
36
 
37
+ {# Context for interactions #}
38
+ {% if item["in-reply-to"] or item["like-of"] or item["repost-of"] or item["bookmark-of"] %}
39
+ <div class="item__context">
40
+ {% if item["in-reply-to"] and item["in-reply-to"].length > 0 %}
41
+ <p class="item__context-label">
42
+ {{ icon("reply") }} {{ __("Reply to") }}:
43
+ <a href="{{ item['in-reply-to'][0] }}" target="_blank" rel="noopener">
44
+ {{ item["in-reply-to"][0] | replace("https://", "") | replace("http://", "") }}
45
+ </a>
46
+ </p>
47
+ {% endif %}
48
+ {% if item["like-of"] and item["like-of"].length > 0 %}
49
+ <p class="item__context-label">
50
+ {{ icon("like") }} {{ __("Liked") }}:
51
+ <a href="{{ item['like-of'][0] }}" target="_blank" rel="noopener">
52
+ {{ item["like-of"][0] | replace("https://", "") | replace("http://", "") }}
53
+ </a>
54
+ </p>
55
+ {% endif %}
56
+ {% if item["repost-of"] and item["repost-of"].length > 0 %}
57
+ <p class="item__context-label">
58
+ {{ icon("repost") }} {{ __("Reposted") }}:
59
+ <a href="{{ item['repost-of'][0] }}" target="_blank" rel="noopener">
60
+ {{ item["repost-of"][0] | replace("https://", "") | replace("http://", "") }}
61
+ </a>
62
+ </p>
63
+ {% endif %}
64
+ {% if item["bookmark-of"] and item["bookmark-of"].length > 0 %}
65
+ <p class="item__context-label">
66
+ {{ icon("bookmark") }} {{ __("Bookmarked") }}:
67
+ <a href="{{ item['bookmark-of'][0] }}" target="_blank" rel="noopener">
68
+ {{ item["bookmark-of"][0] | replace("https://", "") | replace("http://", "") }}
69
+ </a>
70
+ </p>
71
+ {% endif %}
72
+ </div>
73
+ {% endif %}
74
+
31
75
  {% if item.name %}
32
76
  <h2 class="item__title">{{ item.name }}</h2>
33
77
  {% endif %}
@@ -42,43 +86,65 @@
42
86
  </div>
43
87
  {% endif %}
44
88
 
89
+ {# Categories #}
90
+ {% if item.category and item.category.length > 0 %}
91
+ <div class="item-card__categories">
92
+ {% for cat in item.category %}
93
+ <span class="item-card__category">#{{ cat | replace("#", "") }}</span>
94
+ {% endfor %}
95
+ </div>
96
+ {% endif %}
97
+
98
+ {# Photos #}
45
99
  {% if item.photo and item.photo.length > 0 %}
46
100
  <div class="item__photos">
47
101
  {% for photo in item.photo %}
48
- <img src="{{ photo }}" alt="" class="item__photo" loading="lazy">
102
+ <a href="{{ photo }}" target="_blank" rel="noopener">
103
+ <img src="{{ photo }}" alt="" class="item__photo" loading="lazy">
104
+ </a>
49
105
  {% endfor %}
50
106
  </div>
51
107
  {% endif %}
52
108
 
53
- {% if item["in-reply-to"] or item["like-of"] or item["repost-of"] or item["bookmark-of"] %}
54
- <div class="item__context">
55
- {% if item["in-reply-to"] %}
56
- <p>{{ icon("reply") }} {{ __("Reply to") }}: <a href="{{ item["in-reply-to"][0] }}">{{ item["in-reply-to"][0] }}</a></p>
57
- {% endif %}
58
- {% if item["like-of"] %}
59
- <p>{{ icon("like") }} {{ __("Liked") }}: <a href="{{ item["like-of"][0] }}">{{ item["like-of"][0] }}</a></p>
60
- {% endif %}
61
- {% if item["repost-of"] %}
62
- <p>{{ icon("repost") }} {{ __("Reposted") }}: <a href="{{ item["repost-of"][0] }}">{{ item["repost-of"][0] }}</a></p>
63
- {% endif %}
64
- {% if item["bookmark-of"] %}
65
- <p>{{ icon("bookmark") }} {{ __("Bookmarked") }}: <a href="{{ item["bookmark-of"][0] }}">{{ item["bookmark-of"][0] }}</a></p>
66
- {% endif %}
109
+ {# Video #}
110
+ {% if item.video and item.video.length > 0 %}
111
+ <div class="item__media">
112
+ {% for video in item.video %}
113
+ <video src="{{ video }}"
114
+ controls
115
+ preload="metadata"
116
+ {% if item.photo and item.photo.length > 0 %}poster="{{ item.photo[0] }}"{% endif %}>
117
+ </video>
118
+ {% endfor %}
119
+ </div>
120
+ {% endif %}
121
+
122
+ {# Audio #}
123
+ {% if item.audio and item.audio.length > 0 %}
124
+ <div class="item__media">
125
+ {% for audio in item.audio %}
126
+ <audio src="{{ audio }}" controls preload="metadata"></audio>
127
+ {% endfor %}
67
128
  </div>
68
129
  {% endif %}
69
130
 
70
131
  <footer class="item__actions">
71
- <a href="{{ baseUrl }}/compose?replyTo={{ item.url | urlencode }}" class="button button--secondary button--small">
132
+ {% if item.url %}
133
+ <a href="{{ item.url }}" class="button button--secondary button--small" target="_blank" rel="noopener">
134
+ {{ icon("external") }} {{ __("microsub.item.viewOriginal") }}
135
+ </a>
136
+ {% endif %}
137
+ <a href="{{ baseUrl }}/compose?reply={{ item.url | urlencode }}" class="button button--secondary button--small">
72
138
  {{ icon("reply") }} {{ __("microsub.item.reply") }}
73
139
  </a>
74
- <a href="{{ baseUrl }}/compose?likeOf={{ item.url | urlencode }}" class="button button--secondary button--small">
140
+ <a href="{{ baseUrl }}/compose?like={{ item.url | urlencode }}" class="button button--secondary button--small">
75
141
  {{ icon("like") }} {{ __("microsub.item.like") }}
76
142
  </a>
77
- <a href="{{ baseUrl }}/compose?repostOf={{ item.url | urlencode }}" class="button button--secondary button--small">
143
+ <a href="{{ baseUrl }}/compose?repost={{ item.url | urlencode }}" class="button button--secondary button--small">
78
144
  {{ icon("repost") }} {{ __("microsub.item.repost") }}
79
145
  </a>
80
- <a href="{{ item.url }}" class="button button--secondary button--small" target="_blank" rel="noopener">
81
- {{ icon("public") }} {{ __("microsub.item.viewOriginal") }}
146
+ <a href="{{ baseUrl }}/compose?bookmark={{ item.url | urlencode }}" class="button button--secondary button--small">
147
+ {{ icon("bookmark") }} {{ __("microsub.item.bookmark") }}
82
148
  </a>
83
149
  </footer>
84
150
  </article>
@@ -0,0 +1,10 @@
1
+ {#
2
+ Microsub Reader Layout
3
+ Extends document.njk and adds reader-specific stylesheet
4
+ #}
5
+ {% extends "document.njk" %}
6
+
7
+ {% block content %}
8
+ <link rel="stylesheet" href="/assets/@rmdes-indiekit-endpoint-microsub/styles.css">
9
+ {% block reader %}{% endblock %}
10
+ {% endblock %}