Chat option: filter the chat's lines both ways, as setVisibleMessageFilter
The chat keeps where each message came from and the Chat option's abilities. A message the option hides is still dropped on arrival, but a change of the option now also hides the lines already in the chat that it no longer shows, and brings them back when it shows them again (ChatComponent.setVisibleMessageFilter -> refreshTrimmedMessages), on apply_settings. Every Chat::add names its source. The Restrictions screen reads its two flags from ChatAbilities instead of its own copy. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -319,6 +319,7 @@ fn chat(c: &Value, h: i32, atlas: &HudAtlas) -> Vec<GuiDraw> {
|
||||
atlas,
|
||||
comp(&m["text"]),
|
||||
indicator,
|
||||
horizon_client::chat::ChatSource::Client,
|
||||
m["at"].as_u64().unwrap_or(0),
|
||||
false,
|
||||
);
|
||||
|
||||
@@ -72,6 +72,14 @@ impl ChatAbilities {
|
||||
},
|
||||
}
|
||||
}
|
||||
/// `visibleMessagesFilter`: the client's own lines always show.
|
||||
pub fn shows(&self, source: ChatSource) -> bool {
|
||||
match source {
|
||||
ChatSource::Player => self.receive_player_messages,
|
||||
ChatSource::Server => self.receive_system_messages,
|
||||
ChatSource::Client => true,
|
||||
}
|
||||
}
|
||||
/// Whether the chat screen may send `input` (`hasAllowedInput`).
|
||||
pub fn allows(&self, input: &str) -> bool {
|
||||
if input.starts_with('/') {
|
||||
@@ -82,6 +90,12 @@ impl ChatAbilities {
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ChatAbilities {
|
||||
fn default() -> Self {
|
||||
Self::new(ChatVisibility::Full)
|
||||
}
|
||||
}
|
||||
|
||||
/// Where a chat line came from (`GuiMessageSource`): the visibility filters
|
||||
/// player messages and the server's, while the client's own always show.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
@@ -143,6 +157,7 @@ struct Message {
|
||||
added: u64,
|
||||
content: Text,
|
||||
indicator: Option<u32>,
|
||||
source: ChatSource,
|
||||
}
|
||||
/// A wrapped line (`GuiMessage.Line`).
|
||||
#[derive(Clone, Debug)]
|
||||
@@ -177,6 +192,8 @@ pub struct Chat {
|
||||
pub log: bool,
|
||||
/// Messages added since the start, for the control socket's events.
|
||||
pub received: u64,
|
||||
/// The Chat option's `visibleMessageFilter` (`set_abilities`).
|
||||
abilities: ChatAbilities,
|
||||
}
|
||||
impl Chat {
|
||||
pub fn lines_per_page(&self, focused: bool) -> usize {
|
||||
@@ -186,16 +203,32 @@ impl Chat {
|
||||
fn max_width(&self) -> i32 {
|
||||
(f64::from(self.options.width_px()) / self.options.scale).floor() as i32
|
||||
}
|
||||
/// ChatComponent.addMessage: wrap, prepend and trim to 100 lines.
|
||||
/// ChatComponent.addMessage: wrap, prepend and trim to 100 lines. A
|
||||
/// message the Chat option hides is dropped, unlogged.
|
||||
pub fn add(
|
||||
&mut self,
|
||||
atlas: &HudAtlas,
|
||||
content: Text,
|
||||
indicator: Option<u32>,
|
||||
source: ChatSource,
|
||||
ticks: u64,
|
||||
focused: bool,
|
||||
) {
|
||||
// ChatComponent.logChatMessage: every message is in the log.
|
||||
self.add_message(&atlas.font, content, indicator, source, ticks, focused);
|
||||
}
|
||||
fn add_message(
|
||||
&mut self,
|
||||
font: &crate::font::Font,
|
||||
content: Text,
|
||||
indicator: Option<u32>,
|
||||
source: ChatSource,
|
||||
ticks: u64,
|
||||
focused: bool,
|
||||
) {
|
||||
if !self.abilities.shows(source) {
|
||||
return;
|
||||
}
|
||||
// ChatComponent.logChatMessage: every message shown is in the log.
|
||||
crate::logging::write_only(log::Level::Info, &format!("[CHAT] {}", content.plain()));
|
||||
if self.log {
|
||||
println!("[CHAT] {}", content.plain());
|
||||
@@ -204,16 +237,17 @@ impl Chat {
|
||||
added: ticks,
|
||||
content,
|
||||
indicator,
|
||||
source,
|
||||
};
|
||||
self.add_lines(atlas, &message, focused);
|
||||
self.add_lines(font, &message, focused);
|
||||
self.messages.insert(0, message);
|
||||
self.messages.truncate(100);
|
||||
self.received += 1;
|
||||
}
|
||||
fn add_lines(&mut self, atlas: &HudAtlas, message: &Message, focused: bool) {
|
||||
fn add_lines(&mut self, font: &crate::font::Font, message: &Message, focused: bool) {
|
||||
let width = self.max_width();
|
||||
let content = self.displayed(&message.content);
|
||||
for line in atlas.font.wrap_components(&content, width) {
|
||||
for line in font.wrap_components(&content, width) {
|
||||
if focused && self.scroll > 0 {
|
||||
self.new_since_scroll = true;
|
||||
self.scroll_by(1, focused);
|
||||
@@ -239,16 +273,38 @@ impl Chat {
|
||||
content.strip_legacy()
|
||||
}
|
||||
}
|
||||
/// Re-wrap after a width or scale change (ChatComponent.rescaleChat).
|
||||
/// Re-wrap after a width or scale change (ChatComponent.rescaleChat),
|
||||
/// keeping only the messages the Chat option shows.
|
||||
pub fn rescale(&mut self, atlas: &HudAtlas) {
|
||||
self.rewrap(&atlas.font);
|
||||
}
|
||||
fn rewrap(&mut self, font: &crate::font::Font) {
|
||||
self.scroll = 0;
|
||||
self.new_since_scroll = false;
|
||||
self.lines.clear();
|
||||
let messages: Vec<Message> = self.messages.iter().rev().cloned().collect();
|
||||
let messages: Vec<Message> = self
|
||||
.messages
|
||||
.iter()
|
||||
.rev()
|
||||
.filter(|m| self.abilities.shows(m.source))
|
||||
.cloned()
|
||||
.collect();
|
||||
for m in &messages {
|
||||
self.add_lines(atlas, m, false);
|
||||
self.add_lines(font, m, false);
|
||||
}
|
||||
}
|
||||
/// `ChatComponent.setVisibleMessageFilter`: the Chat option changed;
|
||||
/// the messages it now hides leave the chat, those it shows again return.
|
||||
pub fn set_abilities(&mut self, font: &crate::font::Font, abilities: ChatAbilities) {
|
||||
if self.abilities != abilities {
|
||||
self.abilities = abilities;
|
||||
self.rewrap(font);
|
||||
}
|
||||
}
|
||||
/// Whether a message from `source` would show (`visibleMessagesFilter`).
|
||||
pub fn shows(&self, source: ChatSource) -> bool {
|
||||
self.abilities.shows(source)
|
||||
}
|
||||
pub fn clear(&mut self) {
|
||||
self.messages.clear();
|
||||
self.lines.clear();
|
||||
|
||||
@@ -412,3 +412,27 @@ fn restricted_input_says_why_it_will_not_send() {
|
||||
.any(|line| line == "chat_screen.commands_not_allowed")
|
||||
);
|
||||
}
|
||||
|
||||
/// `ChatComponent.setVisibleMessageFilter`: a message the Chat option hides
|
||||
/// on arrival is dropped; one already in the chat leaves it while the option
|
||||
/// hides its kind and comes back when it shows it again.
|
||||
#[test]
|
||||
fn the_chat_option_filters_the_lines_both_ways() {
|
||||
let font = font();
|
||||
let mut chat = Chat::default();
|
||||
let says = |chat: &mut Chat, text: &str, source| {
|
||||
chat.add_message(&font, Text::literal(text), None, source, 0, false);
|
||||
};
|
||||
let shown =
|
||||
|chat: &Chat| -> Vec<String> { chat.lines.iter().map(|l| l.content.plain()).collect() };
|
||||
says(&mut chat, "player", ChatSource::Player);
|
||||
says(&mut chat, "server", ChatSource::Server);
|
||||
chat.set_abilities(&font, ChatAbilities::new(ChatVisibility::System));
|
||||
assert_eq!(shown(&chat), ["server"]);
|
||||
says(&mut chat, "dropped", ChatSource::Player);
|
||||
chat.set_abilities(&font, ChatAbilities::new(ChatVisibility::Hidden));
|
||||
says(&mut chat, "client", ChatSource::Client);
|
||||
assert_eq!(shown(&chat), ["client"]);
|
||||
chat.set_abilities(&font, ChatAbilities::new(ChatVisibility::Full));
|
||||
assert_eq!(shown(&chat), ["client", "server", "player"]);
|
||||
}
|
||||
|
||||
@@ -9136,6 +9136,9 @@ impl Game<'_> {
|
||||
)
|
||||
}
|
||||
fn apply_settings(&mut self) {
|
||||
// LocalPlayer.refreshChatAbilities: the chat shows what the Chat option allows.
|
||||
let abilities = self.chat_abilities();
|
||||
self.ui.chat.set_abilities(&self.gui.atlas.font, abilities);
|
||||
if let Some(r) = &self.renderer {
|
||||
self.settings
|
||||
.clamp_gui_scale([r.config.width as i32, r.config.height as i32]);
|
||||
|
||||
@@ -463,9 +463,8 @@ impl Game<'_> {
|
||||
self.chat_line(text, ChatSource::Server);
|
||||
return;
|
||||
}
|
||||
// ChatListener.handleSystemMessage: the Chat option's Hidden level
|
||||
// takes the server's messages too.
|
||||
if !self.chat_abilities().receive_system_messages {
|
||||
// The Chat option's Hidden level takes the server's messages too.
|
||||
if !self.ui.chat.shows(ChatSource::Server) {
|
||||
return;
|
||||
}
|
||||
let focused = self.screen == Screen::Chat;
|
||||
@@ -474,6 +473,7 @@ impl Game<'_> {
|
||||
&self.gui.atlas,
|
||||
text,
|
||||
Some(SYSTEM_INDICATOR),
|
||||
ChatSource::Server,
|
||||
self.ticks,
|
||||
focused,
|
||||
);
|
||||
@@ -610,9 +610,14 @@ impl Game<'_> {
|
||||
);
|
||||
let focused = self.screen == Screen::Chat;
|
||||
let heard = self.mods.running().then(|| line.plain());
|
||||
self.ui
|
||||
.chat
|
||||
.add(&self.gui.atlas, line, None, self.ticks, focused);
|
||||
self.ui.chat.add(
|
||||
&self.gui.atlas,
|
||||
line,
|
||||
None,
|
||||
ChatSource::Player,
|
||||
self.ticks,
|
||||
focused,
|
||||
);
|
||||
if let Some(line) = heard {
|
||||
self.mods_chat(&line);
|
||||
}
|
||||
@@ -916,9 +921,14 @@ impl Game<'_> {
|
||||
Err(e) => Text::translate("screenshot.failure", &[Text::literal(e.clone())]),
|
||||
};
|
||||
let focused = self.screen == Screen::Chat;
|
||||
self.ui
|
||||
.chat
|
||||
.add(&self.gui.atlas, message, None, self.ticks, focused);
|
||||
self.ui.chat.add(
|
||||
&self.gui.atlas,
|
||||
message,
|
||||
None,
|
||||
ChatSource::Client,
|
||||
self.ticks,
|
||||
focused,
|
||||
);
|
||||
}
|
||||
}
|
||||
/// `yyyy-MM-dd_HH.mm.ss[_n].png` (Util.getFilenameFormattedDateTime, local time
|
||||
|
||||
@@ -2836,9 +2836,14 @@ impl Game<'_> {
|
||||
capture.push(line.clone());
|
||||
}
|
||||
let focused = self.screen == Screen::Chat;
|
||||
self.ui
|
||||
.chat
|
||||
.add(&self.gui.atlas, line, None, self.ticks, focused);
|
||||
self.ui.chat.add(
|
||||
&self.gui.atlas,
|
||||
line,
|
||||
None,
|
||||
horizon_client::chat::ChatSource::Player,
|
||||
self.ticks,
|
||||
focused,
|
||||
);
|
||||
Ok(1)
|
||||
}
|
||||
/// MessageArgument: `@p`-style selectors in a message become names.
|
||||
|
||||
@@ -388,7 +388,10 @@ impl Restrictions {
|
||||
1 => Some("chat_restriction.chat_disabled_by_options"),
|
||||
_ => None,
|
||||
};
|
||||
let (chat, commands) = (chat_visibility == 0, chat_visibility < 2);
|
||||
let abilities = horizon_client::chat::ChatAbilities::new(
|
||||
horizon_client::chat::ChatVisibility::from_option(chat_visibility),
|
||||
);
|
||||
let (chat, commands) = (abilities.send_messages, abilities.send_commands);
|
||||
// The body's widgets: text blocks (lines, top) and the action's top.
|
||||
let mut blocks = Vec::new();
|
||||
let mut action = None;
|
||||
|
||||
@@ -798,20 +798,14 @@ impl Game<'_> {
|
||||
Some(stack)
|
||||
}
|
||||
/// A line in the connection's chat, with its styles, click and hover
|
||||
/// events (ChatComponent.addMessage). `ChatListener` drops the lines the
|
||||
/// Chat option hides before they reach the chat.
|
||||
/// events (ChatComponent.addMessage). The lines the Chat option hides
|
||||
/// are dropped there (its `visibleMessageFilter`).
|
||||
pub(super) fn chat_line(
|
||||
&mut self,
|
||||
content: horizon_client::text::Text,
|
||||
source: horizon_client::chat::ChatSource,
|
||||
) {
|
||||
let abilities = self.chat_abilities();
|
||||
let allowed = match source {
|
||||
horizon_client::chat::ChatSource::Player => abilities.receive_player_messages,
|
||||
horizon_client::chat::ChatSource::Server => abilities.receive_system_messages,
|
||||
horizon_client::chat::ChatSource::Client => true,
|
||||
};
|
||||
if !allowed {
|
||||
if !self.ui.chat.shows(source) {
|
||||
return;
|
||||
}
|
||||
let text = content.plain();
|
||||
@@ -820,6 +814,7 @@ impl Game<'_> {
|
||||
&self.gui.atlas,
|
||||
content,
|
||||
None,
|
||||
source,
|
||||
self.ticks,
|
||||
self.screen == Screen::Chat,
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user