╭──────────╮ │← previous│ ╰──────────╯ ╭ changed ──╮ │src/try.zig│ ╰───────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1459 const feature_new = try obj.writeCommit(.xit, opts, state, ctx.io, ctx.allocator, .{│ │1460 .message = "Revise the feature branch", │ │1461 .author = author_line, │ │1453 - .timestamp = 820, │ │1462 + .timestamp = ctx.timestamp, │ │1463 }, null, .{ .kind = .head, .name = "feature" }); │ │1464 │ │1465 // a branch the push creates has no old tip, so its range is open │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1484 } │ │1485 }; │ │1486 │ │1478 - try transaction(io, repo, Ctx{ .core = &repo.core, .io = io, .allocator = allocator, .users_dir = users_dir, .author = author }); │ │1487 + try transaction(io, repo, Ctx{ .core = &repo.core, .io = io, .allocator = allocator, .users_dir = users_dir, .author = author, .timestamp = timestamp });│ │1488 } │ │1489 │ │1490 // commit `events` onto `ref`, rooted at `parent`, as one transaction. │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ─╮ │src/ui.zig│ ╰──────────╯ ╭────────────────────────────────────────────────────────╮ │19 } else @import("ansi_art"); │ │20 const evt = @import("./event.zig"); │ │21 const inp = @import("./ui/input.zig"); │ │22 + │ │23 +pub const timestamp = @import("./ui/timestamp.zig");│ │24 │ │25 pub const Home = @import("./ui/Home.zig"); │ │26 pub const User = @import("./ui/User.zig"); │ ╰────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────╮ │2375 │ │2376 // serializable data sent down to web client │ │2377 pub const Data = struct { │ │2378 + // the server's reference time for relative timestamps on this page.│ │2379 + now: i64 = 0, │ │2380 user_id: ?[]const u8 = null, │ │2381 // the logged-in user's name and email, for the views that show them│ │2382 user_name: ?[]const u8 = null, │ ╰──────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │2422 return session; │ │2423 } │ │2424 │ │2425 + // web views share the page's reference time; terminals use the live clock. │ │2426 + pub fn nowSeconds(self: *const Self) !i64 { │ │2427 + if (builtin.target.cpu.arch == .wasm32 or !self.is_terminal) return self.data.now; │ │2428 + const io = self.io orelse return error.NoClock; │ │2429 + return std.math.cast(i64, std.Io.Timestamp.now(io, .real).toSeconds()) orelse error.TimestampOutOfRange;│ │2430 + } │ │2431 + │ │2432 // load the logged-in user's name and persisted preferences from the db │ │2433 pub fn loadUser(self: *Self) !void { │ │2434 const user_id = self.userId() orelse return; │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ──────────────╮ │src/ui/Repo/Comment.zig│ ╰───────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │198 try bar.children.put(allocator, parent.getFocus().id, .{ .widget = .{ .text_box = parent }, .rect = null, .min_size = .{ .width = @max(parent_text.len, " replying to ".len) + 2, .height = null } }); │ │199 } │ │200 │ │201 - const timestamp = try ui.Repo.Undo.formatTimestamp(session.page_arena.allocator(), std.math.cast(i64, entry.comment.created_timestamp) orelse -1); │ │202 - var permalink = try linkBox(allocator, session, timestamp, commentsRoute(thread_kind, identity, &entry.comment.event.thread_id, &entry.id, 0) orelse return error.RouteTooLong); │ │201 + const timestamp = try ui.timestamp.format(session.page_arena.allocator(), std.math.cast(i64, entry.comment.created_timestamp) orelse -1, try session.nowSeconds()); │ │202 + var permalink = try linkBox(allocator, session, timestamp.relative, commentsRoute(thread_kind, identity, &entry.comment.event.thread_id, &entry.id, 0) orelse return error.RouteTooLong); │ │203 errdefer permalink.deinit(allocator); │ │204 permalink.options.top_label.text = " permalink "; │ │205 - try bar.children.put(allocator, permalink.getFocus().id, .{ .widget = .{ .text_box = permalink }, .rect = null, .min_size = .{ .width = @max(timestamp.len, " permalink ".len) + 2, .height = null } }); │ │205 + permalink.options.bottom_label.text = timestamp.exact; │ │206 + try bar.children.put(allocator, permalink.getFocus().id, .{ .widget = .{ .text_box = permalink }, .rect = null, .min_size = .{ .width = @max(timestamp.relative.len, timestamp.exact.len) + 2, .height = null } });│ │207 │ │208 if (entry.can_modify and !entry.comment.removed) { │ │209 var remove = try linkBox(allocator, session, "✕", removeRoute(thread_kind, identity, &entry.comment.event.thread_id, &entry.id) orelse return error.RouteTooLong); │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ──────────────╮ │src/ui/Repo/Commits.zig│ ╰───────────────────────╯ ╭────────────────────────────────────────────────────────╮ │3 const ui = @import("../../ui.zig"); │ │4 const xit = @import("xit"); │ │5 const rp = xit.repo; │ │6 -const Undo = @import("Undo.zig"); │ │6 const obj = xit.object; │ │7 const mrg = xit.merge; │ │8 const srch_cmmt = @import("../../search_commit.zig");│ ╰────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────╮ │26 oid: []const u8, │ │27 // what its diff is against: the first parent, all zeros for a root commit│ │28 parent_oid: []const u8, │ │30 - date: []const u8, // "YYYY-MM-DD" │ │29 message: []const u8, // trimmed, may be multi-line │ │30 // whether `message` is a shortened preview. │ │31 message_truncated: bool = false, │ ╰──────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────╮ │32 author: ui.Author = .unknown, │ │33 // .unknown when the committer is the author.│ │34 committer: ui.Author = .unknown, │ │37 - // the committer timestamp, human-readable. │ │38 - timestamp: []const u8, │ │35 + // the committer timestamp, in unix seconds. │ │36 + timestamp: i64, │ │37 // whether it has a second parent. │ │38 merge: bool = false, │ │39 stats: ?xit.patch.CommitStats = null, │ ╰─────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │233 return .{ │ │234 .oid = try aa.dupe(u8, &commit_object.oid), │ │235 .parent_oid = if (md.firstParent()) |parent| try aa.dupe(u8, parent) else &@as([xit.hash.hexLen(repo_opts.hash)]u8, @splat('0')),│ │238 - .date = try formatDate(aa, md.timestamp), │ │236 .message = text, │ │237 .message_truncated = truncated, │ │238 .author = try ui.Author.init(admin_moment, arena, md.author orelse ""), │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯