mirror of
https://github.com/mattermost/mattermost-mobile.git
synced 2026-11-06 10:50:13 +00:00
* Add phone number matching helpers for tel: autolinks. Detect E.164, grouped national numbers, and tel: URIs while skipping bare digit runs that look like ticket IDs or dates. Co-authored-by: joram <joram@mattermost.com> * Autolink phone numbers in post markdown as tel: links. Wrap recognized numbers in markdown link nodes after parsing so tapping opens the device dialer. Skip code, existing links, and unsafe posts. Co-authored-by: joram <joram@mattermost.com> * Copy phone numbers from tel: markdown links on long-press. Treat tel: like mailto: in link and channel-header sheets, and cover opening tel: URLs through the existing openLink path. Co-authored-by: joram <joram@mattermost.com> * Reject truncated phone matches inside longer digit sequences. Skip past hyphens and dots when checking match boundaries so overlong numbers and numeric prefixes are not autolinked. Keep spaces and parentheses as boundaries so adjacent numbers still match. Also address CodeRabbit test nits: assert match counts, prefix autolinkPhoneNumbers cases with should, and assert rendered tel: links. Co-authored-by: joram <joram@mattermost.com> --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com>
163 lines
5.2 KiB
TypeScript
163 lines
5.2 KiB
TypeScript
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
// See LICENSE.txt for license information.
|
|
|
|
import {useManagedConfig} from '@mattermost/react-native-emm';
|
|
import Clipboard from '@react-native-clipboard/clipboard';
|
|
import React, {Children, type ReactElement, useCallback, useMemo} from 'react';
|
|
import {useIntl, defineMessages} from 'react-intl';
|
|
import {StyleSheet, Text, View} from 'react-native';
|
|
import urlParse from 'url-parse';
|
|
|
|
import SlideUpPanelItem, {ITEM_HEIGHT} from '@components/slide_up_panel_item';
|
|
import {useServerUrl} from '@context/server';
|
|
import {usePreventDoubleTap} from '@hooks/utils';
|
|
import {bottomSheet, dismissBottomSheet} from '@screens/navigation';
|
|
import {bottomSheetSnapPoint, isEmail} from '@utils/helpers';
|
|
import {isTelHref} from '@utils/phone_number';
|
|
import {openLink} from '@utils/url/links';
|
|
|
|
type MarkdownLinkProps = {
|
|
children: ReactElement;
|
|
experimentalNormalizeMarkdownLinks: string;
|
|
href: string;
|
|
siteURL: string;
|
|
onLinkLongPress?: (url?: string) => void;
|
|
}
|
|
|
|
const messages = defineMessages({
|
|
copyEmail: {
|
|
id: 'mobile.markdown.link.copy_email',
|
|
defaultMessage: 'Copy Email Address',
|
|
},
|
|
copyPhone: {
|
|
id: 'mobile.markdown.link.copy_phone',
|
|
defaultMessage: 'Copy Phone Number',
|
|
},
|
|
copyURL: {
|
|
id: 'mobile.markdown.link.copy_url',
|
|
defaultMessage: 'Copy URL',
|
|
},
|
|
});
|
|
|
|
const style = StyleSheet.create({
|
|
bottomSheet: {
|
|
flex: 1,
|
|
},
|
|
});
|
|
|
|
const parseLinkLiteral = (literal: string) => {
|
|
let nextLiteral = literal;
|
|
|
|
const WWW_REGEX = /\b^(?:www.)/i;
|
|
if (nextLiteral.match(WWW_REGEX)) {
|
|
nextLiteral = literal.replace(WWW_REGEX, 'www.');
|
|
}
|
|
|
|
const parsed = urlParse(nextLiteral, {});
|
|
|
|
return parsed.href;
|
|
};
|
|
|
|
function getCopyLinkMessage(href: string) {
|
|
if (isTelHref(href)) {
|
|
return messages.copyPhone;
|
|
}
|
|
|
|
if (isEmail(href.replace(/^(mailto:|tel:)/i, ''))) {
|
|
return messages.copyEmail;
|
|
}
|
|
|
|
return messages.copyURL;
|
|
}
|
|
|
|
const MarkdownLink = ({children, experimentalNormalizeMarkdownLinks, href, siteURL, onLinkLongPress}: MarkdownLinkProps) => {
|
|
const intl = useIntl();
|
|
const managedConfig = useManagedConfig<ManagedConfig>();
|
|
const serverUrl = useServerUrl();
|
|
|
|
const handlePress = usePreventDoubleTap(useCallback(() => {
|
|
openLink(href, serverUrl, siteURL ?? '', intl);
|
|
}, [href, intl, serverUrl, siteURL]));
|
|
|
|
const handleLongPress = useCallback(() => {
|
|
if (managedConfig?.copyAndPasteProtection !== 'true') {
|
|
if (onLinkLongPress) {
|
|
onLinkLongPress(href);
|
|
return;
|
|
}
|
|
|
|
const cleanHref = href.replace(/^(mailto:|tel:)/i, '');
|
|
const copyMessage = getCopyLinkMessage(href);
|
|
|
|
const renderContent = () => {
|
|
return (
|
|
<View
|
|
testID='at_mention.bottom_sheet'
|
|
style={style.bottomSheet}
|
|
>
|
|
<SlideUpPanelItem
|
|
leftIcon='content-copy'
|
|
onPress={() => {
|
|
dismissBottomSheet();
|
|
Clipboard.setString(cleanHref);
|
|
}}
|
|
testID='at_mention.bottom_sheet.copy_url'
|
|
text={intl.formatMessage(copyMessage)}
|
|
/>
|
|
<SlideUpPanelItem
|
|
destructive={true}
|
|
leftIcon='cancel'
|
|
onPress={() => {
|
|
dismissBottomSheet();
|
|
}}
|
|
testID='at_mention.bottom_sheet.cancel'
|
|
text={intl.formatMessage({id: 'mobile.post.cancel', defaultMessage: 'Cancel'})}
|
|
/>
|
|
</View>
|
|
);
|
|
};
|
|
|
|
bottomSheet(renderContent, [1, bottomSheetSnapPoint(2, ITEM_HEIGHT)]);
|
|
}
|
|
}, [managedConfig?.copyAndPasteProtection, onLinkLongPress, href, intl]);
|
|
|
|
const renderedChildren = useMemo(() => {
|
|
if (!experimentalNormalizeMarkdownLinks) {
|
|
return children;
|
|
}
|
|
|
|
return Children.map(children, (c) => {
|
|
const child = c as ReactElement<any>;
|
|
if (!child.props.literal || typeof child.props.literal !== 'string' || (child.props.context && child.props.context.length && !child.props.context.includes('link'))) {
|
|
return child;
|
|
}
|
|
|
|
const {props, ...otherChildProps} = child;
|
|
|
|
const {literal, ...otherProps} = props;
|
|
|
|
const nextProps = {
|
|
literal: parseLinkLiteral(literal),
|
|
...otherProps,
|
|
};
|
|
|
|
return {
|
|
props: nextProps,
|
|
...otherChildProps,
|
|
};
|
|
});
|
|
}, [children, experimentalNormalizeMarkdownLinks]);
|
|
|
|
return (
|
|
<Text
|
|
onPress={handlePress}
|
|
onLongPress={handleLongPress}
|
|
testID='markdown_link'
|
|
>
|
|
{renderedChildren}
|
|
</Text>
|
|
);
|
|
};
|
|
|
|
export default MarkdownLink;
|