Skip to content
This repository has been archived by the owner on Sep 27, 2024. It is now read-only.

Commit

Permalink
Merge pull request #1009 from matrix-org/langleyd/auto_replace_emoji
Browse files Browse the repository at this point in the history
Auto-replace plain text with emoji
  • Loading branch information
langleyd authored Jul 18, 2024
2 parents 4c50704 + 0bb2c9e commit 1c6e60f
Show file tree
Hide file tree
Showing 30 changed files with 319 additions and 44 deletions.
12 changes: 12 additions & 0 deletions bindings/wysiwyg-ffi/src/ffi_composer_model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,16 @@ impl ComposerModel {
Ok(Arc::new(ComposerUpdate::from(update)))
}

pub fn set_custom_suggestion_patterns(
self: &Arc<Self>,
custom_suggestion_patterns: Vec<String>,
) {
self.inner
.lock()
.unwrap()
.set_custom_suggestion_patterns(custom_suggestion_patterns)
}

pub fn get_content_as_html(self: &Arc<Self>) -> String {
self.inner.lock().unwrap().get_content_as_html().to_string()
}
Expand Down Expand Up @@ -139,11 +149,13 @@ impl ComposerModel {
self: &Arc<Self>,
new_text: String,
suggestion: SuggestionPattern,
append_space: bool,
) -> Arc<ComposerUpdate> {
Arc::new(ComposerUpdate::from(
self.inner.lock().unwrap().replace_text_suggestion(
Utf16String::from_str(&new_text),
wysiwyg::SuggestionPattern::from(suggestion),
append_space,
),
))
}
Expand Down
21 changes: 20 additions & 1 deletion bindings/wysiwyg-ffi/src/ffi_composer_update.rs
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,25 @@ mod test {
)
}

#[test]
fn menu_action_is_updated_for_custom_suggestion() {
let model = Arc::new(ComposerModel::new());
model.set_custom_suggestion_patterns(vec![":)".into()]);
let update = model.replace_text("That's great! :)".into());

assert_eq!(
update.menu_action(),
MenuAction::Suggestion {
suggestion_pattern: SuggestionPattern {
key: crate::PatternKey::Custom(":)".into()),
text: ":)".into(),
start: 14,
end: 16,
}
},
)
}

#[test]
fn test_replace_whole_suggestion_with_mention_ffi() {
let mut model = Arc::new(ComposerModel::new());
Expand Down Expand Up @@ -229,7 +248,7 @@ mod test {

#[test]
fn test_replace_text_with_escaped_html_in_mention_ffi() {
let mut model = Arc::new(ComposerModel::new());
let model = Arc::new(ComposerModel::new());
model.replace_text("hello ".into());

let update = model.replace_text("@alic".into());
Expand Down
3 changes: 3 additions & 0 deletions bindings/wysiwyg-ffi/src/ffi_pattern_key.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ pub enum PatternKey {
At,
Hash,
Slash,
Custom(String),
}

impl From<wysiwyg::PatternKey> for PatternKey {
Expand All @@ -25,6 +26,7 @@ impl From<wysiwyg::PatternKey> for PatternKey {
wysiwyg::PatternKey::At => Self::At,
wysiwyg::PatternKey::Hash => Self::Hash,
wysiwyg::PatternKey::Slash => Self::Slash,
wysiwyg::PatternKey::Custom(key) => Self::Custom(key),
}
}
}
Expand All @@ -35,6 +37,7 @@ impl From<PatternKey> for wysiwyg::PatternKey {
PatternKey::At => Self::At,
PatternKey::Hash => Self::Hash,
PatternKey::Slash => Self::Slash,
PatternKey::Custom(key) => Self::Custom(key),
}
}
}
65 changes: 57 additions & 8 deletions bindings/wysiwyg-wasm/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,20 @@ impl ToUtf16TupleVec for js_sys::Map {
}
}

trait ToStringVec {
fn into_vec(self) -> Vec<String>;
}

impl ToStringVec for js_sys::Array {
fn into_vec(self) -> Vec<String> {
let mut vec = vec![];
self.for_each(&mut |element, _, _| {
vec.push(element.as_string().unwrap());
});
vec
}
}

#[wasm_bindgen]
#[derive(Default)]
pub struct ComposerModel {
Expand Down Expand Up @@ -185,10 +199,12 @@ impl ComposerModel {
&mut self,
new_text: &str,
suggestion: &SuggestionPattern,
append_space: bool,
) -> ComposerUpdate {
ComposerUpdate::from(self.inner.replace_text_suggestion(
Utf16String::from_str(new_text),
wysiwyg::SuggestionPattern::from(suggestion.clone()),
append_space,
))
}

Expand Down Expand Up @@ -316,6 +332,15 @@ impl ComposerModel {
))
}

pub fn set_custom_suggestion_patterns(
&mut self,
custom_suggestion_patterns: js_sys::Array,
) {
self.inner.set_custom_suggestion_patterns(
custom_suggestion_patterns.into_vec(),
);
}

/// Creates an at-room mention node and inserts it into the composer at the current selection
pub fn insert_at_room_mention(
&mut self,
Expand Down Expand Up @@ -687,28 +712,52 @@ impl From<SuggestionPattern> for wysiwyg::SuggestionPattern {

#[wasm_bindgen]
#[derive(Clone)]
pub enum PatternKey {
pub enum PatternKeyType {
At,
Hash,
Slash,
Custom,
}

#[derive(Clone)]
#[wasm_bindgen(getter_with_clone)]
pub struct PatternKey {
pub key_type: PatternKeyType,
pub custom_key_value: Option<String>,
}

impl From<wysiwyg::PatternKey> for PatternKey {
fn from(inner: wysiwyg::PatternKey) -> Self {
match inner {
wysiwyg::PatternKey::At => Self::At,
wysiwyg::PatternKey::Hash => Self::Hash,
wysiwyg::PatternKey::Slash => Self::Slash,
wysiwyg::PatternKey::At => Self {
key_type: PatternKeyType::At,
custom_key_value: None,
},
wysiwyg::PatternKey::Hash => Self {
key_type: PatternKeyType::Hash,
custom_key_value: None,
},
wysiwyg::PatternKey::Slash => Self {
key_type: PatternKeyType::Slash,
custom_key_value: None,
},
wysiwyg::PatternKey::Custom(key) => Self {
key_type: PatternKeyType::Custom,
custom_key_value: Some(key),
},
}
}
}

impl From<PatternKey> for wysiwyg::PatternKey {
fn from(key: PatternKey) -> Self {
match key {
PatternKey::At => Self::At,
PatternKey::Hash => Self::Hash,
PatternKey::Slash => Self::Slash,
match key.key_type {
PatternKeyType::At => Self::At,
PatternKeyType::Hash => Self::Hash,
PatternKeyType::Slash => Self::Slash,
PatternKeyType::Custom => {
Self::Custom(key.custom_key_value.unwrap())
}
}
}
}
Expand Down
16 changes: 15 additions & 1 deletion crates/wysiwyg/src/composer_model/base.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ use crate::{
ComposerAction, ComposerUpdate, DomHandle, Location, ToHtml, ToMarkdown,
ToTree,
};
use std::collections::HashMap;
use std::collections::{HashMap, HashSet};

#[derive(Clone, Default)]
pub struct ComposerModel<S>
Expand All @@ -42,6 +42,9 @@ where

/// The states of the buttons for each action e.g. bold, undo
pub(crate) action_states: HashMap<ComposerAction, ActionState>,

/// Suggestion patterns provided by the client at runtime
pub(crate) custom_suggestion_patterns: HashSet<String>,
}

impl<S> ComposerModel<S>
Expand All @@ -54,6 +57,7 @@ where
previous_states: Vec::new(),
next_states: Vec::new(),
action_states: HashMap::new(), // TODO: Calculate state based on ComposerState
custom_suggestion_patterns: HashSet::new(),
};
instance.compute_menu_state(MenuStateComputeType::AlwaysUpdate);
instance
Expand All @@ -65,6 +69,7 @@ where
previous_states: Vec::new(),
next_states: Vec::new(),
action_states: HashMap::new(), // TODO: Calculate state based on ComposerState
custom_suggestion_patterns: HashSet::new(),
}
}

Expand All @@ -85,6 +90,7 @@ where
previous_states: Vec::new(),
next_states: Vec::new(),
action_states: HashMap::new(), // TODO: Calculate state based on ComposerState
custom_suggestion_patterns: HashSet::new(),
};
model.compute_menu_state(MenuStateComputeType::AlwaysUpdate);
Self::post_process_dom(&mut model.state.dom);
Expand Down Expand Up @@ -125,6 +131,14 @@ where
self.set_content_from_html(&html)
}

pub fn set_custom_suggestion_patterns(
&mut self,
custom_suggestion_patterns: Vec<String>,
) {
self.custom_suggestion_patterns =
HashSet::from_iter(custom_suggestion_patterns)
}

pub fn action_states(&self) -> &HashMap<ComposerAction, ActionState> {
&self.action_states
}
Expand Down
22 changes: 17 additions & 5 deletions crates/wysiwyg/src/composer_model/menu_action.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
// See the License for the specific language governing permissions and
// limitations under the License.

use std::collections::HashSet;

use crate::{
dom::{
unicode_string::{UnicodeStr, UnicodeStringExt},
Expand All @@ -37,7 +39,12 @@ where
return MenuAction::None;
}
let (raw_text, start, end) = self.extended_text(range);
if let Some((key, text)) = Self::pattern_for_text(raw_text, start) {

if let Some((key, text)) = Self::pattern_for_text(
raw_text,
start,
&self.custom_suggestion_patterns,
) {
MenuAction::Suggestion(SuggestionPattern {
key,
text,
Expand Down Expand Up @@ -79,14 +86,19 @@ where
fn pattern_for_text(
mut text: S,
start_location: usize,
custom_suggestion_patterns: &HashSet<String>,
) -> Option<(PatternKey, String)> {
let Some(first_char) = text.pop_first() else {
return None;
};
let Some(key) = PatternKey::from_char(first_char) else {
let Some(key) = PatternKey::from_string_and_suggestions(
text.to_string(),
custom_suggestion_patterns,
) else {
return None;
};

if key.is_static_pattern() {
text.pop_first();
}

// Exclude slash patterns that are not at the beginning of the document
// and any selection that contains inner whitespaces.
if (key == PatternKey::Slash && start_location > 0)
Expand Down
10 changes: 8 additions & 2 deletions crates/wysiwyg/src/composer_model/replace_text.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,16 @@ where
&mut self,
new_text: S,
suggestion: SuggestionPattern,
append_space: bool,
) -> ComposerUpdate<S> {
self.push_state_to_history();
self.do_replace_text_in(new_text, suggestion.start, suggestion.end);
self.do_replace_text(" ".into())
let replace_suggestion_update =
self.do_replace_text_in(new_text, suggestion.start, suggestion.end);
if append_space {
self.do_replace_text(" ".into())
} else {
replace_suggestion_update
}
}

#[deprecated(since = "0.20.0")]
Expand Down
20 changes: 18 additions & 2 deletions crates/wysiwyg/src/pattern_key.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,32 @@
// See the License for the specific language governing permissions and
// limitations under the License.

use std::collections::HashSet;

#[derive(Clone, Debug, PartialEq, Eq)]
pub enum PatternKey {
At,
Hash,
Slash,
Custom(String),
}

impl PatternKey {
pub(crate) fn from_char(char: char) -> Option<Self> {
match char {
pub(crate) fn is_static_pattern(&self) -> bool {
matches!(self, Self::At | Self::Hash | Self::Slash)
}

pub(crate) fn from_string_and_suggestions(
string: String,
custom_suggestion_patterns: &HashSet<String>,
) -> Option<Self> {
if custom_suggestion_patterns.contains(&string) {
return Some(Self::Custom(string));
}
let Some(first_char) = string.chars().nth(0) else {
return None;
};
match first_char {
'\u{0040}' => Some(Self::At),
'\u{0023}' => Some(Self::Hash),
'\u{002F}' => Some(Self::Slash),
Expand Down
1 change: 1 addition & 0 deletions crates/wysiwyg/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

pub mod test_characters;
pub mod test_deleting;
pub mod test_emoji_replacement;
pub mod test_formatting;
pub mod test_get_link_action;
pub mod test_links;
Expand Down
Loading

0 comments on commit 1c6e60f

Please sign in to comment.