From 7d00b964a7557758c3a6d31d299cdfc57cb211cb Mon Sep 17 00:00:00 2001 From: Simon Laux Date: Sat, 14 Oct 2023 05:07:54 +0200 Subject: [PATCH 1/4] start with mentions --- CHANGELOG.md | 4 + spec.md | 44 +++++++---- src/parser/mod.rs | 11 ++- src/parser/parse_from_text/mod.rs | 24 ++++++ src/parser/parse_from_text/text_elements.rs | 14 ++++ tests/mentions.rs | 24 ++++++ tests/test.rs | 1 + tests/text_to_ast/text_only.rs | 83 +++++++++++++++++++++ 8 files changed, 187 insertions(+), 18 deletions(-) create mode 100644 tests/mentions.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 2d675d9..07205cf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +- add @mentions + - new `Element::Mention{ address: &str }` + - new api `extract_mention_addresses` to extract + ## 0.8.0 - Nom 7 and more Generic URI Schemes ### Changed diff --git a/spec.md b/spec.md index 8975c73..e1b3d6a 100644 --- a/spec.md +++ b/spec.md @@ -6,6 +6,7 @@ - Text only - [Email addresses: `hello@delta.chat`](#email-addresses) + - [Mentions `@hello@delta.chat`](#mentions) - [Links: `https://delta.chat` and `mailto:hello@delta.chat`](#links) - [Bot `/commands`](#bot-commands) - [Hashtags: `#tag`](#hashtag) @@ -34,6 +35,33 @@ Text elements that are displayed as is with no change to the content, just enhan Make email addresses clickable, opens the chat with that contact and creates it if it does not already exist. +#### Format + +- format should follow standards (as time of writing the current implementation is still fairly dumb) +- trailing `.` is not part of the email address and should not be parsed with it. + + + +### Mentions `@hello@delta.chat` + +Clickable mentions, opens profile view for a contact. +UI may replace email address by display name +and highlight it to distinguish it from normal text. +(like other messengers do it, look at telegram, discord, element and so on) + +#### Format + +A valid email address preceded by an `@` character. +Reuses parsing logic from [email address](#email-addresses). + +#### Other uses + +There will be a dedicated api that just extracts mentions from a text that will be used by deltachat core to be able to notify users when they are mentioned. + +#### In Message Composer + +the message input field should provide autocomletions as user types @Displayname or @user@email.address + ### `https://delta.chat` and `mailto:example@example.com` - Links @@ -187,21 +215,7 @@ URL parsing allows all valid URLs, no restrictions on schemes, no whitelist is n - ':' + [A-z0-9_-] + ':' ? - could also be used for custom DC emojis in the future - -### Mentions `@username` - -Clickable. (could get replaced with a user hash/email/ID on send/on receive so that it's still valid on name change.) - -On sending/receiving, this is transformed into an internal representation: - -Implementation idea: - -1. user types @Displayname and at best gets autocompletion while typing the URL -2. on sending, the username is converted to the transmission format (special format that contains the email address as ID) -3. on receiving/storing the message inside the database, this format is converted again to contain the local contact ID to allow for future email address migration/rotation. - (4.) on forwarding/sharing as chat history, the ID representation needs to be converted from the contact ID format to the transmission format again - -see discords mention code for reference/inspiration https://blog.discordapp.com/how-discord-renders-rich-messages-on-the-android-app-67b0e5d56fbe +- In Message Composer: should provide auto completions that replace the text by the unicode emoji, or the complete code if it is a custom emoji. ### `$[inline TeX]$` and `$$[Tex displayed in block(new line)]$$` diff --git a/src/parser/mod.rs b/src/parser/mod.rs index dc71d0f..e20fcb3 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -22,10 +22,10 @@ pub enum Element<'a> { destination: LinkDestination<'a>, }, EmailAddress(&'a str), + Mention { + address: &'a str, + }, // Later: - // Mention { - // internal_id: &str - // }, /// On click, the command gets prefilled as the draft, so it can be easily send. BotCommandSuggestion(&'a str), @@ -68,3 +68,8 @@ pub fn parse_only_text(input: &str) -> std::vec::Vec { pub fn parse_desktop_set(input: &str) -> std::vec::Vec { parse_from_text::parse_desktop_set(input) } + +/// Extract mentions as email addresses from a text +pub fn extract_mention_addresses(input: &str) -> Vec { + parse_from_text::extract_mention_addresses(input) +} \ No newline at end of file diff --git a/src/parser/parse_from_text/mod.rs b/src/parser/parse_from_text/mod.rs index a3180f4..4b3d29a 100644 --- a/src/parser/parse_from_text/mod.rs +++ b/src/parser/parse_from_text/mod.rs @@ -1,3 +1,7 @@ +use nom::bytes::complete::take_until; + +use self::base_parsers::CustomError; + use super::Element; pub(crate) mod base_parsers; @@ -77,3 +81,23 @@ pub(crate) fn parse_desktop_set(input: &str) -> std::vec::Vec { } result } + +/// Extract mentions as email addresses from a text +/// The addresses are sorted and deduplicated +pub(crate) fn extract_mention_addresses(input: &str) -> Vec { + let mut result = Vec::new(); + let mut remaining = input; + while !remaining.is_empty() { + if let Ok((rest, Element::Mention { address })) = text_elements::mention(remaining) { + result.push(address.to_owned()); + remaining = rest; + } else if let Ok((_, rest)) = take_until::<&str, &str, CustomError<&str>>("@")(remaining) { + remaining = rest; + } else { + break; + } + } + result.sort(); + result.dedup(); + result +} diff --git a/src/parser/parse_from_text/text_elements.rs b/src/parser/parse_from_text/text_elements.rs index 161d8c6..06720b5 100644 --- a/src/parser/parse_from_text/text_elements.rs +++ b/src/parser/parse_from_text/text_elements.rs @@ -94,6 +94,18 @@ pub(crate) fn email_address(input: &str) -> IResult<&str, Element, CustomError<& } } +pub(crate) fn mention(input: &str) -> IResult<&str, Element, CustomError<&str>> { + let (input, _) = tag("@")(input)?; + let (input, email) = email_address(input)?; + if let Element::EmailAddress(address) = email { + Ok((input, Element::Mention { address })) + } else { + Err(nom::Err::Error(CustomError::UnxepectedError( + "no email, should not happen".to_string(), + ))) + } +} + fn not_link_part_char(c: char) -> bool { !matches!(c, ':' | '\n' | '\r' | '\t' | ' ') } @@ -263,6 +275,8 @@ pub(crate) fn parse_text_element( if let Ok((i, elm)) = hashtag(input) { Ok((i, elm)) + } else if let Ok((i, elm)) = mention(input) { + Ok((i, elm)) } else if let Ok((i, elm)) = email_address(input) { Ok((i, elm)) } else if let Ok((i, elm)) = link(input) { diff --git a/tests/mentions.rs b/tests/mentions.rs new file mode 100644 index 0000000..aa64db2 --- /dev/null +++ b/tests/mentions.rs @@ -0,0 +1,24 @@ +use deltachat_message_parser::parser::extract_mention_addresses; + +#[test] +fn extract_mentions() { + let mention_text = "Ping @email@address.tld and @email1@address.tld!"; + assert_eq!( + extract_mention_addresses(mention_text), + vec!["email@address.tld", "email1@address.tld"] + ) +} + +#[test] +fn extract_mentions_are_deduped_and_sorted() { + let mention_text = + "Ping @email@address.tld, @abc@example.com, @abc@example.com and @email1@address.tld!\n@email1@address.tld your opinion would be especially helpful."; + assert_eq!( + extract_mention_addresses(mention_text), + vec![ + "@abc@example.com", + "email@address.tld", + "email1@address.tld" + ] + ) +} diff --git a/tests/test.rs b/tests/test.rs index f37c391..39657e9 100644 --- a/tests/test.rs +++ b/tests/test.rs @@ -1 +1,2 @@ mod text_to_ast; +mod mentions; \ No newline at end of file diff --git a/tests/text_to_ast/text_only.rs b/tests/text_to_ast/text_only.rs index 2650973..4d6a8ba 100644 --- a/tests/text_to_ast/text_only.rs +++ b/tests/text_to_ast/text_only.rs @@ -271,6 +271,89 @@ fn email_address_do_not_parse_last_char_if_special() { ); } +#[test] +fn mention_example() { + assert_eq!( + parse_only_text("This is not very easy.\n Please ask @hello@example.com"), + vec![ + Text("This is not very easy."), + Linebreak, + Text(" Please ask "), + Mention { + address: "hello@example.com" + }, + ] + ); +} + +#[test] +fn mention_do_not_parse_last_dot() { + assert_eq!( + parse_only_text("you can ping me like this @me@provider.tld."), + vec![ + Text("you can ping me like this "), + Mention { + address: "me@provider.tld" + }, + Text(".") + ] + ); +} + +#[test] +fn mention_do_not_parse_last_char_if_special() { + assert_eq!( + parse_only_text("you can ping me via @me@provider.tld!"), + vec![ + Text("you can ping me via "), + Mention { + address: "me@provider.tld" + }, + Text("!") + ] + ); + assert_eq!( + parse_only_text("you can ping me via @me@provider.tld?"), + vec![ + Text("you can ping me via "), + Mention { + address: "me@provider.tld" + }, + Text("?") + ] + ); + assert_eq!( + parse_only_text("you can ping me via @me@provider.tld,"), + vec![ + Text("you can ping me via "), + Mention { + address: "me@provider.tld" + }, + Text(",") + ] + ); + assert_eq!( + parse_only_text("you can ping me via @me@provider.tld:"), + vec![ + Text("you can ping me via "), + Mention { + address: "me@provider.tld" + }, + Text(":") + ] + ); + assert_eq!( + parse_only_text("you can ping me via @me@provider.tld;"), + vec![ + Text("you can ping me via "), + Mention { + address: "me@provider.tld" + }, + Text(";") + ] + ); +} + #[test] fn link() { let test_cases = vec![ From 1bbcc6095d9cb748e9b56f32a461c0fafd1967c8 Mon Sep 17 00:00:00 2001 From: Simon Laux Date: Sat, 14 Oct 2023 05:27:41 +0200 Subject: [PATCH 2/4] wasm update and more testing --- message_parser_wasm/example.html | 8 ++++++++ message_parser_wasm/example.js | 9 +++++++++ message_parser_wasm/src/lib.rs | 1 + message_parser_wasm/src/manual_typings.ts | 1 + tests/mentions.rs | 9 +++++++++ 5 files changed, 28 insertions(+) diff --git a/message_parser_wasm/example.html b/message_parser_wasm/example.html index 266d3c2..f7dd12b 100644 --- a/message_parser_wasm/example.html +++ b/message_parser_wasm/example.html @@ -36,6 +36,14 @@ border-radius: 2px; padding: 2px; } + + .mention { + color: rgb(0, 126, 168); + } + + body { + font-family: sans-serif; + }

Input