feat(app): reveal external link destinations on hover (#3176)

This commit is contained in:
omster
2026-10-03 19:44:03 +02:00
committed by GitHub
parent 7b39e5a79d
commit 0de7dde1ce
2 changed files with 40 additions and 2 deletions
@@ -55,6 +55,7 @@ import {
import {Logger} from '@app/features/platform/utils/AppLogger';
import StreamerMode from '@app/features/streamer_mode/state/StreamerMode';
import * as ThemeCommands from '@app/features/theme/commands/ThemeCommands';
import linkRendererStyles from '@app/features/theme/styles/LinkRenderer.module.css';
import markupStyles from '@app/features/theme/styles/Markup.module.css';
import * as ThemeUtils from '@app/features/theme/utils/ThemeUtils';
import TrustedDomain from '@app/features/trusted_domain/state/TrustedDomain';
@@ -66,6 +67,8 @@ import * as ModalCommands from '@app/features/ui/commands/ModalCommands';
import {modal} from '@app/features/ui/commands/ModalCommands';
import * as TextCopyCommands from '@app/features/ui/commands/TextCopyCommands';
import FocusRing from '@app/features/ui/focus_ring/FocusRing';
import MobileLayout from '@app/features/ui/state/MobileLayout';
import {Tooltip} from '@app/features/ui/tooltip/Tooltip';
import {APP_PROTOCOL_PREFIX, APP_PROTOCOL_SCHEME, isAppProtocolUrl} from '@app/features/ui/utils/AppProtocol';
import {openExternalUrl} from '@app/features/ui/utils/NativeUtils';
import {
@@ -81,7 +84,7 @@ import {isProbablyAValidSnowflake} from '@fluxer/snowflake/src/SnowflakeUtils';
import type {I18n} from '@lingui/core';
import {msg} from '@lingui/core/macro';
import {useLingui} from '@lingui/react';
import {CaretRightIcon, ChatTeardropIcon, LockIcon} from '@phosphor-icons/react';
import {CaretRightIcon, ChatTeardropIcon, LockIcon, WarningIcon} from '@phosphor-icons/react';
import {clsx} from 'clsx';
import {observer} from 'mobx-react-lite';
import type React from 'react';
@@ -995,6 +998,8 @@ export const LinkRenderer = observer(function LinkRenderer({
const shouldShowAccessDeniedModal = Boolean(jumpTarget && !jumpChannel);
let isInternal = false;
let handleClick: ((e: React.MouseEvent) => void) | undefined;
let isExternalDestination = false;
let isTrustedExternalDestination = false;
if (userProfileId) {
handleClick = (event) => {
event.preventDefault();
@@ -1150,6 +1155,8 @@ export const LinkRenderer = observer(function LinkRenderer({
}
if (!isInternal && !inviteCode) {
const isTrusted = TrustedDomain.isTrustedDomain(parsed.hostname);
isExternalDestination = true;
isTrustedExternalDestination = isTrusted;
if (!isTrusted) {
handleClick = (e) => {
e.preventDefault();
@@ -1170,7 +1177,8 @@ export const LinkRenderer = observer(function LinkRenderer({
}
const href = AttachmentUrlRefresher.fresh(url);
const warmAttachmentUrl = () => AttachmentUrlRefresher.warm(url);
return (
const showDestinationTooltip = isExternalDestination && text != null;
const linkElement = (
<FocusRing key={id} offset={-2} data-flx="messaging.markdown.renderers.link-renderer.focus-ring--2">
<a
href={href}
@@ -1204,4 +1212,22 @@ export const LinkRenderer = observer(function LinkRenderer({
</a>
</FocusRing>
);
if (!showDestinationTooltip) {
return linkElement;
}
const destinationTooltipText = MobileLayout.enabled
? url
: () => (
<span className={linkRendererStyles.destination}>
{!isTrustedExternalDestination && (
<WarningIcon size={16} weight="fill" className={linkRendererStyles.destinationWarningIcon} />
)}
<span>{url}</span>
</span>
);
return (
<Tooltip text={destinationTooltipText} type="normal" position="bottom" maxWidth="xl">
{linkElement}
</Tooltip>
);
});
@@ -0,0 +1,12 @@
/* SPDX-License-Identifier: AGPL-3.0-or-later */
.destination:has(> svg) {
display: flex;
align-items: center;
gap: 0.25rem;
}
.destinationWarningIcon {
color: var(--text-warning);
flex-shrink: 0;
}