diff --git a/modules/url-prewarm/expo-module.config.json b/modules/url-prewarm/expo-module.config.json new file mode 100644 index 0000000000..4fe1ee4534 --- /dev/null +++ b/modules/url-prewarm/expo-module.config.json @@ -0,0 +1,6 @@ +{ + "platforms": ["ios"], + "ios": { + "modules": ["UrlPrewarmModule"] + } +} diff --git a/modules/url-prewarm/index.ts b/modules/url-prewarm/index.ts new file mode 100644 index 0000000000..5e3ef963cc --- /dev/null +++ b/modules/url-prewarm/index.ts @@ -0,0 +1 @@ +export * from './src/UrlPrewarmModule' diff --git a/modules/url-prewarm/ios/UrlPrewarm.podspec b/modules/url-prewarm/ios/UrlPrewarm.podspec new file mode 100644 index 0000000000..5567874437 --- /dev/null +++ b/modules/url-prewarm/ios/UrlPrewarm.podspec @@ -0,0 +1,25 @@ +Pod::Spec.new do |s| + s.name = 'UrlPrewarm' + s.version = '1.0.0' + s.summary = 'A sample project summary' + s.description = 'A sample project description' + s.author = '' + s.homepage = 'https://docs.expo.dev/modules/' + s.platforms = { + :ios => '15.1', + :tvos => '15.1' + } + s.source = { git: '' } + s.static_framework = true + + s.dependency 'ExpoModulesCore' + + # Swift/Objective-C compatibility + s.pod_target_xcconfig = { + 'DEFINES_MODULE' => 'YES', + } + + s.frameworks = 'SafariServices' + + s.source_files = "**/*.{h,m,mm,swift,hpp,cpp}" +end diff --git a/modules/url-prewarm/ios/UrlPrewarmModule.swift b/modules/url-prewarm/ios/UrlPrewarmModule.swift new file mode 100644 index 0000000000..4e2ea2ba7e --- /dev/null +++ b/modules/url-prewarm/ios/UrlPrewarmModule.swift @@ -0,0 +1,13 @@ +import ExpoModulesCore +import SafariServices + +public class UrlPrewarmModule: Module { + public func definition() -> ModuleDefinition { + Name("UrlPrewarm") + + AsyncFunction("prewarmUrlsAsync") { (urls: [String]) in + let validUrls = urls.compactMap { URL(string: $0) } + SFSafariViewController.prewarmConnections(to: validUrls) + } + } +} diff --git a/modules/url-prewarm/src/UrlPrewarmModule.ios.ts b/modules/url-prewarm/src/UrlPrewarmModule.ios.ts new file mode 100644 index 0000000000..5ad72f4c3e --- /dev/null +++ b/modules/url-prewarm/src/UrlPrewarmModule.ios.ts @@ -0,0 +1,7 @@ +import {requireNativeModule} from 'expo' + +const NativeModule = requireNativeModule('UrlPrewarm') + +export function prewarmUrlsAsync(urls: string[]): Promise { + return NativeModule.prewarmUrlsAsync(urls) +} diff --git a/modules/url-prewarm/src/UrlPrewarmModule.ts b/modules/url-prewarm/src/UrlPrewarmModule.ts new file mode 100644 index 0000000000..2d0699eb5c --- /dev/null +++ b/modules/url-prewarm/src/UrlPrewarmModule.ts @@ -0,0 +1,5 @@ +export function prewarmUrlsAsync(_urls: string[]): Promise { + throw new Error( + '[Not available] prewarmUrlsAsync is not available on this platform', + ) +} diff --git a/src/components/Post/Embed/ExternalEmbed/index.tsx b/src/components/Post/Embed/ExternalEmbed/index.tsx index 4e970ad7f8..d5ef17d5d8 100644 --- a/src/components/Post/Embed/ExternalEmbed/index.tsx +++ b/src/components/Post/Embed/ExternalEmbed/index.tsx @@ -7,6 +7,7 @@ import {useLingui} from '@lingui/react' import {parseAltFromGIFDescription} from '#/lib/gif-alt-text' import {useHaptics} from '#/lib/haptics' +import {prewarmUrl} from '#/lib/hooks/useOpenLink' import {shareUrl} from '#/lib/sharing' import {parseEmbedPlayerFromUrl} from '#/lib/strings/embed-player' import {toNiceDomain} from '#/lib/strings/url-helpers' @@ -79,6 +80,7 @@ export const ExternalEmbed = ({ label={link.title || _(msg`Open link to ${niceUrl}`)} to={link.uri} shouldProxy={true} + onPressIn={() => prewarmUrl(link.uri)} onPress={onPress} onLongPress={onShareExternal}> {({hovered}) => ( diff --git a/src/components/RichText.tsx b/src/components/RichText.tsx index 9908679c57..28ca7a553f 100644 --- a/src/components/RichText.tsx +++ b/src/components/RichText.tsx @@ -2,6 +2,7 @@ import React from 'react' import {type StyleProp, type TextStyle} from 'react-native' import {AppBskyRichtextFacet, RichText as RichTextAPI} from '@atproto/api' +import {prewarmUrl} from '#/lib/hooks/useOpenLink' import {toShortUrl} from '#/lib/strings/url-helpers' import {atoms as a, flatten, type TextStyleProp} from '#/alf' import {isOnlyEmoji} from '#/alf/typography' @@ -108,7 +109,7 @@ export function RichText({ style={interactiveStyles} // @ts-ignore TODO dataSet={WORD_WRAP} - shouldProxy={shouldProxyLinks} + shouldProxy={false} onPress={onLinkPress}> {segment.text} @@ -128,6 +129,7 @@ export function RichText({ dataSet={WORD_WRAP} shareOnLongPress shouldProxy={shouldProxyLinks} + onPressIn={() => prewarmUrl(link.uri, shouldProxyLinks)} onPress={onLinkPress} emoji> {toShortUrl(segment.text)} diff --git a/src/lib/hooks/useOpenLink.ts b/src/lib/hooks/useOpenLink.ts index 6e3abef62f..9df4082eb2 100644 --- a/src/lib/hooks/useOpenLink.ts +++ b/src/lib/hooks/useOpenLink.ts @@ -12,12 +12,13 @@ import { toNiceDomain, } from '#/lib/strings/url-helpers' import {logger} from '#/logger' -import {isNative} from '#/platform/detection' +import {isIOS, isNative} from '#/platform/detection' import {useInAppBrowser} from '#/state/preferences/in-app-browser' import {useTheme} from '#/alf' import {useDialogContext} from '#/components/Dialog' import {useSheetWrapper} from '#/components/Dialog/sheet-wrapper' import {useGlobalDialogsControlContext} from '#/components/dialogs/Context' +import * as UrlPrewarm from '../../../modules/url-prewarm' export function useOpenLink() { const enabled = useInAppBrowser() @@ -81,3 +82,15 @@ export function useOpenLink() { return openLink } + +export async function prewarmUrl(url: string, shouldProxy: boolean = true) { + if (!isIOS) return + + if (!isBskyAppUrl(url)) { + const urls = [url] + if (shouldProxy) { + urls.push(createProxiedUrl(url)) + } + await UrlPrewarm.prewarmUrlsAsync(urls) + } +}