Skip to content

Commit

Permalink
fix(chat): Fix user tagging in messages (#1765)
Browse files Browse the repository at this point in the history
  • Loading branch information
Flemmli97 authored Feb 1, 2024
1 parent 45d4e10 commit b100aa3
Show file tree
Hide file tree
Showing 15 changed files with 227 additions and 150 deletions.
49 changes: 4 additions & 45 deletions common/src/state/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ pub use chats::{Chat, Chats};
use dioxus_desktop::tao::window::WindowId;
pub use friends::Friends;
pub use identity::Identity;
use regex::Regex;
pub use route::Route;
pub use settings::Settings;
pub use ui::{Theme, ToastNotification, UI};
Expand Down Expand Up @@ -280,12 +279,7 @@ impl State {
m.set_conversation_id(id);
m.set_sender(sender);
m.set_lines(msg);
let m = ui_adapter::Message {
inner: m,
in_reply_to: None,
key: Uuid::new_v4().to_string(),
..Default::default()
};
let m = ui_adapter::Message::new(m, None, Uuid::new_v4().to_string());
self.add_msg_to_chat(id, m);
}
// ===== Media =====
Expand Down Expand Up @@ -452,13 +446,8 @@ impl State {
conversation_id,
mut message,
} => {
if let Some(ids) = self
.get_chat_by_id(conversation_id)
.map(|c| self.chat_participants(&c))
{
message.insert_did(&ids, &self.get_own_identity().did_key());
}
let ping = message.is_mention;
let own = self.get_own_identity().did_key();
let ping = message.is_mention_self(&own);
self.update_identity_status_hack(&message.inner.sender());
let id = self.identities.get(&message.inner.sender()).cloned();
// todo: don't load all the messages by default. if the user scrolled up, for example, this incoming message may not need to be fetched yet.
Expand Down Expand Up @@ -535,15 +524,6 @@ impl State {
self.update_identity_status_hack(&message.inner.sender());
let own = self.get_own_identity().did_key();
if let Some(chat) = self.chats.all.get_mut(&conversation_id) {
message.insert_did(
&chat
.participants
.iter()
.filter_map(|id| self.identities.get(id))
.cloned()
.collect::<Vec<_>>(),
&own,
);
let id = message.inner.id();
if let Some(msg) = chat.messages.iter_mut().find(|msg| msg.inner.id() == id) {
*msg = message.clone();
Expand All @@ -559,7 +539,7 @@ impl State {
*msg = message.inner.clone();
}

if message.is_mention {
if message.is_mention_self(&own) {
if let Some(msg) = chat.mentions.iter_mut().find(|m| m.inner.id() == id) {
*msg = message.clone();
}
Expand Down Expand Up @@ -1928,24 +1908,3 @@ pub fn pending_group_messages<'a>(
messages,
})
}

pub fn mention_regex_pattern(id: &Identity, username: bool) -> Regex {
Regex::new(&format!(
"(^| )@{}( |$)",
if username {
id.username()
} else {
id.did_key().to_string()
}
))
.unwrap()
}

pub fn mention_replacement_pattern(id: &Identity, visual: bool) -> String {
format!(
r#"<div class="message-user-tag {}" value="{}">@{}</div>"#,
if visual { "visual-only" } else { "" },
id.did_key(),
id.username()
)
}
14 changes: 2 additions & 12 deletions common/src/state/pending_message.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,7 @@ impl PendingMessage {
inner.set_id(m_id);
}
inner.set_lines(text);
let message = Message {
inner,
in_reply_to: None,
key: String::new(),
..Default::default()
};
let message = Message::new(inner, None, Uuid::new_v4().to_string());
PendingMessage {
attachments: attachments
.iter()
Expand Down Expand Up @@ -60,12 +55,7 @@ impl PendingMessage {
.cloned()
.collect::<Vec<_>>();

let message = Message {
inner,
in_reply_to: None,
key: Uuid::new_v4().to_string(),
..Default::default()
};
let message = Message::new(inner, None, Uuid::new_v4().to_string());
PendingMessage {
attachments: attachments
.iter()
Expand Down
113 changes: 112 additions & 1 deletion common/src/state/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,22 @@ use std::{
path::{Path, PathBuf},
};

use once_cell::sync::Lazy;
use regex::{Captures, Regex, Replacer};
use titlecase::titlecase;
use tracing::log;
use uuid::Uuid;
use walkdir::WalkDir;
use warp::crypto::DID;

use crate::{get_extras_dir, STATIC_ARGS};

use super::{ui::Font, Theme};
use super::{ui::Font, Identity, State, Theme};

pub static USER_NAME_TAGS_REGEX: Lazy<Regex> =
Lazy::new(|| mention_regex_epattern("[A-z0-9]+#[A-z0-9]{8}"));
pub static USER_DID_TAGS_REGEX: Lazy<Regex> =
Lazy::new(|| mention_regex_epattern("did:key:[A-z0-9]{48}"));

pub fn get_available_themes() -> Vec<Theme> {
let mut themes = vec![];
Expand Down Expand Up @@ -96,6 +105,108 @@ pub fn get_available_fonts() -> Vec<Font> {
fonts
}

struct TagReplacer<'a, F: Fn(&Identity) -> String> {
participants: &'a [Identity],
own: &'a DID,
is_mention: bool,
is_username: bool,
replacement: F,
}

impl<F: Fn(&Identity) -> String> Replacer for TagReplacer<'_, F> {
fn replace_append(&mut self, caps: &Captures<'_>, dst: &mut String) {
if !caps[0].starts_with('`') {
let value = &caps[2];
let key = &value[1..];
if key.eq(&self.own.to_string()) {
self.is_mention = true;
}
dst.push_str(&caps[1]);
if let Some(id) = self.participants.iter().find(|id| {
if self.is_username {
let name = format!("{}#{}", id.username(), id.short_id());
name.eq(key)
} else {
id.did_key().to_string().eq(key)
}
}) {
dst.push_str(&(self.replacement)(id))
} else {
dst.push_str(value);
};
dst.push_str(&caps[3]);
} else {
dst.push_str(&caps[0]);
}
}
}

pub fn mention_regex_epattern(value: &str) -> Regex {
// This detects codeblocks
// When replacing this needs to be explicitly checked
let mut pattern = String::from(r#"(?:`{3}|`{1,2})+[^`]*(?:`{3}|`{1,2})"#);
// Second capture group contains the mention
// Since codeblocks are checked before they are basically "excluded"
// First and third are any leading/trailing whitespaces
pattern.push_str(&format!(r#"|(^|\s)(@{})($|\s)"#, value));
Regex::new(&pattern).unwrap()
}

pub fn parse_mention_state(
message: &str,
state: &State,
chat: Uuid,
replacement: impl Fn(&Identity) -> String,
) -> (String, bool) {
parse_mentions(
message,
&state
.get_chat_by_id(chat)
.map(|c| state.chat_participants(&c))
.unwrap_or_default(),
&state.did_key(),
false,
replacement,
)
}

// Parse a message replacing mentions with a given function
pub fn parse_mentions(
message: &str,
participants: &[Identity],
own: &DID,
is_username: bool,
replacement: impl Fn(&Identity) -> String,
) -> (String, bool) {
let mut replacer = TagReplacer {
participants,
own,
is_username,
is_mention: false,
replacement,
};
let result = if is_username {
USER_NAME_TAGS_REGEX.replace_all(message, replacer.by_ref())
} else {
USER_DID_TAGS_REGEX.replace_all(message, replacer.by_ref())
};
(result.to_string(), replacer.is_mention)
}

pub fn mention_to_did_key(id: &Identity) -> String {
format!("@{}", id.did_key())
}

// Replacement pattern converting a user tag to a highlight div
pub fn mention_replacement_pattern(id: &Identity, visual: bool) -> String {
format!(
r#"<div class="message-user-tag {}" value="{}">@{}</div>"#,
if visual { "visual-only" } else { "" },
id.did_key(),
id.username()
)
}

#[cfg(test)]
mod test {
use super::*;
Expand Down
18 changes: 6 additions & 12 deletions common/src/testing/mock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -105,12 +105,11 @@ fn generate_fake_chat(participants: Vec<Identity>, conversation: Uuid) -> Chat {
default_message.set_sender(sender.did_key());
default_message.set_lines(vec![lipsum(word_count)]);

messages.push_back(ui_adapter::Message {
inner: default_message,
in_reply_to: None,
key: Uuid::new_v4().to_string(),
..Default::default()
});
messages.push_back(ui_adapter::Message::new(
default_message,
None,
Uuid::new_v4().to_string(),
));
}

let pinned_messages: Vec<_> = messages
Expand Down Expand Up @@ -258,12 +257,7 @@ fn generate_fake_message(conversation_id: Uuid, identities: &[Identity]) -> ui_a
default_message.set_replied(None);
default_message.set_lines(vec![text.into()]);

ui_adapter::Message {
inner: default_message,
in_reply_to: None,
key: Uuid::new_v4().to_string(),
..Default::default()
}
ui_adapter::Message::new(default_message, None, Uuid::new_v4().to_string())
}

fn generate_fake_storage() -> Storage {
Expand Down
56 changes: 23 additions & 33 deletions common/src/warp_runner/ui_adapter/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ pub use multipass_event::{convert_multipass_event, MultiPassEvent};
pub use raygun_event::{convert_raygun_event, RayGunEvent};
use uuid::Uuid;

use crate::state::{self, chats, Identity, MAX_PINNED_MESSAGES};
use crate::state::{self, chats, utils::mention_regex_epattern, Identity, MAX_PINNED_MESSAGES};
use futures::{stream::FuturesOrdered, FutureExt, StreamExt};
use serde::{Deserialize, Serialize};
use std::{
Expand All @@ -39,49 +39,39 @@ use super::{
pub struct Message {
pub inner: warp::raygun::Message,
pub in_reply_to: Option<(String, Vec<File>, DID)>,
pub lines_to_render: Option<String>,
pub is_mention: bool,
is_mention: Option<bool>,
/// this field exists so that the UI can tell Dioxus when a message has been edited and thus
/// needs to be re-rendered. Before the addition of this field, the compose view was
/// using the message Uuid, but this doesn't change when a message is edited.
pub key: String,
}

impl Message {
// resolve the message lines to e.g. format user mentions correctly
pub fn insert_did(&mut self, participants: &[state::Identity], own: &DID) {
if self.lines_to_render.is_some() {
return;
pub fn new(
inner: warp::raygun::Message,
in_reply_to: Option<(String, Vec<File>, DID)>,
key: String,
) -> Message {
Message {
inner,
in_reply_to,
key,
..Default::default()
}
// Better if warp provides a way of saving mentions in the message
// so dont need to loop over all participants
let lines = self.inner.lines().join("\n");
let (lines, is_mention) = format_mentions(lines, participants, own, false);
self.is_mention = is_mention;
self.lines_to_render = Some(lines);
}
}

pub fn format_mentions(
message: String,
participants: &[state::Identity],
own: &DID,
visual: bool,
) -> (String, bool) {
if !message.contains('@') {
return (message, false);
}
let mut result = message;
let mut is_mention = false;
participants.iter().for_each(|id| {
let reg = state::mention_regex_pattern(id, false);
let replaced = reg.replace_all(&result, state::mention_replacement_pattern(id, visual));
if own.eq(&id.did_key()) && !replaced.eq(&result) {
is_mention = true;
// Lazily evaluate if the user is mentioned
pub fn is_mention_self(&mut self, own: &DID) -> bool {
if self.is_mention.is_none() {
let reg = mention_regex_epattern(&own.to_string());
self.is_mention = Some(
reg.find(&self.inner.lines().join("\n"))
.map(|c| !c.as_str().starts_with('`'))
.unwrap_or_default(),
);
}
result = replaced.to_string();
});
(result, is_mention)
self.is_mention.unwrap()
}
}

#[derive(Clone)]
Expand Down
Loading

0 comments on commit b100aa3

Please sign in to comment.