Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add @Mentions #49

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from 3 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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 8 additions & 0 deletions message_parser_wasm/example.html
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,14 @@
border-radius: 2px;
padding: 2px;
}

.mention {
color: rgb(0, 126, 168);
}

body {
font-family: sans-serif;
}
</style>
<h3>Input</h3>
<textarea
Expand Down
9 changes: 9 additions & 0 deletions message_parser_wasm/example.js
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,15 @@ function renderElement(elm) {
email.innerText = elm.c;
email.href = "mailto:" + elm.c;
return email;

case "Mention":
let mention = document.createElement("span");
mention.innerText = "@" + elm.c.address;
mention.onclick = () => {
alert(`Clicked on a Mention, this should open view profile view for ${elm.c.address}`)
}
mention.className = "mention"
return mention

case "BotCommandSuggestion":
let bcs = document.createElement("a");
Expand Down
1 change: 1 addition & 0 deletions message_parser_wasm/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ export type ParsedElement =
| { t: "InlineCode"; c: { content: string } }
| { t: "CodeBlock"; c: { language: null | string; content: string } }
| { t: "EmailAddress"; c: string }
| { t: "Mention"; c: { address: string } }
| { t: "BotCommandSuggestion"; c: string }
| { t: "Link"; c: { destination: LinkDestination } }
| {
Expand Down
1 change: 1 addition & 0 deletions message_parser_wasm/src/manual_typings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export type ParsedElement =
| { t: "InlineCode"; c: { content: string } }
| { t: "CodeBlock"; c: { language: null | string; content: string } }
| { t: "EmailAddress"; c: string }
| { t: "Mention"; c: { address: string } }
| { t: "BotCommandSuggestion"; c: string }
| { t: "Link"; c: { destination: LinkDestination } }
| {
Expand Down
44 changes: 29 additions & 15 deletions spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

- Text only
- [Email addresses: `[email protected]`](#email-addresses)
- [Mentions `@[email protected]`](#mentions)
- [Links: `https://delta.chat` and `mailto:[email protected]`](#links)
- [Bot `/commands`](#bot-commands)
- [Hashtags: `#tag`](#hashtag)
Expand Down Expand Up @@ -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.
iequidoo marked this conversation as resolved.
Show resolved Hide resolved

<a name="mentions" id="mentions"></a>

### Mentions `@[email protected]`
Simon-Laux marked this conversation as resolved.
Show resolved Hide resolved

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 @[email protected]

<a name="links" id="links"></a>

### `https://delta.chat` and `mailto:[email protected]` - Links
Expand Down Expand Up @@ -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)]$$`

Expand Down
11 changes: 8 additions & 3 deletions src/parser/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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),

Expand Down Expand Up @@ -68,3 +68,8 @@ pub fn parse_only_text(input: &str) -> std::vec::Vec<Element> {
pub fn parse_desktop_set(input: &str) -> std::vec::Vec<Element> {
parse_from_text::parse_desktop_set(input)
}

/// Extract mentions as email addresses from a text
pub fn extract_mention_addresses(input: &str) -> Vec<String> {
parse_from_text::extract_mention_addresses(input)
}
27 changes: 27 additions & 0 deletions src/parser/parse_from_text/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
use nom::bytes::complete::take_until;

use self::base_parsers::CustomError;

use super::Element;

pub(crate) mod base_parsers;
Expand Down Expand Up @@ -77,3 +81,26 @@ pub(crate) fn parse_desktop_set(input: &str) -> std::vec::Vec<Element> {
}
result
}

/// Extract mentions as email addresses from a text
/// The addresses are sorted and deduplicated
pub(crate) fn extract_mention_addresses(input: &str) -> Vec<String> {
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;
continue;
}
if let Ok((rest, _)) = take_until::<&str, &str, CustomError<&str>>(" @")(remaining) {
remaining = rest;
} else {
// there is no mention anymore in this message
break;
}
}
result.sort();
iequidoo marked this conversation as resolved.
Show resolved Hide resolved
result.dedup();
result
}
14 changes: 14 additions & 0 deletions src/parser/parse_from_text/text_elements.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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' | ' ')
}
Expand Down Expand Up @@ -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) {
Expand Down
29 changes: 29 additions & 0 deletions tests/mentions.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
use deltachat_message_parser::parser::extract_mention_addresses;

#[test]
fn extract_mentions() {
let mention_text = "Ping @[email protected] and @[email protected]!";
assert_eq!(
extract_mention_addresses(mention_text),
vec!["[email protected]", "[email protected]"]
)
}

#[test]
fn extract_mentions_are_deduped_and_sorted() {
let mention_text =
"Ping @[email protected], @[email protected], @[email protected] and @[email protected]!\n@[email protected] your opinion would be especially helpful.";
assert_eq!(
extract_mention_addresses(mention_text),
vec!["[email protected]", "[email protected]", "[email protected]"]
)
}

#[test]
fn extract_mentions_false_positive() {
let mention_text = "my text@[email protected], more text";
assert_eq!(
extract_mention_addresses(mention_text),
Vec::<String>::new()
);
}
1 change: 1 addition & 0 deletions tests/test.rs
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
mod text_to_ast;
mod mentions;
83 changes: 83 additions & 0 deletions tests/text_to_ast/text_only.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 @[email protected]"),
vec![
Text("This is not very easy."),
Linebreak,
Text(" Please ask "),
Mention {
address: "[email protected]"
},
]
);
}

#[test]
fn mention_do_not_parse_last_dot() {
assert_eq!(
parse_only_text("you can ping me like this @[email protected]."),
vec![
Text("you can ping me like this "),
Mention {
address: "[email protected]"
},
Text(".")
]
);
}

#[test]
fn mention_do_not_parse_last_char_if_special() {
assert_eq!(
parse_only_text("you can ping me via @[email protected]!"),
vec![
Text("you can ping me via "),
Mention {
address: "[email protected]"
},
Text("!")
iequidoo marked this conversation as resolved.
Show resolved Hide resolved
]
);
assert_eq!(
parse_only_text("you can ping me via @[email protected]?"),
vec![
Text("you can ping me via "),
Mention {
address: "[email protected]"
},
Text("?")
]
);
assert_eq!(
parse_only_text("you can ping me via @[email protected],"),
vec![
Text("you can ping me via "),
Mention {
address: "[email protected]"
},
Text(",")
]
);
assert_eq!(
parse_only_text("you can ping me via @[email protected]:"),
vec![
Text("you can ping me via "),
Mention {
address: "[email protected]"
},
Text(":")
]
);
assert_eq!(
parse_only_text("you can ping me via @[email protected];"),
vec![
Text("you can ping me via "),
Mention {
address: "[email protected]"
},
Text(";")
]
);
}

#[test]
fn link() {
let test_cases = vec![
Expand Down
Loading