diff --git a/src/domain/session/room/timeline/MessageBody.js b/src/domain/session/room/timeline/MessageBody.js index 325c79e1..5f346017 100644 --- a/src/domain/session/room/timeline/MessageBody.js +++ b/src/domain/session/room/timeline/MessageBody.js @@ -1,4 +1,5 @@ import { linkify } from "./linkify/linkify.js"; +import { getIdentifierColorNumber, avatarInitials } from "../../../avatar.js"; /** * Parse text into parts such as newline, links and text. @@ -92,6 +93,24 @@ export class ImagePart { get type() { return "image"; } }; +export class PillPart { + constructor(id, href, children) { + this.id = id; + this.href = href; + this.children = children; + } + + get type() { return "pill"; } + + get avatarColorNumber() { + return getIdentifierColorNumber(this.id); + } + + get avatarInitials() { + return avatarInitials(this.id); + } +} + export class LinkPart { constructor(url, inlines) { this.url = url; diff --git a/src/domain/session/room/timeline/deserialize.js b/src/domain/session/room/timeline/deserialize.js index 68a08c9d..c02c4117 100644 --- a/src/domain/session/room/timeline/deserialize.js +++ b/src/domain/session/room/timeline/deserialize.js @@ -1,5 +1,6 @@ -import { MessageBody, HeaderBlock, ListBlock, CodeBlock, FormatPart, NewLinePart, RulePart, TextPart, LinkPart, ImagePart } from "./MessageBody.js" +import { MessageBody, HeaderBlock, ListBlock, CodeBlock, PillPart, FormatPart, NewLinePart, RulePart, TextPart, LinkPart, ImagePart } from "./MessageBody.js" import { linkify } from "./linkify/linkify.js"; +import { parsePillLink } from "./pills.js" /* At the time of writing (Jul 1 2021), Matrix Spec recommends * allowing the following HTML tags: @@ -24,6 +25,10 @@ class Deserializer { // TODO Not equivalent to `node.href`! // Add another HTMLParseResult method? const href = this.result.getAttributeValue(node, "href"); + const pillData = href && parsePillLink(href); + if (pillData && pillData.userId) { + return new PillPart(pillData.userId, href, children); + } return new LinkPart(href, children); } diff --git a/src/domain/session/room/timeline/pills.js b/src/domain/session/room/timeline/pills.js new file mode 100644 index 00000000..a638ea58 --- /dev/null +++ b/src/domain/session/room/timeline/pills.js @@ -0,0 +1,12 @@ +const baseUrl = 'https://matrix.to'; +const linkPrefix = `${baseUrl}/#/`; + +export function parsePillLink(link) { + if (!link.startsWith(linkPrefix)) { + return null; + } + const contents = link.substring(linkPrefix.length); + if (contents[0] === '@') { + return { userId: contents } + } +} diff --git a/src/platform/web/ui/css/avatar.css b/src/platform/web/ui/css/avatar.css index d369f85f..143ea899 100644 --- a/src/platform/web/ui/css/avatar.css +++ b/src/platform/web/ui/css/avatar.css @@ -76,3 +76,11 @@ limitations under the License. line-height: var(--avatar-size); font-size: calc(var(--avatar-size) * 0.6); } + +.hydrogen .avatar.size-12 { + --avatar-size: 12px; + width: var(--avatar-size); + height: var(--avatar-size); + line-height: var(--avatar-size); + font-size: calc(var(--avatar-size) * 0.6); +} diff --git a/src/platform/web/ui/css/themes/element/timeline.css b/src/platform/web/ui/css/themes/element/timeline.css index 07afdf16..e5490570 100644 --- a/src/platform/web/ui/css/themes/element/timeline.css +++ b/src/platform/web/ui/css/themes/element/timeline.css @@ -216,6 +216,21 @@ only loads when the top comes into view*/ padding: 10px; } +.Timeline_messageBody .pill { + padding: 0px 5px 0px 5px; + border-radius: 15px; + background-color: #f6f6f6; + border: 1px solid rgb(206, 206, 206); + text-decoration: none; + display: inline-flex; + align-items: center; +} + +.Timeline_messageBody .pill div.avatar { + display: inline-block; + margin-right: 3px; +} + .Timeline_message.unsent .Timeline_messageBody { color: #ccc; } diff --git a/src/platform/web/ui/session/room/timeline/TextMessageView.js b/src/platform/web/ui/session/room/timeline/TextMessageView.js index e244ab3a..ff674753 100644 --- a/src/platform/web/ui/session/room/timeline/TextMessageView.js +++ b/src/platform/web/ui/session/room/timeline/TextMessageView.js @@ -15,7 +15,7 @@ limitations under the License. */ import {StaticView} from "../../../general/StaticView.js"; -import {tag, text} from "../../../general/html.js"; +import {tag, text, classNames} from "../../../general/html.js"; import {BaseMessageView} from "./BaseMessageView.js"; export class TextMessageView extends BaseMessageView { @@ -53,6 +53,14 @@ function renderImage(imagePart) { return tag.img(attributes, []); } +function renderPill(pillPart) { + const classes = `avatar size-12 usercolor${pillPart.avatarColorNumber}`; + const avatar = tag.div({class: classes}, text(pillPart.avatarInitials)); + const children = renderParts(pillPart.children); + children.unshift(avatar); + return tag.a({ class: "pill", href: pillPart.href }, children); +} + /** * Map from part to function that outputs DOM for the part */ @@ -63,6 +71,7 @@ const formatFunction = { code: codePart => tag.code({}, text(codePart.text)), text: textPart => text(textPart.text), link: linkPart => tag.a({ href: linkPart.url, target: "_blank", rel: "noopener" }, renderParts(linkPart.inlines)), + pill: renderPill, format: formatPart => tag[formatPart.format]({}, renderParts(formatPart.children)), list: renderList, image: renderImage,