gitshark

Clone repository

git clone https://gitshark.de/git/workaround/Gitshark.git
git clone git@gitshark.de:workaround/Gitshark.git

← Commits

๐Ÿ’„ (comments): Relative comment times, card styling, spacing

86fecba80926a50e37586a5b6a64d3c9d5d4478e ยท Michael Hainz ยท 2026-07-20T11:41:47Z

Changes

7 files changed, +138 -4

MODIFY docs/users/comments.md +3 -1
diff --git a/docs/users/comments.md b/docs/users/comments.md
index 81038f5..59bf24a 100644
--- a/docs/users/comments.md
+++ b/docs/users/comments.md
@@ -11,7 +11,9 @@
11 11 comment* note instead of the form.
12 12
13 13 Comments are plain text and appear in the order they were posted, each showing the
14 -author's avatar, username, and the time it was posted.
14 +author's avatar, username, and a relative post time (*just now*, *10min ago*,
15 +*6h ago*, or a date for anything older than a day); hover the time to see the exact
16 +timestamp.
15 17
16 18 Any authenticated user who can read the repository may comment โ€” you do not need to
17 19 be the owner or a collaborator. On a private repository, only users who can read it
ADD src/main/java/de/workaround/web/RelativeTime.java +54 -0
diff --git a/src/main/java/de/workaround/web/RelativeTime.java b/src/main/java/de/workaround/web/RelativeTime.java
new file mode 100644
index 0000000..99d3dd7
--- /dev/null
+++ b/src/main/java/de/workaround/web/RelativeTime.java
@@ -0,0 +1,54 @@
1 +package de.workaround.web;
2 +
3 +import java.time.Duration;
4 +import java.time.Instant;
5 +import java.time.ZoneOffset;
6 +import java.time.format.DateTimeFormatter;
7 +
8 +import io.quarkus.qute.TemplateExtension;
9 +
10 +/**
11 + * Human-friendly relative timestamps for the UI. Raw ISO instants (e.g. 2026-07-20T11:26:09.565647Z) are
12 + * hard to read at a glance, so render "just now", "10min ago" or "6h ago" for recent times and fall back to
13 + * a plain UTC date once something is more than a day old. Exposed to Qute as {instant.since}.
14 + */
15 +public final class RelativeTime
16 +{
17 + private static final DateTimeFormatter DATE = DateTimeFormatter.ofPattern("yyyy-MM-dd").withZone(ZoneOffset.UTC);
18 +
19 + private RelativeTime()
20 + {
21 + }
22 +
23 + @TemplateExtension
24 + public static String since(Instant instant)
25 + {
26 + return format(instant, Instant.now());
27 + }
28 +
29 + /** Package-visible with an explicit "now" so the formatting is deterministically testable. */
30 + static String format(Instant instant, Instant now)
31 + {
32 + if (instant == null)
33 + {
34 + return "";
35 + }
36 + long seconds = Duration.between(instant, now).getSeconds();
37 + if (seconds < 60)
38 + {
39 + // covers future timestamps (negative durations) too
40 + return "just now";
41 + }
42 + long minutes = seconds / 60;
43 + if (minutes < 60)
44 + {
45 + return minutes + "min ago";
46 + }
47 + long hours = minutes / 60;
48 + if (hours < 24)
49 + {
50 + return hours + "h ago";
51 + }
52 + return DATE.format(instant);
53 + }
54 +}
MODIFY src/main/resources/META-INF/resources/shark.css +26 -0
diff --git a/src/main/resources/META-INF/resources/shark.css b/src/main/resources/META-INF/resources/shark.css
index 7d73ac7..9db220f 100644
--- a/src/main/resources/META-INF/resources/shark.css
+++ b/src/main/resources/META-INF/resources/shark.css
@@ -889,6 +889,11 @@
889 889 gap: var(--s2);
890 890 }
891 891
892 +/* the discussion comment box and its submit button would otherwise touch */
893 +.comment-form .form-actions {
894 + margin-top: var(--s3);
895 +}
896 +
892 897 details.archive {
893 898 margin-top: var(--s5);
894 899 }
@@ -1537,6 +1542,27 @@
1537 1542 font-family: var(--font);
1538 1543 }
1539 1544
1545 +/* discussion comments render as distinct cards (author/time header + message body); the
1546 + inline diff-anchored variant (.dl-comment-row) keeps its own styling and is unaffected */
1547 +.comment-row {
1548 + display: flex;
1549 + align-items: flex-start;
1550 + gap: var(--s2);
1551 + margin-bottom: var(--s3);
1552 +}
1553 +
1554 +.comment-row .comment {
1555 + padding: var(--s3);
1556 + background: var(--surface);
1557 + border: 1px solid var(--border);
1558 + border-radius: var(--radius);
1559 +}
1560 +
1561 +.comment-row:hover .comment-del,
1562 +.comment-row:focus-within .comment-del {
1563 + opacity: 1;
1564 +}
1565 +
1540 1566 .comment {
1541 1567 flex: 1 1 auto;
1542 1568 max-width: 640px;
MODIFY src/main/resources/templates/IssueResource/issue.html +1 -1
diff --git a/src/main/resources/templates/IssueResource/issue.html b/src/main/resources/templates/IssueResource/issue.html
index cc3cf67..54673f2 100644
--- a/src/main/resources/templates/IssueResource/issue.html
+++ b/src/main/resources/templates/IssueResource/issue.html
@@ -40,7 +40,7 @@
40 40 <div class="comment">
41 41 <div class="comment-head">
42 42 <span class="who">{#avatar user=c.author /} <strong>{c.author.username}</strong></span>
43 - <span class="muted comment-when">{c.createdAt}</span>
43 + <span class="muted comment-when" title="{c.createdAt}">{c.createdAt.since}</span>
44 44 </div>
45 45 <div class="comment-body">{c.body}</div>
46 46 </div>
MODIFY src/main/resources/templates/MergeRequestResource/mergeRequest.html +1 -1
diff --git a/src/main/resources/templates/MergeRequestResource/mergeRequest.html b/src/main/resources/templates/MergeRequestResource/mergeRequest.html
index f3fe823..9a7b088 100644
--- a/src/main/resources/templates/MergeRequestResource/mergeRequest.html
+++ b/src/main/resources/templates/MergeRequestResource/mergeRequest.html
@@ -117,7 +117,7 @@
117 117 <div class="comment">
118 118 <div class="comment-head">
119 119 <span class="who">{#avatar user=c.author /} <strong>{c.author.username}</strong></span>
120 - <span class="muted comment-when">{c.createdAt}</span>
120 + <span class="muted comment-when" title="{c.createdAt}">{c.createdAt.since}</span>
121 121 </div>
122 122 <div class="comment-body">{c.body}</div>
123 123 </div>
MODIFY src/test/java/de/workaround/web/MergeRequestDiscussionUiTest.java +5 -1
diff --git a/src/test/java/de/workaround/web/MergeRequestDiscussionUiTest.java b/src/test/java/de/workaround/web/MergeRequestDiscussionUiTest.java
index 1048b81..e52f194 100644
--- a/src/test/java/de/workaround/web/MergeRequestDiscussionUiTest.java
+++ b/src/test/java/de/workaround/web/MergeRequestDiscussionUiTest.java
@@ -85,7 +85,11 @@
85 85 given().when().get(detail)
86 86 .then().statusCode(200)
87 87 .body(containsString("general remark on this MR"))
88 - .body(containsString("mrd-owner"));
88 + .body(containsString("mrd-owner"))
89 + // the just-posted comment shows a friendly relative time (via the {createdAt.since}
90 + // template extension) with the exact instant kept in a hover tooltip
91 + .body(containsString(">just now</span>"))
92 + .body(containsString("class=\"muted comment-when\" title=\""));
89 93 }
90 94
91 95 @Test
ADD src/test/java/de/workaround/web/RelativeTimeTest.java +48 -0
diff --git a/src/test/java/de/workaround/web/RelativeTimeTest.java b/src/test/java/de/workaround/web/RelativeTimeTest.java
new file mode 100644
index 0000000..17d0fdd
--- /dev/null
+++ b/src/test/java/de/workaround/web/RelativeTimeTest.java
@@ -0,0 +1,48 @@
1 +package de.workaround.web;
2 +
3 +import java.time.Instant;
4 +
5 +import org.junit.jupiter.api.Test;
6 +
7 +import static org.junit.jupiter.api.Assertions.assertEquals;
8 +
9 +class RelativeTimeTest
10 +{
11 + private static final Instant NOW = Instant.parse("2026-07-20T12:00:00Z");
12 +
13 + @Test
14 + void withinAMinuteReadsJustNow()
15 + {
16 + assertEquals("just now", RelativeTime.format(NOW.minusSeconds(30), NOW));
17 + }
18 +
19 + @Test
20 + void withinAnHourReadsMinutesAgo()
21 + {
22 + assertEquals("10min ago", RelativeTime.format(NOW.minusSeconds(10 * 60), NOW));
23 + }
24 +
25 + @Test
26 + void withinADayReadsHoursAgo()
27 + {
28 + assertEquals("6h ago", RelativeTime.format(NOW.minusSeconds(6 * 3600), NOW));
29 + }
30 +
31 + @Test
32 + void olderThanADayReadsAsADate()
33 + {
34 + assertEquals("2026-07-17", RelativeTime.format(NOW.minusSeconds(3 * 24 * 3600), NOW));
35 + }
36 +
37 + @Test
38 + void futureTimestampsClampToJustNow()
39 + {
40 + assertEquals("just now", RelativeTime.format(NOW.plusSeconds(5), NOW));
41 + }
42 +
43 + @Test
44 + void nullIsEmpty()
45 + {
46 + assertEquals("", RelativeTime.format(null, NOW));
47 + }
48 +}

Keyboard shortcuts

?Show this help
g hGo home
EscClose dialog