diff --git a/README.md b/README.md index d78e43f..537aa71 100644 --- a/README.md +++ b/README.md @@ -185,6 +185,7 @@ The following namespaces are available for use: - `message:reactions:view`: show the list of reactions that can be sent for message - `message:reactions:react`: react to a message (eg. user clicks on an existing reaction to send it or retract it) - `message:file:view`: view a file from a message (eg. user clicks on a file to expand or download the file) +- `message:link:open`: open a link contained in a message (eg. user clicks on a link) - `message:history:view`: notify when a message from history enters or leaves view area, with the `visible` and `hidden` states (eg. scrolling up or down in the timeline) - `message:history:seek`: request to load `backwards` or `forwards` history (eg. scrolling back in time to load past messages) diff --git a/src/messaging/components/message/message.js b/src/messaging/components/message/message.js index aa25c02..ff287a7 100644 --- a/src/messaging/components/message/message.js +++ b/src/messaging/components/message/message.js @@ -7,6 +7,7 @@ // IMPORTS +import { nextTick } from "petite-vue"; import { htmlEscape as _e } from "escape-goat"; import linkifyHtml from "linkify-html"; import snarkdown from "snarkdown"; @@ -241,6 +242,10 @@ function MessagePartText(content) { mounted() { // Generate text message HTML this.html = this.__generateHTML(content); + + // Bind link click events + // Notice: ensure DOM has been rendered w/ HTML content + nextTick(this.__bindLinkClickEvents); }, /** @@ -260,11 +265,72 @@ function MessagePartText(content) { htmlContent = linkifyHtml(htmlContent, TEXT_LINKIFY_OPTIONS); return htmlContent; + }, + + /** + * Binds link click events + * @private + * @return {undefined} + */ + __bindLinkClickEvents() { + // Bind click event on all links? (if any text element) + // Notice: since we are generating custom HTML code outside of Vue, then \ + // we cannot use the standard '@click' event and have to resort to \ + // using the non-Vue 'addEventListener()'. + if (this.$refs.textInner) { + const linkElements = + this.$refs.textInner.getElementsByTagName("a") || []; + + if (linkElements.length > 0) { + for (const linkElement of linkElements) { + linkElement.addEventListener("click", this.__onLinkClick); + } + } + } + }, + + // --> EVENT LISTENERS <-- + + /** + * Triggers when a link is clicked + * @private + * @param {object} event + * @return {undefined} + */ + __onLinkClick(event) { + // Do not open link (let the implementing app choose what to do) + event.preventDefault(); + + // Handle link? (if any) + const linkUrl = event.target?.href || null; + + if (linkUrl !== null) { + // Extract protocol from link (if any) + const protocolSeparatorIndex = linkUrl.indexOf(":"); + + let linkProtocol = null; + + if (protocolSeparatorIndex > 0) { + linkProtocol = linkUrl + .substring(0, protocolSeparatorIndex) + .toLowerCase(); + } + + // Emit message link open event + $event._emit("message:link:open", { + id: content.id, + + link: { + url: linkUrl, + protocol: linkProtocol + } + }); + } } }; } -function MessagePartFile(content, file) { +function MessagePartFile(file) { return { // --> TEMPLATE <-- diff --git a/src/messaging/messaging.html b/src/messaging/messaging.html index ba5dd7d..2bcaf21 100644 --- a/src/messaging/messaging.html +++ b/src/messaging/messaging.html @@ -138,7 +138,7 @@ @@ -220,7 +220,12 @@