mirror of https://git.lenooby09.tech/LeNooby09/social-app.git
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at ruby-v 439 lines 12 kB view raw
1import React, {ComponentProps, memo, useMemo} from 'react' 2import { 3 GestureResponderEvent, 4 Platform, 5 Pressable, 6 StyleProp, 7 TextProps, 8 TextStyle, 9 TouchableOpacity, 10 View, 11 ViewStyle, 12} from 'react-native' 13import {sanitizeUrl} from '@braintree/sanitize-url' 14import {StackActions, useLinkProps} from '@react-navigation/native' 15 16import { 17 DebouncedNavigationProp, 18 useNavigationDeduped, 19} from '#/lib/hooks/useNavigationDeduped' 20import {useOpenLink} from '#/lib/hooks/useOpenLink' 21import {getTabState, TabState} from '#/lib/routes/helpers' 22import { 23 convertBskyAppUrlIfNeeded, 24 isExternalUrl, 25 linkRequiresWarning, 26} from '#/lib/strings/url-helpers' 27import {TypographyVariant} from '#/lib/ThemeContext' 28import {isAndroid, isWeb} from '#/platform/detection' 29import {emitSoftReset} from '#/state/events' 30import {useModalControls} from '#/state/modals' 31import {WebAuxClickWrapper} from '#/view/com/util/WebAuxClickWrapper' 32import {useTheme} from '#/alf' 33import {router} from '../../../routes' 34import {PressableWithHover} from './PressableWithHover' 35import {Text} from './text/Text' 36 37type Event = 38 | React.MouseEvent<HTMLAnchorElement, MouseEvent> 39 | GestureResponderEvent 40 41interface Props extends ComponentProps<typeof TouchableOpacity> { 42 testID?: string 43 style?: StyleProp<ViewStyle> 44 href?: string 45 title?: string 46 children?: React.ReactNode 47 hoverStyle?: StyleProp<ViewStyle> 48 noFeedback?: boolean 49 asAnchor?: boolean 50 dataSet?: Object | undefined 51 anchorNoUnderline?: boolean 52 navigationAction?: 'push' | 'replace' | 'navigate' 53 onPointerEnter?: () => void 54 onPointerLeave?: () => void 55 onBeforePress?: () => void 56} 57 58export const Link = memo(function Link({ 59 testID, 60 style, 61 href, 62 title, 63 children, 64 noFeedback, 65 asAnchor, 66 accessible, 67 anchorNoUnderline, 68 navigationAction, 69 onBeforePress, 70 accessibilityActions, 71 onAccessibilityAction, 72 ...props 73}: Props) { 74 const t = useTheme() 75 const {closeModal} = useModalControls() 76 const navigation = useNavigationDeduped() 77 const anchorHref = asAnchor ? sanitizeUrl(href) : undefined 78 const openLink = useOpenLink() 79 80 const onPress = React.useCallback( 81 (e?: Event) => { 82 onBeforePress?.() 83 if (typeof href === 'string') { 84 return onPressInner( 85 closeModal, 86 navigation, 87 sanitizeUrl(href), 88 navigationAction, 89 openLink, 90 e, 91 ) 92 } 93 }, 94 [closeModal, navigation, navigationAction, href, openLink, onBeforePress], 95 ) 96 97 const accessibilityActionsWithActivate = [ 98 ...(accessibilityActions || []), 99 {name: 'activate', label: title}, 100 ] 101 102 if (noFeedback) { 103 return ( 104 <WebAuxClickWrapper> 105 <Pressable 106 testID={testID} 107 onPress={onPress} 108 accessible={accessible} 109 accessibilityRole="link" 110 accessibilityActions={accessibilityActionsWithActivate} 111 onAccessibilityAction={e => { 112 if (e.nativeEvent.actionName === 'activate') { 113 onPress() 114 } else { 115 onAccessibilityAction?.(e) 116 } 117 }} 118 {...props} 119 android_ripple={{ 120 color: t.atoms.bg_contrast_25.backgroundColor, 121 }} 122 unstable_pressDelay={isAndroid ? 90 : undefined}> 123 {/* @ts-ignore web only -prf */} 124 <View style={style} href={anchorHref}> 125 {children ? children : <Text>{title || 'link'}</Text>} 126 </View> 127 </Pressable> 128 </WebAuxClickWrapper> 129 ) 130 } 131 132 if (anchorNoUnderline) { 133 // @ts-ignore web only -prf 134 props.dataSet = props.dataSet || {} 135 // @ts-ignore web only -prf 136 props.dataSet.noUnderline = 1 137 } 138 139 if (title && !props.accessibilityLabel) { 140 props.accessibilityLabel = title 141 } 142 143 const Com = props.hoverStyle ? PressableWithHover : Pressable 144 return ( 145 <Com 146 testID={testID} 147 style={style} 148 onPress={onPress} 149 accessible={accessible} 150 accessibilityRole="link" 151 // @ts-ignore web only -prf 152 href={anchorHref} 153 {...props}> 154 {children ? children : <Text>{title || 'link'}</Text>} 155 </Com> 156 ) 157}) 158 159export const TextLink = memo(function TextLink({ 160 testID, 161 type = 'md', 162 style, 163 href, 164 text, 165 numberOfLines, 166 lineHeight, 167 dataSet, 168 title, 169 onPress, 170 onBeforePress, 171 disableMismatchWarning, 172 navigationAction, 173 anchorNoUnderline, 174 ...orgProps 175}: { 176 testID?: string 177 type?: TypographyVariant 178 style?: StyleProp<TextStyle> 179 href: string 180 text: string | JSX.Element | React.ReactNode 181 numberOfLines?: number 182 lineHeight?: number 183 dataSet?: any 184 title?: string 185 disableMismatchWarning?: boolean 186 navigationAction?: 'push' | 'replace' | 'navigate' 187 anchorNoUnderline?: boolean 188 onBeforePress?: () => void 189} & TextProps) { 190 const {...props} = useLinkProps({to: sanitizeUrl(href)}) 191 const navigation = useNavigationDeduped() 192 const {openModal, closeModal} = useModalControls() 193 const openLink = useOpenLink() 194 195 if (!disableMismatchWarning && typeof text !== 'string') { 196 console.error('Unable to detect mismatching label') 197 } 198 199 if (anchorNoUnderline) { 200 dataSet = dataSet ?? {} 201 dataSet.noUnderline = 1 202 } 203 204 props.onPress = React.useCallback( 205 (e?: Event) => { 206 const requiresWarning = 207 !disableMismatchWarning && 208 linkRequiresWarning(href, typeof text === 'string' ? text : '') 209 if (requiresWarning) { 210 e?.preventDefault?.() 211 openModal({ 212 name: 'link-warning', 213 text: typeof text === 'string' ? text : '', 214 href, 215 }) 216 } 217 if ( 218 isWeb && 219 href !== '#' && 220 e != null && 221 isModifiedEvent(e as React.MouseEvent) 222 ) { 223 // Let the browser handle opening in new tab etc. 224 return 225 } 226 onBeforePress?.() 227 if (onPress) { 228 e?.preventDefault?.() 229 // @ts-ignore function signature differs by platform -prf 230 return onPress() 231 } 232 return onPressInner( 233 closeModal, 234 navigation, 235 sanitizeUrl(href), 236 navigationAction, 237 openLink, 238 e, 239 ) 240 }, 241 [ 242 onBeforePress, 243 onPress, 244 closeModal, 245 openModal, 246 navigation, 247 href, 248 text, 249 disableMismatchWarning, 250 navigationAction, 251 openLink, 252 ], 253 ) 254 const hrefAttrs = useMemo(() => { 255 const isExternal = isExternalUrl(href) 256 if (isExternal) { 257 return { 258 target: '_blank', 259 // rel: 'noopener noreferrer', 260 } 261 } 262 return {} 263 }, [href]) 264 265 return ( 266 <Text 267 testID={testID} 268 type={type} 269 style={style} 270 numberOfLines={numberOfLines} 271 lineHeight={lineHeight} 272 dataSet={dataSet} 273 title={title} 274 // @ts-ignore web only -prf 275 hrefAttrs={hrefAttrs} // hack to get open in new tab to work on safari. without this, safari will open in a new window 276 {...props} 277 {...orgProps}> 278 {text} 279 </Text> 280 ) 281}) 282 283/** 284 * Only acts as a link on desktop web 285 */ 286interface TextLinkOnWebOnlyProps extends TextProps { 287 testID?: string 288 type?: TypographyVariant 289 style?: StyleProp<TextStyle> 290 href: string 291 text: string | JSX.Element 292 numberOfLines?: number 293 lineHeight?: number 294 accessible?: boolean 295 accessibilityLabel?: string 296 accessibilityHint?: string 297 title?: string 298 navigationAction?: 'push' | 'replace' | 'navigate' 299 disableMismatchWarning?: boolean 300 onBeforePress?: () => void 301 onPointerEnter?: () => void 302 anchorNoUnderline?: boolean 303} 304export const TextLinkOnWebOnly = memo(function DesktopWebTextLink({ 305 testID, 306 type = 'md', 307 style, 308 href, 309 text, 310 numberOfLines, 311 lineHeight, 312 navigationAction, 313 disableMismatchWarning, 314 onBeforePress, 315 ...props 316}: TextLinkOnWebOnlyProps) { 317 if (isWeb) { 318 return ( 319 <TextLink 320 testID={testID} 321 type={type} 322 style={style} 323 href={href} 324 text={text} 325 numberOfLines={numberOfLines} 326 lineHeight={lineHeight} 327 title={props.title} 328 navigationAction={navigationAction} 329 disableMismatchWarning={disableMismatchWarning} 330 onBeforePress={onBeforePress} 331 {...props} 332 /> 333 ) 334 } 335 return ( 336 <Text 337 testID={testID} 338 type={type} 339 style={style} 340 numberOfLines={numberOfLines} 341 lineHeight={lineHeight} 342 title={props.title} 343 {...props}> 344 {text} 345 </Text> 346 ) 347}) 348 349const EXEMPT_PATHS = ['/robots.txt', '/security.txt', '/.well-known/'] 350 351// NOTE 352// we can't use the onPress given by useLinkProps because it will 353// match most paths to the HomeTab routes while we actually want to 354// preserve the tab the app is currently in 355// 356// we also have some additional behaviors - closing the current modal, 357// converting bsky urls, and opening http/s links in the system browser 358// 359// this method copies from the onPress implementation but adds our 360// needed customizations 361// -prf 362function onPressInner( 363 closeModal = () => {}, 364 navigation: DebouncedNavigationProp, 365 href: string, 366 navigationAction: 'push' | 'replace' | 'navigate' = 'push', 367 openLink: (href: string) => void, 368 e?: Event, 369) { 370 let shouldHandle = false 371 const isLeftClick = 372 // @ts-ignore Web only -prf 373 Platform.OS === 'web' && (e.button == null || e.button === 0) 374 // @ts-ignore Web only -prf 375 const isMiddleClick = Platform.OS === 'web' && e.button === 1 376 const isMetaKey = 377 // @ts-ignore Web only -prf 378 Platform.OS === 'web' && (e.metaKey || e.altKey || e.ctrlKey || e.shiftKey) 379 const newTab = isMetaKey || isMiddleClick 380 381 if (Platform.OS !== 'web' || !e) { 382 shouldHandle = e ? !e.defaultPrevented : true 383 } else if ( 384 !e.defaultPrevented && // onPress prevented default 385 (isLeftClick || isMiddleClick) && // ignore everything but left and middle clicks 386 // @ts-ignore Web only -prf 387 [undefined, null, '', 'self'].includes(e.currentTarget?.target) // let browser handle "target=_blank" etc. 388 ) { 389 e.preventDefault() 390 shouldHandle = true 391 } 392 393 if (shouldHandle) { 394 href = convertBskyAppUrlIfNeeded(href) 395 if ( 396 newTab || 397 href.startsWith('http') || 398 href.startsWith('mailto') || 399 EXEMPT_PATHS.some(path => href.startsWith(path)) 400 ) { 401 openLink(href) 402 } else { 403 closeModal() // close any active modals 404 405 const [routeName, params] = router.matchPath(href) 406 if (navigationAction === 'push') { 407 // @ts-ignore we're not able to type check on this one -prf 408 navigation.dispatch(StackActions.push(routeName, params)) 409 } else if (navigationAction === 'replace') { 410 // @ts-ignore we're not able to type check on this one -prf 411 navigation.dispatch(StackActions.replace(routeName, params)) 412 } else if (navigationAction === 'navigate') { 413 const state = navigation.getState() 414 const tabState = getTabState(state, routeName) 415 if (tabState === TabState.InsideAtRoot) { 416 emitSoftReset() 417 } else { 418 // @ts-ignore we're not able to type check on this one -prf 419 navigation.navigate(routeName, params) 420 } 421 } else { 422 throw Error('Unsupported navigator action.') 423 } 424 } 425 } 426} 427 428function isModifiedEvent(e: React.MouseEvent): boolean { 429 const eventTarget = e.currentTarget as HTMLAnchorElement 430 const target = eventTarget.getAttribute('target') 431 return ( 432 (target && target !== '_self') || 433 e.metaKey || 434 e.ctrlKey || 435 e.shiftKey || 436 e.altKey || 437 (e.nativeEvent && e.nativeEvent.which === 2) 438 ) 439}