Skip to content

Commit 37e4465

Browse files
authored
Merge pull request #1597 from squidowl/limit-reactions
Setting to limit reaction text length
2 parents 5cc0677 + f2a88f6 commit 37e4465

File tree

8 files changed

+96
-2
lines changed

8 files changed

+96
-2
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# Unreleased
22

3+
Added:
4+
5+
- Setting to limit reaction text length (`buffer.channel.message.max_reaction_chars`)
6+
37
# 2026.4 (2026-03-03)
48

59
Added:

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

book/src/configuration/buffer/channel/message.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ Message settings within a channel buffer.
55
- [Message](#message)
66
- [Configuration](#configuration)
77
- [nickname\_color](#nickname_color)
8+
- [show\_emoji\_reacts](#show_emoji_reacts)
9+
- [max\_reaction\_chars](#max_reaction_chars)
810

911
## Configuration
1012

@@ -33,3 +35,17 @@ Whether to display emoji reactions on messages (if [IRCv3 React](https://ircv3.n
3335
[buffer.channel.message]
3436
show_emoji_reacts = true
3537
```
38+
39+
### max_reaction_chars
40+
41+
Maximum number of user-visible characters (Unicode grapheme clusters) in a reaction.
42+
If a reaction exceeds this value, it is truncated to the first `max_reaction_chars` grapheme clusters.
43+
44+
```toml
45+
# Type: integer
46+
# Values: positive integers
47+
# Default: 5
48+
49+
[buffer.channel.message]
50+
max_reaction_chars = 5
51+
```

data/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ tokio-stream = { workspace = true, features = ["time", "fs"] }
3131
timeago = { workspace = true }
3232
itertools = { workspace = true }
3333
emojis = { workspace = true }
34+
unicode-segmentation = { workspace = true }
3435
rand = { workspace = true }
3536
rand_chacha = { workspace = true }
3637
palette = { workspace = true }

data/src/config/buffer/channel.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,16 @@ pub struct Channel {
1919
pub struct Message {
2020
pub nickname_color: Color,
2121
pub show_emoji_reacts: bool,
22+
#[serde(deserialize_with = "crate::serde::deserialize_positive_integer")]
23+
pub max_reaction_chars: u32,
2224
}
2325

2426
impl Default for Message {
2527
fn default() -> Self {
2628
Self {
2729
nickname_color: Color::default(),
2830
show_emoji_reacts: true,
31+
max_reaction_chars: 5,
2932
}
3033
}
3134
}

data/src/reaction.rs

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
use chrono::{DateTime, Utc};
22
use irc::proto::Command;
33
use serde::{Deserialize, Serialize};
4+
use unicode_segmentation::UnicodeSegmentation;
45

56
use crate::isupport;
67
use crate::message::{Encoded, Id};
@@ -28,6 +29,7 @@ impl Reaction {
2829
chantypes: &[char],
2930
statusmsg: &[char],
3031
casemapping: isupport::CaseMap,
32+
max_reaction_chars: u32,
3133
) -> Option<Context> {
3234
let user = message.user(casemapping)?;
3335
let (text, unreact) = match (
@@ -38,6 +40,7 @@ impl Reaction {
3840
(None, Some(s)) => (s.clone(), true),
3941
_ => return None,
4042
};
43+
let text = truncate_text(&text, max_reaction_chars as usize);
4144
let in_reply_to = message.in_reply_to()?;
4245
let server_time = message.server_time();
4346

@@ -60,6 +63,18 @@ impl Reaction {
6063
}
6164
}
6265

66+
pub fn truncate_text(text: &str, max_chars: usize) -> String {
67+
if UnicodeSegmentation::graphemes(text, true).count() <= max_chars {
68+
return text.to_string();
69+
}
70+
71+
let mut truncated = UnicodeSegmentation::graphemes(text, true)
72+
.take(max_chars)
73+
.collect::<String>();
74+
truncated.push_str("...");
75+
truncated
76+
}
77+
6378
#[derive(Debug)]
6479
pub struct Pending {
6580
pub reactions: Vec<Reaction>,
@@ -74,3 +89,38 @@ impl Pending {
7489
}
7590
}
7691
}
92+
93+
#[cfg(test)]
94+
mod tests {
95+
use super::truncate_text;
96+
97+
#[test]
98+
fn keeps_short_reaction_text() {
99+
assert_eq!(truncate_text("hello", 5), "hello");
100+
}
101+
102+
#[test]
103+
fn truncates_ascii_to_limit() {
104+
assert_eq!(truncate_text("hello world", 5), "hello...");
105+
}
106+
107+
#[test]
108+
fn truncates_unicode_graphemes() {
109+
assert_eq!(truncate_text("cafe\u{301}", 4), "cafe\u{301}");
110+
}
111+
112+
#[test]
113+
fn limit_one_keeps_first_grapheme_when_truncated() {
114+
assert_eq!(truncate_text("👍🏽👍🏽", 1), "👍🏽...");
115+
}
116+
117+
#[test]
118+
fn does_not_split_zwj_emoji_clusters() {
119+
assert_eq!(truncate_text("👨‍👩‍👧‍👦x", 1), "👨‍👩‍👧‍👦...");
120+
}
121+
122+
#[test]
123+
fn does_not_split_combining_mark_clusters() {
124+
assert_eq!(truncate_text("a\u{0301}b", 1), "a\u{0301}...");
125+
}
126+
}

src/buffer/scroll_view.rs

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1164,10 +1164,26 @@ impl State {
11641164
}
11651165
}
11661166
Message::Reacted { msgid, text } => {
1167-
send_reaction(clients, buffer, history, msgid, text, false);
1167+
send_reaction(
1168+
clients,
1169+
buffer,
1170+
history,
1171+
msgid,
1172+
text,
1173+
false,
1174+
config.buffer.channel.message.max_reaction_chars,
1175+
);
11681176
}
11691177
Message::Unreacted { msgid, text } => {
1170-
send_reaction(clients, buffer, history, msgid, text, true);
1178+
send_reaction(
1179+
clients,
1180+
buffer,
1181+
history,
1182+
msgid,
1183+
text,
1184+
true,
1185+
config.buffer.channel.message.max_reaction_chars,
1186+
);
11711187
}
11721188
}
11731189
(Task::none(), None)
@@ -1394,8 +1410,10 @@ fn send_reaction(
13941410
msgid: message::Id,
13951411
text: String,
13961412
unreact: bool,
1413+
max_reaction_chars: u32,
13971414
) -> Option<()> {
13981415
let buffer = buffer?;
1416+
let text = reaction::truncate_text(&text, max_reaction_chars as usize);
13991417
let server = buffer.server();
14001418
let target = buffer.target()?;
14011419
let command = match unreact {

src/main.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1558,6 +1558,7 @@ fn handle_client_events(
15581558
clients.get_chantypes(server),
15591559
clients.get_statusmsg(server),
15601560
clients.get_casemapping(server),
1561+
config.buffer.channel.message.max_reaction_chars,
15611562
) {
15621563
reactions.push(
15631564
dashboard

0 commit comments

Comments
 (0)