diff --git a/bindings/wysiwyg-ffi/src/ffi_composer_model.rs b/bindings/wysiwyg-ffi/src/ffi_composer_model.rs index 68480d72c..b377b459a 100644 --- a/bindings/wysiwyg-ffi/src/ffi_composer_model.rs +++ b/bindings/wysiwyg-ffi/src/ffi_composer_model.rs @@ -8,6 +8,7 @@ use crate::ffi_composer_state::ComposerState; use crate::ffi_composer_update::ComposerUpdate; use crate::ffi_dom_creation_error::DomCreationError; use crate::ffi_link_actions::LinkAction; +use crate::ffi_mentions_state::MentionsState; use crate::into_ffi::IntoFfi; use crate::{ActionState, ComposerAction, SuggestionPattern}; @@ -370,6 +371,10 @@ impl ComposerModel { self.inner.lock().unwrap().get_link_action().into() } + pub fn get_mentions_state(self: &Arc) -> MentionsState { + self.inner.lock().unwrap().get_mentions_state().into() + } + /// Force a panic for test purposes pub fn debug_panic(self: &Arc) { #[cfg(debug_assertions)] diff --git a/bindings/wysiwyg-ffi/src/ffi_mentions_state.rs b/bindings/wysiwyg-ffi/src/ffi_mentions_state.rs new file mode 100644 index 000000000..d15fdd579 --- /dev/null +++ b/bindings/wysiwyg-ffi/src/ffi_mentions_state.rs @@ -0,0 +1,18 @@ +#[derive(uniffi::Record)] +pub struct MentionsState { + pub user_ids: Vec, + pub room_ids: Vec, + pub room_aliases: Vec, + pub has_at_room_mention: bool, +} + +impl From for MentionsState { + fn from(value: wysiwyg::MentionsState) -> Self { + Self { + user_ids: value.user_ids.into_iter().collect(), + room_ids: value.room_ids.into_iter().collect(), + room_aliases: value.room_aliases.into_iter().collect(), + has_at_room_mention: value.has_at_room_mention, + } + } +} diff --git a/bindings/wysiwyg-ffi/src/lib.rs b/bindings/wysiwyg-ffi/src/lib.rs index 708957ab1..268ccd33a 100644 --- a/bindings/wysiwyg-ffi/src/lib.rs +++ b/bindings/wysiwyg-ffi/src/lib.rs @@ -21,6 +21,7 @@ mod ffi_composer_state; mod ffi_composer_update; mod ffi_dom_creation_error; mod ffi_link_actions; +mod ffi_mentions_state; mod ffi_menu_action; mod ffi_menu_state; mod ffi_pattern_key; @@ -38,6 +39,7 @@ pub use crate::ffi_composer_state::ComposerState; pub use crate::ffi_composer_update::ComposerUpdate; pub use crate::ffi_dom_creation_error::DomCreationError; pub use crate::ffi_link_actions::LinkAction; +pub use crate::ffi_mentions_state::MentionsState; pub use crate::ffi_menu_action::MenuAction; pub use crate::ffi_menu_state::MenuState; pub use crate::ffi_pattern_key::PatternKey; diff --git a/crates/matrix_mentions/src/lib.rs b/crates/matrix_mentions/src/lib.rs index 09b6ae493..2f30c59f3 100644 --- a/crates/matrix_mentions/src/lib.rs +++ b/crates/matrix_mentions/src/lib.rs @@ -14,4 +14,4 @@ mod mention; -pub use crate::mention::{Mention, MentionKind}; +pub use crate::mention::{Mention, MentionKind, RoomIdentificationType}; diff --git a/crates/matrix_mentions/src/mention.rs b/crates/matrix_mentions/src/mention.rs index b485340da..3363457ae 100644 --- a/crates/matrix_mentions/src/mention.rs +++ b/crates/matrix_mentions/src/mention.rs @@ -26,10 +26,22 @@ pub struct Mention { #[derive(Clone, Debug, PartialEq, Eq)] pub enum MentionKind { - Room, + Room(RoomIdentificationType), User, } +impl MentionKind { + pub fn is_room(&self) -> bool { + matches!(self, MentionKind::Room(_)) + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum RoomIdentificationType { + Id, + Alias, +} + impl Mention { fn new( uri: String, @@ -134,9 +146,16 @@ impl Mention { fn from_room(room_uri: &str) -> Option { // In all cases, use the alias/room ID being linked to as the // anchor’s text. + let room_id_type: RoomIdentificationType; let text = match parse_matrix_id(room_uri)? { - MatrixId::Room(room_id) => room_id.to_string(), - MatrixId::RoomAlias(room_alias) => room_alias.to_string(), + MatrixId::Room(room_id) => { + room_id_type = RoomIdentificationType::Id; + room_id.to_string() + } + MatrixId::RoomAlias(room_alias) => { + room_id_type = RoomIdentificationType::Alias; + room_alias.to_string() + } _ => return None, }; @@ -144,7 +163,7 @@ impl Mention { room_uri.to_string(), text.clone(), text, - MentionKind::Room, + MentionKind::Room(room_id_type), )) } } @@ -200,7 +219,7 @@ fn parse_external_id(uri: &str) -> Result { mod test { use ruma_common::{MatrixToUri, MatrixUri}; - use crate::mention::{Mention, MentionKind}; + use crate::mention::{Mention, MentionKind, RoomIdentificationType}; #[test] fn parse_uri_matrix_to_valid_user() { @@ -232,7 +251,10 @@ mod test { assert_eq!(parsed.uri(), uri); assert_eq!(parsed.mx_id(), "!roomid:example.org"); assert_eq!(parsed.display_text(), "!roomid:example.org"); - assert_eq!(parsed.kind(), &MentionKind::Room); + assert_eq!( + parsed.kind(), + &MentionKind::Room(RoomIdentificationType::Id) + ); } #[test] @@ -243,7 +265,10 @@ mod test { assert_eq!(parsed.uri(), uri); assert_eq!(parsed.mx_id(), "!roomid:example.org"); assert_eq!(parsed.display_text(), "!roomid:example.org"); - assert_eq!(parsed.kind(), &MentionKind::Room); + assert_eq!( + parsed.kind(), + &MentionKind::Room(RoomIdentificationType::Id) + ); } #[test] @@ -254,7 +279,10 @@ mod test { assert_eq!(parsed.uri(), uri); assert_eq!(parsed.mx_id(), "#room:example.org"); assert_eq!(parsed.display_text(), "#room:example.org"); - assert_eq!(parsed.kind(), &MentionKind::Room); + assert_eq!( + parsed.kind(), + &MentionKind::Room(RoomIdentificationType::Alias) + ); } #[test] @@ -265,7 +293,10 @@ mod test { assert_eq!(parsed.uri(), uri); assert_eq!(parsed.mx_id(), "#room:example.org"); assert_eq!(parsed.display_text(), "#room:example.org"); - assert_eq!(parsed.kind(), &MentionKind::Room); + assert_eq!( + parsed.kind(), + &MentionKind::Room(RoomIdentificationType::Alias) + ); } #[test] @@ -319,7 +350,10 @@ mod test { assert_eq!(parsed.uri(), uri); assert_eq!(parsed.mx_id(), "!roomid:example.org"); assert_eq!(parsed.display_text(), "!roomid:example.org"); - assert_eq!(parsed.kind(), &MentionKind::Room); + assert_eq!( + parsed.kind(), + &MentionKind::Room(RoomIdentificationType::Id) + ); } #[test] @@ -347,7 +381,10 @@ mod test { assert_eq!(parsed.uri(), uri); assert_eq!(parsed.mx_id(), "!room:example.org"); assert_eq!(parsed.display_text(), "!room:example.org"); // note the display_text is overridden - assert_eq!(parsed.kind(), &MentionKind::Room); + assert_eq!( + parsed.kind(), + &MentionKind::Room(RoomIdentificationType::Id) + ); } #[test] @@ -361,7 +398,10 @@ mod test { assert_eq!(parsed.uri(), uri); assert_eq!(parsed.mx_id(), "#room:example.org"); assert_eq!(parsed.display_text(), "#room:example.org"); // note the display_text is overridden - assert_eq!(parsed.kind(), &MentionKind::Room); + assert_eq!( + parsed.kind(), + &MentionKind::Room(RoomIdentificationType::Alias) + ); } #[test] diff --git a/crates/wysiwyg/src/composer_model/mentions.rs b/crates/wysiwyg/src/composer_model/mentions.rs index 916be1d2e..d18dfedb2 100644 --- a/crates/wysiwyg/src/composer_model/mentions.rs +++ b/crates/wysiwyg/src/composer_model/mentions.rs @@ -13,15 +13,52 @@ // limitations under the License. use crate::{ - dom::{nodes::MentionNode, DomLocation}, - ComposerModel, ComposerUpdate, DomNode, Location, SuggestionPattern, - UnicodeString, + dom::{ + nodes::{MentionNode, MentionNodeKind}, + DomLocation, + }, + ComposerModel, ComposerUpdate, DomNode, Location, MentionsState, + SuggestionPattern, UnicodeString, }; impl ComposerModel where S: UnicodeString, { + /// Returns the current mentions state of the content of the RTE editor. + pub fn get_mentions_state(&self) -> MentionsState { + let mut mentions_state = MentionsState::default(); + for node in self.state.dom.iter_mentions() { + match node.kind() { + MentionNodeKind::AtRoom => { + mentions_state.has_at_room_mention = true + } + MentionNodeKind::MatrixUri { mention } => match mention.kind() { + matrix_mentions::MentionKind::Room(id_type) => { + match id_type { + matrix_mentions::RoomIdentificationType::Id => { + mentions_state + .room_ids + .insert(mention.mx_id().to_string()); + } + matrix_mentions::RoomIdentificationType::Alias => { + mentions_state + .room_aliases + .insert(mention.mx_id().to_string()); + } + } + } + matrix_mentions::MentionKind::User => { + mentions_state + .user_ids + .insert(mention.mx_id().to_string()); + } + }, + } + } + mentions_state + } + /// Checks to see if the mention should be inserted and also if the mention can be created. /// If both of these checks are passed it will remove the suggestion and then insert a mention. pub fn insert_mention_at_suggestion( diff --git a/crates/wysiwyg/src/dom/iter.rs b/crates/wysiwyg/src/dom/iter.rs index 662dbb090..d352b3667 100644 --- a/crates/wysiwyg/src/dom/iter.rs +++ b/crates/wysiwyg/src/dom/iter.rs @@ -23,7 +23,7 @@ use crate::{DomHandle, DomNode, UnicodeString}; use std::collections::HashSet; use super::{ - nodes::{ContainerNode, TextNode}, + nodes::{ContainerNode, MentionNode, TextNode}, Dom, }; @@ -48,6 +48,12 @@ where self.iter().filter_map(DomNode::as_container) } + /// Returns an iterator over all the mention nodes of this DOM, in depth-first + /// order + pub fn iter_mentions(&self) -> impl Iterator> { + self.iter().filter_map(DomNode::as_mention) + } + /// Return an iterator over all nodes of the DOM from the passed node, /// depth-first order (including self). pub fn iter_from<'a>(&'a self, node: &'a DomNode) -> DomNodeIterator { diff --git a/crates/wysiwyg/src/dom/nodes/dom_node.rs b/crates/wysiwyg/src/dom/nodes/dom_node.rs index cdc18414d..0b70681bc 100644 --- a/crates/wysiwyg/src/dom/nodes/dom_node.rs +++ b/crates/wysiwyg/src/dom/nodes/dom_node.rs @@ -245,6 +245,14 @@ where } } + pub(crate) fn as_mention(&self) -> Option<&MentionNode> { + if let Self::Mention(v) = self { + Some(v) + } else { + None + } + } + pub fn kind(&self) -> DomNodeKind { match self { DomNode::Text(_) => DomNodeKind::Text, diff --git a/crates/wysiwyg/src/dom/nodes/mention_node.rs b/crates/wysiwyg/src/dom/nodes/mention_node.rs index d7e6b6642..8d9f0168f 100644 --- a/crates/wysiwyg/src/dom/nodes/mention_node.rs +++ b/crates/wysiwyg/src/dom/nodes/mention_node.rs @@ -165,7 +165,7 @@ impl MentionNode { // this is now only required for us to attach a custom style attribute for web let mut attrs = self.attributes.clone(); let data_mention_type = match mention.kind() { - MentionKind::Room => "room", + MentionKind::Room(_) => "room", MentionKind::User => "user", }; attrs.push(( @@ -177,12 +177,11 @@ impl MentionNode { attrs }; - let display_text = - if as_message && mention.kind() == &MentionKind::Room { - S::from(mention.mx_id()) - } else { - self.display_text() - }; + let display_text = if as_message && mention.kind().is_room() { + S::from(mention.mx_id()) + } else { + self.display_text() + }; self.fmt_tag_open(tag, formatter, &Some(attributes)); formatter.push(display_text); @@ -286,7 +285,7 @@ where // For a mention in a message, display the `mx_id` for a room mention, `display_text` otherwise let text = match this.kind() { MentionNodeKind::MatrixUri { mention } - if mention.kind() == &MentionKind::Room => + if mention.kind().is_room() => { S::from(mention.mx_id()) } @@ -305,7 +304,7 @@ where match this.kind() { MentionNodeKind::MatrixUri { mention } => { data_mention_type = match mention.kind() { - MentionKind::Room => "room", + MentionKind::Room(_) => "room", MentionKind::User => "user", }; href = mention.uri(); diff --git a/crates/wysiwyg/src/lib.rs b/crates/wysiwyg/src/lib.rs index e62ca050f..c2afa0caf 100644 --- a/crates/wysiwyg/src/lib.rs +++ b/crates/wysiwyg/src/lib.rs @@ -23,6 +23,7 @@ mod format_type; mod link_action; mod list_type; mod location; +mod mentions_state; mod menu_action; mod menu_state; mod pattern_key; @@ -51,6 +52,7 @@ pub use crate::link_action::LinkAction; pub use crate::link_action::LinkActionUpdate; pub use crate::list_type::ListType; pub use crate::location::Location; +pub use crate::mentions_state::MentionsState; pub use crate::menu_action::MenuAction; pub use crate::menu_action::MenuActionSuggestion; pub use crate::menu_state::MenuState; diff --git a/crates/wysiwyg/src/mentions_state.rs b/crates/wysiwyg/src/mentions_state.rs new file mode 100644 index 000000000..64ea02a6b --- /dev/null +++ b/crates/wysiwyg/src/mentions_state.rs @@ -0,0 +1,23 @@ +// Copyright 2023 The Matrix.org Foundation C.I.C. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::collections::HashSet; + +#[derive(Default, Debug, PartialEq, Eq)] +pub struct MentionsState { + pub user_ids: HashSet, + pub room_ids: HashSet, + pub room_aliases: HashSet, + pub has_at_room_mention: bool, +} diff --git a/crates/wysiwyg/src/tests/test_mentions.rs b/crates/wysiwyg/src/tests/test_mentions.rs index 793e6c7e5..484d22864 100644 --- a/crates/wysiwyg/src/tests/test_mentions.rs +++ b/crates/wysiwyg/src/tests/test_mentions.rs @@ -16,7 +16,7 @@ use widestring::Utf16String; use crate::{ tests::testutils_composer_model::{cm, tx}, - ComposerModel, MenuAction, + ComposerModel, MentionsState, MenuAction, }; /** * INSERTING WITH PARSING @@ -594,6 +594,123 @@ fn can_insert_at_room_mention() { assert_eq!(tx(&model), "@room |") } +#[test] +fn get_mentions_state_for_no_mentions() { + let model = cm("

hello!|

"); + assert_eq!(model.get_mentions_state(), MentionsState::default()) +} + +#[test] +fn get_mentions_state_for_user_mention() { + let model = cm("

hello Alice!|

"); + let mut state = MentionsState::default(); + state.user_ids.insert("@alice:matrix.org".into()); + assert_eq!(model.get_mentions_state(), state) +} + +#[test] +fn get_mentions_state_for_multiple_user_mentions() { + let model = cm("

hello Alice and Bob!|

"); + let mut state = MentionsState::default(); + state.user_ids.insert("@alice:matrix.org".into()); + state.user_ids.insert("@bob:matrix.org".into()); + assert_eq!(model.get_mentions_state(), state) +} + +#[test] +fn get_mentions_state_for_at_room_mention() { + let model = cm("

hello @room|"); + let state = MentionsState { + user_ids: Default::default(), + room_ids: Default::default(), + room_aliases: Default::default(), + has_at_room_mention: true, + }; + assert_eq!(model.get_mentions_state(), state) +} + +#[test] +fn get_mentions_state_for_at_room_plain_mention() { + let model = cm("

hello @room|"); + let state = MentionsState { + user_ids: Default::default(), + room_ids: Default::default(), + room_aliases: Default::default(), + has_at_room_mention: true, + }; + assert_eq!(model.get_mentions_state(), state) +} + +#[test] +fn get_mentions_state_for_multiple_user_and_at_room_mentions() { + let model = cm("

hello Alice, Bob and @room!|

"); + let mut state = MentionsState::default(); + state.user_ids.insert("@alice:matrix.org".into()); + state.user_ids.insert("@bob:matrix.org".into()); + state.has_at_room_mention = true; + assert_eq!(model.get_mentions_state(), state) +} + +#[test] +fn get_mentions_state_for_user_mention_with_custom_link() { + let model = cm("

hello Alice!|

"); + let mut state = MentionsState::default(); + state.user_ids.insert("@alice:matrix.org".into()); + assert_eq!(model.get_mentions_state(), state) +} + +#[test] +fn get_mentions_state_empty_for_non_intentional_at_room_mention() { + let model = cm("
hello @room!|
"); + assert_eq!(model.get_mentions_state(), MentionsState::default()) +} + +#[test] +fn get_mentions_state_with_duplications() { + let model = cm("

hello Alice, Alice, @room and @room, be sure to check Room and Room|

"); + let mut state = MentionsState::default(); + state.user_ids.insert("@alice:matrix.org".into()); + state.has_at_room_mention = true; + state.room_aliases.insert("#room:matrix.org".into()); + assert_eq!(model.get_mentions_state(), state) +} + +#[test] +fn get_mentions_state_for_room_alias() { + let model = cm("

check this Room|

"); + let mut state = MentionsState::default(); + state.room_aliases.insert("#room:matrix.org".into()); + assert_eq!(model.get_mentions_state(), state) +} + +#[test] +fn get_mentions_state_for_room_id() { + let model = cm("

check this Room|

"); + let mut state = MentionsState::default(); + state.room_ids.insert("!room:matrix.org".into()); + assert_eq!(model.get_mentions_state(), state) +} + +#[test] +fn get_mentions_state_for_room_id_and_room_alias() { + let model = cm("

check this Room and this check this Room|

"); + let mut state = MentionsState::default(); + state.room_ids.insert("!room:matrix.org".into()); + state.room_aliases.insert("#other_room:matrix.org".into()); + assert_eq!(model.get_mentions_state(), state) +} + +#[test] +fn get_mentions_state_for_multiple_mentions() { + let model = cm("

hello Alice and Bob check this Room and this check this Room|

"); + let mut state = MentionsState::default(); + state.room_ids.insert("!room:matrix.org".into()); + state.room_aliases.insert("#other_room:matrix.org".into()); + state.user_ids.insert("@alice:matrix.org".into()); + state.user_ids.insert("@bob:matrix.org".into()); + assert_eq!(model.get_mentions_state(), state) +} + /** * HELPER FUNCTIONS */