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

Auto-replace plain text with emoji #1009

Merged
merged 14 commits into from
Jul 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading