an experimental irc client

message: make messages always have a timestamp

rockorager.dev 1c6f318b cc0d859b

verified
+132 -139
.zed/settings.json

This is a binary file and will not be displayed.

+132 -139
src/irc.zig
··· 121 121 history_requested: bool = false, 122 122 who_requested: bool = false, 123 123 at_oldest: bool = false, 124 - last_read: i64 = 0, 124 + // The MARKREAD state of this channel 125 + last_read: u32 = 0, 126 + // The location of the last read indicator. This doesn't necessarily match the state of 127 + // last_read 128 + last_read_indicator: u32 = 0, 125 129 has_unread: bool = false, 126 130 has_unread_highlight: bool = false, 127 131 ··· 299 303 ctx.redraw = true; 300 304 } 301 305 306 + pub fn insertMessage(self: *Channel, msg: Message) !void { 307 + try self.messages.append(msg); 308 + std.sort.insertion(Message, self.messages.items, {}, Message.compareTime); 309 + } 310 + 302 311 fn onChange(ptr: ?*anyopaque, _: *vxfw.EventContext, input: []const u8) anyerror!void { 303 312 const self: *Channel = @ptrCast(@alignCast(ptr orelse unreachable)); 304 313 if (std.mem.startsWith(u8, input, "/")) { ··· 337 346 } 338 347 339 348 pub fn compareRecentMessages(self: *Channel, lhs: Member, rhs: Member) bool { 340 - var l: i64 = 0; 341 - var r: i64 = 0; 349 + var l: u32 = 0; 350 + var r: u32 = 0; 342 351 var iter = std.mem.reverseIterator(self.messages.items); 343 352 while (iter.next()) |msg| { 344 353 if (msg.source()) |source| { 345 354 const bang = std.mem.indexOfScalar(u8, source, '!') orelse source.len; 346 355 const nick = source[0..bang]; 347 356 348 - if (l == 0 and msg.time() != null and std.mem.eql(u8, lhs.user.nick, nick)) { 349 - l = msg.time().?.unixTimestamp(); 350 - } else if (r == 0 and msg.time() != null and std.mem.eql(u8, rhs.user.nick, nick)) 351 - r = msg.time().?.unixTimestamp(); 357 + if (l == 0 and std.mem.eql(u8, lhs.user.nick, nick)) { 358 + l = msg.timestamp_s; 359 + } else if (r == 0 and std.mem.eql(u8, rhs.user.nick, nick)) 360 + r = msg.timestamp_s; 352 361 } 353 362 if (l > 0 and r > 0) break; 354 363 } ··· 502 511 self.has_unread = false; 503 512 self.has_unread_highlight = false; 504 513 const last_msg = self.messages.getLastOrNull() orelse return; 505 - const time = last_msg.time() orelse return; 506 - if (time.unixTimestamp() > self.last_read) { 514 + if (last_msg.timestamp_s > self.last_read) { 507 515 const time_tag = last_msg.getTag("time") orelse return; 508 516 try self.client.print( 509 517 "MARKREAD {s} timestamp={s}\r\n", ··· 876 884 fn drawMessageView(self: *Channel, ctx: vxfw.DrawContext) Allocator.Error!vxfw.Surface { 877 885 self.message_view.hovered_message = null; 878 886 const max = ctx.max.size(); 879 - if (max.width == 0 or 880 - max.height == 0 or 881 - self.messages.items.len == 0) 882 - { 887 + if (max.width == 0 or max.height == 0 or self.messages.items.len == 0) { 883 888 return .{ 884 889 .size = max, 885 890 .widget = self.messageViewWidget(), ··· 898 903 const messages = self.messages.items[0..offset]; 899 904 var iter = std.mem.reverseIterator(messages); 900 905 901 - var sender: []const u8 = ""; 902 - var maybe_instant: ?zeit.Instant = null; 903 - 904 - { 905 - assert(messages.len > 0); 906 - // Initialize sender and maybe_instant to the last message values 907 - const last_msg = iter.next() orelse unreachable; 908 - // Reset iter index 909 - iter.index += 1; 910 - sender = last_msg.senderNick() orelse ""; 911 - maybe_instant = last_msg.localTime(&self.client.app.tz); 912 - } 906 + assert(messages.len > 0); 907 + // Initialize sender and maybe_instant to the last message values 908 + const last_msg = iter.next() orelse unreachable; 909 + // Reset iter index 910 + iter.index += 1; 911 + var sender = last_msg.senderNick() orelse ""; 912 + var this_instant = last_msg.localTime(&self.client.app.tz); 913 913 914 914 while (iter.next()) |msg| { 915 915 // Break if we have gone past the top of the screen ··· 936 936 937 937 defer { 938 938 // After this loop, we want to save these values for the next iteration 939 - maybe_instant = maybe_next_instant; 939 + if (maybe_next_instant) |next_instant| { 940 + this_instant = next_instant; 941 + } 940 942 sender = next_sender; 941 943 } 942 944 ··· 1004 1006 .surface = surface, 1005 1007 }); 1006 1008 1007 - // If we have a time, print it in the gutter 1008 - if (maybe_instant) |instant| { 1009 - var style: vaxis.Style = .{ .dim = true }; 1010 - 1011 - // The time text we will print 1012 - const buf: []const u8 = blk: { 1013 - const time = instant.time(); 1014 - // Check our next time. If *this* message occurs on a different day, we want to 1015 - // print the date 1016 - if (maybe_next_instant) |next_instant| { 1017 - const next_time = next_instant.time(); 1018 - if (time.day != next_time.day) { 1019 - style = .{}; 1020 - break :blk try std.fmt.allocPrint( 1021 - ctx.arena, 1022 - "{d:0>2}/{d:0>2}", 1023 - .{ @intFromEnum(time.month), time.day }, 1024 - ); 1025 - } 1026 - } 1009 + var style: vaxis.Style = .{ .dim = true }; 1027 1010 1028 - // if it is the first message, we also want to print the date 1029 - if (iter.index == 0) { 1011 + // The time text we will print 1012 + const buf: []const u8 = blk: { 1013 + const time = this_instant.time(); 1014 + // Check our next time. If *this* message occurs on a different day, we want to 1015 + // print the date 1016 + if (maybe_next_instant) |next_instant| { 1017 + const next_time = next_instant.time(); 1018 + if (time.day != next_time.day) { 1030 1019 style = .{}; 1031 1020 break :blk try std.fmt.allocPrint( 1032 1021 ctx.arena, ··· 1034 1023 .{ @intFromEnum(time.month), time.day }, 1035 1024 ); 1036 1025 } 1026 + } 1037 1027 1038 - // Otherwise, we print clock time 1028 + // if it is the first message, we also want to print the date 1029 + if (iter.index == 0) { 1030 + style = .{}; 1039 1031 break :blk try std.fmt.allocPrint( 1040 1032 ctx.arena, 1041 - "{d:0>2}:{d:0>2}", 1042 - .{ time.hour, time.minute }, 1033 + "{d:0>2}/{d:0>2}", 1034 + .{ @intFromEnum(time.month), time.day }, 1043 1035 ); 1044 - }; 1045 - 1046 - // If the message has our nick, we'll highlight the time 1047 - if (std.mem.indexOf(u8, content, self.client.nickname())) |_| { 1048 - style.fg = .{ .index = 3 }; 1049 - style.reverse = true; 1050 1036 } 1051 1037 1052 - const time_text: vxfw.Text = .{ 1053 - .text = buf, 1054 - .style = style, 1055 - .softwrap = false, 1056 - }; 1057 - try children.append(.{ 1058 - .origin = .{ .row = row, .col = 0 }, 1059 - .surface = try time_text.draw(child_ctx), 1060 - }); 1038 + // Otherwise, we print clock time 1039 + break :blk try std.fmt.allocPrint( 1040 + ctx.arena, 1041 + "{d:0>2}:{d:0>2}", 1042 + .{ time.hour, time.minute }, 1043 + ); 1044 + }; 1045 + 1046 + // If the message has our nick, we'll highlight the time 1047 + if (std.mem.indexOf(u8, content, self.client.nickname())) |_| { 1048 + style.fg = .{ .index = 3 }; 1049 + style.reverse = true; 1061 1050 } 1062 1051 1052 + const time_text: vxfw.Text = .{ 1053 + .text = buf, 1054 + .style = style, 1055 + .softwrap = false, 1056 + }; 1057 + try children.append(.{ 1058 + .origin = .{ .row = row, .col = 0 }, 1059 + .surface = try time_text.draw(child_ctx), 1060 + }); 1061 + 1063 1062 var printed_sender: bool = false; 1064 1063 // Check if we need to print the sender of this message. We do this when the timegap 1065 1064 // between this message and next message is > 5 minutes, or if the sender is 1066 1065 // different 1067 1066 if (sender.len > 0 and 1068 - printSender(sender, next_sender, maybe_instant, maybe_next_instant)) 1067 + printSender(sender, next_sender, this_instant, maybe_next_instant)) 1069 1068 { 1070 1069 // Back up one row to print 1071 1070 row -= 1; ··· 1106 1105 // Check if we should print a "last read" line. If the next message we will print is 1107 1106 // before the last_read, and this message is after the last_read then it is our border. 1108 1107 // Before 1109 - if (maybe_next_instant != null and maybe_instant != null) { 1110 - const this = maybe_instant.?.unixTimestamp(); 1111 - const next = maybe_next_instant.?.unixTimestamp(); 1112 - if (this > self.last_read and next <= self.last_read) { 1113 - const bot = "─"; 1114 - var writer = try std.ArrayList(u8).initCapacity(ctx.arena, bot.len * max.width); 1115 - try writer.writer().writeBytesNTimes(bot, max.width); 1108 + const next_instant = maybe_next_instant orelse continue; 1109 + const this = this_instant.unixTimestamp(); 1110 + const next = next_instant.unixTimestamp(); 1116 1111 1117 - const border: vxfw.Text = .{ 1118 - .text = writer.items, 1119 - .style = .{ .fg = .{ .index = 1 } }, 1120 - .softwrap = false, 1121 - }; 1112 + if (this > self.last_read_indicator and next <= self.last_read_indicator) { 1113 + const bot = "─"; 1114 + var writer = try std.ArrayList(u8).initCapacity(ctx.arena, bot.len * max.width); 1115 + try writer.writer().writeBytesNTimes(bot, max.width); 1116 + 1117 + const border: vxfw.Text = .{ 1118 + .text = writer.items, 1119 + .style = .{ .fg = .{ .index = 1 } }, 1120 + .softwrap = false, 1121 + }; 1122 1122 1123 - // We don't need to backup a line if we printed the sender 1124 - if (!printed_sender) row -= 1; 1123 + // We don't need to backup a line if we printed the sender 1124 + if (!printed_sender) row -= 1; 1125 1125 1126 - const unread: vxfw.SubSurface = .{ 1127 - .origin = .{ .col = 0, .row = row }, 1128 - .surface = try border.draw(ctx), 1129 - }; 1130 - try children.append(unread); 1131 - const new: vxfw.RichText = .{ 1132 - .text = &.{ 1133 - .{ .text = "", .style = .{ .fg = .{ .index = 1 } } }, 1134 - .{ .text = " New ", .style = .{ .fg = .{ .index = 1 }, .reverse = true } }, 1135 - }, 1136 - .softwrap = false, 1137 - }; 1138 - const new_sub: vxfw.SubSurface = .{ 1139 - .origin = .{ .col = max.width - 6, .row = row }, 1140 - .surface = try new.draw(ctx), 1141 - }; 1142 - try children.append(new_sub); 1143 - } 1126 + const unread: vxfw.SubSurface = .{ 1127 + .origin = .{ .col = 0, .row = row }, 1128 + .surface = try border.draw(ctx), 1129 + }; 1130 + try children.append(unread); 1131 + const new: vxfw.RichText = .{ 1132 + .text = &.{ 1133 + .{ .text = "", .style = .{ .fg = .{ .index = 1 } } }, 1134 + .{ .text = " New ", .style = .{ .fg = .{ .index = 1 }, .reverse = true } }, 1135 + }, 1136 + .softwrap = false, 1137 + }; 1138 + const new_sub: vxfw.SubSurface = .{ 1139 + .origin = .{ .col = max.width - 6, .row = row }, 1140 + .surface = try new.draw(ctx), 1141 + }; 1142 + try children.append(new_sub); 1144 1143 } 1145 1144 } 1146 1145 ··· 1234 1233 /// an irc message 1235 1234 pub const Message = struct { 1236 1235 bytes: []const u8, 1236 + timestamp_s: u32 = 0, 1237 + 1238 + pub fn init(bytes: []const u8) Message { 1239 + var msg: Message = .{ .bytes = bytes }; 1240 + if (msg.getTag("time")) |time_str| { 1241 + const inst = zeit.instant(.{ .source = .{ .iso8601 = time_str } }) catch |err| { 1242 + log.warn("couldn't parse time: '{s}', error: {}", .{ time_str, err }); 1243 + msg.timestamp_s = @intCast(std.time.timestamp()); 1244 + return msg; 1245 + }; 1246 + msg.timestamp_s = @intCast(inst.unixTimestamp()); 1247 + } else { 1248 + msg.timestamp_s = @intCast(std.time.timestamp()); 1249 + } 1250 + return msg; 1251 + } 1237 1252 1238 1253 pub const ParamIterator = struct { 1239 1254 params: ?[]const u8, ··· 1412 1427 return null; 1413 1428 } 1414 1429 1415 - pub fn time(self: Message) ?zeit.Instant { 1416 - const val = self.getTag("time") orelse return null; 1417 - 1418 - // Return null if we can't parse the time 1419 - const instant = zeit.instant(.{ 1420 - .source = .{ .iso8601 = val }, 1421 - .timezone = &zeit.utc, 1422 - }) catch return null; 1423 - 1424 - return instant; 1430 + pub fn time(self: Message) zeit.Instant { 1431 + return zeit.instant(.{ 1432 + .source = .{ .unix_timestamp = self.timestamp_s }, 1433 + }) catch unreachable; 1425 1434 } 1426 1435 1427 - pub fn localTime(self: Message, tz: *const zeit.TimeZone) ?zeit.Instant { 1428 - const utc = self.time() orelse return null; 1436 + pub fn localTime(self: Message, tz: *const zeit.TimeZone) zeit.Instant { 1437 + const utc = self.time(); 1429 1438 return utc.in(tz); 1430 1439 } 1431 1440 1432 1441 pub fn compareTime(_: void, lhs: Message, rhs: Message) bool { 1433 - const lhs_time = lhs.time() orelse return false; 1434 - const rhs_time = rhs.time() orelse return false; 1435 - 1436 - return lhs_time.timestamp_ns < rhs_time.timestamp_ns; 1442 + return lhs.timestamp_s < rhs.timestamp_s; 1437 1443 } 1438 1444 1439 1445 /// Returns the NICK of the sender of the message ··· 1754 1760 } 1755 1761 1756 1762 pub fn handleEvent(self: *Client, line: []const u8, ctx: *vxfw.EventContext) !void { 1757 - const msg: Message = .{ .bytes = line }; 1763 + const msg = Message.init(line); 1758 1764 const client = self; 1759 1765 switch (msg.command()) { 1760 1766 .unknown => {}, ··· 2093 2099 return; 2094 2100 }; 2095 2101 var channel = try client.getOrCreateChannel(target); 2096 - channel.last_read = last_read.unixTimestamp(); 2102 + channel.last_read = @intCast(last_read.unixTimestamp()); 2097 2103 const last_msg = channel.messages.getLastOrNull() orelse return; 2098 - const time = last_msg.time() orelse return; 2099 - if (time.unixTimestamp() > channel.last_read) 2104 + if (last_msg.timestamp_s > channel.last_read) 2100 2105 channel.has_unread = true 2101 2106 else 2102 2107 channel.has_unread = false; ··· 2127 2132 }, 2128 2133 .PRIVMSG, .NOTICE => { 2129 2134 // syntax: <target> :<message> 2130 - const msg2: Message = .{ 2131 - .bytes = try self.app.alloc.dupe(u8, msg.bytes), 2132 - }; 2135 + const msg2 = Message.init(try self.app.alloc.dupe(u8, msg.bytes)); 2133 2136 var iter = msg2.paramIterator(); 2134 2137 const target = blk: { 2135 2138 const tgt = iter.next() orelse return; ··· 2150 2153 if (msg2.getTag("batch")) |tag| { 2151 2154 const entry = client.batches.getEntry(tag) orelse @panic("TODO"); 2152 2155 var channel = entry.value_ptr.*; 2153 - try channel.messages.append(msg2); 2154 - std.sort.insertion(Message, channel.messages.items, {}, Message.compareTime); 2156 + try channel.insertMessage(msg2); 2155 2157 if (channel.scroll.msg_offset) |offset| { 2156 2158 channel.scroll.msg_offset = offset + 1; 2157 2159 } 2158 2160 channel.at_oldest = false; 2159 - const time = msg2.time() orelse return; 2160 - if (time.unixTimestamp() > channel.last_read) { 2161 + if (msg2.timestamp_s > channel.last_read) { 2161 2162 channel.has_unread = true; 2162 2163 const content = iter.next() orelse return; 2163 2164 if (std.mem.indexOf(u8, content, client.nickname())) |_| { ··· 2167 2168 } else { 2168 2169 // standard handling 2169 2170 var channel = try client.getOrCreateChannel(target); 2170 - try channel.messages.append(msg2); 2171 + try channel.insertMessage(msg2); 2171 2172 const content = iter.next() orelse return; 2172 2173 var has_highlight = false; 2173 2174 { ··· 2194 2195 try ctx.sendNotification(title, content); 2195 2196 has_highlight = true; 2196 2197 } 2197 - const time = msg2.time() orelse return; 2198 - if (time.unixTimestamp() > channel.last_read) { 2198 + if (msg2.timestamp_s > channel.last_read) { 2199 2199 channel.has_unread_highlight = has_highlight; 2200 2200 channel.has_unread = true; 2201 2201 } 2202 2202 // If we get a message from the current user mark the channel as 2203 2203 // read, since they must have just sent the message. 2204 - const sender: []const u8 = blk: { 2205 - const src = msg2.source() orelse break :blk ""; 2206 - const l = std.mem.indexOfScalar(u8, src, '!') orelse 2207 - std.mem.indexOfScalar(u8, src, '@') orelse 2208 - src.len; 2209 - break :blk src[0..l]; 2210 - }; 2204 + const sender = msg2.senderNick() orelse ""; 2211 2205 if (std.mem.eql(u8, sender, client.nickname())) { 2212 2206 self.app.markSelectedChannelRead(); 2213 2207 } ··· 2223 2217 } 2224 2218 }, 2225 2219 .TAGMSG => { 2226 - const msg2: Message = .{ .bytes = msg.bytes }; 2220 + const msg2 = Message.init(msg.bytes); 2227 2221 // We only care about typing tags 2228 2222 const typing = msg2.getTag("+typing") orelse return; 2229 2223 ··· 2262 2256 return; 2263 2257 } 2264 2258 if (std.mem.eql(u8, "active", typing)) { 2265 - const time = msg2.time() orelse return; 2266 - member.typing = @intCast(time.unixTimestamp()); 2259 + member.typing = msg2.timestamp_s; 2267 2260 channel.typing_last_active = member.typing; 2268 2261 ctx.redraw = true; 2269 2262 return;