Port over linkifyJS to shared-components. (#32731)
* Port over linkifyJS to shared-components. * Drop rubbish * update lock * quickfix test * drop group id * Modernize tests * Remove stories that aren't in use. * Complete working version * Add copyright * tidy up * update lock * Update snaps * update snap * undo change * remove unused * More test updates * fix typo * fix margin on preview * move margin block * snapupdate * prettier * cleanup a test mistake * Fixup sonar issues * Don't expose linkifyjs to applications, just provide helper functions. * Add story for documentation. * remove $ * Use a const * typo * cleanup var name * remove console line * Changes checkpoint * Convert to context * Revert unrelated change. * more cleanup * Add a test to cover ignoring incoming data elements * Make tests happy * Update tests for LinkedText * Underlines! * fix lock * remove unused linkify packages * import move * Remove mod to remove underline * undo * fix snap * another snapshot fix * Tidy up based on review. * fix story * Pass in args
This commit is contained in:
@@ -28,7 +28,7 @@ import { TooltipProvider } from "@vector-im/compound-web";
|
||||
// what-input helps improve keyboard accessibility
|
||||
import "what-input";
|
||||
import sanitizeHtml from "sanitize-html";
|
||||
import { I18nContext } from "@element-hq/web-shared-components";
|
||||
import { I18nContext, LinkedTextContext, LinkedText } from "@element-hq/web-shared-components";
|
||||
import { LockSolidIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
|
||||
|
||||
import PosthogTrackers from "../../PosthogTrackers";
|
||||
@@ -125,7 +125,7 @@ import { viewUserDeviceSettings } from "../../actions/handlers/viewUserDeviceSet
|
||||
import GenericToast from "../views/toasts/GenericToast";
|
||||
import RovingSpotlightDialog from "../views/dialogs/spotlight/SpotlightDialog";
|
||||
import { findDMForUser } from "../../utils/dm/findDMForUser";
|
||||
import { getHtmlText, Linkify } from "../../HtmlUtils";
|
||||
import { getHtmlText } from "../../HtmlUtils";
|
||||
import { NotificationLevel } from "../../stores/notifications/NotificationLevel";
|
||||
import { type UserTab } from "../views/dialogs/UserTab";
|
||||
import { shouldSkipSetupEncryption } from "../../utils/crypto/shouldSkipSetupEncryption";
|
||||
@@ -139,7 +139,7 @@ import { setTheme } from "../../theme";
|
||||
import { type OpenForwardDialogPayload } from "../../dispatcher/payloads/OpenForwardDialogPayload";
|
||||
import { ShareFormat, type SharePayload } from "../../dispatcher/payloads/SharePayload";
|
||||
import Markdown from "../../Markdown";
|
||||
import { sanitizeHtmlParams } from "../../Linkify";
|
||||
import { LinkedTextConfiguration, sanitizeHtmlParams } from "../../Linkify";
|
||||
import { isOnlyAdmin } from "../../utils/membership";
|
||||
import { ModuleApi } from "../../modules/Api.ts";
|
||||
|
||||
@@ -1458,7 +1458,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
||||
key,
|
||||
title: userNotice.title,
|
||||
props: {
|
||||
description: <Linkify>{userNotice.description}</Linkify>,
|
||||
description: <LinkedText>{userNotice.description}</LinkedText>,
|
||||
primaryLabel: _t("action|ok"),
|
||||
onPrimaryClick: () => {
|
||||
ToastStore.sharedInstance().dismissToast(key);
|
||||
@@ -2291,7 +2291,9 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
||||
<ErrorBoundary>
|
||||
<I18nContext.Provider value={ModuleApi.instance.i18n}>
|
||||
<SDKContext.Provider value={this.stores}>
|
||||
<TooltipProvider>{view}</TooltipProvider>
|
||||
<LinkedTextContext.Provider value={LinkedTextConfiguration}>
|
||||
<TooltipProvider>{view}</TooltipProvider>
|
||||
</LinkedTextContext.Provider>
|
||||
</SDKContext.Provider>
|
||||
</I18nContext.Provider>
|
||||
</ErrorBoundary>
|
||||
|
||||
@@ -41,6 +41,7 @@ import { sortBy, uniqBy } from "lodash";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
import { KnownMembership, type SpaceChildEventContent } from "matrix-js-sdk/src/types";
|
||||
import { ChevronDownIcon, CheckIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
|
||||
import { LinkedText } from "@element-hq/web-shared-components";
|
||||
|
||||
import defaultDispatcher from "../../dispatcher/dispatcher";
|
||||
import { _t } from "../../languageHandler";
|
||||
@@ -55,7 +56,7 @@ import InfoTooltip from "../views/elements/InfoTooltip";
|
||||
import TextWithTooltip from "../views/elements/TextWithTooltip";
|
||||
import { useStateToggle } from "../../hooks/useStateToggle";
|
||||
import { getChildOrder } from "../../stores/spaces/SpaceStore";
|
||||
import { Linkify, topicToHtml } from "../../HtmlUtils";
|
||||
import { topicToHtml } from "../../HtmlUtils";
|
||||
import { useDispatcher } from "../../hooks/useDispatcher";
|
||||
import { Action } from "../../dispatcher/actions";
|
||||
import { type IState, RovingTabIndexProvider, useRovingTabIndex } from "../../accessibility/RovingTabIndex";
|
||||
@@ -233,20 +234,12 @@ const Tile: React.FC<ITileProps> = ({
|
||||
|
||||
let topicSection: ReactNode | undefined;
|
||||
if (topic) {
|
||||
// prevent clicks on links from bubbling up to the room tile
|
||||
topicSection = (
|
||||
<Linkify
|
||||
options={{
|
||||
attributes: {
|
||||
onClick(ev: MouseEvent) {
|
||||
// prevent clicks on links from bubbling up to the room tile
|
||||
ev.stopPropagation();
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
<LinkedText onLinkClick={(ev) => ev.stopPropagation()}>
|
||||
{" · "}
|
||||
{topic}
|
||||
</Linkify>
|
||||
</LinkedText>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ Please see LICENSE files in the repository root for full details.
|
||||
import React, { type JSX } from "react";
|
||||
import { type MatrixClient } from "matrix-js-sdk/src/matrix";
|
||||
import { type AuthType } from "matrix-js-sdk/src/interactive-auth";
|
||||
import { LinkedText } from "@element-hq/web-shared-components";
|
||||
|
||||
import { _t } from "../../../languageHandler";
|
||||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
@@ -21,7 +22,6 @@ import InteractiveAuth, {
|
||||
} from "../../structures/InteractiveAuth";
|
||||
import { type ContinueKind, SSOAuthEntry } from "../auth/InteractiveAuthEntryComponents";
|
||||
import BaseDialog from "./BaseDialog";
|
||||
import { Linkify } from "../../../Linkify";
|
||||
|
||||
type DialogAesthetics = Partial<{
|
||||
[x in AuthType]: {
|
||||
@@ -163,9 +163,9 @@ export default class InteractiveAuthDialog<T> extends React.Component<Interactiv
|
||||
if (this.state.authError) {
|
||||
content = (
|
||||
<div id="mx_Dialog_content">
|
||||
<Linkify>
|
||||
<div role="alert">{this.state.authError.message || this.state.authError.toString()}</div>
|
||||
</Linkify>
|
||||
<LinkedText role="alert">
|
||||
{this.state.authError.message || this.state.authError.toString()}
|
||||
</LinkedText>
|
||||
<br />
|
||||
<AccessibleButton onClick={this.onDismissClick} className="mx_GeneralButton" autoFocus={true}>
|
||||
{_t("action|dismiss")}
|
||||
|
||||
@@ -10,6 +10,7 @@ import React, { type JSX, useCallback, useContext, useState } from "react";
|
||||
import { type Room, EventType } from "matrix-js-sdk/src/matrix";
|
||||
import classNames from "classnames";
|
||||
import { Tooltip } from "@vector-im/compound-web";
|
||||
import { LinkedText } from "@element-hq/web-shared-components";
|
||||
|
||||
import { useTopic } from "../../../hooks/room/useTopic";
|
||||
import { _t } from "../../../languageHandler";
|
||||
@@ -20,7 +21,7 @@ import InfoDialog from "../dialogs/InfoDialog";
|
||||
import { useDispatcher } from "../../../hooks/useDispatcher";
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
import AccessibleButton from "./AccessibleButton";
|
||||
import { Linkify, topicToHtml } from "../../../HtmlUtils";
|
||||
import { topicToHtml } from "../../../HtmlUtils";
|
||||
import { tryTransformPermalinkToLocalHref } from "../../../utils/permalinks/Permalinks";
|
||||
|
||||
interface IProps extends React.HTMLProps<HTMLDivElement> {
|
||||
@@ -74,19 +75,7 @@ export default function RoomTopic({ room, className, ...props }: IProps): JSX.El
|
||||
title: room.name,
|
||||
description: (
|
||||
<div>
|
||||
<Linkify
|
||||
options={{
|
||||
attributes: {
|
||||
onClick(e: React.MouseEvent<HTMLDivElement>) {
|
||||
onClick(e);
|
||||
modal.close();
|
||||
},
|
||||
},
|
||||
}}
|
||||
as="p"
|
||||
>
|
||||
{body}
|
||||
</Linkify>
|
||||
<LinkedText onLinkClick={() => modal.close()}>{body}</LinkedText>
|
||||
{canSetTopic && (
|
||||
<AccessibleButton
|
||||
kind="primary_outline"
|
||||
@@ -122,7 +111,7 @@ export default function RoomTopic({ room, className, ...props }: IProps): JSX.El
|
||||
onFocus={onHover}
|
||||
aria-label={_t("room|read_topic")}
|
||||
>
|
||||
<Linkify>{body}</Linkify>
|
||||
<LinkedText>{body}</LinkedText>
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
|
||||
@@ -8,7 +8,7 @@ Please see LICENSE files in the repository root for full details.
|
||||
|
||||
import React, { type JSX, createRef, type SyntheticEvent, type MouseEvent } from "react";
|
||||
import { MsgType } from "matrix-js-sdk/src/matrix";
|
||||
import { EventContentBodyView } from "@element-hq/web-shared-components";
|
||||
import { EventContentBodyView, LINKIFIED_DATA_ATTRIBUTE } from "@element-hq/web-shared-components";
|
||||
|
||||
import { EventContentBodyViewModel } from "../../../viewmodels/message-body/EventContentBodyViewModel";
|
||||
import { formatDate } from "../../../DateUtils";
|
||||
@@ -26,7 +26,6 @@ import LinkPreviewGroup from "../rooms/LinkPreviewGroup";
|
||||
import { type IBodyProps } from "./IBodyProps";
|
||||
import RoomContext from "../../../contexts/RoomContext";
|
||||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
import { options as linkifyOpts } from "../../../linkify-matrix";
|
||||
import { getParentEventId } from "../../../utils/Reply";
|
||||
import { EditWysiwygComposer } from "../rooms/wysiwyg_composer";
|
||||
import { type IEventTileOps } from "../rooms/EventTile";
|
||||
@@ -250,7 +249,7 @@ export default class TextualBody extends React.Component<IBodyProps, IState> {
|
||||
private onBodyLinkClick = (e: MouseEvent): void => {
|
||||
let target: HTMLLinkElement | null = e.target as HTMLLinkElement;
|
||||
// links processed by linkifyjs have their own handler so don't handle those here
|
||||
if (target.classList.contains(linkifyOpts.className as string)) return;
|
||||
if (target.dataset[LINKIFIED_DATA_ATTRIBUTE]) return;
|
||||
if (target.nodeName !== "A") {
|
||||
// Jump to parent as the `<a>` may contain children, e.g. an anchor wrapping an inline code section
|
||||
target = target.closest<HTMLLinkElement>("a");
|
||||
|
||||
@@ -39,14 +39,14 @@ import ErrorIcon from "@vector-im/compound-design-tokens/assets/web/icons/error"
|
||||
import ErrorSolidIcon from "@vector-im/compound-design-tokens/assets/web/icons/error-solid";
|
||||
import ChevronDownIcon from "@vector-im/compound-design-tokens/assets/web/icons/chevron-down";
|
||||
import { JoinRule, type Room } from "matrix-js-sdk/src/matrix";
|
||||
import { Box, Flex, HistoryVisibilityBadge } from "@element-hq/web-shared-components";
|
||||
import { Box, Flex, HistoryVisibilityBadge, LinkedText } from "@element-hq/web-shared-components";
|
||||
|
||||
import BaseCard from "./BaseCard.tsx";
|
||||
import { _t } from "../../../languageHandler.tsx";
|
||||
import RoomAvatar from "../avatars/RoomAvatar.tsx";
|
||||
import { E2EStatus } from "../../../utils/ShieldUtils.ts";
|
||||
import { type RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks.ts";
|
||||
import { Linkify, topicToHtml } from "../../../HtmlUtils.tsx";
|
||||
import { topicToHtml } from "../../../HtmlUtils.tsx";
|
||||
import { useRoomSummaryCardViewModel } from "../../viewmodels/right_panel/RoomSummaryCardViewModel.tsx";
|
||||
import { useRoomTopicViewModel } from "../../viewmodels/right_panel/RoomSummaryCardTopicViewModel.tsx";
|
||||
import { useRoomName } from "../../../hooks/useRoomName.ts";
|
||||
@@ -89,7 +89,7 @@ const RoomTopic: React.FC<Pick<IProps, "room">> = ({ room }): JSX.Element | null
|
||||
);
|
||||
}
|
||||
|
||||
const content = vm.expanded ? <Linkify>{body}</Linkify> : body;
|
||||
const content = vm.expanded ? <LinkedText as="span">{body}</LinkedText> : body;
|
||||
|
||||
return (
|
||||
<Flex
|
||||
|
||||
@@ -12,6 +12,7 @@ import { type Room, type MatrixEvent } from "matrix-js-sdk/src/matrix";
|
||||
import EMOTICON_REGEX from "emojibase-regex/emoticon";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
import { EMOTICON_TO_EMOJI } from "@matrix-org/emojibase-bindings";
|
||||
import { isLinkable } from "@element-hq/web-shared-components";
|
||||
|
||||
import type EditorModel from "../../../editor/model";
|
||||
import HistoryManager from "../../../editor/history";
|
||||
@@ -40,7 +41,6 @@ import { type ICompletion } from "../../../autocomplete/Autocompleter";
|
||||
import { getKeyBindingsManager } from "../../../KeyBindingsManager";
|
||||
import { ALTERNATE_KEY_NAME, KeyBindingAction } from "../../../accessibility/KeyboardShortcuts";
|
||||
import { _t } from "../../../languageHandler";
|
||||
import { linkify } from "../../../linkify-matrix";
|
||||
import { SdkContextClass } from "../../../contexts/SDKContext";
|
||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||
import { Landmark, LandmarkNavigation } from "../../../accessibility/LandmarkNavigation";
|
||||
@@ -357,7 +357,7 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
|
||||
const range = getRangeForSelection(this.editorRef.current, model, document.getSelection()!);
|
||||
|
||||
// If the user is pasting a link, and has a range selected which is not a link, wrap the range with the link
|
||||
if (plainText && range.length > 0 && linkify.test(plainText) && !linkify.test(range.text)) {
|
||||
if (plainText && range.length > 0 && isLinkable(plainText) && !isLinkable(range.text)) {
|
||||
formatRangeAsLink(range, plainText);
|
||||
} else {
|
||||
replaceRangeAndMoveCaret(range, parts);
|
||||
|
||||
@@ -9,8 +9,8 @@ Please see LICENSE files in the repository root for full details.
|
||||
import React, { type JSX, type ComponentProps, createRef, type ReactNode } from "react";
|
||||
import { decode } from "html-entities";
|
||||
import { type MatrixEvent, type IPreviewUrlResponse } from "matrix-js-sdk/src/matrix";
|
||||
import { LinkedText } from "@element-hq/web-shared-components";
|
||||
|
||||
import { Linkify } from "../../../HtmlUtils";
|
||||
import Modal from "../../../Modal";
|
||||
import * as ImageUtils from "../../../ImageUtils";
|
||||
import { mediaFromMxc } from "../../../customisations/Media";
|
||||
@@ -128,7 +128,7 @@ export default class LinkPreviewWidget extends React.Component<IProps> {
|
||||
)}
|
||||
</div>
|
||||
<div className="mx_LinkPreviewWidget_description">
|
||||
<Linkify>{description}</Linkify>
|
||||
<LinkedText>{description}</LinkedText>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -6,11 +6,11 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React, { type JSX, useContext } from "react";
|
||||
import React, { type JSX, useContext, useMemo } from "react";
|
||||
import { EventType, type Room, type User, type MatrixClient } from "matrix-js-sdk/src/matrix";
|
||||
import { KnownMembership } from "matrix-js-sdk/src/types";
|
||||
import { ErrorSolidIcon, UserAddIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
|
||||
import { EventTileBubble } from "@element-hq/web-shared-components";
|
||||
import { EventTileBubble, LinkedText } from "@element-hq/web-shared-components";
|
||||
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
import DMRoomMap from "../../../utils/DMRoomMap";
|
||||
@@ -32,7 +32,7 @@ import { LocalRoom } from "../../../models/LocalRoom";
|
||||
import { shouldEncryptRoomWithSingle3rdPartyInvite } from "../../../utils/room/shouldEncryptRoomWithSingle3rdPartyInvite";
|
||||
import { useScopedRoomContext } from "../../../contexts/ScopedRoomContext.tsx";
|
||||
import { useTopic } from "../../../hooks/room/useTopic";
|
||||
import { topicToHtml, Linkify } from "../../../HtmlUtils";
|
||||
import { topicToHtml } from "../../../HtmlUtils";
|
||||
|
||||
function hasExpectedEncryptionSettings(matrixClient: MatrixClient, room: Room): boolean {
|
||||
const isEncrypted: boolean = matrixClient.isRoomEncrypted(room.roomId);
|
||||
@@ -56,14 +56,23 @@ const NewRoomIntro: React.FC = () => {
|
||||
const cli = useContext(MatrixClientContext);
|
||||
const { room, roomId } = useScopedRoomContext("room", "roomId");
|
||||
const topic = useTopic(room);
|
||||
const isLocalRoom = room instanceof LocalRoom;
|
||||
|
||||
let dmPartner: string | undefined;
|
||||
if (isLocalRoom) {
|
||||
dmPartner = room?.targets[0]?.userId;
|
||||
} else if (roomId) {
|
||||
dmPartner = DMRoomMap.shared().getUserIdForRoomId(roomId);
|
||||
}
|
||||
|
||||
const renderedTopic = useMemo(
|
||||
() => (dmPartner ? undefined : <LinkedText as="span">{topicToHtml(topic?.text, topic?.html)}</LinkedText>),
|
||||
[topic, dmPartner],
|
||||
);
|
||||
if (!room || !roomId) {
|
||||
throw new Error("Unable to create a NewRoomIntro without room and roomId");
|
||||
}
|
||||
|
||||
const isLocalRoom = room instanceof LocalRoom;
|
||||
const dmPartner = isLocalRoom ? room.targets[0]?.userId : DMRoomMap.shared().getUserIdForRoomId(roomId);
|
||||
|
||||
let body: JSX.Element;
|
||||
if (dmPartner) {
|
||||
const { shouldEncrypt: encryptedSingle3rdPartyInvite } = shouldEncryptRoomWithSingle3rdPartyInvite(room);
|
||||
@@ -137,15 +146,11 @@ const NewRoomIntro: React.FC = () => {
|
||||
{sub}
|
||||
</AccessibleButton>
|
||||
),
|
||||
topic: () => <Linkify>{topicToHtml(topic?.text, topic?.html)}</Linkify>,
|
||||
topic: renderedTopic,
|
||||
},
|
||||
);
|
||||
} else if (topic) {
|
||||
topicText = _t(
|
||||
"room|intro|display_topic",
|
||||
{},
|
||||
{ topic: () => <Linkify>{topicToHtml(topic?.text, topic?.html)}</Linkify> },
|
||||
);
|
||||
topicText = _t("room|intro|display_topic", {}, { topic: renderedTopic });
|
||||
} else if (canAddTopic) {
|
||||
topicText = _t(
|
||||
"room|intro|no_topic",
|
||||
|
||||
Reference in New Issue
Block a user