tangled
alpha
login
or
join now
rockorager.dev
/
comlink
2
fork
atom
an experimental irc client
2
fork
atom
overview
issues
pulls
pipelines
ui: implement typing display
rockorager.dev
1 year ago
33819fcc
254d636a
+150
2 changed files
expand all
collapse all
unified
split
src
app.zig
irc.zig
+1
src/app.zig
···
264
try irc.Client.retryTickHandler(client, ctx, .tick);
265
}
266
client.drainFifo(ctx);
0
267
}
268
try ctx.tick(8, self.widget());
269
},
···
264
try irc.Client.retryTickHandler(client, ctx, .tick);
265
}
266
client.drainFifo(ctx);
267
+
client.checkTypingStatus(ctx);
268
}
269
try ctx.tick(8, self.widget());
270
},
+149
src/irc.zig
···
67
NOTICE,
68
PART,
69
PRIVMSG,
0
70
71
unknown,
72
···
98
.{ "NOTICE", .NOTICE },
99
.{ "PART", .PART },
100
.{ "PRIVMSG", .PRIVMSG },
0
101
});
102
103
pub fn parse(cmd: []const u8) Command {
···
148
149
completer: Completer,
150
completer_shown: bool = false,
0
151
152
// Gutter (left side where time is printed) width
153
const gutter_width = 6;
···
160
161
channel: *Channel,
162
has_mouse: bool = false,
0
163
164
pub fn compare(_: void, lhs: Member, rhs: Member) bool {
165
return if (lhs.prefix != ' ' and rhs.prefix == ' ')
···
620
.surface = scrollbar_surface,
621
});
622
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
623
{
624
// Draw the character limit. 14 is length of message overhead "PRIVMSG :\r\n"
625
const max_limit = maximum_message_size -| self.name.len -| 14 -| self.name.len;
···
1116
// In any other case, we
1117
return false;
1118
}
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
1119
};
1120
1121
pub const User = struct {
···
1639
self.read_buf.replaceRangeAssumeCapacity(0, i, "");
1640
}
1641
0
0
0
0
0
0
0
0
0
0
0
0
0
1642
pub fn handleEvent(self: *Client, line: []const u8, ctx: *vxfw.EventContext) !void {
1643
const msg: Message = .{ .bytes = line };
1644
const client = self;
···
2093
};
2094
if (std.mem.eql(u8, sender, client.nickname())) {
2095
self.app.markSelectedChannelRead();
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
2096
}
2097
}
2098
},
···
67
NOTICE,
68
PART,
69
PRIVMSG,
70
+
TAGMSG,
71
72
unknown,
73
···
99
.{ "NOTICE", .NOTICE },
100
.{ "PART", .PART },
101
.{ "PRIVMSG", .PRIVMSG },
102
+
.{ "TAGMSG", .TAGMSG },
103
});
104
105
pub fn parse(cmd: []const u8) Command {
···
150
151
completer: Completer,
152
completer_shown: bool = false,
153
+
typing_last_active: u32 = 0,
154
155
// Gutter (left side where time is printed) width
156
const gutter_width = 6;
···
163
164
channel: *Channel,
165
has_mouse: bool = false,
166
+
typing: u32 = 0,
167
168
pub fn compare(_: void, lhs: Member, rhs: Member) bool {
169
return if (lhs.prefix != ' ' and rhs.prefix == ' ')
···
624
.surface = scrollbar_surface,
625
});
626
627
+
// Draw typers
628
+
typing: {
629
+
var buf: [3]*User = undefined;
630
+
const typers = self.getTypers(&buf);
631
+
632
+
switch (typers.len) {
633
+
0 => break :typing,
634
+
1 => {
635
+
const text = try std.fmt.allocPrint(
636
+
ctx.arena,
637
+
"{s} is typing...",
638
+
.{typers[0].nick},
639
+
);
640
+
const typer: vxfw.Text = .{ .text = text, .style = .{ .dim = true } };
641
+
const typer_ctx = ctx.withConstraints(.{}, ctx.max);
642
+
try children.append(.{
643
+
.origin = .{ .col = 0, .row = max.height - 2 },
644
+
.surface = try typer.draw(typer_ctx),
645
+
});
646
+
},
647
+
2 => {
648
+
const text = try std.fmt.allocPrint(
649
+
ctx.arena,
650
+
"{s} and {s} are typing...",
651
+
.{ typers[0].nick, typers[1].nick },
652
+
);
653
+
const typer: vxfw.Text = .{ .text = text, .style = .{ .dim = true } };
654
+
const typer_ctx = ctx.withConstraints(.{}, ctx.max);
655
+
try children.append(.{
656
+
.origin = .{ .col = 0, .row = max.height - 2 },
657
+
.surface = try typer.draw(typer_ctx),
658
+
});
659
+
},
660
+
else => {
661
+
const text = "Several people are typing...";
662
+
const typer: vxfw.Text = .{ .text = text, .style = .{ .dim = true } };
663
+
const typer_ctx = ctx.withConstraints(.{}, ctx.max);
664
+
try children.append(.{
665
+
.origin = .{ .col = 0, .row = max.height - 2 },
666
+
.surface = try typer.draw(typer_ctx),
667
+
});
668
+
},
669
+
}
670
+
}
671
+
672
{
673
// Draw the character limit. 14 is length of message overhead "PRIVMSG :\r\n"
674
const max_limit = maximum_message_size -| self.name.len -| 14 -| self.name.len;
···
1165
// In any other case, we
1166
return false;
1167
}
1168
+
1169
+
fn getTypers(self: *Channel, buf: []*User) []*User {
1170
+
const now: u32 = @intCast(std.time.timestamp());
1171
+
var i: usize = 0;
1172
+
for (self.members.items) |member| {
1173
+
if (i == buf.len) {
1174
+
return buf[0..i];
1175
+
}
1176
+
// The spec says we should consider people as typing if the last typing message was
1177
+
// received within 6 seconds from now
1178
+
if (member.typing + 6 >= now) {
1179
+
buf[i] = member.user;
1180
+
i += 1;
1181
+
}
1182
+
}
1183
+
return buf[0..i];
1184
+
}
1185
+
1186
+
fn typingCount(self: *Channel) usize {
1187
+
const now: u32 = @intCast(std.time.timestamp());
1188
+
1189
+
var n: usize = 0;
1190
+
for (self.members.items) |member| {
1191
+
// The spec says we should consider people as typing if the last typing message was
1192
+
// received within 6 seconds from now
1193
+
if (member.typing + 6 >= now) {
1194
+
n += 1;
1195
+
}
1196
+
}
1197
+
return n;
1198
+
}
1199
};
1200
1201
pub const User = struct {
···
1719
self.read_buf.replaceRangeAssumeCapacity(0, i, "");
1720
}
1721
1722
+
// Checks if any channel has an expired typing status. The typing status is considered expired
1723
+
// if the last typing status received is more than 6 seconds ago. In this case, we set the last
1724
+
// typing time to 0 and redraw.
1725
+
pub fn checkTypingStatus(self: *Client, ctx: *vxfw.EventContext) void {
1726
+
const now: u32 = @intCast(std.time.timestamp());
1727
+
for (self.channels.items) |channel| {
1728
+
if (channel.typing_last_active > 0 and
1729
+
channel.typing_last_active + 6 >= now) continue;
1730
+
channel.typing_last_active = 0;
1731
+
ctx.redraw = true;
1732
+
}
1733
+
}
1734
+
1735
pub fn handleEvent(self: *Client, line: []const u8, ctx: *vxfw.EventContext) !void {
1736
const msg: Message = .{ .bytes = line };
1737
const client = self;
···
2186
};
2187
if (std.mem.eql(u8, sender, client.nickname())) {
2188
self.app.markSelectedChannelRead();
2189
+
}
2190
+
2191
+
// Set the typing time to 0
2192
+
for (channel.members.items) |*member| {
2193
+
if (!std.mem.eql(u8, member.user.nick, sender)) {
2194
+
continue;
2195
+
}
2196
+
member.typing = 0;
2197
+
return;
2198
+
}
2199
+
}
2200
+
},
2201
+
.TAGMSG => {
2202
+
const msg2: Message = .{ .bytes = msg.bytes };
2203
+
// We only care about typing tags
2204
+
const typing = msg2.getTag("+typing") orelse return;
2205
+
2206
+
var iter = msg2.paramIterator();
2207
+
const target = blk: {
2208
+
const tgt = iter.next() orelse return;
2209
+
if (mem.eql(u8, tgt, client.nickname())) {
2210
+
// If the target is us, it likely has our
2211
+
// hostname in it.
2212
+
const source = msg2.source() orelse return;
2213
+
const n = mem.indexOfScalar(u8, source, '!') orelse source.len;
2214
+
break :blk source[0..n];
2215
+
} else break :blk tgt;
2216
+
};
2217
+
const sender: []const u8 = blk: {
2218
+
const src = msg2.source() orelse break :blk "";
2219
+
const l = std.mem.indexOfScalar(u8, src, '!') orelse
2220
+
std.mem.indexOfScalar(u8, src, '@') orelse
2221
+
src.len;
2222
+
break :blk src[0..l];
2223
+
};
2224
+
// if (std.mem.eql(u8, sender, client.nickname())) {
2225
+
// // We never considuer ourselves as typing
2226
+
// return;
2227
+
// }
2228
+
const channel = try client.getOrCreateChannel(target);
2229
+
2230
+
for (channel.members.items) |*member| {
2231
+
if (!std.mem.eql(u8, member.user.nick, sender)) {
2232
+
continue;
2233
+
}
2234
+
if (std.mem.eql(u8, "done", typing)) {
2235
+
member.typing = 0;
2236
+
ctx.redraw = true;
2237
+
return;
2238
+
}
2239
+
if (std.mem.eql(u8, "active", typing)) {
2240
+
const time = msg2.time() orelse return;
2241
+
member.typing = @intCast(time.unixTimestamp());
2242
+
channel.typing_last_active = member.typing;
2243
+
ctx.redraw = true;
2244
+
return;
2245
}
2246
}
2247
},