Updated version of #4550. Closes#337, closes#872 (#337 can't be fully
addressed — tracking nick changes isn't really possible atm).
<img width="253" height="149" alt="image"
src="https://github.com/user-attachments/assets/8a961868-0aa9-406b-92e2-f9d4858e2d0d"
/>
Away support:
<img width="214" alt="image"
src="https://user-images.githubusercontent.com/8675906/165882385-6f928c8e-5a0b-4502-8bd3-3cd0bd70e0e0.png">
## Summary
Query windows show whether the person behind them is online, and whether
they are away, using
[MONITOR](https://ircv3.net/specs/extensions/monitor) rather than
polling. `Network` keeps the monitor list: queries are registered in
batched `MONITOR +` commands once RPL_ISUPPORT tells us the server
supports it, and one at a time as new query windows open. The limit
advertised in `MONITOR=<n>` is respected, with anything over it waiting
for a slot; `MONITOR=0` means no monitoring at all, a bare `MONITOR`
means no limit. A nick change on a monitored query swaps the target in
place so the query keeps its slot.
[extended-monitor](https://ircv3.net/specs/extensions/extended-monitor)
is requested too, so away, nick, account and chghost updates arrive for
monitored nicks we share no channel with. Away state for people we do
share a channel with comes from a WHO on join.
The indicator is a dot on the query in the sidebar and next to the nick
in the message bubble, with a tooltip and an `img` role so it isn't
colour-only, and it can be turned off under Appearance.
Known limitation: a server that enforces a monitor limit it doesn't
advertise will reject part of a batch with `ERR_MONLISTFULL`; the user
gets a message in the lobby, but the client's own list isn't reconciled
until reconnect.
## Test Plan
- Unit tests for the monitor list: add, case-insensitive dedupe,
queueing at the limit, removal draining the queue, batching, and the
rename path.
- Ran the server against a local IRCd and watched the wire: opening
queries sends `MONITOR + <nick>` each, a reconnect with four open
queries sends a single `MONITOR + bob,carol,dave,mallory`, 730/731 drive
the online/offline state, a rename sends `MONITOR - bob` then `MONITOR +
bob2`, `MONITOR=0` sends nothing at all, and a bare `MONITOR` behaves as
unlimited.
Closes#5052
Multi-line input is currently sent as one PRIVMSG per line, so a paste
arrives as N unrelated messages. On networks that support
[draft/multiline](https://ircv3.net/specs/extensions/multiline) it can
be sent as a single BATCH and stays one message everywhere.
## Summary
Sending goes through `sayMultiline()` in `plugins/inputs/msg.ts` when
the network advertises limits we can respect (`multilineLimits()`); if
the batch is over max-bytes/max-lines the send falls back to individual
messages, and the local echo follows whichever path was actually taken
so the sender sees what everyone else got. `Client.input()` stops
splitting on newlines when the target network has the cap. Inbound
batches arrive from irc-framework already reassembled and are flagged
`multiline` on the `Msg`, which drives line-by-line rendering (`parse()`
joins lines with `<br>`), one entry per message in the text logs, and a
first-non-empty-line notification preview. `MULTILINE_*` standard
replies get readable text.
No pin bump: `master` already points at the irc-framework commit that
carries multiline (kiwiirc/irc-framework#414).
## Test Plan
- New unit tests for the send path (batch, fallback on max-bytes, no
limits advertised, blank lines), the text log format, `parse()`, and the
standard replies.
- Ran the server against a local IRCd advertising `draft/multiline` and
confirmed on the wire: a 3-line message goes out as one `BATCH …
draft/multiline` with three frames; with the cap advertised bare (no
limits) or with `max-bytes` too small it degrades to three plain
PRIVMSGs and the local echo degrades with it; a CRLF paste sends clean
frames and echoes without stray `\r`; an inbound batch from another user
renders as one message.
- Not verified: rendering was checked through unit tests rather than in
a browser.
Relies on https://github.com/kiwiirc/irc-framework/pull/411https://ircv3.net/specs/client-tags/reply.html
Design feedback very welcome
<img width="1718" height="278" alt="Screenshot 2026-04-11 at 21-00-22
#thelounge — The Lounge"
src="https://github.com/user-attachments/assets/ba6e7c69-2477-4a7b-b196-901d19ca9cba"
/>
1. we lack a 'thread' view (irccloud, slack, etc). can be a followup if
we do want it
2. need to test scrolling to messages outside of the current buffer
3. due to lack of client support, i opted to auto prefill the input with
\`<targetNick>: \` so people can follow along even without reply support
4. CSS can definitely be improved (once a design is settled on)
---------
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>
https://github.com/thelounge/thelounge/pull/5044 added the type + value
to handles, but now we need to persist it in the database so e.g.
mentions work after a server restart.
`msgid` is optional
## What changed
- Match nickname tokens with Unicode letters and numbers while
preserving IRC nickname characters.
- Use Unicode-aware word boundaries for nickname highlights.
- Add regression coverage for Polish text, Unicode nicknames, and
existing IRC nickname characters.
## Why
The previous regular expressions used ASCII-only character classes. This
caused a one-character nickname such as `D` to be detected inside words
containing Polish diacritics, for example `dzięki`.
## Before / after
| Before (v4.5.2) | After (this PR) |
|:---:|:---:|
|

|

|
| Scenario | Before | After |
|---|---|---|
| Nick `Ądam42` appears in a message | Not recognized as a nick —
rendered as plain text (no color, not clickable) | Recognized and
colored like any other nick |
| `Ćadam` / `adamć` with your nick `adam` | Falsely highlighted as a
mention of you | Not highlighted |
| Genuine mention `hey adam` | Highlighted | Highlighted (no regression)
|
<sub>Screenshots are a faithful reproduction rendered with TheLounge's
real <code>client/css/style.css</code> and the actual old/new regexes
(not a live IRC capture).</sub>
## Validation
- Direct Unicode nickname extraction and boundary checks with Node.js 24
- Full Vitest/lint suite not run because repository dependencies were
unavailable and dependency installation stalled in the local environment
Fixes#4930
Ratified two days ago
https://ircv3.net/specs/client-tags/channel-context
> This specification defines a client-only message tag to indicate the
channel a private PRIVMSG or NOTICE should be displayed in.
Fixes#5103
Today, `getMessages()` filters by `(network, channel)` but sorts by
`time DESC, id DESC` and no index covers both (and a new one supersedes
`network_channel`). This has always been an issue but
https://github.com/thelounge/thelounge/pull/5055 made this code
synchronous which exposed it.
| | Before | After |
|--------|--------|--------|
| history load, 5 channels (4.4M rows total, busiest 3M) | 6.9s, event
loop blocked | 80ms |
| single channel with 3M messages | 4.6s | 18ms |
| query plan | index scan + temp B-tree sort | index only |
When sending a message that is too long, thelounge will split it up into
multiple PRIVMSGs. These will be saved to the sqlite database with
identical timestamps. Thelounge retrieves messages from the DB using
`ORDER BY time DESC`, which under certain circumstances causes these
messages to be loaded and displayed in the wrong order. This PR
addresses that by also ordering by id (only affects messages with
identical timestamps).
Before:
<img width="1050" height="107" alt="image"
src="https://github.com/user-attachments/assets/54fac085-a989-48fa-9538-1faecb09aa44"
/>
After:
<img width="1058" height="114" alt="image"
src="https://github.com/user-attachments/assets/0bb99db9-5a5d-481d-b799-87162d0535e8"
/>
Found by claude:
```
server/plugins/inputs/mute.ts — /mute slash command never persists. Missing client.save(). Mutes set via the command vanish on restart. The socket handler (server/server.ts:768-797) does save.
server/plugins/inputs/mute.ts:18 — /mute #Foo fails when channel was joined as #foo. Uses case-sensitive c.name === arg; IRC channel names are case-insensitive.
```
- Mentions were doing a bunch of mutations; not just uses derived state
- no need for a separate `ClientMention`
- added `extractInputHistory` for shared logic
- tests made by claude
Replace the manual deleteFolder() recursive implementation with
fs.rmSync({recursive: true, force: true}), which handles both files
and directories. This also resolves the ENOTDIR error caused by
.DS_Store and other non-directory entries in the storage folder.
Apparently sqlite started validating this in certain versions.
Crashes on BSDs with:
SQLite version 3.50.4 2025-07-30 19:33:53
sqlite> SELECT msg, type, time, network, channel FROM messages WHERE type = "message";
Parse error: no such column: "message" - should this be a string literal in single-quotes?
time, network, channel FROM messages WHERE type = "message";
error here ---^
The framework may emit messages which do not have a time stamp.
We tried to unconditionally convert the time field, fix that.
The Msg constructor replaces falsey time fields with the current
date so we can also remove the duplication from that codepath.
The publicClient interface is utterly horrific.
It allows any client to inject arbitrary events into the socket.io
event stream.
This should get wrapped into a "plugin" event so that it can get properly
typed, better yet, this should get removed completely.