mirror of
https://github.com/bulwarkmail/native.git
synced 2026-11-01 08:06:13 +00:00
A meeting invitation puts the join URL in the description - Teams, Meet, Zoom and the plain iTIP fallbacks all do - and the event detail sheet rendered it as bare <Text>. The one actionable thing in the event could not be tapped: you had to select the URL by hand and paste it into a browser. The mail body already linkifies (plainTextToSafeHtml, rendered in the WebView); only the calendar was left out. `splitTextLinks` (lib/linkify-text) splits the text into runs and marks the http(s) URLs, using the same rule as plainTextToSafeHtml so a message linkifies identically in the body and in the calendar. DetailRow gains an opt-in `linkify` prop - set on the description row only, not on rows the app composed itself - and hands each URL to `openExternalUrl` with `confirm: true`, the same guard the "Open link" button on the video row already uses: scheme allow-list plus the host confirmation, since this text arrives in someone else's data. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018QPPAw4SK4M9Z7euVPM2cL
43 lines
1.5 KiB
TypeScript
43 lines
1.5 KiB
TypeScript
/**
|
|
* Split plain text into runs, marking the http(s) URLs inside it.
|
|
*
|
|
* For text rendered as React Native <Text>, where an <a> is not an option:
|
|
* the caller maps each segment to a <Text>, giving the ones carrying a `url`
|
|
* an onPress. The URL terminates at whitespace or any of `<>"'`, so the
|
|
* `<https://…>` form mail clients emit yields a bare URL rather than one with
|
|
* a trailing bracket.
|
|
*
|
|
* Same rule as `plainTextToSafeHtml` in lib/email-html - the two must agree,
|
|
* or the same message linkifies differently in the body and in the calendar.
|
|
* The result is still only a hint: hand every URL to `openExternalUrl`, which
|
|
* is what actually checks the scheme.
|
|
*/
|
|
|
|
export interface TextSegment {
|
|
text: string;
|
|
/** Set when this run is an http(s) URL. */
|
|
url?: string;
|
|
}
|
|
|
|
const URL_RE = /(https?:\/\/[^\s<>"']+)/g;
|
|
|
|
export function splitTextLinks(text: string): TextSegment[] {
|
|
const segments: TextSegment[] = [];
|
|
let lastIndex = 0;
|
|
let match: RegExpExecArray | null;
|
|
URL_RE.lastIndex = 0;
|
|
while ((match = URL_RE.exec(text)) !== null) {
|
|
if (match.index > lastIndex) segments.push({ text: text.slice(lastIndex, match.index) });
|
|
segments.push({ text: match[0], url: match[0] });
|
|
lastIndex = match.index + match[0].length;
|
|
}
|
|
if (lastIndex < text.length) segments.push({ text: text.slice(lastIndex) });
|
|
return segments;
|
|
}
|
|
|
|
/** True when the text holds at least one http(s) URL. */
|
|
export function hasTextLink(text: string): boolean {
|
|
URL_RE.lastIndex = 0;
|
|
return URL_RE.test(text);
|
|
}
|