11/*
2+ Copyright 2026 Element Creations Ltd.
23Copyright 2024, 2025 New Vector Ltd.
34Copyright 2024 The Matrix.org Foundation C.I.C.
45
56SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
67Please see LICENSE files in the repository root for full details.
78*/
89
9- import React , { type ReactElement } from "react" ;
1010import sanitizeHtml , { type IOptions } from "sanitize-html" ;
11- import { merge } from "lodash" ;
12- import _Linkify from "linkify-react" ;
11+ import {
12+ PERMITTED_URL_SCHEMES ,
13+ linkifyString as _linkifyString ,
14+ linkifyHtml as _linkifyHtml ,
15+ LinkifyMatrixOpaqueIdType ,
16+ generateLinkedTextOptions ,
17+ type LinkEventListener ,
18+ } from "@element-hq/web-shared-components" ;
19+ import { getHttpUriForMxc , User } from "matrix-js-sdk/src/matrix" ;
1320
14- import { _linkifyString , _linkifyHtml , ELEMENT_URL_PATTERN , options as linkifyMatrixOptions } from "./linkify-matrix" ;
15- import { tryTransformPermalinkToLocalHref } from "./utils/permalinks/Permalinks" ;
21+ import { ELEMENT_URL_PATTERN } from "./linkify-matrix" ;
1622import { mediaFromMxc } from "./customisations/Media" ;
17- import { PERMITTED_URL_SCHEMES } from "./utils/UrlUtils" ;
23+ import {
24+ parsePermalink ,
25+ tryTransformEntityToPermalink ,
26+ tryTransformPermalinkToLocalHref ,
27+ } from "./utils/permalinks/Permalinks" ;
28+ import dis from "./dispatcher/dispatcher" ;
29+ import { Action } from "./dispatcher/actions" ;
30+ import { type ViewUserPayload } from "./dispatcher/payloads/ViewUserPayload" ;
31+ import { type ViewRoomPayload } from "./dispatcher/payloads/ViewRoomPayload" ;
32+ import { MatrixClientPeg } from "./MatrixClientPeg" ;
1833
1934const COLOR_REGEX = / ^ # [ 0 - 9 a - f A - F ] { 6 } $ / ;
2035const MEDIA_API_MXC_REGEX = / \/ _ m a t r i x \/ m e d i a \/ r 0 \/ (?: d o w n l o a d | t h u m b n a i l ) \/ ( .+ ?) \/ ( .+ ?) (?: [ ? / ] | $ ) / ;
@@ -29,7 +44,7 @@ export const transformTags: NonNullable<IOptions["transformTags"]> = {
2944 const transformed = tryTransformPermalinkToLocalHref ( attribs . href ) ; // only used to check if it is a link that can be handled locally
3045 if (
3146 transformed !== attribs . href || // it could be converted so handle locally symbols e.g. @user :server.tdl, matrix: and matrix.to
32- attribs . href . match ( ELEMENT_URL_PATTERN ) // for https links to Element domains
47+ ELEMENT_URL_PATTERN . test ( attribs . href ) // for https links to Element domains
3348 ) {
3449 delete attribs . target ;
3550 }
@@ -193,43 +208,199 @@ export const sanitizeHtmlParams: IOptions = {
193208 nestingLimit : 50 ,
194209} ;
195210
196- /* Wrapper around linkify-react merging in our default linkify options */
197- export function Linkify ( { as, options, children } : React . ComponentProps < typeof _Linkify > ) : ReactElement {
198- return (
199- < _Linkify as = { as } options = { merge ( { } , linkifyMatrixOptions , options ) } >
200- { children }
201- </ _Linkify >
202- ) ;
211+ /**
212+ * Handler function when a UserID link is clicked.
213+ * @param event The click event
214+ * @param userId The linked UserID
215+ */
216+ function onUserClick ( event : MouseEvent , userId : string ) : void {
217+ event . preventDefault ( ) ;
218+ dis . dispatch < ViewUserPayload > ( {
219+ action : Action . ViewUser ,
220+ member : new User ( userId ) ,
221+ } ) ;
222+ }
223+
224+ /**
225+ * Handler function when a Room Alias link is clicked.
226+ * @param event The click event
227+ * @param roomAlias The linked room alias
228+ */
229+ function onAliasClick ( event : MouseEvent , roomAlias : string ) : void {
230+ event . preventDefault ( ) ;
231+ dis . dispatch < ViewRoomPayload > ( {
232+ action : Action . ViewRoom ,
233+ room_alias : roomAlias ,
234+ metricsTrigger : "Timeline" ,
235+ metricsViaKeyboard : false ,
236+ } ) ;
237+ }
238+
239+ /**
240+ * Generates a set of event handlers for a regular URL link.
241+ *
242+ * @param href The link location.
243+ * @returns Event listenenrs compatible with linkifyjs.
244+ */
245+ function urlEventListeners ( href : string ) : LinkEventListener {
246+ // intercept local permalinks to users and show them like userids (in userinfo of current room)
247+ try {
248+ const permalink = parsePermalink ( href ) ;
249+ if ( permalink ?. userId ) {
250+ return {
251+ click : function ( e : MouseEvent ) {
252+ onUserClick ( e , permalink . userId ! ) ;
253+ } ,
254+ } ;
255+ } else {
256+ // for events, rooms etc. (anything other than users)
257+ const localHref = tryTransformPermalinkToLocalHref ( href ) ;
258+ if ( localHref !== href ) {
259+ // it could be converted to a localHref -> therefore handle locally
260+ return {
261+ click : function ( e : MouseEvent ) {
262+ e . preventDefault ( ) ;
263+ globalThis . location . hash = localHref ;
264+ } ,
265+ } ;
266+ }
267+ }
268+ } catch {
269+ // OK fine, it's not actually a permalink
270+ }
271+ return { } ;
203272}
204273
274+ /**
275+ * Generates a set of event handlers for a UserID link.
276+ *
277+ * @param href A link that contains a userId.
278+ * @returns Event listenenrs compatible with linkifyjs.
279+ */
280+ export function userIdEventListeners ( href : string ) : LinkEventListener {
281+ return {
282+ click : function ( e : MouseEvent ) {
283+ e . preventDefault ( ) ;
284+ const userId = parsePermalink ( href ) ?. userId ?? href ;
285+ if ( userId ) onUserClick ( e , userId ) ;
286+ } ,
287+ } ;
288+ }
289+
290+ /**
291+ * Generates a set of event handlers for a UserID link.
292+ *
293+ * @param href A link that contains a room alias.
294+ * @returns Event listenenrs compatible with linkifyjs.
295+ */
296+ export function roomAliasEventListeners ( href : string ) : LinkEventListener {
297+ return {
298+ click : function ( e : MouseEvent ) {
299+ e . preventDefault ( ) ;
300+ const alias = parsePermalink ( href ) ?. roomIdOrAlias ?? href ;
301+ if ( alias ) onAliasClick ( e , alias ) ;
302+ } ,
303+ } ;
304+ }
305+
306+ /**
307+ * Generates a `target` attribute for the anchor element
308+ * for the given `href` value.
309+ *
310+ * @param href A URL from a link.
311+ * @returns The resulting `target` value.
312+ */
313+ function urlTargetTransformFunction ( href : string ) : string {
314+ try {
315+ const transformed = tryTransformPermalinkToLocalHref ( href ) ;
316+ if (
317+ transformed !== href || // if it could be converted to handle locally for matrix symbols e.g. @user :server.tdl and matrix.to
318+ ELEMENT_URL_PATTERN . test ( decodeURIComponent ( href ) ) // for https links to Element domains
319+ ) {
320+ return "" ;
321+ } else {
322+ return "_blank" ;
323+ }
324+ } catch {
325+ // malformed URI
326+ }
327+ return "" ;
328+ }
329+
330+ /**
331+ * Generates the result `href` value based on an incoming `href` value and a link type.
332+ *
333+ * @param href A URL from a link.
334+ * @param type The type of link beinh handled.
335+ * @returns The resulting `href` value.
336+ */
337+ export function formatHref ( href : string , type : LinkifyMatrixOpaqueIdType ) : string {
338+ switch ( type ) {
339+ case LinkifyMatrixOpaqueIdType . URL :
340+ if ( href . startsWith ( "mxc://" ) && MatrixClientPeg . get ( ) ) {
341+ return getHttpUriForMxc (
342+ MatrixClientPeg . get ( ) ! . baseUrl ,
343+ href ,
344+ undefined ,
345+ undefined ,
346+ undefined ,
347+ false ,
348+ true ,
349+ ) ;
350+ }
351+ // fallthrough
352+ case LinkifyMatrixOpaqueIdType . RoomAlias :
353+ case LinkifyMatrixOpaqueIdType . UserId :
354+ default : {
355+ return tryTransformEntityToPermalink ( MatrixClientPeg . safeGet ( ) , href ) ?? "" ;
356+ }
357+ }
358+ }
359+
360+ /**
361+ * The standard configuration for a LinkedTextContext.Provider
362+ * within Element Web.
363+ */
364+ export const LinkedTextConfiguration = {
365+ userIdListener : userIdEventListeners ,
366+ roomAliasListener : roomAliasEventListeners ,
367+ urlListener : urlEventListeners ,
368+ hrefTransformer : formatHref ,
369+ urlTargetTransformer : urlTargetTransformFunction ,
370+ } ;
371+
205372/**
206373 * Linkifies the given string. This is a wrapper around 'linkifyjs/string'.
207374 *
208- * @param { string } str string to linkify
209- * @param { object } [options] Options for linkifyString. Default: linkifyMatrixOptions
210- * @returns { string } Linkified string
375+ * @param str string to linkify
376+ * @param [options] Options for linkifyString.
377+ * @returns Linkified string
211378 */
212- export function linkifyString ( str : string , options = linkifyMatrixOptions ) : string {
213- return _linkifyString ( str , options ) ;
379+ export function linkifyString ( value : string , options = generateLinkedTextOptions ( LinkedTextConfiguration ) ) : string {
380+ return _linkifyString ( value , options ) ;
214381}
215382
216383/**
217384 * Linkifies the given HTML-formatted string. This is a wrapper around 'linkifyjs/html'.
218385 *
219- * @param { string } str HTML string to linkify
220- * @param { object } [options] Options for linkifyHtml. Default: linkifyMatrixOptions
221- * @returns { string } Linkified string
386+ * @param str HTML string to linkify
387+ * @param [options] Options for linkifyHtml.
388+ * @returns Linkified string
222389 */
223- export function linkifyHtml ( str : string , options = linkifyMatrixOptions ) : string {
224- return _linkifyHtml ( str , options ) ;
390+ export function linkifyHtml ( value : string , options = generateLinkedTextOptions ( LinkedTextConfiguration ) ) : string {
391+ return _linkifyHtml ( value , options ) ;
225392}
393+
226394/**
227395 * Linkify the given string and sanitize the HTML afterwards.
228396 *
229- * @param { string } dirtyHtml The HTML string to sanitize and linkify
230- * @param { object } [options] Options for linkifyString. Default: linkifyMatrixOptions
231- * @returns { string }
397+ * @param dirtyString The string to linkify, and then sanitize.
398+ * @param [options] Options for linkifyString. Default: linkifyMatrixOptions
399+ * @returns HTML string
232400 */
233- export function linkifyAndSanitizeHtml ( dirtyHtml : string , options = linkifyMatrixOptions ) : string {
401+ export function linkifyAndSanitizeHtml (
402+ dirtyHtml : string ,
403+ options = generateLinkedTextOptions ( LinkedTextConfiguration ) ,
404+ ) : string {
234405 return sanitizeHtml ( linkifyString ( dirtyHtml , options ) , sanitizeHtmlParams ) ;
235406}
0 commit comments