fix: implement external link handling with Tauri shell in UrlPreviewCard and react-custom-html-parser
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
import React, { MouseEvent, useCallback, useEffect, useRef, useState } from 'react';
|
||||||
import { IPreviewUrlResponse } from 'matrix-js-sdk';
|
import { IPreviewUrlResponse } from 'matrix-js-sdk';
|
||||||
import { Box, Icon, IconButton, Icons, Scroll, Spinner, Text, as, color, config } from 'folds';
|
import { Box, Icon, IconButton, Icons, Scroll, Spinner, Text, as, color, config } from 'folds';
|
||||||
import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback';
|
import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback';
|
||||||
@@ -12,9 +12,21 @@ import * as css from './UrlPreviewCard.css';
|
|||||||
import { tryDecodeURIComponent } from '../../utils/dom';
|
import { tryDecodeURIComponent } from '../../utils/dom';
|
||||||
import { mxcUrlToHttp } from '../../utils/matrix';
|
import { mxcUrlToHttp } from '../../utils/matrix';
|
||||||
import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
|
import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
|
||||||
|
import { openExternalUrl } from '../../utils/tauri';
|
||||||
|
|
||||||
const linkStyles = { color: color.Success.Main };
|
const linkStyles = { color: color.Success.Main };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Click handler for external links - uses Tauri shell on desktop/mobile
|
||||||
|
*/
|
||||||
|
const handleExternalLinkClick = (e: MouseEvent<HTMLAnchorElement>) => {
|
||||||
|
const href = e.currentTarget.href;
|
||||||
|
if (href && (href.startsWith('http://') || href.startsWith('https://'))) {
|
||||||
|
e.preventDefault();
|
||||||
|
openExternalUrl(href);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export const UrlPreviewCard = as<'div', { url: string; ts: number }>(
|
export const UrlPreviewCard = as<'div', { url: string; ts: number }>(
|
||||||
({ url, ts, ...props }, ref) => {
|
({ url, ts, ...props }, ref) => {
|
||||||
const mx = useMatrixClient();
|
const mx = useMatrixClient();
|
||||||
@@ -45,6 +57,7 @@ export const UrlPreviewCard = as<'div', { url: string; ts: number }>(
|
|||||||
rel="no-referrer"
|
rel="no-referrer"
|
||||||
size="T200"
|
size="T200"
|
||||||
priority="300"
|
priority="300"
|
||||||
|
onClick={handleExternalLinkClick}
|
||||||
>
|
>
|
||||||
{typeof prev['og:site_name'] === 'string' && `${prev['og:site_name']} | `}
|
{typeof prev['og:site_name'] === 'string' && `${prev['og:site_name']} | `}
|
||||||
{tryDecodeURIComponent(url)}
|
{tryDecodeURIComponent(url)}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
/* eslint-disable jsx-a11y/alt-text */
|
/* eslint-disable jsx-a11y/alt-text */
|
||||||
import React, {
|
import React, {
|
||||||
ComponentPropsWithoutRef,
|
ComponentPropsWithoutRef,
|
||||||
|
MouseEvent,
|
||||||
ReactEventHandler,
|
ReactEventHandler,
|
||||||
Suspense,
|
Suspense,
|
||||||
lazy,
|
lazy,
|
||||||
@@ -42,15 +43,28 @@ import { onEnterOrSpace } from '../utils/keyboard';
|
|||||||
import { copyToClipboard, tryDecodeURIComponent } from '../utils/dom';
|
import { copyToClipboard, tryDecodeURIComponent } from '../utils/dom';
|
||||||
import { useTimeoutToggle } from '../hooks/useTimeoutToggle';
|
import { useTimeoutToggle } from '../hooks/useTimeoutToggle';
|
||||||
import { AuthenticatedImg } from '../components/authenticated-media';
|
import { AuthenticatedImg } from '../components/authenticated-media';
|
||||||
|
import { openExternalUrl } from '../utils/tauri';
|
||||||
|
|
||||||
const ReactPrism = lazy(() => import('./react-prism/ReactPrism'));
|
const ReactPrism = lazy(() => import('./react-prism/ReactPrism'));
|
||||||
|
|
||||||
const EMOJI_REG_G = new RegExp(`${URL_NEG_LB}(${EMOJI_PATTERN})`, 'g');
|
const EMOJI_REG_G = new RegExp(`${URL_NEG_LB}(${EMOJI_PATTERN})`, 'g');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Click handler for external links - uses Tauri shell on desktop/mobile
|
||||||
|
*/
|
||||||
|
const handleExternalLinkClick = (e: MouseEvent<HTMLAnchorElement>) => {
|
||||||
|
const href = e.currentTarget.href;
|
||||||
|
if (href && (href.startsWith('http://') || href.startsWith('https://'))) {
|
||||||
|
e.preventDefault();
|
||||||
|
openExternalUrl(href);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export const LINKIFY_OPTS: LinkifyOpts = {
|
export const LINKIFY_OPTS: LinkifyOpts = {
|
||||||
attributes: {
|
attributes: {
|
||||||
target: '_blank',
|
target: '_blank',
|
||||||
rel: 'noreferrer noopener',
|
rel: 'noreferrer noopener',
|
||||||
|
onClick: handleExternalLinkClick,
|
||||||
},
|
},
|
||||||
validate: {
|
validate: {
|
||||||
url: (value) => /^(https|http|ftp|mailto|magnet)?:/.test(value),
|
url: (value) => /^(https|http|ftp|mailto|magnet)?:/.test(value),
|
||||||
@@ -161,7 +175,7 @@ export const factoryRenderLinkifyWithMention = (
|
|||||||
if (mention) return mention;
|
if (mention) return mention;
|
||||||
}
|
}
|
||||||
|
|
||||||
return <a {...attributes}>{content}</a>;
|
return <a {...attributes} onClick={handleExternalLinkClick}>{content}</a>;
|
||||||
};
|
};
|
||||||
return render;
|
return render;
|
||||||
};
|
};
|
||||||
@@ -441,19 +455,31 @@ export const getReactCustomHtmlParser = (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (name === 'a' && testMatrixTo(tryDecodeURIComponent(props.href))) {
|
if (name === 'a') {
|
||||||
const content = children.find((child) => !(child instanceof DOMText))
|
const href = tryDecodeURIComponent(props.href);
|
||||||
? undefined
|
// Handle matrix.to links as mentions
|
||||||
: children.map((c) => (c instanceof DOMText ? c.data : '')).join();
|
if (testMatrixTo(href)) {
|
||||||
|
const content = children.find((child) => !(child instanceof DOMText))
|
||||||
|
? undefined
|
||||||
|
: children.map((c) => (c instanceof DOMText ? c.data : '')).join();
|
||||||
|
|
||||||
const mention = renderMatrixMention(
|
const mention = renderMatrixMention(
|
||||||
mx,
|
mx,
|
||||||
roomId,
|
roomId,
|
||||||
tryDecodeURIComponent(props.href),
|
href,
|
||||||
makeMentionCustomProps(params.handleMentionClick, content)
|
makeMentionCustomProps(params.handleMentionClick, content)
|
||||||
);
|
);
|
||||||
|
|
||||||
if (mention) return mention;
|
if (mention) return mention;
|
||||||
|
}
|
||||||
|
// Handle external links - add click handler for Tauri
|
||||||
|
if (props.href?.startsWith('http://') || props.href?.startsWith('https://')) {
|
||||||
|
return (
|
||||||
|
<a {...props} onClick={handleExternalLinkClick}>
|
||||||
|
{domToReact(children, opts)}
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (name === 'span' && 'data-mx-spoiler' in props) {
|
if (name === 'span' && 'data-mx-spoiler' in props) {
|
||||||
|
|||||||
Reference in New Issue
Block a user