200 Commits

Author SHA1 Message Date
Max Leiter
16d4e8354f Merge remote-tracking branch 'origin/master' into max/next
# Conflicts:
#	client/components/Channel.vue
#	client/components/ChatInput.vue
#	client/components/Message.vue
#	client/components/Settings/Account.vue
#	client/components/Settings/Appearance.vue
#	client/components/Settings/General.vue
#	client/components/Settings/Navigation.vue
#	client/components/Settings/Notifications.vue
#	client/components/StatusIcon.vue
#	client/css/settings.css
#	client/css/style.css
#	client/js/helpers/parse.ts
#	client/js/router.ts
#	client/js/settings.ts
#	client/js/socket-events/index.ts
#	client/js/socket-events/users.ts
#	server/client.ts
#	server/models/msg.ts
#	server/models/network.ts
#	server/plugins/inputs/msg.ts
#	server/plugins/irc-events/connection.ts
#	server/plugins/irc-events/join.ts
#	server/plugins/irc-events/message.ts
#	server/plugins/irc-events/nick.ts
#	server/plugins/irc-events/standard-reply.ts
#	server/plugins/irc-events/tagmsg.ts
#	server/server.ts
#	server/types/modules/irc-framework.d.ts
#	shared/types/msg.ts
#	shared/types/network.ts
#	shared/types/socket-events.d.ts
#	test/models/network.ts
#	test/plugins/irc-events/standardReplyTest.ts
#	test/plugins/tagmsg.ts
#	yarn.lock
2026-07-28 14:35:07 -07:00
Max Leiter
78cadc5ff1 ircv3: monitor and extended-monitor (#5045)
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.
2026-07-22 09:46:58 -07:00
Max Leiter
8ca7bca70b ircv3: draft/multiline (#5092)
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.
2026-07-20 14:45:45 -07:00
Max Leiter
f77e4d1e6f ircv3: add +reply support (#5062)
Relies on https://github.com/kiwiirc/irc-framework/pull/411

https://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>
2026-07-15 20:18:09 -07:00
Max Leiter
5867e6dc29 sqlite: store msgid column, add migration (#5073)
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
2026-07-15 20:17:05 -07:00
Jam Balaya
0d838fa69d Fix Unicode nickname matching (#5121)
## 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) |
|:---:|:---:|
|
![Before](https://raw.githubusercontent.com/JamBalaya56562/thelounge/pr5121-assets/pr5121-before.png)
|
![After](https://raw.githubusercontent.com/JamBalaya56562/thelounge/pr5121-assets/pr5121-after.png)
|

| 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
2026-07-15 20:16:44 -07:00
Max Leiter
db5a183117 ircv3: +typing support (#5050)
https://ircv3.net/specs/client-tags/typing

https://libera.chat/news/new-and-upcoming-features-3#client-tags

Very open to style feedback: I took some tips from discord:
- bump padding on the bottom to the chat to fit it (20px, not very
noticeable)
- when scrolled up, a gradient is applied
- I dont love adding the padding but idk a better solution and IMO its
not noticeable



https://github.com/user-attachments/assets/a911994e-1564-45bf-8475-37742ad8b4f5


<img width="1288" height="330" alt="image"
src="https://github.com/user-attachments/assets/19441440-be8c-477c-8513-9c43b83c009b"
/>
<img width="1608" height="1086" alt="image"
src="https://github.com/user-attachments/assets/339dff30-6719-450d-9d4f-e01ec354de3f"
/>

Closes https://github.com/thelounge/thelounge/issues/2419
Supersedes/closes #4611

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-07-14 13:12:57 -07:00
Max Leiter
366b82d1da ircv3: support +channel-context message tag (#5065)
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.
2026-07-14 13:09:58 -07:00
Max Leiter
c3c15020fc chore: bump typescript from 5.4.5 to 6.0.2 (#5071)
Prep for TypeScript 7

Changelog:
https://devblogs.microsoft.com/typescript/announcing-typescript-6-0/

relevant points:
-
[`ignoreDeprecations`](https://devblogs.microsoft.com/typescript/announcing-typescript-6-0/#preparing-for-typescript-7.0)
-
[`RegExp.escape`](https://devblogs.microsoft.com/typescript/announcing-typescript-6-0/#regexp.escape)
-[`"strict":
"true"](https://devblogs.microsoft.com/typescript/announcing-typescript-6-0/#the-dom-lib-now-contains-dom.iterable-and-dom.asynciterable)
is now default
2026-07-14 13:09:12 -07:00
Max Leiter
a7280c39ac ircv3: add bot mode support and [bot] userlist indicator (#5043)
This PR adds support for ircv3's bot mode:
https://ircv3.net/specs/extensions/bot-mode

irc-framework already exposes it in message tags, but we don't do
anything with it at the moment. This adds it to our `User` model and
adds an indicator in the userlist.

<img width="176" height="264" alt="image"
src="https://github.com/user-attachments/assets/03248607-4094-4fff-9417-607ae360097a"
/>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-07-14 13:08:37 -07:00
Max Leiter
2ecdbf9ec9 ircv3: support standard-replies (#5047)
https://ircv3.net/specs/extensions/standard-replies

irc-framework already supports it (behind a flag but unpublished to npm)
so its trivial for us to support and unblocks more ircv3 features
2026-07-14 13:07:42 -07:00
Max Leiter
8a4d5a09f2 replace web-push with web-push-neo (#5061)
https://github.com/web-push-libs/web-push relies on outdated libraries
that use `SlowBuffer` and thus are incompatible with Node 25

`web-push-neo` (https://github.com/ryoppippi/web-push-neo) is new and
not battle tested, but is single-dependency and uses modern web
standards. It also unblocks node 25.

closes https://github.com/thelounge/thelounge/issues/4997

one todo is that `web-push-neo` is ESM only so we need to dynamic
`import` it -- this can be fixed once we switch to ESM
2026-07-11 18:27:48 -07:00
Max Leiter
1499f7d9d4 Merge branch 'master' into max/next
# Conflicts:
#	package.json
#	server/rootpath.ts
#	server/server.ts
#	test/server.ts
#	vite.config.ts
#	yarn.lock
2026-06-26 22:53:14 -07:00
Max Leiter
dc7f79fe92 sqlite: add message history index (#5107)
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 |
2026-06-14 22:02:13 -07:00
Max Leiter
d21c6fb088 Replace is-utf8, read-chunk dependencies with Node replacements (#5077)
Available since node 18:
https://nodejs.org/api/buffer.html#bufferisutf8input
2026-06-02 21:00:59 +00:00
Max Leiter
0fe011235e remove webpack, babel, mocha; switch to vite/vitest (#5064)
```
Webpack vs Vite Build Comparison

  ┌────────────┬──────────────────────┬────────────────────┬───────────────┐
  │   Metric   │      Webpack 5       │ Vite 8 (Rolldown)  │    Change     │
  ├────────────┼──────────────────────┼────────────────────┼───────────────┤
  │ Build time │ 1,961ms (2.96s wall) │ 612ms (0.82s wall) │ 3.2x faster   │
  ├────────────┼──────────────────────┼────────────────────┼───────────────┤
  │ CPU time   │ 11.0s user           │ 1.3s user          │ 8.5x less CPU │
  └────────────┴──────────────────────┴────────────────────┴───────────────┘

  Bundle sizes (gzipped)

  ┌────────────────┬─────────┬────────┬────────┐
  │     Asset      │ Webpack │  Vite  │ Change │
  ├────────────────┼─────────┼────────┼────────┤
  │ App JS         │ 152.6K  │ 104.9K │ -31%   │
  ├────────────────┼─────────┼────────┼────────┤
  │ Vendor JS      │ 251.1K  │ 81.6K  │ -68%   │
  ├────────────────┼─────────┼────────┼────────┤
  │ Runtime JS     │ —       │ 0.5K   │ new    │
  ├────────────────┼─────────┼────────┼────────┤
  │ Total JS       │ 403.7K  │ 187.0K │ -54%   │
  ├────────────────┼─────────┼────────┼────────┤
  │ CSS            │ 12.5K   │ 11.9K  │ -5%    │
  ├────────────────┼─────────┼────────┼────────┤
  │ Total transfer │ 416.2K  │ 198.9K │ -52%   │
  └────────────────┴─────────┴────────┴────────┘

  Raw (uncompressed)

  ┌───────────────┬─────────┬──────┬────────┐
  │     Asset     │ Webpack │ Vite │ Change │
  ├───────────────┼─────────┼──────┼────────┤
  │ App JS        │ 1.3M    │ 304K │ -77%   │
  ├───────────────┼─────────┼──────┼────────┤
  │ Vendor JS     │ 1.1M    │ 236K │ -79%   │
  ├───────────────┼─────────┼──────┼────────┤
  │ CSS           │ 64K     │ 56K  │ -13%   │
  ├───────────────┼─────────┼──────┼────────┤
  │ Total public/ │ 5.1M    │ 3.4M │ -33%   │
  └───────────────┴─────────┴──────┴────────┘
```

---------

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
2026-06-01 09:02:09 -07:00
Max Leiter
2865bb20e4 Merge remote-tracking branch 'origin/max/04-10-ircv3-reply' into max/next
# Conflicts:
#	server/plugins/inputs/msg.ts
#	yarn.lock
2026-05-21 01:00:52 -07:00
Max Leiter
946122ce3a Merge remote-tracking branch 'origin/max/05-01-multiline' into max/next
# Conflicts:
#	package.json
#	server/plugins/inputs/msg.ts
#	yarn.lock
2026-05-21 00:58:34 -07:00
Max Leiter
d421397edf Merge remote-tracking branch 'origin/master' into max/04-10-ircv3-reply 2026-05-21 00:36:18 -07:00
Max Leiter
ef468097e5 merge 2026-05-21 00:31:07 -07:00
Max Leiter
e584b818a4 Merge remote-tracking branch 'origin/master' into max/next
# Conflicts:
#	client/js/router.ts
2026-05-12 20:52:49 -07:00
Pavel Djundik
a63b7b45b7 Force generated certificate serial to be positive, use 20 bytes (#5097)
Fixes #5096
@craftbyte
2026-05-11 14:23:05 -07:00
Max Leiter
097c6a3b62 deps: bump irc-framework pin to include tags-action; use action() with tags
irc-framework PR #411 lets action() accept tags directly, replacing
the sendMessage('PRIVMSG', ..., '\x01ACTION ...\x01', tags) workaround
that was needed to attach +reply tags to /me messages.
2026-05-02 14:15:41 -07:00
Max Leiter
8dc295377c plugins/inputs: thread replyTo via extras param instead of client state
Replaces client._pendingReplyTo (set/used/cleared in a sync block) with
an explicit fifth argument on PluginInputHandler. The information was
never about labeled-response correlation — it's UI context that needed
to reach the input handler — so the cleanest fix is to pass it as a
parameter rather than stash it on the Client instance.
2026-05-02 14:12:13 -07:00
Max Leiter
0f3880d037 Merge branch 'max/05-01-emoji' into max/next 2026-05-02 12:21:58 -07:00
Max Leiter
56e5d6a997 css 2026-05-02 12:14:29 -07:00
Max Leiter
03ee145680 Emoji selector 2026-05-02 12:05:19 -07:00
Max Leiter
68a1bcb344 cmt 2026-05-01 21:57:44 -07:00
Max Leiter
983749ab11 fix contributor count 2026-05-01 21:56:50 -07:00
Max Leiter
10b46607a8 Merge branch 'max/05-01-multiline' into max/next
# Conflicts:
#	package.json
#	server/client.ts
#	server/models/network.ts
#	server/plugins/inputs/msg.ts
#	server/plugins/irc-events/message.ts
#	server/plugins/irc-events/standard-reply.ts
#	server/types/modules/irc-framework.d.ts
#	shared/types/msg.ts
#	test/.mocharc.yml
2026-05-01 21:56:06 -07:00
Max Leiter
4546dcd473 ircv3: draft/multiline 2026-05-01 21:49:21 -07:00
Max Leiter
58496ea869 Merge branch 'max/04-09-monitor' into max/next
# Conflicts:
#	server/models/network.ts
#	server/plugins/irc-events/connection.ts
#	shared/types/network.ts
2026-04-25 15:14:35 -07:00
Max Leiter
1bee1ed77c respect network preferences 2026-04-25 15:11:24 -07:00
Max Leiter
bffaa5e688 Merge remote-tracking branch 'origin/master' into max/04-09-monitor 2026-04-19 20:27:45 -07:00
Max Leiter
f589c31cca Merge remote-tracking branch 'origin/master' into max/next 2026-04-18 16:50:57 -07:00
Jay2k1
0b2fc8d890 fix order of messages with identical timestamps (#5087)
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"
/>
2026-04-18 16:35:10 -07:00
Max Leiter
4bb6420e04 fix: escape user-supplied LDAP filter value as per RFC 4515 (#5084)
Pointed out by claude and @xPaw 

https://datatracker.ietf.org/doc/html/rfc4515#section-3

Tests written by Claude
2026-04-18 16:13:23 -07:00
Reto
51b620c589 sqlite: don't interpolate the deletion query (#5086)
While it was safe and not attacker controlled, it's better to be safe
than sorry. Plus it trips the scanners up
2026-04-18 16:12:24 -07:00
Max Leiter
d9236c80d8 lint 2026-04-18 11:38:35 -07:00
Max Leiter
ed9c1c53ba Merge remote-tracking branch 'origin/master' into max/04-10-ircv3-reply 2026-04-18 11:38:17 -07:00
Max Leiter
5b83f0ab4e chore: replace uuid package with crypto.randomUUID (#5081)
Available since Node 15:
https://nodejs.org/docs/latest-v20.x/api/crypto.html
2026-04-18 09:23:19 -07:00
Max Leiter
21f0e70696 fix /mute not persisting, case-insensitive channel check (#5083)
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.
```
2026-04-18 09:23:07 -07:00
Max Leiter
fb96789666 Merge branch 'max/04-17-fix-mute' into max/next 2026-04-17 11:00:06 -07:00
Max Leiter
2c293cd1d1 Merge branch 'max/04-17-rm-uuid' into max/next 2026-04-17 10:59:57 -07:00
Max Leiter
567073555a fix /mute not persisting, case-insensitive channel check 2026-04-17 10:27:53 -07:00
Max Leiter
9adc7b5310 chore: replace uuid package with crypto.randomUUID 2026-04-17 10:22:17 -07:00
Max Leiter
109e4f38f7 revert the monitor revert, restoring all monitor/extended-monitor code 2026-04-15 10:22:38 -07:00
Max Leiter
eeac1dc4fd Merge branch 'max/04-09-monitor' into max/next 2026-04-15 10:01:33 -07:00
Max Leiter
037a0bb0a6 batch monitor 2026-04-15 09:55:29 -07:00
Max Leiter
a21690781d Revert "Merge #5045: ircv3: monitor and extended-monitor"
Causes excess flood on networks.
2026-04-14 11:08:45 -07:00