use ratatui::text::{ Line, Span };
use crate::
{
t,
tn,
options,
network::
{
codes::OnlineUser,
client::{ ClientEvent, image as client_image },
},
};
use super::
{
theme,
state::{ self, App },
tofu::Prompt,
login::Stage,
};
impl App
{
pub fn apply(&mut self, event: ClientEvent)
{
match event
{
ClientEvent::Register =>
{
if let Some(login) = self.login.as_mut() { login.ask(Stage::Password { register: true }, None); }
self.answer_step(Stage::Password { register: true });
self.dirty = true;
},
ClientEvent::Login =>
{
if let Some(login) = self.login.as_mut() { login.ask(Stage::Password { register: false }, None); }
self.answer_step(Stage::Password { register: false });
self.dirty = true;
},
ClientEvent::FirstUser =>
{
self.push_styled(t!("event.first_user"), theme::notice());
},
ClientEvent::Authenticated(role) =>
{
self.login = None; self.role = role;
self.push_styled(t!("event.login_ok"), theme::ok());
self.reconnect.accepted();
self.icon_request = true;
},
ClientEvent::Connected(server_name) =>
{
self.push_styled(t!("event.connected", server_name), theme::ok());
self.server_name = server_name;
},
ClientEvent::Message(message, username, id, message_id, colors, channel, timestamp, reply) =>
{
match channel.map(Option::unwrap_or_default)
{
Some(channel) if channel != self.channel =>
self.park_entry(channel, state::Entry::Message { username, id, message_id, timestamp, text: message, colors, reply, hearts: Vec::new(), edited: false }),
_ =>
{
self.stopped_typing(&username);
self.push_message(username, id, message_id, timestamp, message, colors, reply);
},
}
},
ClientEvent::Typing(username) => self.set_typing(username),
ClientEvent::ImageDisplay(username, filename, message_id, timestamp, image, color) =>
self.push_image(username, filename, message_id, timestamp, image, color),
ClientEvent::ImagePending(username, filename, message_id, timestamp, hash, color) =>
{
self.push_caption(username, filename, message_id, timestamp, hash, state::Picture::Waiting, color);
self.image_requests.push(hash);
},
ClientEvent::ImageOffer(username, filename, message_id, timestamp, hash, color) =>
self.push_caption(username, filename, message_id, timestamp, hash, state::Picture::Absent, color),
ClientEvent::ImageParked(channel, username, filename, message_id, timestamp, hash, username_color) =>
{
let picture = match client_image::auto_show_images()
{
true => state::Picture::Deferred,
false => state::Picture::Absent,
};
match channel == self.channel
{
true => self.push_caption(username, filename, message_id, timestamp, hash, picture, username_color),
false => self.park_entry(channel, state::Entry::Image { username, filename, message_id, timestamp, username_color, hash: Some(hash), picture, hearts: Vec::new() }),
}
},
ClientEvent::ImageRequest(hash) => self.image_requests.push(hash),
ClientEvent::ImageFailed(username, filename, ..) => self.push_styled(
t!("event.image_failed", username, filename), theme::error()),
ClientEvent::PrivateMessageSent(to, id, msg, colors) => self.push_private(true, to, id, msg, colors),
ClientEvent::PrivateMessageRecv(from, id, msg, colors) => self.push_private(false, from, id, msg, colors),
ClientEvent::TofuPrompt(request) =>
{
self.tofu = Some(Prompt::new(request));
self.dirty = true;
},
ClientEvent::TofuError => self.quit(1, None),
ClientEvent::ReconnectFailed => self.disconnected(t!("event.reconnect_failed")),
ClientEvent::HandshakeFailed(reason) => self.disconnected(reason),
ClientEvent::TofuSkip(hash) =>
{
self.push_styled(t!("event.tofu_skip.title"), theme::error());
self.push_styled(t!("event.tofu_skip.body"), theme::notice());
self.push_styled(hash, theme::notice());
},
ClientEvent::VoiceActivity(users) =>
{
self.voice_activity = users;
self.rebuild_voice();
},
ClientEvent::VoiceRoster(clients) =>
{
self.voice_roster = clients.into_iter().collect();
self.rebuild_voice();
},
ClientEvent::VoiceJoin(id, username) =>
{
self.voice_roster.insert(id, username);
self.rebuild_voice();
},
ClientEvent::VoiceLeave(id) =>
{
self.voice_roster.remove(&id);
self.rebuild_voice();
},
ClientEvent::Join(uname, username_color, id, device, role) =>
{
if let Some(device) = device.clone() { self.devices.insert(uname.clone(), device); }
self.push(Line::from(vec!
[
Span::styled(format!("[{}] ", options::get_server_username()), theme::dim()),
Span::styled(t!("event.joined", username = &uname), theme::ok()),
]));
self.offline.remove(&uname);
if !self.online.iter().any(|user| user.id == id)
{
self.online.push(OnlineUser
{
username: uname,
username_color,
id,
channel: None, device,
role,
});
self.sort_online();
}
},
ClientEvent::Leave(uname, id, registered) =>
{
self.push(Line::from(vec!
[
Span::styled(format!("[{}] ", options::get_server_username()), theme::dim()),
Span::styled(t!("event.left", username = &uname), theme::dim()),
]));
let color = self.online.iter().find(|user| user.id == id).and_then(|user| user.username_color);
self.online.retain(|user| user.id != id);
self.devices.remove(&uname);
self.stopped_typing(&uname);
if self.offline_listed && registered { self.offline.insert(uname, color); }
if self.voice_roster.remove(&id).is_some() { self.rebuild_voice(); }
self.channels = self.online.iter().filter_map(|user| user.channel.clone()).collect();
self.prune_panes();
},
ClientEvent::Muted =>
{
self.push_styled(t!("event.muted"), theme::notice());
},
ClientEvent::Deleted(message_id) => self.delete_message(message_id),
ClientEvent::Hearts(message_id, hearts) => self.set_hearts(message_id, hearts),
ClientEvent::Edited(message_id, text) => self.edit_message(message_id, text),
ClientEvent::InvalidUsage =>
{
self.settings.saving = false;
self.push_styled(t!("invalid.usage"), theme::error());
},
ClientEvent::UnsafeVersion(newer_versions, current_version, newest_version) =>
{
self.push_styled(tn!("event.unsafe_version", newer_versions, current_version, newest_version), theme::notice());
},
ClientEvent::Username(disabled_registration, min_uname, max_uname) =>
{
let hint = if disabled_registration
{
t!("login.registration_disabled").to_owned()
} else
{
t!("login.username_rules", min = min_uname, max = max_uname)
};
if let Some(login) = self.login.as_mut() { login.ask(Stage::Username, Some(hint)); }
self.answer_step(Stage::Username);
self.dirty = true;
},
ClientEvent::VoiceEnabled =>
{
self.voice_enabled = true;
self.rebuild_voice();
self.push_styled(t!("event.voice_enabled"), theme::ok());
},
ClientEvent::VoiceDeviceFailed =>
{
self.push_styled(t!("event.device_failed"), theme::error());
#[cfg(feature = "client_voice")]
self.settings.refresh_devices();
},
ClientEvent::VoiceHandshakeFailed =>
{
self.push_styled(t!("event.voice_handshake_failed"), theme::error());
},
ClientEvent::VoiceDisabled =>
{
self.voice_enabled = false;
self.voice_activity.clear();
self.rebuild_voice();
self.push_styled(t!("event.voice_disabled"), theme::dim());
},
ClientEvent::ServerSay(message) =>
{
self.push(Line::from(vec!
[
Span::styled(format!("[{}] ", options::get_server_username()), theme::dim()),
Span::styled(message, theme::notice()),
]));
},
ClientEvent::Role(role, username) =>
{
self.online.iter_mut().filter(|user| user.username == username).for_each(|user| user.role = role);
self.sort_online();
self.dirty = true;
if self.settings.open && self.settings.profile() && self.settings.subject == username { self.settings.role = role; }
if username == self.username
{
self.role = role;
self.push_styled(t!("event.role", role), theme::notice());
}
},
ClientEvent::History(messages, start, more, kept, older) =>
{
let auto_show = client_image::auto_show_images();
let entries = messages.into_iter().map(|message| match message.image
{
Some(hash) => state::Entry::Image
{
username: message.username,
filename: message.text,
message_id: message.message_id,
timestamp: message.timestamp,
username_color: message.colors.username_color,
hash: Some(hash),
picture: match auto_show
{
true => state::Picture::Deferred,
false => state::Picture::Absent,
},
hearts: message.hearts,
},
None => state::Entry::History
{
username: message.username,
message_id: message.message_id,
timestamp: message.timestamp,
text: message.text,
colors: message.colors,
reply: message.reply,
hearts: message.hearts,
edited: message.edited,
},
}).collect();
match older
{
true => self.prepend_history(entries, start, more),
false =>
{
self.push_styled(t!("event.history", count = kept), theme::title());
self.start_history(entries, start, more);
},
}
},
ClientEvent::ImageData(hash, image) =>
{
self.fetched(&hash);
if self.wants_icon(&hash) { self.deliver_icon(hash, image.clone()); }
match self.wants_avatar(&hash)
{
true => self.deliver_avatar(hash, image),
false => self.deliver_image(hash, image),
}
},
ClientEvent::ServerSettings(settings, saved) =>
{
match saved
{
true =>
{
if self.settings.open && self.settings.server() { self.settings.stored(settings); }
self.push_styled(t!("event.server_settings_saved"), theme::ok());
},
false => self.settings.open_server(settings),
}
self.dirty = true;
},
ClientEvent::Passwd(changed) =>
{
match changed
{
true =>
{
if let Some(form) = self.account.take() { self.reconnect.passwd(form.new_password()); }
self.push_styled(t!("event.password_changed"), theme::ok());
},
false =>
{
let message = t!("event.password_refused").to_owned();
match self.account.as_mut()
{
Some(form) => form.rejected(message),
None => self.push_styled(message, theme::error()),
}
},
}
self.dirty = true;
},
ClientEvent::AccountDeleted(deleted) =>
{
match deleted
{
true =>
{
self.account = None;
self.disconnect_reason = Some(t!("event.account_deleted").to_owned());
},
false =>
{
let message = t!("event.wrong_password").to_owned();
match self.account.as_mut()
{
Some(form) => form.rejected(message),
None => self.push_styled(message, theme::error()),
}
},
}
self.dirty = true;
},
ClientEvent::Profile(username, profile, own, saved) =>
{
match saved
{
true =>
{
if self.settings.open && self.settings.profile()
{
self.settings.stored_profile(username, profile);
}
self.push_styled(t!("event.profile_saved"), theme::ok());
},
false => self.settings.open_profile(username, profile, own),
}
if self.settings.open && self.settings.picture.is_none()
&& let Some(hash) = self.settings.avatar
{
self.image_loads.push(hash);
}
self.dirty = true;
},
ClientEvent::AvatarFailed(error) =>
{
self.settings.saving = false;
self.push_styled(error, theme::error());
},
ClientEvent::Colors(Some(colors)) =>
{
self.color = colors.username_color;
self.dirty = true;
},
ClientEvent::ServerIcon(hash, save) =>
{
if save
{
self.push_styled(match hash
{
Some(_) => t!("event.server_icon_set"),
None => t!("event.server_icon_removed"),
}, theme::ok());
}
self.set_server_icon(hash);
},
ClientEvent::Colors(None) => self.push_styled(t!("event.color_set"), theme::ok()),
ClientEvent::ServerBans(users, ips) =>
{
if users.is_empty() && ips.is_empty()
{
self.push_styled(t!("event.bans.none"), theme::dim());
} else
{
self.push_styled(t!("event.bans.title", count = users.len() + ips.len()), theme::title());
let sections = [(t!("event.bans.users"), users), (t!("event.bans.addresses"), ips)];
let last_section = sections.iter().filter(|(_, bans)| !bans.is_empty()).count().saturating_sub(1);
let mut section_index = 0;
for (name, bans) in sections
{
if bans.is_empty() { continue; }
let last = section_index == last_section;
section_index += 1;
self.push(Line::from(vec!
[
Span::styled(super::branch(last), theme::border()),
Span::raw(name),
]));
let trunk = format!("{} ", if last { " " } else { "│" });
let width = id_width(bans.iter().map(|ban| ban.id));
let last_ban = bans.len() - 1;
for (index, ban) in bans.into_iter().enumerate()
{
let mut spans = vec![Span::styled(format!("{trunk}{}", super::branch(index == last_ban)), theme::border())];
spans.extend(id_column(ban.id, width));
spans.push(Span::raw(ban.subject));
self.push(Line::from(spans));
}
}
}
},
ClientEvent::List(users, registered) =>
{
self.online = users;
self.offline_listed = registered.is_some();
self.offline = registered.unwrap_or_default().into_iter()
.map(|user| (user.username, user.username_color)).collect();
self.sort_online();
self.dirty = true;
self.devices = self.online.iter()
.filter_map(|user| Some((user.username.clone(), user.device.clone()?)))
.collect();
self.channels = self.online.iter().filter_map(|user| user.channel.clone()).collect();
self.prune_panes();
if self.list_requested
{
self.list_requested = false;
let here = options::get_channel();
let width = id_width(self.online.iter().map(|user| user.id));
let last = self.online.len().saturating_sub(1);
self.push_styled(t!("event.list.title", count = self.online.len()), theme::title());
let rows = self.online.iter().enumerate().map(|(index, user)|
{
let mut spans = vec![Span::styled(super::branch(index == last), theme::border())];
spans.extend(id_column(user.id, width));
spans.push(self.theme.colorize(user.username.clone(), user.username_color));
if let Some(device) = self.devices.get(&user.username)
{
spans.push(Span::styled(format!(" {}", super::device_label(device)), theme::dim()));
}
if let Some(channel) = user.channel.clone()
{
let style = if channel == here { theme::accent() } else { theme::dim() };
spans.push(Span::styled(format!(" #{channel}"), style));
}
Line::from(spans)
}).collect::<Vec<Line<'static>>>();
for row in rows { self.push(row); }
}
self.dirty = true;
},
ClientEvent::Upload(uid, filename, size) =>
{
self.push_transfer(uid, filename, size, true, false);
},
ClientEvent::Image(uid, filename, size) =>
{
self.push_transfer(uid, filename, size, true, true);
},
ClientEvent::UploadDone(uid, _) =>
{
self.finish_transfer(uid, true);
},
ClientEvent::TransferProgress(uid, done) =>
{
self.update_transfer(uid, done);
},
ClientEvent::Uploaded(username, filename) =>
{
self.push(Line::from(vec!
[
Span::styled(format!("[{}] ", options::get_server_username()), theme::dim()),
Span::raw(t!("event.uploaded", username, filename)),
]));
},
ClientEvent::Download(uid, filename, size) =>
{
self.push_transfer(uid, filename, size, false, false);
},
ClientEvent::Downloaded(uid, _) =>
{
self.finish_transfer(uid, true);
},
ClientEvent::DownloadFailed(uid, _) =>
{
self.finish_transfer(uid, false);
},
ClientEvent::Files(users) =>
{
if users.is_empty()
{
self.push_styled(t!("event.files.none"), theme::dim());
} else
{
self.push_styled(t!("event.files.title", count = users.len()), theme::title());
let width = id_width(users.iter().map(|user| user.id));
let last = users.len() - 1;
for (index, user) in users.into_iter().enumerate()
{
let mut spans = vec![Span::styled(super::branch(index == last), theme::border())];
spans.extend(id_column(user.id, width));
spans.push(Span::raw(user.username.clone()));
self.push(Line::from(spans));
let trunk = format!("{} ", if index == last { " " } else { "│" });
let file_width = id_width(user.upload.iter().map(|(_, id)| *id));
let last_file = user.upload.len().saturating_sub(1);
for (file, (filename, file_id)) in user.upload.into_iter().enumerate()
{
let mut spans = vec![Span::styled(format!("{trunk}{}", super::branch(file == last_file)), theme::border())];
spans.extend(id_column(file_id, file_width));
spans.push(Span::raw(filename));
self.push(Line::from(spans));
}
}
}
},
ClientEvent::Screens(users) =>
{
#[cfg(feature = "client_screen")]
{ self.screens_requested = false; }
if users.is_empty()
{
self.push_styled(t!("event.screens.none"), theme::dim());
} else
{
self.push_styled(t!("event.screens.title", count = users.len()), theme::title());
let width = id_width(users.iter().map(|user| user.id));
let last = users.len() - 1;
for (index, user) in users.into_iter().enumerate()
{
let mut spans = vec![Span::styled(super::branch(index == last), theme::border())];
spans.extend(id_column(user.id, width));
spans.push(Span::raw(user.username));
self.push(Line::from(spans));
}
}
},
ClientEvent::UploadLimit =>
{
self.push_styled(t!("event.upload_limit"), theme::error());
},
ClientEvent::Screen(enabled) =>
{
self.push_styled(if enabled { t!("event.screen.started") } else { t!("event.screen.stopped") }, theme::ok());
},
ClientEvent::ScreenFailed(reason) =>
{
self.push_styled(t!("event.screen.failed", reason), theme::error());
},
ClientEvent::Attach(username) =>
{
self.push_text(t!("event.screen.attach", username));
},
ClientEvent::Deattach(username) =>
{
self.push_text(t!("event.screen.deattach", username));
},
ClientEvent::Screenshare(username) if username != self.username =>
{
self.push(Line::from(vec!
[
Span::styled(format!("[{}] ", options::get_server_username()), theme::dim()),
Span::styled(t!("event.screen.peer_started", username), theme::notice()),
]));
},
ClientEvent::ScreenshareEnd(username) if username != self.username =>
{
self.push(Line::from(vec!
[
Span::styled(format!("[{}] ", options::get_server_username()), theme::dim()),
Span::styled(t!("event.screen.peer_stopped", username), theme::dim()),
]));
},
ClientEvent::Screenshare(_) | ClientEvent::ScreenshareEnd(_) => {},
ClientEvent::Attached(username) =>
{
self.push_text(t!("event.screen.attached", username));
},
ClientEvent::Deattached(username) =>
{
self.push_text(t!("event.screen.deattached", username));
},
ClientEvent::IncompatibleVersion(version, server_version) =>
{
self.disconnect_reason = Some(t!("event.incompatible_version", version, server_version));
},
ClientEvent::VersionMismatch(client_version, server_version) =>
{
self.push_styled(t!("event.version_mismatch", client_version, server_version), theme::notice());
},
ClientEvent::UsernameRejected =>
{
match self.login.as_mut()
{
Some(login) => login.error = Some(t!("login.username_rejected").to_owned()),
None => self.push_styled(t!("login.username_rejected"), theme::error()),
}
self.reconnect.forget();
self.dirty = true;
},
ClientEvent::PasswordRejected(min_pass) =>
{
let message = t!("login.password_rejected", min = min_pass);
match self.login.as_mut()
{
Some(login) => login.error = Some(message),
None => self.push_styled(message, theme::error()),
}
self.dirty = true;
},
ClientEvent::SpamWarning =>
{
self.push_styled(t!("event.spam_warning"), theme::notice());
},
ClientEvent::Socks5Voice =>
{
self.push_styled(t!("event.socks5_voice"), theme::error());
},
ClientEvent::DisabledFeature =>
{
self.push_styled(t!("event.disabled_feature"), theme::error());
},
ClientEvent::VersionFailed =>
{
self.push_styled(t!("event.version_failed"), theme::notice());
},
ClientEvent::Quit(said) =>
{
if self.leaving
{
self.quit(0, Some(t!("event.quit").to_owned()));
} else if self.logging_out
{
self.disconnected(t!("event.logged_out"));
} else
{
if said { self.reconnect.forget(); }
let reason = self.disconnect_reason.take();
self.disconnected(reason.unwrap_or_else(|| t!("event.server_quit").to_owned()));
}
},
ClientEvent::ChannelChanged(channel) =>
{
self.switch_channel(channel.clone().unwrap_or_default());
if let Some(name) = channel.clone() { self.channels.insert(name); }
let me = self.username.clone();
if let Some(user) = self.online.iter_mut().find(|user| user.username == me)
{
user.channel = channel;
}
self.voice_roster.clear();
self.voice_activity.clear();
self.rebuild_voice();
},
ClientEvent::ChannelCreated(name) =>
{
self.channels.insert(name);
self.dirty = true;
},
ClientEvent::ChannelDestroyed(name) =>
{
self.channels.remove(&name);
self.panes.remove(&name);
self.dirty = true;
},
}
}
pub fn quit(&mut self, code: i32, message: Option<String>)
{
self.should_quit = true;
self.exit_code = code;
self.quit_message = message;
self.dirty = true;
}
}
fn id_width(ids: impl Iterator<Item = usize>) -> usize
{
ids.map(|id| id.to_string().len()).max().unwrap_or(1)
}
fn id_column(id: usize, width: usize) -> Vec<Span<'static>>
{
vec![Span::styled(format!("{id:>width$} "), theme::dim())]
}