167 lines
4.5 KiB
Rust
167 lines
4.5 KiB
Rust
use crossterm::style::{Color, ContentStyle, Stylize};
|
|
use time::OffsetDateTime;
|
|
use toss::styled::Styled;
|
|
|
|
use crate::store::Msg;
|
|
use crate::ui::ChatMsg;
|
|
|
|
use super::api::{Snowflake, Time};
|
|
use super::util;
|
|
|
|
fn nick_char(ch: char) -> bool {
|
|
// Closely following the heim mention regex:
|
|
// https://github.com/euphoria-io/heim/blob/978c921063e6b06012fc8d16d9fbf1b3a0be1191/client/lib/stores/chat.js#L14-L15
|
|
match ch {
|
|
',' | '.' | '!' | '?' | ';' | '&' | '<' | '\'' | '"' => false,
|
|
_ => !ch.is_whitespace(),
|
|
}
|
|
}
|
|
|
|
fn nick_char_(ch: Option<&char>) -> bool {
|
|
ch.filter(|c| nick_char(**c)).is_some()
|
|
}
|
|
|
|
fn room_char(ch: char) -> bool {
|
|
// Basically just \w, see also
|
|
// https://github.com/euphoria-io/heim/blob/978c921063e6b06012fc8d16d9fbf1b3a0be1191/client/lib/ui/MessageText.js#L66
|
|
ch.is_ascii_alphanumeric() || ch == '_'
|
|
}
|
|
|
|
fn room_char_(ch: Option<&char>) -> bool {
|
|
ch.filter(|c| room_char(**c)).is_some()
|
|
}
|
|
|
|
// TODO Allocate less?
|
|
fn highlight_content(content: &str, base_style: ContentStyle) -> Styled {
|
|
let mut result = Styled::default();
|
|
let mut current = String::new();
|
|
let mut chars = content.chars().peekable();
|
|
let mut possible_room_or_mention = true;
|
|
|
|
while let Some(char) = chars.next() {
|
|
match char {
|
|
'@' if possible_room_or_mention && nick_char_(chars.peek()) => {
|
|
result = result.then(¤t, base_style);
|
|
current.clear();
|
|
|
|
let mut nick = String::new();
|
|
while let Some(ch) = chars.peek() {
|
|
if nick_char(*ch) {
|
|
nick.push(*ch);
|
|
} else {
|
|
break;
|
|
}
|
|
chars.next();
|
|
}
|
|
|
|
let (r, g, b) = util::nick_color(&nick);
|
|
let style = base_style.with(Color::Rgb { r, g, b }).bold();
|
|
result = result.then("@", style).then(nick, style);
|
|
}
|
|
'&' if possible_room_or_mention && room_char_(chars.peek()) => {
|
|
result = result.then(¤t, base_style);
|
|
current.clear();
|
|
|
|
let mut room = "&".to_string();
|
|
while let Some(ch) = chars.peek() {
|
|
if room_char(*ch) {
|
|
room.push(*ch);
|
|
} else {
|
|
break;
|
|
}
|
|
chars.next();
|
|
}
|
|
|
|
let style = base_style.blue().bold();
|
|
result = result.then(room, style);
|
|
}
|
|
_ => current.push(char),
|
|
}
|
|
|
|
// More permissive than the heim web client
|
|
possible_room_or_mention = !char.is_alphanumeric();
|
|
}
|
|
|
|
result = result.then(current, base_style);
|
|
|
|
result
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub struct SmallMessage {
|
|
pub id: Snowflake,
|
|
pub parent: Option<Snowflake>,
|
|
pub time: Time,
|
|
pub nick: String,
|
|
pub content: String,
|
|
}
|
|
|
|
fn as_me(content: &str) -> Option<&str> {
|
|
content.strip_prefix("/me")
|
|
}
|
|
|
|
fn style_me() -> ContentStyle {
|
|
ContentStyle::default().grey().italic()
|
|
}
|
|
|
|
fn styled_nick(nick: &str) -> Styled {
|
|
Styled::new_plain("[")
|
|
.then(nick, util::nick_style(nick))
|
|
.then_plain("]")
|
|
}
|
|
|
|
fn styled_nick_me(nick: &str) -> Styled {
|
|
let style = style_me();
|
|
Styled::new("*", style).then(nick, util::nick_style(nick).italic())
|
|
}
|
|
|
|
fn styled_content(content: &str) -> Styled {
|
|
highlight_content(content.trim(), ContentStyle::default())
|
|
}
|
|
|
|
fn styled_content_me(content: &str) -> Styled {
|
|
let style = style_me();
|
|
highlight_content(content.trim(), style).then("*", style)
|
|
}
|
|
|
|
fn styled_editor_content(content: &str) -> Styled {
|
|
highlight_content(content, ContentStyle::default())
|
|
}
|
|
|
|
impl Msg for SmallMessage {
|
|
type Id = Snowflake;
|
|
|
|
fn id(&self) -> Self::Id {
|
|
self.id
|
|
}
|
|
|
|
fn parent(&self) -> Option<Self::Id> {
|
|
self.parent
|
|
}
|
|
|
|
fn last_possible_id() -> Self::Id {
|
|
Snowflake::MAX
|
|
}
|
|
}
|
|
|
|
impl ChatMsg for SmallMessage {
|
|
fn time(&self) -> OffsetDateTime {
|
|
self.time.0
|
|
}
|
|
|
|
fn styled(&self) -> (Styled, Styled) {
|
|
Self::pseudo(&self.nick, &self.content)
|
|
}
|
|
|
|
fn edit(nick: &str, content: &str) -> (Styled, Styled) {
|
|
(styled_nick(nick), styled_editor_content(content))
|
|
}
|
|
|
|
fn pseudo(nick: &str, content: &str) -> (Styled, Styled) {
|
|
if let Some(content) = as_me(content) {
|
|
(styled_nick_me(nick), styled_content_me(content))
|
|
} else {
|
|
(styled_nick(nick), styled_content(content))
|
|
}
|
|
}
|
|
}
|