mirror of https://git.lenooby09.tech/LeNooby09/social-app.git
1import React from 'react'
2import {GestureResponderEvent} from 'react-native'
3import {sanitizeUrl} from '@braintree/sanitize-url'
4import {StackActions, useLinkProps} from '@react-navigation/native'
5
6import {AllNavigatorParams} from '#/lib/routes/types'
7import {shareUrl} from '#/lib/sharing'
8import {
9 convertBskyAppUrlIfNeeded,
10 isExternalUrl,
11 linkRequiresWarning,
12} from '#/lib/strings/url-helpers'
13import {isNative, isWeb} from '#/platform/detection'
14import {useModalControls} from '#/state/modals'
15import {useOpenLink} from '#/state/preferences/in-app-browser'
16import {useNavigationDeduped} from 'lib/hooks/useNavigationDeduped'
17import {atoms as a, flatten, TextStyleProp, useTheme, web} from '#/alf'
18import {Button, ButtonProps} from '#/components/Button'
19import {useInteractionState} from '#/components/hooks/useInteractionState'
20import {Text, TextProps} from '#/components/Typography'
21import {router} from '#/routes'
22
23/**
24 * Only available within a `Link`, since that inherits from `Button`.
25 * `InlineLink` provides no context.
26 */
27export {useButtonContext as useLinkContext} from '#/components/Button'
28
29type BaseLinkProps = Pick<
30 Parameters<typeof useLinkProps<AllNavigatorParams>>[0],
31 'to'
32> & {
33 testID?: string
34
35 /**
36 * Label for a11y. Defaults to the href.
37 */
38 label?: string
39
40 /**
41 * The React Navigation `StackAction` to perform when the link is pressed.
42 */
43 action?: 'push' | 'replace' | 'navigate'
44
45 /**
46 * If true, will warn the user if the link text does not match the href.
47 *
48 * Note: atm this only works for `InlineLink`s with a string child.
49 */
50 disableMismatchWarning?: boolean
51
52 /**
53 * Callback for when the link is pressed. Prevent default and return `false`
54 * to exit early and prevent navigation.
55 *
56 * DO NOT use this for navigation, that's what the `to` prop is for.
57 */
58 onPress?: (e: GestureResponderEvent) => void | false
59
60 /**
61 * Web-only attribute. Sets `download` attr on web.
62 */
63 download?: string
64
65 /**
66 * Native-only attribute. If true, will open the share sheet on long press.
67 */
68 shareOnLongPress?: boolean
69}
70
71export function useLink({
72 to,
73 displayText,
74 action = 'push',
75 disableMismatchWarning,
76 onPress: outerOnPress,
77 shareOnLongPress,
78}: BaseLinkProps & {
79 displayText: string
80}) {
81 const navigation = useNavigationDeduped()
82 const {href} = useLinkProps<AllNavigatorParams>({
83 to:
84 typeof to === 'string' ? convertBskyAppUrlIfNeeded(sanitizeUrl(to)) : to,
85 })
86 const isExternal = isExternalUrl(href)
87 const {openModal, closeModal} = useModalControls()
88 const openLink = useOpenLink()
89
90 const onPress = React.useCallback(
91 (e: GestureResponderEvent) => {
92 const exitEarlyIfFalse = outerOnPress?.(e)
93
94 if (exitEarlyIfFalse === false) return
95
96 const requiresWarning = Boolean(
97 !disableMismatchWarning &&
98 displayText &&
99 isExternal &&
100 linkRequiresWarning(href, displayText),
101 )
102
103 if (requiresWarning) {
104 e.preventDefault()
105
106 openModal({
107 name: 'link-warning',
108 text: displayText,
109 href: href,
110 })
111 } else {
112 e.preventDefault()
113
114 if (isExternal) {
115 openLink(href)
116 } else {
117 /**
118 * A `GestureResponderEvent`, but cast to `any` to avoid using a bunch
119 * of @ts-ignore below.
120 */
121 const event = e as any
122 const isMiddleClick = isWeb && event.button === 1
123 const isMetaKey =
124 isWeb &&
125 (event.metaKey || event.altKey || event.ctrlKey || event.shiftKey)
126 const shouldOpenInNewTab = isMetaKey || isMiddleClick
127
128 if (
129 shouldOpenInNewTab ||
130 href.startsWith('http') ||
131 href.startsWith('mailto')
132 ) {
133 openLink(href)
134 } else {
135 closeModal() // close any active modals
136
137 if (action === 'push') {
138 navigation.dispatch(StackActions.push(...router.matchPath(href)))
139 } else if (action === 'replace') {
140 navigation.dispatch(
141 StackActions.replace(...router.matchPath(href)),
142 )
143 } else if (action === 'navigate') {
144 // @ts-ignore
145 navigation.navigate(...router.matchPath(href))
146 } else {
147 throw Error('Unsupported navigator action.')
148 }
149 }
150 }
151 }
152 },
153 [
154 outerOnPress,
155 disableMismatchWarning,
156 displayText,
157 isExternal,
158 href,
159 openModal,
160 openLink,
161 closeModal,
162 action,
163 navigation,
164 ],
165 )
166
167 const handleLongPress = React.useCallback(() => {
168 const requiresWarning = Boolean(
169 !disableMismatchWarning &&
170 displayText &&
171 isExternal &&
172 linkRequiresWarning(href, displayText),
173 )
174
175 if (requiresWarning) {
176 openModal({
177 name: 'link-warning',
178 text: displayText,
179 href: href,
180 share: true,
181 })
182 } else {
183 shareUrl(href)
184 }
185 }, [disableMismatchWarning, displayText, href, isExternal, openModal])
186
187 const onLongPress =
188 isNative && isExternal && shareOnLongPress ? handleLongPress : undefined
189
190 return {
191 isExternal,
192 href,
193 onPress,
194 onLongPress,
195 }
196}
197
198export type LinkProps = Omit<BaseLinkProps, 'disableMismatchWarning'> &
199 Omit<ButtonProps, 'onPress' | 'disabled' | 'label'>
200
201/**
202 * A interactive element that renders as a `<a>` tag on the web. On mobile it
203 * will translate the `href` to navigator screens and params and dispatch a
204 * navigation action.
205 *
206 * Intended to behave as a web anchor tag. For more complex routing, use a
207 * `Button`.
208 */
209export function Link({
210 children,
211 to,
212 action = 'push',
213 onPress: outerOnPress,
214 download,
215 ...rest
216}: LinkProps) {
217 const {href, isExternal, onPress} = useLink({
218 to,
219 displayText: typeof children === 'string' ? children : '',
220 action,
221 onPress: outerOnPress,
222 })
223
224 return (
225 <Button
226 label={href}
227 {...rest}
228 style={[a.justify_start, flatten(rest.style)]}
229 role="link"
230 accessibilityRole="link"
231 href={href}
232 onPress={download ? undefined : onPress}
233 {...web({
234 hrefAttrs: {
235 target: download ? undefined : isExternal ? 'blank' : undefined,
236 rel: isExternal ? 'noopener noreferrer' : undefined,
237 download,
238 },
239 dataSet: {
240 // no underline, only `InlineLink` has underlines
241 noUnderline: '1',
242 },
243 })}>
244 {children}
245 </Button>
246 )
247}
248
249export type InlineLinkProps = React.PropsWithChildren<
250 BaseLinkProps & TextStyleProp & Pick<TextProps, 'selectable'>
251>
252
253export function InlineLinkText({
254 children,
255 to,
256 action = 'push',
257 disableMismatchWarning,
258 style,
259 onPress: outerOnPress,
260 download,
261 selectable,
262 label,
263 shareOnLongPress,
264 ...rest
265}: InlineLinkProps) {
266 const t = useTheme()
267 const stringChildren = typeof children === 'string'
268 const {href, isExternal, onPress, onLongPress} = useLink({
269 to,
270 displayText: stringChildren ? children : '',
271 action,
272 disableMismatchWarning,
273 onPress: outerOnPress,
274 shareOnLongPress,
275 })
276 const {
277 state: hovered,
278 onIn: onHoverIn,
279 onOut: onHoverOut,
280 } = useInteractionState()
281 const {state: focused, onIn: onFocus, onOut: onBlur} = useInteractionState()
282 const {
283 state: pressed,
284 onIn: onPressIn,
285 onOut: onPressOut,
286 } = useInteractionState()
287 const flattenedStyle = flatten(style) || {}
288
289 return (
290 <Text
291 selectable={selectable}
292 accessibilityHint=""
293 accessibilityLabel={label || href}
294 {...rest}
295 style={[
296 {color: t.palette.primary_500},
297 (hovered || focused || pressed) && {
298 ...web({outline: 0}),
299 textDecorationLine: 'underline',
300 textDecorationColor: flattenedStyle.color ?? t.palette.primary_500,
301 },
302 flattenedStyle,
303 ]}
304 role="link"
305 onPress={download ? undefined : onPress}
306 onLongPress={onLongPress}
307 onPressIn={onPressIn}
308 onPressOut={onPressOut}
309 onFocus={onFocus}
310 onBlur={onBlur}
311 onMouseEnter={onHoverIn}
312 onMouseLeave={onHoverOut}
313 accessibilityRole="link"
314 href={href}
315 {...web({
316 hrefAttrs: {
317 target: download ? undefined : isExternal ? 'blank' : undefined,
318 rel: isExternal ? 'noopener noreferrer' : undefined,
319 download,
320 },
321 dataSet: {
322 // default to no underline, apply this ourselves
323 noUnderline: '1',
324 },
325 })}>
326 {children}
327 </Text>
328 )
329}