Files
native/src/lib/linkify-text.ts
T
lucletoffeandClaude Opus 5 c2584c2bd5 fix(calendar): make URLs in the event description tappable
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
2026-09-04 09:49:44 +02:00

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);
}