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

Amend mention message html output #716

Merged
merged 12 commits into from
Jun 13, 2023
Merged
Show file tree
Hide file tree
Changes from 11 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
11 changes: 10 additions & 1 deletion crates/wysiwyg/src/composer_model/mentions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,16 @@ where
let (start, end) = self.safe_selection();
let range = self.state.dom.find_range(start, end);

let new_node = DomNode::new_mention(url, text, attributes);
// use the display text or the presence of a `data-mention-type => at-room` attribute to decide the mention type
let new_node = if text == "@room".into()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Eventually this logic could be shared with the HTML parsing logic (#695 / #692). So we could add a note here to remind us to update when it's ready.

|| attributes.iter().any(|(k, v)| {
k == &S::from("data-mention-type") && v == &S::from("at-room")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe it's an @room mention if and only if there is an occurrence of @room in the text making this condition redundant

Copy link
Contributor Author

@artcodespace artcodespace Jun 13, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great - I wanted to think that was the case but wasn't sure. Updated in e8bd655

}) {
DomNode::new_at_room_mention(attributes)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As this function is able to create an @room mention, we might want to reconsider the API, and either:

Alternatively, we could add a new function so that we have:

  • do_insert_mention(text, url) // with mandatory args
  • do_insert_at_room_mention()

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I'd be tempted to go a different way and have this function call DomNode::new_mention with url, display_text, attributes as we currently have and then have MentionNode::new create either a room, user, or at-room mention as appropriate.

What do you think to that solution?

If you're happy with me adding a note as the work may be further informed by the WIP mentions utils work (which is what I'm picking up next), I'll do that and address in that subsequent PR.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

have this function call DomNode::new_mention with url, display_text, attributes as we currently have and then have MentionNode::new create either a room, user, or at-room mention as appropriate.

Yeah that makes sense. I still think the API could be more self explanatory in terms of URL being optional, and some documentation of that behaviour if we do keep a single function for both. But happy for that to be addressed in a later PR

} else {
DomNode::new_mention(url, text, attributes)
Comment on lines +86 to +87
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When trying to insert an invalid mention, should the editor still do that? I'm wondering if it could return an error result and/or fall back to DomNode::new_link().

It might not be possible to do without being able to parse mentions but could be worth adding a note

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a good question, and for now I believe we don't have a way of telling if it's valid or not. Will add a note to handle that (after parsing gives us that ability).

};

let new_cursor_index = start + new_node.text_len();

let handle = self.state.dom.insert_node_at_cursor(&range, new_node);
Expand Down
32 changes: 22 additions & 10 deletions crates/wysiwyg/src/dom/nodes/mention_node.rs
Original file line number Diff line number Diff line change
Expand Up @@ -129,22 +129,34 @@ impl<S: UnicodeString> MentionNode<S> {
let cur_pos = formatter.len();
match self.kind() {
MentionNodeKind::MatrixUrl { display_text, url } => {
let mut attributes = self.attributes.clone();
attributes.push(("href".into(), url.clone()));

if !as_message {
attributes.push(("contenteditable".into(), "false".into()))
// TODO: data-mention-type = "user" | "room"
}
// if formatting as a message, only include the href attribute
let attributes = if as_message {
vec![("href".into(), url.clone())]
} else {
let mut attributes_for_composer = self.attributes.clone();
attributes_for_composer.push(("href".into(), url.clone()));
attributes_for_composer
.push(("contenteditable".into(), "false".into()));
attributes_for_composer
};

self.fmt_tag_open(tag, formatter, &Some(attributes));

formatter.push(display_text.clone());

self.fmt_tag_close(tag, formatter);
}
MentionNodeKind::AtRoom => {
formatter.push(self.display_text());
// if formatting as a message, simply use the display text (@room)
if as_message {
formatter.push(self.display_text())
} else {
let mut attributes = self.attributes.clone();
attributes.push(("href".into(), "#".into())); // designates a placeholder link in html
attributes.push(("contenteditable".into(), "false".into()));

self.fmt_tag_open(tag, formatter, &Some(attributes));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this tag be <span> (or <mention>), rather than <a>?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the time being keeping everything as <a> - there's a separate issue for moving to a custom html tag type here to try and keep these PRs small and digestible

formatter.push(self.display_text());
self.fmt_tag_close(tag, formatter);
};
}
}

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 @@ -30,6 +30,7 @@ pub mod test_selection;
pub mod test_set_content;
pub mod test_suggestions;
pub mod test_to_markdown;
pub mod test_to_message_html;
pub mod test_to_plain_text;
pub mod test_to_raw_text;
pub mod test_to_tree;
Expand Down
89 changes: 89 additions & 0 deletions crates/wysiwyg/src/tests/test_to_message_html.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
// 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 crate::tests::testutils_composer_model::{cm, tx};

#[test]
fn replaces_empty_paragraphs_with_newline_characters() {
let mut model = cm("|");
model.replace_text("hello".into());
model.enter();
model.enter();
model.enter();
model.enter();
model.replace_text("Alice".into());

assert_eq!(
tx(&model),
"<p>hello</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>Alice|</p>"
);
let message_output = model.get_content_as_message_html();
assert_eq!(message_output, "<p>hello</p>\n\n\n<p>Alice</p>");
}
#[test]
fn only_outputs_href_attribute_on_user_mention() {
let mut model = cm("|");
model.insert_mention(
"www.url.com".into(),
"inner text".into(),
vec![
("data-mention-type".into(), "user".into()),
("style".into(), "some css".into()),
],
);
assert_eq!(tx(&model), "<a data-mention-type=\"user\" style=\"some css\" href=\"www.url.com\" contenteditable=\"false\">inner text</a>&nbsp;|");

let message_output = model.get_content_as_message_html();
assert_eq!(
message_output,
"<a href=\"www.url.com\">inner text</a>\u{a0}"
);
}

#[test]
fn only_outputs_href_attribute_on_room_mention() {
let mut model = cm("|");
model.insert_mention(
"www.url.com".into(),
"inner text".into(),
vec![
("data-mention-type".into(), "room".into()),
("style".into(), "some css".into()),
],
);
assert_eq!(tx(&model), "<a data-mention-type=\"room\" style=\"some css\" href=\"www.url.com\" contenteditable=\"false\">inner text</a>&nbsp;|");

let message_output = model.get_content_as_message_html();
assert_eq!(
message_output,
"<a href=\"www.url.com\">inner text</a>\u{a0}"
);
}

#[test]
fn only_outputs_href_inner_text_for_at_room_mention() {
let mut model = cm("|");
model.insert_mention(
"anything".into(), // this should be ignored in favour of a # placeholder
"@room".into(),
vec![
("data-mention-type".into(), "at-room".into()),
("style".into(), "some css".into()),
],
);
assert_eq!(tx(&model), "<a data-mention-type=\"at-room\" style=\"some css\" href=\"#\" contenteditable=\"false\">@room</a>&nbsp;|");

let message_output = model.get_content_as_message_html();
assert_eq!(message_output, "@room\u{a0}");
}
1 change: 1 addition & 0 deletions platforms/web/lib/composer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ export function processInput(
{
actions: formattingFunctions,
content: () => composerModel.get_content_as_html(),
messageContent: () => composerModel.get_content_as_message_html(),
},
editor,
inputEventProcessor,
Expand Down
1 change: 1 addition & 0 deletions platforms/web/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ export type FormattingFunctions = Record<
export type Wysiwyg = {
actions: FormattingFunctions;
content: () => string;
messageContent: () => string;
};

export type InputEventProcessor = (
Expand Down
1 change: 1 addition & 0 deletions platforms/web/lib/useListeners/event.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ function getInputFromKeyDown(
{
actions: formattingFunctions,
content: () => composerModel.get_content_as_html(),
messageContent: () => composerModel.get_content_as_message_html(),
},
editor,
inputEventProcessor,
Expand Down
50 changes: 40 additions & 10 deletions platforms/web/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ function App() {
if (debug.testRef.current) {
debug.traceAction(null, 'send', `${wysiwyg.content()}`);
}
console.log(`SENDING: ${wysiwyg.content()}`);
console.log(`SENDING MESSAGE HTML: ${wysiwyg.messageContent()}`);
wysiwyg.actions.clear();
return null;
}
Expand All @@ -97,6 +97,11 @@ function App() {
actionStates.unorderedList === 'reversed' ||
actionStates.orderedList === 'reversed';

const commandExists = suggestion && suggestion.type === 'command';
const mentionExists = suggestion && suggestion.type === 'mention';
const shouldDisplayAtMention = mentionExists && suggestion.keyChar === '@';
const shouldDisplayHashMention =
mentionExists && suggestion.keyChar === '#';
return (
<div className="wrapper">
<div>
Expand Down Expand Up @@ -187,26 +192,51 @@ function App() {
<button type="button" onClick={(_e) => wysiwyg.clear()}>
clear
</button>
{suggestion && suggestion.type === 'mention' && (
{shouldDisplayAtMention && (
<>
<button
type="button"
onClick={(_e) =>
wysiwyg.mention(
'https://matrix.to/#/@alice_user:element.io',
'Alice',
{
'data-mention-type': 'user',
},
)
}
>
Add User mention
</button>
<button
type="button"
onClick={(_e) =>
wysiwyg.mention('#', '@room', {
'data-mention-type': 'at-room',
})
}
>
Add at-room mention
</button>
</>
)}
{shouldDisplayHashMention && (
<button
type="button"
onClick={(_e) =>
wysiwyg.mention(
'https://matrix.to/#/@alice_user:element.io',
'Alice',
'https://matrix.to/#/#my_room:element.io',
'My room',
{
'data-mention-type':
suggestion.keyChar === '@'
? 'user'
: 'room',
'data-mention-type': 'room',
},
)
}
>
Add {suggestion.keyChar}mention
Add Room mention
</button>
)}
{suggestion && suggestion.type === 'command' && (
{commandExists && (
<button
type="button"
onClick={(_e) => wysiwyg.command('/spoiler')}
Expand Down