From 6712a5b1c54202d7c670a7bdb213e18e3b9eb4cf Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 20 Jan 2022 10:18:47 -0700 Subject: [PATCH] Parse matrix-schemed URIs (#7453) Co-authored-by: J. Ryan Stinnett Co-authored-by: Dariusz Niemczyk Co-authored-by: Timo K With this pr all href use matrix matrix.to links. As a consequence right-click copy link will always return get you a sharable matrix.to link. --- package.json | 7 +- src/HtmlUtils.tsx | 7 +- src/components/views/elements/Pill.js | 4 +- src/linkify-matrix.ts | 70 +++++++----- .../MatrixSchemePermalinkConstructor.ts | 105 ++++++++++++++++++ ...tor.ts => MatrixToPermalinkConstructor.ts} | 2 +- src/utils/permalinks/Permalinks.ts | 61 +++++----- src/utils/pillify.tsx | 4 +- .../views/messages/TextualBody-test.js | 5 +- .../views/rooms/SendMessageComposer-test.tsx | 12 +- test/linkify-matrix-test.ts | 40 ++++++- yarn.lock | 37 +++--- 12 files changed, 254 insertions(+), 100 deletions(-) create mode 100644 src/utils/permalinks/MatrixSchemePermalinkConstructor.ts rename src/utils/permalinks/{SpecPermalinkConstructor.ts => MatrixToPermalinkConstructor.ts} (97%) diff --git a/package.json b/package.json index e3c992872a..898ee2cc47 100644 --- a/package.json +++ b/package.json @@ -58,6 +58,9 @@ }, "dependencies": { "@babel/runtime": "^7.12.5", + "@matrix-org/linkify-element": "^4.0.0-rc.5", + "@matrix-org/linkify-string": "^4.0.0-rc.5", + "@matrix-org/linkifyjs": "^4.0.0-rc.5", "@sentry/browser": "^6.11.0", "@sentry/tracing": "^6.11.0", "@types/geojson": "^7946.0.8", @@ -85,9 +88,6 @@ "is-ip": "^3.1.0", "jszip": "^3.7.0", "katex": "^0.12.0", - "linkify-element": "^3.0.4", - "linkify-string": "^3.0.4", - "linkifyjs": "^3.0.5", "lodash": "^4.17.20", "maplibre-gl": "^1.15.2", "matrix-analytics-events": "https://github.com/matrix-org/matrix-analytics-events.git#1eab4356548c97722a183912fda1ceabbe8cc7c1", @@ -147,7 +147,6 @@ "@types/file-saver": "^2.0.3", "@types/flux": "^3.1.9", "@types/jest": "^26.0.20", - "@types/linkifyjs": "^2.1.3", "@types/lodash": "^4.14.168", "@types/modernizr": "^3.5.3", "@types/node": "^14.14.22", diff --git a/src/HtmlUtils.tsx b/src/HtmlUtils.tsx index 0b94cfd891..b526d48edd 100644 --- a/src/HtmlUtils.tsx +++ b/src/HtmlUtils.tsx @@ -175,9 +175,10 @@ const transformTags: IExtendedSanitizeOptions["transformTags"] = { // custom to if (attribs.href) { attribs.target = '_blank'; // by default - const transformed = tryTransformPermalinkToLocalHref(attribs.href); - if (transformed !== attribs.href || attribs.href.match(ELEMENT_URL_PATTERN)) { - attribs.href = transformed; + const transformed = tryTransformPermalinkToLocalHref(attribs.href); // only used to check if it is a link that can be handled locally + if (transformed !== attribs.href || // it could be converted so handle locally symbols e.g. @user:server.tdl, matrix: and matrix.to + attribs.href.match(ELEMENT_URL_PATTERN) // for https:vector|riot... + ) { delete attribs.target; } } diff --git a/src/components/views/elements/Pill.js b/src/components/views/elements/Pill.js index 2e99a0c57f..9bb67840d0 100644 --- a/src/components/views/elements/Pill.js +++ b/src/components/views/elements/Pill.js @@ -24,7 +24,7 @@ import * as sdk from '../../../index'; import dis from '../../../dispatcher/dispatcher'; import { MatrixClientPeg } from '../../../MatrixClientPeg'; import FlairStore from "../../../stores/FlairStore"; -import { getPrimaryPermalinkEntity, parseAppLocalLink } from "../../../utils/permalinks/Permalinks"; +import { getPrimaryPermalinkEntity, parsePermalink } from "../../../utils/permalinks/Permalinks"; import MatrixClientContext from "../../../contexts/MatrixClientContext"; import { Action } from "../../../dispatcher/actions"; import { mediaFromMxc } from "../../../customisations/Media"; @@ -85,7 +85,7 @@ class Pill extends React.Component { if (nextProps.url) { if (nextProps.inMessage) { - const parts = parseAppLocalLink(nextProps.url); + const parts = parsePermalink(nextProps.url); resourceId = parts.primaryEntityId; // The room/user ID prefix = parts.sigil; // The first character of prefix } else { diff --git a/src/linkify-matrix.ts b/src/linkify-matrix.ts index ee85e66b94..0b21ac1543 100644 --- a/src/linkify-matrix.ts +++ b/src/linkify-matrix.ts @@ -15,13 +15,14 @@ See the License for the specific language governing permissions and limitations under the License. */ -import * as linkifyjs from 'linkifyjs'; -import linkifyElement from 'linkify-element'; -import linkifyString from 'linkify-string'; +import * as linkifyjs from '@matrix-org/linkifyjs'; +import linkifyElement from '@matrix-org/linkify-element'; +import linkifyString from '@matrix-org/linkify-string'; import { RoomMember } from 'matrix-js-sdk/src/models/room-member'; -import { registerPlugin } from 'linkifyjs'; +import { registerCustomProtocol, registerPlugin } from '@matrix-org/linkifyjs'; -import { baseUrl } from "./utils/permalinks/SpecPermalinkConstructor"; +//linkifyjs/src/core/fsm +import { baseUrl } from "./utils/permalinks/MatrixToPermalinkConstructor"; import { parsePermalink, tryTransformEntityToPermalink, @@ -31,7 +32,7 @@ import dis from './dispatcher/dispatcher'; import { Action } from './dispatcher/actions'; import { ViewUserPayload } from './dispatcher/payloads/ViewUserPayload'; -enum Type { +export enum Type { URL = "url", UserId = "userid", RoomAlias = "roomalias", @@ -53,41 +54,29 @@ function matrixOpaqueIdLinkifyParser({ name: Type; }) { const { - DOMAIN, DOT, - // A generic catchall text token - TEXT, + // IPV4 necessity NUM, TLD, COLON, SYM, + HYPHEN, UNDERSCORE, // because 'localhost' is tokenised to the localhost token, // usernames @localhost:foo.com are otherwise not matched! LOCALHOST, + domain, } = scanner.tokens; const S_START = parser.start; const matrixSymbol = utils.createTokenClass(name, { isLink: true }); - const localpartTokens = [ - DOMAIN, - // IPV4 necessity - NUM, - TLD, - - // because 'localhost' is tokenised to the localhost token, - // usernames @localhost:foo.com are otherwise not matched! - LOCALHOST, - SYM, - UNDERSCORE, - TEXT, - ]; - const domainpartTokens = [DOMAIN, NUM, TLD, LOCALHOST]; + const localpartTokens = [domain, TLD, LOCALHOST, SYM, UNDERSCORE, HYPHEN]; + const domainpartTokens = [domain, TLD, LOCALHOST, HYPHEN]; const INITIAL_STATE = S_START.tt(token); - const LOCALPART_STATE = INITIAL_STATE.tt(DOMAIN); + const LOCALPART_STATE = INITIAL_STATE.tt(domain); for (const token of localpartTokens) { INITIAL_STATE.tt(token, LOCALPART_STATE); LOCALPART_STATE.tt(token, LOCALPART_STATE); @@ -98,7 +87,7 @@ function matrixOpaqueIdLinkifyParser({ } const DOMAINPART_STATE_DOT = LOCALPART_STATE.tt(COLON); - const DOMAINPART_STATE = DOMAINPART_STATE_DOT.tt(DOMAIN); + const DOMAINPART_STATE = DOMAINPART_STATE_DOT.tt(domain); DOMAINPART_STATE.tt(DOT, DOMAINPART_STATE_DOT); for (const token of domainpartTokens) { DOMAINPART_STATE.tt(token, DOMAINPART_STATE); @@ -117,6 +106,7 @@ function matrixOpaqueIdLinkifyParser({ } function onUserClick(event: MouseEvent, userId: string) { + event.preventDefault(); const member = new RoomMember(null, userId); if (!member) { return; } dis.dispatch({ @@ -124,6 +114,7 @@ function onUserClick(event: MouseEvent, userId: string) { member: member, }); } + function onAliasClick(event: MouseEvent, roomAlias: string) { event.preventDefault(); dis.dispatch({ @@ -131,6 +122,7 @@ function onAliasClick(event: MouseEvent, roomAlias: string) { room_alias: roomAlias, }); } + function onGroupClick(event: MouseEvent, groupId: string) { event.preventDefault(); dis.dispatch({ action: 'view_group', group_id: groupId }); @@ -168,6 +160,19 @@ export const options = { onUserClick(e, permalink.userId); }, }; + } else { + // for events, rooms etc. (anything other then users) + const localHref = tryTransformPermalinkToLocalHref(href); + if (localHref !== href) { + // it could be converted to a localHref -> therefore handle locally + return { + // @ts-ignore see https://linkify.js.org/docs/options.html + click: function(e) { + e.preventDefault(); + window.location.hash = localHref; + }, + }; + } } } catch (e) { // OK fine, it's not actually a permalink @@ -178,21 +183,24 @@ export const options = { return { // @ts-ignore see https://linkify.js.org/docs/options.html click: function(e) { - onUserClick(e, href); + const userId = parsePermalink(href).userId; + onUserClick(e, userId); }, }; case Type.RoomAlias: return { // @ts-ignore see https://linkify.js.org/docs/options.html click: function(e) { - onAliasClick(e, href); + const alias = parsePermalink(href).roomIdOrAlias; + onAliasClick(e, alias); }, }; case Type.GroupId: return { // @ts-ignore see https://linkify.js.org/docs/options.html click: function(e) { - onGroupClick(e, href); + const groupId = parsePermalink(href).groupId; + onGroupClick(e, groupId); }, }; } @@ -219,7 +227,9 @@ export const options = { if (type === Type.URL) { try { const transformed = tryTransformPermalinkToLocalHref(href); - if (transformed !== href || decodeURIComponent(href).match(ELEMENT_URL_PATTERN)) { + if (transformed !== href || // if it could be converted to handle locally for matrix symbols e.g. @user:server.tdl and matrix.to + decodeURIComponent(href).match(ELEMENT_URL_PATTERN) // for https:vector|riot... + ) { return null; } else { return '_blank'; @@ -266,6 +276,8 @@ registerPlugin(Type.UserId, ({ scanner, parser, utils }) => { }); }); +registerCustomProtocol("matrix", true); + export const linkify = linkifyjs; export const _linkifyElement = linkifyElement; export const _linkifyString = linkifyString; diff --git a/src/utils/permalinks/MatrixSchemePermalinkConstructor.ts b/src/utils/permalinks/MatrixSchemePermalinkConstructor.ts new file mode 100644 index 0000000000..70622af7c2 --- /dev/null +++ b/src/utils/permalinks/MatrixSchemePermalinkConstructor.ts @@ -0,0 +1,105 @@ +/* +Copyright 2022 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import PermalinkConstructor, { PermalinkParts } from "./PermalinkConstructor"; + +/** + * Generates matrix: scheme permalinks + */ +export default class MatrixSchemePermalinkConstructor extends PermalinkConstructor { + constructor() { + super(); + } + + private encodeEntity(entity: string): string { + if (entity[0] === "!") { + return `roomid/${entity.slice(1)}`; + } else if (entity[0] === "#") { + return `r/${entity.slice(1)}`; + } else if (entity[0] === "@") { + return `u/${entity.slice(1)}`; + } else if (entity[0] === "$") { + return `e/${entity.slice(1)}`; + } + + throw new Error("Cannot encode entity: " + entity); + } + + forEvent(roomId: string, eventId: string, serverCandidates: string[]): string { + return `matrix:${this.encodeEntity(roomId)}` + + `/${this.encodeEntity(eventId)}${this.encodeServerCandidates(serverCandidates)}`; + } + + forRoom(roomIdOrAlias: string, serverCandidates: string[]): string { + return `matrix:${this.encodeEntity(roomIdOrAlias)}${this.encodeServerCandidates(serverCandidates)}`; + } + + forUser(userId: string): string { + return `matrix:${this.encodeEntity(userId)}`; + } + + forGroup(groupId: string): string { + throw new Error("Deliberately not implemented"); + } + + forEntity(entityId: string): string { + return `matrix:${this.encodeEntity(entityId)}`; + } + + isPermalinkHost(testHost: string): boolean { + // TODO: Change API signature to accept the URL for checking + return testHost === ""; + } + + encodeServerCandidates(candidates: string[]) { + if (!candidates || candidates.length === 0) return ''; + return `?via=${candidates.map(c => encodeURIComponent(c)).join("&via=")}`; + } + + parsePermalink(fullUrl: string): PermalinkParts { + if (!fullUrl || !fullUrl.startsWith("matrix:")) { + throw new Error("Does not appear to be a permalink"); + } + + const parts = fullUrl.substring("matrix:".length).split('/'); + + const identifier = parts[0]; + const entityNoSigil = parts[1]; + if (identifier === 'u') { + // Probably a user, no further parsing needed. + return PermalinkParts.forUser(`@${entityNoSigil}`); + } else if (identifier === 'r' || identifier === 'roomid') { + const sigil = identifier === 'r' ? '#' : '!'; + + if (parts.length === 2) { // room without event permalink + const [roomId, query = ""] = entityNoSigil.split("?"); + const via = query.split(/&?via=/g).filter(p => !!p); + return PermalinkParts.forRoom(`${sigil}${roomId}`, via); + } + + if (parts[2] === 'e') { // event permalink + const eventIdAndQuery = parts.length > 3 ? parts.slice(3).join('/') : ""; + const [eventId, query = ""] = eventIdAndQuery.split("?"); + const via = query.split(/&?via=/g).filter(p => !!p); + return PermalinkParts.forEvent(`${sigil}${entityNoSigil}`, `$${eventId}`, via); + } + + throw new Error("Faulty room permalink"); + } else { + throw new Error("Unknown entity type in permalink"); + } + } +} diff --git a/src/utils/permalinks/SpecPermalinkConstructor.ts b/src/utils/permalinks/MatrixToPermalinkConstructor.ts similarity index 97% rename from src/utils/permalinks/SpecPermalinkConstructor.ts rename to src/utils/permalinks/MatrixToPermalinkConstructor.ts index a24df40f16..1782cc984d 100644 --- a/src/utils/permalinks/SpecPermalinkConstructor.ts +++ b/src/utils/permalinks/MatrixToPermalinkConstructor.ts @@ -22,7 +22,7 @@ export const baseUrl = `https://${host}`; /** * Generates matrix.to permalinks */ -export default class SpecPermalinkConstructor extends PermalinkConstructor { +export default class MatrixToPermalinkConstructor extends PermalinkConstructor { constructor() { super(); } diff --git a/src/utils/permalinks/Permalinks.ts b/src/utils/permalinks/Permalinks.ts index 3a9200259f..c79e8af826 100644 --- a/src/utils/permalinks/Permalinks.ts +++ b/src/utils/permalinks/Permalinks.ts @@ -23,11 +23,12 @@ import { RoomMember } from "matrix-js-sdk/src/models/room-member"; import { logger } from "matrix-js-sdk/src/logger"; import { MatrixClientPeg } from "../../MatrixClientPeg"; -import SpecPermalinkConstructor, { baseUrl as matrixtoBaseUrl } from "./SpecPermalinkConstructor"; +import MatrixToPermalinkConstructor, { baseUrl as matrixtoBaseUrl } from "./MatrixToPermalinkConstructor"; import PermalinkConstructor, { PermalinkParts } from "./PermalinkConstructor"; import ElementPermalinkConstructor from "./ElementPermalinkConstructor"; import SdkConfig from "../../SdkConfig"; import { ELEMENT_URL_PATTERN } from "../../linkify-matrix"; +import MatrixSchemePermalinkConstructor from "./MatrixSchemePermalinkConstructor"; // The maximum number of servers to pick when working out which servers // to add to permalinks. The servers are appended as ?via=example.org @@ -312,14 +313,14 @@ export function makeGroupPermalink(groupId: string): string { export function isPermalinkHost(host: string): boolean { // Always check if the permalink is a spec permalink (callers are likely to call // parsePermalink after this function). - if (new SpecPermalinkConstructor().isPermalinkHost(host)) return true; + if (new MatrixToPermalinkConstructor().isPermalinkHost(host)) return true; return getPermalinkConstructor().isPermalinkHost(host); } /** * Transforms an entity (permalink, room alias, user ID, etc) into a local URL - * if possible. If the given entity is not found to be valid enough to be converted - * then a null value will be returned. + * if possible. If it is already a permalink (matrix.to) it gets returned + * unchanged. * @param {string} entity The entity to transform. * @returns {string|null} The transformed permalink or null if unable. */ @@ -327,12 +328,31 @@ export function tryTransformEntityToPermalink(entity: string): string { if (!entity) return null; // Check to see if it is a bare entity for starters - if (entity[0] === '#' || entity[0] === '!') return makeRoomPermalink(entity); + {if (entity[0] === '#' || entity[0] === '!') return makeRoomPermalink(entity);} if (entity[0] === '@') return makeUserPermalink(entity); if (entity[0] === '+') return makeGroupPermalink(entity); - // Then try and merge it into a permalink - return tryTransformPermalinkToLocalHref(entity); + if (entity.slice(0, 7) === "matrix:") { + try { + const permalinkParts = parsePermalink(entity); + if (permalinkParts) { + if (permalinkParts.roomIdOrAlias) { + const eventIdPart = permalinkParts.eventId ? `/${permalinkParts.eventId}` : ''; + let pl = matrixtoBaseUrl+`/#/${permalinkParts.roomIdOrAlias}${eventIdPart}`; + if (permalinkParts.viaServers.length > 0) { + pl += new MatrixToPermalinkConstructor().encodeServerCandidates(permalinkParts.viaServers); + } + return pl; + } else if (permalinkParts.groupId) { + return matrixtoBaseUrl + `/#/${permalinkParts.groupId}`; + } else if (permalinkParts.userId) { + return matrixtoBaseUrl + `/#/${permalinkParts.userId}`; + } + } + } catch {} + } + + return entity; } /** @@ -342,7 +362,7 @@ export function tryTransformEntityToPermalink(entity: string): string { * @returns {string} The transformed permalink or original URL if unable. */ export function tryTransformPermalinkToLocalHref(permalink: string): string { - if (!permalink.startsWith("http:") && !permalink.startsWith("https:")) { + if (!permalink.startsWith("http:") && !permalink.startsWith("https:") && !permalink.startsWith("matrix:")) { return permalink; } @@ -364,7 +384,7 @@ export function tryTransformPermalinkToLocalHref(permalink: string): string { const eventIdPart = permalinkParts.eventId ? `/${permalinkParts.eventId}` : ''; permalink = `#/room/${permalinkParts.roomIdOrAlias}${eventIdPart}`; if (permalinkParts.viaServers.length > 0) { - permalink += new SpecPermalinkConstructor().encodeServerCandidates(permalinkParts.viaServers); + permalink += new MatrixToPermalinkConstructor().encodeServerCandidates(permalinkParts.viaServers); } } else if (permalinkParts.groupId) { permalink = `#/group/${permalinkParts.groupId}`; @@ -411,13 +431,15 @@ function getPermalinkConstructor(): PermalinkConstructor { return new ElementPermalinkConstructor(elementPrefix); } - return new SpecPermalinkConstructor(); + return new MatrixToPermalinkConstructor(); } export function parsePermalink(fullUrl: string): PermalinkParts { const elementPrefix = SdkConfig.get()['permalinkPrefix']; if (decodeURIComponent(fullUrl).startsWith(matrixtoBaseUrl)) { - return new SpecPermalinkConstructor().parsePermalink(decodeURIComponent(fullUrl)); + return new MatrixToPermalinkConstructor().parsePermalink(decodeURIComponent(fullUrl)); + } else if (fullUrl.startsWith("matrix:")) { + return new MatrixSchemePermalinkConstructor().parsePermalink(fullUrl); } else if (elementPrefix && fullUrl.startsWith(elementPrefix)) { return new ElementPermalinkConstructor(elementPrefix).parsePermalink(fullUrl); } @@ -425,23 +447,6 @@ export function parsePermalink(fullUrl: string): PermalinkParts { return null; // not a permalink we can handle } -/** - * Parses an app local link (`#/(user|room|group)/identifer`) to a Matrix entity - * (room, user, group). Such links are produced by `HtmlUtils` when encountering - * links, which calls `tryTransformPermalinkToLocalHref` in this module. - * @param {string} localLink The app local link - * @returns {PermalinkParts} - */ -export function parseAppLocalLink(localLink: string): PermalinkParts { - try { - const segments = localLink.replace("#/", ""); - return ElementPermalinkConstructor.parseAppRoute(segments); - } catch (e) { - // Ignore failures - } - return null; -} - function getServerName(userId: string): string { return userId.split(":").splice(1).join(":"); } diff --git a/src/utils/pillify.tsx b/src/utils/pillify.tsx index 22240fcda5..396ddd5dbf 100644 --- a/src/utils/pillify.tsx +++ b/src/utils/pillify.tsx @@ -22,7 +22,7 @@ import { MatrixEvent } from "matrix-js-sdk/src/models/event"; import { MatrixClientPeg } from '../MatrixClientPeg'; import SettingsStore from "../settings/SettingsStore"; import Pill from "../components/views/elements/Pill"; -import { parseAppLocalLink } from "./permalinks/Permalinks"; +import { parsePermalink } from "./permalinks/Permalinks"; /** * Recurses depth-first through a DOM tree, converting matrix.to links @@ -46,7 +46,7 @@ export function pillifyLinks(nodes: ArrayLike, mxEvent: MatrixEvent, pi if (node.tagName === "A" && node.getAttribute("href")) { const href = node.getAttribute("href"); - const parts = parseAppLocalLink(href); + const parts = parsePermalink(href); // If the link is a (localised) matrix.to link, replace it with a pill // We don't want to pill event permalinks, so those are ignored. if (parts && !parts.eventId) { diff --git a/test/components/views/messages/TextualBody-test.js b/test/components/views/messages/TextualBody-test.js index 9a83a63efe..48ab8ed244 100644 --- a/test/components/views/messages/TextualBody-test.js +++ b/test/components/views/messages/TextualBody-test.js @@ -245,7 +245,7 @@ describe("", () => { const content = wrapper.find(".mx_EventTile_body"); expect(content.html()).toBe( '' + - 'An event link with text', ); @@ -274,7 +274,8 @@ describe("", () => { const content = wrapper.find(".mx_EventTile_body"); expect(content.html()).toBe( '' + - 'A ', () => { ); @@ -188,7 +188,7 @@ describe('', () => { @@ -234,7 +234,7 @@ describe('', () => { ); @@ -263,7 +263,7 @@ describe('', () => { @@ -297,7 +297,7 @@ describe('', () => { { const linkTypesByInitialCharacter = { @@ -177,6 +177,18 @@ describe('linkify-matrix', () => { isLink: true, }])); }); + it('accept hyphens in name ' + char + 'foo-bar:server.com', () => { + const test = '' + char + 'foo-bar:server.com'; + const found = linkify.find(test); + expect(found).toEqual(([{ + href: char + "foo-bar:server.com", + type, + value: char + "foo-bar:server.com", + start: 0, + end: test.length, + isLink: true, + }])); + }); it('ignores trailing `:`', () => { const test = '' + char + 'foo:bar.com:'; const found = linkify.find(test); @@ -264,4 +276,30 @@ describe('linkify-matrix', () => { describe('userid plugin', () => { genTests('@'); }); + + describe('matrix uri', () => { + const AcceptedMatrixUris = [ + 'matrix:u/foo_bar:server.uk', + 'matrix:r/foo-bar:server.uk', + 'matrix:roomid/somewhere:example.org?via=elsewhere.ca', + 'matrix:r/somewhere:example.org', + 'matrix:r/somewhere:example.org/e/event', + 'matrix:roomid/somewhere:example.org/e/event?via=elsewhere.ca', + 'matrix:u/alice:example.org?action=chat', + ]; + for (const matrixUri of AcceptedMatrixUris) { + it('accepts ' + matrixUri, () => { + const test = matrixUri; + const found = linkify.find(test); + expect(found).toEqual(([{ + href: matrixUri, + type: Type.URL, + value: matrixUri, + end: matrixUri.length, + start: 0, + isLink: true, + }])); + }); + } + }); }); diff --git a/yarn.lock b/yarn.lock index 7d95f61f51..d3402d2930 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1384,6 +1384,21 @@ resolved "https://registry.yarnpkg.com/@mapbox/whoots-js/-/whoots-js-3.1.0.tgz#497c67a1cef50d1a2459ba60f315e448d2ad87fe" integrity sha512-Es6WcD0nO5l+2BOQS4uLfNPYQaNDfbot3X1XUoloz+x0mPDS3eeORZJl06HXjwBG1fOGwCRnzK88LMdxKRrd6Q== +"@matrix-org/linkify-element@^4.0.0-rc.5": + version "4.0.0-rc.5" + resolved "https://registry.yarnpkg.com/@matrix-org/linkify-element/-/linkify-element-4.0.0-rc.5.tgz#0c27e81272638674ba4162fec2fd131b2c36c163" + integrity sha512-6sdJ5x9EZpUNVZqc2Dig8Q4hySdL2fdE/ivehk4L3y3rV7tMD0fRs5rXQQ4wcgCMhGAAGbscbNiS8pyZIy9Hlg== + +"@matrix-org/linkify-string@^4.0.0-rc.5": + version "4.0.0-rc.5" + resolved "https://registry.yarnpkg.com/@matrix-org/linkify-string/-/linkify-string-4.0.0-rc.5.tgz#139ba23c70a4f5b531656365a6109c122177b132" + integrity sha512-WFyu6+kVEPJsDwZwgCSrtUDeIMDdWIFzRRq5z+MLYHiO3J8G19jvRjRnNm4dwjDUqROWhvWS9b8JG7rbuwjkLQ== + +"@matrix-org/linkifyjs@^4.0.0-rc.5": + version "4.0.0-rc.5" + resolved "https://registry.yarnpkg.com/@matrix-org/linkifyjs/-/linkifyjs-4.0.0-rc.5.tgz#3a2885754a8de51164a30e6e09909173e348d6bb" + integrity sha512-HGmEZuUzCOzdsUFM5dQK2R2KhBFnxRfye5CYJhM2EpRTO4t5aTcR6Ey09HuJ/DZevQ9GTFUjkuKAKurQhnAfOA== + "@matrix-org/olm@https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.8.tgz": version "3.2.8" resolved "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.8.tgz#8d53636d045e1776e2a2ec6613e57330dd9ce856" @@ -1815,13 +1830,6 @@ resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" integrity sha1-7ihweulOEdK4J7y+UnC86n8+ce4= -"@types/linkifyjs@^2.1.3": - version "2.1.4" - resolved "https://registry.yarnpkg.com/@types/linkifyjs/-/linkifyjs-2.1.4.tgz#6b14e35d8d211f2666f602dcabcdc6859617516f" - integrity sha512-UuF0hyWNnLTT4xNJdrQx6OWYMNlPRBtt3fKCaROIx48boQyXkQ4YDDwTEQNi9mlsRX0Hpc6AnFKkDZ6IXkxD4g== - dependencies: - "@types/react" "*" - "@types/lodash@^4.14.168": version "4.14.178" resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.178.tgz#341f6d2247db528d4a13ddbb374bcdc80406f4f8" @@ -5968,21 +5976,6 @@ lines-and-columns@^1.1.6: resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632" integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg== -linkify-element@^3.0.4: - version "3.0.4" - resolved "https://registry.yarnpkg.com/linkify-element/-/linkify-element-3.0.4.tgz#3566a3b48d4c211a684f42a23a9964bf53f3a31a" - integrity sha512-xrj2Upv4/XUxvvczoDwojEnzKnfJCHlorAxYmdFPSGNwLz2sXYkYyB7Lw1flkGS7L0yS0dq/HwOotG0Kpaiaxw== - -linkify-string@^3.0.4: - version "3.0.4" - resolved "https://registry.yarnpkg.com/linkify-string/-/linkify-string-3.0.4.tgz#6abf1a5e436e800c729274ae08f5703484647f84" - integrity sha512-OnNqqRjlYXaXipIAbBC8sDXsSumI1ftatzFg141Pw9HEXWjTVLFcMZoKbFupshqWRavtNJ6QHLa+u6AlxxgeRw== - -linkifyjs@^3.0.5: - version "3.0.5" - resolved "https://registry.yarnpkg.com/linkifyjs/-/linkifyjs-3.0.5.tgz#99e51a3a0c0e232fcb63ebb89eea3ff923378f34" - integrity sha512-1Y9XQH65eQKA9p2xtk+zxvnTeQBG7rdAXSkUG97DmuI/Xhji9uaUzaWxRj6rf9YC0v8KKHkxav7tnLX82Sz5Fg== - loader-utils@^2.0.0: version "2.0.2" resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-2.0.2.tgz#d6e3b4fb81870721ae4e0868ab11dd638368c129"