feat: relative timestamps in reader (Release 2)
Add Alpine.js directive x-relative-time that converts absolute dates to human-friendly relative strings: just now, 5m, 3h, 2d, Mar 3. Updates every 60s for posts less than 24h old. Server-rendered absolute time stays as no-JS fallback and hover tooltip. Applied to item cards, quote embeds, and notification cards. Bump version to 2.5.2. Confab-Link: http://localhost:8080/sessions/e9d666ac-3c90-4298-9e92-9ac9d142bc06
This commit is contained in:
@@ -0,0 +1,87 @@
|
|||||||
|
/**
|
||||||
|
* Relative timestamps — Alpine.js directive that converts absolute
|
||||||
|
* datetime attributes to human-friendly relative strings.
|
||||||
|
*
|
||||||
|
* Usage: <time datetime="2026-03-03T12:00:00Z" x-data x-relative-time>...</time>
|
||||||
|
*
|
||||||
|
* The server-rendered absolute time stays as fallback for no-JS clients.
|
||||||
|
* Alpine enhances it to relative on hydration, updates every 60s for
|
||||||
|
* recent posts, and shows the absolute time on hover via title attribute.
|
||||||
|
*
|
||||||
|
* Format rules (matching Mastodon/Elk conventions):
|
||||||
|
* < 1 minute: "just now"
|
||||||
|
* < 60 minutes: "Xm" (e.g. "5m")
|
||||||
|
* < 24 hours: "Xh" (e.g. "3h")
|
||||||
|
* < 7 days: "Xd" (e.g. "2d")
|
||||||
|
* same year: "Mar 3"
|
||||||
|
* older: "Mar 3, 2025"
|
||||||
|
*/
|
||||||
|
|
||||||
|
document.addEventListener("alpine:init", () => {
|
||||||
|
// eslint-disable-next-line no-undef
|
||||||
|
Alpine.directive("relative-time", (el) => {
|
||||||
|
const iso = el.getAttribute("datetime");
|
||||||
|
if (!iso) return;
|
||||||
|
|
||||||
|
const date = new Date(iso);
|
||||||
|
if (Number.isNaN(date.getTime())) return;
|
||||||
|
|
||||||
|
// Store the original formatted text as the title (hover tooltip)
|
||||||
|
const original = el.textContent.trim();
|
||||||
|
if (original && !el.getAttribute("title")) {
|
||||||
|
el.setAttribute("title", original);
|
||||||
|
}
|
||||||
|
|
||||||
|
function update() {
|
||||||
|
el.textContent = formatRelative(date);
|
||||||
|
}
|
||||||
|
|
||||||
|
update();
|
||||||
|
|
||||||
|
// Only set up interval for recent posts (< 24h old)
|
||||||
|
const ageMs = Date.now() - date.getTime();
|
||||||
|
if (ageMs < 86_400_000) {
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
update();
|
||||||
|
// Stop updating once older than 24h
|
||||||
|
if (Date.now() - date.getTime() >= 86_400_000) {
|
||||||
|
clearInterval(interval);
|
||||||
|
}
|
||||||
|
}, 60_000);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format a Date as a relative time string.
|
||||||
|
* @param {Date} date
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
function formatRelative(date) {
|
||||||
|
const now = Date.now();
|
||||||
|
const diffMs = now - date.getTime();
|
||||||
|
const diffSec = Math.floor(diffMs / 1000);
|
||||||
|
|
||||||
|
if (diffSec < 60) return "just now";
|
||||||
|
|
||||||
|
const diffMin = Math.floor(diffSec / 60);
|
||||||
|
if (diffMin < 60) return `${diffMin}m`;
|
||||||
|
|
||||||
|
const diffHour = Math.floor(diffMin / 60);
|
||||||
|
if (diffHour < 24) return `${diffHour}h`;
|
||||||
|
|
||||||
|
const diffDay = Math.floor(diffHour / 24);
|
||||||
|
if (diffDay < 7) return `${diffDay}d`;
|
||||||
|
|
||||||
|
// Older than 7 days — use formatted date
|
||||||
|
const months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun",
|
||||||
|
"Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
|
||||||
|
const month = months[date.getMonth()];
|
||||||
|
const day = date.getDate();
|
||||||
|
|
||||||
|
if (date.getFullYear() === new Date().getFullYear()) {
|
||||||
|
return `${month} ${day}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${month} ${day}, ${date.getFullYear()}`;
|
||||||
|
}
|
||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@rmdes/indiekit-endpoint-activitypub",
|
"name": "@rmdes/indiekit-endpoint-activitypub",
|
||||||
"version": "2.5.1",
|
"version": "2.5.2",
|
||||||
"description": "ActivityPub federation endpoint for Indiekit via Fedify. Adds full fediverse support: actor, inbox, outbox, followers, following, syndication, and Mastodon migration.",
|
"description": "ActivityPub federation endpoint for Indiekit via Fedify. Adds full fediverse support: actor, inbox, outbox, followers, following, syndication, and Mastodon migration.",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"indiekit",
|
"indiekit",
|
||||||
|
|||||||
@@ -7,6 +7,8 @@
|
|||||||
<script defer src="/assets/@rmdes-indiekit-endpoint-activitypub/reader-autocomplete.js"></script>
|
<script defer src="/assets/@rmdes-indiekit-endpoint-activitypub/reader-autocomplete.js"></script>
|
||||||
{# Tab components — apExploreTabs #}
|
{# Tab components — apExploreTabs #}
|
||||||
<script defer src="/assets/@rmdes-indiekit-endpoint-activitypub/reader-tabs.js"></script>
|
<script defer src="/assets/@rmdes-indiekit-endpoint-activitypub/reader-tabs.js"></script>
|
||||||
|
{# Relative timestamps — converts absolute dates to "5m", "3h", "2d" etc. #}
|
||||||
|
<script defer src="/assets/@rmdes-indiekit-endpoint-activitypub/reader-relative-time.js"></script>
|
||||||
|
|
||||||
{# Alpine.js for client-side reactivity (CW toggles, interaction buttons, infinite scroll) #}
|
{# Alpine.js for client-side reactivity (CW toggles, interaction buttons, infinite scroll) #}
|
||||||
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.14.9/dist/cdn.min.js"></script>
|
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.14.9/dist/cdn.min.js"></script>
|
||||||
|
|||||||
@@ -60,7 +60,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{% if item.published %}
|
{% if item.published %}
|
||||||
<a href="{{ mountPath }}/admin/reader/post?url={{ (item.uid or item.url) | urlencode }}" class="ap-card__timestamp-link" title="{{ __('activitypub.reader.post.title') }}">
|
<a href="{{ mountPath }}/admin/reader/post?url={{ (item.uid or item.url) | urlencode }}" class="ap-card__timestamp-link" title="{{ __('activitypub.reader.post.title') }}">
|
||||||
<time datetime="{{ item.published }}" class="ap-card__timestamp">
|
<time datetime="{{ item.published }}" class="ap-card__timestamp" x-data x-relative-time>
|
||||||
{{ item.published | date("PPp") }}
|
{{ item.published | date("PPp") }}
|
||||||
</time>
|
</time>
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@@ -68,7 +68,7 @@
|
|||||||
|
|
||||||
{# Timestamp #}
|
{# Timestamp #}
|
||||||
{% if item.published %}
|
{% if item.published %}
|
||||||
<time datetime="{{ item.published }}" class="ap-notification__time">
|
<time datetime="{{ item.published }}" class="ap-notification__time" x-data x-relative-time>
|
||||||
{{ item.published | date("PPp") }}
|
{{ item.published | date("PPp") }}
|
||||||
</time>
|
</time>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% if item.quote.published %}
|
{% if item.quote.published %}
|
||||||
<time datetime="{{ item.quote.published }}" class="ap-quote-embed__time">{{ item.quote.published | date("PPp") }}</time>
|
<time datetime="{{ item.quote.published }}" class="ap-quote-embed__time" x-data x-relative-time>{{ item.quote.published | date("PPp") }}</time>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</header>
|
</header>
|
||||||
{% if item.quote.name %}
|
{% if item.quote.name %}
|
||||||
|
|||||||
Reference in New Issue
Block a user