╭──────────╮ │← previous│ ╰──────────╯ ╭ changed ──────────────╮ │src/ui/Repo/Commits.zig│ ╰───────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │240 (if (std.mem.eql(u8, identityOf(committer), identityOf(md.author orelse ""))) .unknown else try ui.Author.init(admin_moment, arena, committer))│ │241 else │ │242 .unknown, │ │246 - .timestamp = try Undo.formatTimestamp(aa, std.math.cast(i64, md.timestamp) orelse -1), │ │243 + .timestamp = std.math.cast(i64, md.timestamp) orelse -1, │ │244 .merge = if (md.parent_oids) |parent_oids| parent_oids.len > 1 else false, │ │245 .stats = if (repo_kind == .xit) try repo.commitStats(io, gpa, .{ .oid = &commit_object.oid }) else null, │ │246 }; │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────╮ │291 .commits = &.{}, │ │292 .next_start = null, │ │293 }; │ │297 -} │ │298 - │ │299 -// "YYYY-MM-DD" for a unix timestamp. │ │300 -fn formatDate(arena: std.mem.Allocator, timestamp: u64) ![]const u8 { │ │301 - const epoch_secs = std.time.epoch.EpochSeconds{ .secs = timestamp }; │ │302 - const year_day = epoch_secs.getEpochDay().calculateYearDay(); │ │303 - const month_day = year_day.calculateMonthDay(); │ │304 - return arena.print("{d:0>4}-{d:0>2}-{d:0>2}", .{ │ │305 - year_day.year, │ │306 - month_day.month.numeric(), │ │307 - month_day.day_index + 1, │ │308 - }); │ │294 } │ │295 │ │296 // the commit message, trimmed and read into `aa`, plus whether its preview was│ ╰────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │637 tb.options.top_label.text = " committer "; │ │638 try row.children.put(allocator, tb.getFocus().id, .{ .widget = .{ .text_box = tb }, .rect = null, .min_size = null }); │ │639 } │ │655 - var tb = try wgt.TextBox.init(allocator, commit.timestamp, .{ .border = .single, .round_corners = true, .wrap_kind = .none }); │ │640 + const timestamp = try ui.timestamp.format(pa.allocator(), commit.timestamp, try self.session.nowSeconds()); │ │641 + var tb = try wgt.TextBox.init(allocator, timestamp.relative, .{ .border = .single, .round_corners = true, .wrap_kind = .none, .bottom_label = .{ .text = timestamp.exact } });│ │642 errdefer tb.deinit(allocator); │ │643 tb.getFocus().mode = .all; │ │644 try row.children.put(allocator, tb.getFocus().id, .{ .widget = .{ .text_box = tb }, .rect = null, .min_size = null }); │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ───────────╮ │src/ui/Repo/Undo.zig│ ╰────────────────────╯ ╭─────────────────────────────────────────────╮ │32 index: u64, │ │33 action: []const u8, │ │34 description: []const u8, │ │35 - timestamp: []const u8, │ │35 + timestamp: i64, │ │36 link: []const u8, │ │37 form: []const u8, │ │38 buttons: []const DetailButton = &.{},│ ╰─────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │ 95 .action = shown.action, │ │ 96 .description = shown.description, │ │ 97 .buttons = shown.buttons, │ │ 98 - .timestamp = if (record) |value| try formatTimestamp(aa, value.timestamp) else "timestamp unavailable",│ │ 98 + .timestamp = if (record) |value| value.timestamp else -1, │ │ 99 .link = try aa.print("ai:{s}", .{url}), │ │100 .form = try aa.print("form:{s}/undo", .{url}), │ │101 .undone = row_undone, │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │308 return try cursor.readUint(); │ │309 } │ │310 │ │311 -pub fn formatTimestamp(aa: std.mem.Allocator, timestamp: i64) ![]const u8 { │ │312 - if (timestamp < 0) return aa.dupe(u8, "timestamp unavailable"); │ │313 - const seconds = std.time.epoch.EpochSeconds{ .secs = @intCast(timestamp) }; │ │314 - const year = seconds.getEpochDay().calculateYearDay(); │ │315 - const month = year.calculateMonthDay(); │ │316 - const day = seconds.getDaySeconds(); │ │317 - const months = [_][]const u8{ "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" }; │ │318 - return aa.print("{s} {d}, {d}, {d:0>2}:{d:0>2}:{d:0>2} UTC", .{ months[month.month.numeric() - 1], month.day_index + 1, year.year, day.getHoursIntoDay(), day.getMinutesIntoHour(), day.getSecondsIntoMinute() });│ │319 -} │ │320 - │ │311 pub fn execute(io: std.Io, allocator: std.mem.Allocator, source: ui.RepoSource, index: u64) !void { │ │312 if (source.repo_kind != .xit) return error.NotFound; │ │313 var any_repo = try rp.AnyRepo(.xit, .{}).open(io, allocator, source.localInitOpts()); │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────╮ │382 const detail_min_width = 40; │ │383 const header_index = 0; │ │384 const content_index = 1; │ │395 - // the undo button and the timestamp; everything below them is rebuilt │ │385 + // the undo button and timestamp stay; everything below them is rebuilt │ │386 const fixed_rows = 2; │ │387 │ │388 pub fn init(allocator: std.mem.Allocator, data: *const Self, session: *ui.Session) !View {│ ╰───────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │437 errdefer details.deinit(allocator); │ │438 if (data.items.len > 0) { │ │439 try addText(allocator, &details, "", null, .single); │ │450 - try addLabeled(allocator, &details, " timestamp ", "", null); │ │440 + try addText(allocator, &details, "", null, .single); │ │441 details.getFocus().child_id = details.children.keys()[0]; │ │442 } │ │443 break :blk try wgt.Scroll(ui.Widget).init(allocator, .{ .box = details }, .{ .direction = .vert, .web_native = !session.is_terminal, .fill = true });│ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │481 try box.children.put(allocator, row.getFocus().id, .{ .widget = .{ .text_box = row }, .rect = null, .min_size = null }); │ │482 } │ │483 │ │494 - // a labeled row below the timestamp, added as the selected action needs it │ │484 + // a labeled box, added as the selected action needs it │ │485 fn addLabeled(allocator: std.mem.Allocator, box: *wgt.Box(ui.Widget), label: []const u8, text: []const u8, link: ?[]const u8) !void {│ │486 try addText(allocator, box, text, link, .single); │ │487 box.children.values()[box.children.count() - 1].widget.text_box.options.top_label.text = label; │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │547 try button.setContent(allocator, if (item.index == 0) "initial state cannot be undone" else if (item.index == self.data.count - 1) "undo this" else "undo this and all above it");│ │548 button.getFocus().kind = if (enabled) .{ .custom = "submit" } else .text_box; │ │549 } │ │560 - try details.children.values()[1].widget.text_box.setContent(allocator, item.timestamp); │ │550 + const timestamp = try ui.timestamp.format(self.session.page_arena.allocator(), item.timestamp, try self.session.nowSeconds()); │ │551 + try details.children.values()[1].widget.text_box.setContent(allocator, timestamp.relative); │ │552 + details.children.values()[1].widget.text_box.options.bottom_label.text = timestamp.exact; │ │553 │ │554 // some actions put buttons where the description would go │ │555 if (item.buttons.len == 0) { │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯