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