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