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