mirror of https://git.lenooby09.tech/LeNooby09/social-app.git
1import React, {ComponentProps, memo, useMemo} from 'react'
2import {
3 Linking,
4 GestureResponderEvent,
5 Platform,
6 StyleProp,
7 TextStyle,
8 TextProps,
9 View,
10 ViewStyle,
11 Pressable,
12 TouchableWithoutFeedback,
13 TouchableOpacity,
14} from 'react-native'
15import {
16 useLinkProps,
17 useNavigation,
18 StackActions,
19} from '@react-navigation/native'
20import {Text} from './text/Text'
21import {TypographyVariant} from 'lib/ThemeContext'
22import {NavigationProp} from 'lib/routes/types'
23import {router} from '../../../routes'
24import {useStores, RootStoreModel} from 'state/index'
25import {
26 convertBskyAppUrlIfNeeded,
27 isExternalUrl,
28 linkRequiresWarning,
29} from 'lib/strings/url-helpers'
30import {isAndroid} from 'platform/detection'
31import {sanitizeUrl} from '@braintree/sanitize-url'
32import {PressableWithHover} from './PressableWithHover'
33import FixedTouchableHighlight from '../pager/FixedTouchableHighlight'
34import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
35
36type Event =
37 | React.MouseEvent<HTMLAnchorElement, MouseEvent>
38 | GestureResponderEvent
39
40interface Props extends ComponentProps<typeof TouchableOpacity> {
41 testID?: string
42 style?: StyleProp<ViewStyle>
43 href?: string
44 title?: string
45 children?: React.ReactNode
46 hoverStyle?: StyleProp<ViewStyle>
47 noFeedback?: boolean
48 asAnchor?: boolean
49 anchorNoUnderline?: boolean
50}
51
52export const Link = memo(function Link({
53 testID,
54 style,
55 href,
56 title,
57 children,
58 noFeedback,
59 asAnchor,
60 accessible,
61 anchorNoUnderline,
62 ...props
63}: Props) {
64 const store = useStores()
65 const navigation = useNavigation<NavigationProp>()
66 const anchorHref = asAnchor ? sanitizeUrl(href) : undefined
67
68 const onPress = React.useCallback(
69 (e?: Event) => {
70 if (typeof href === 'string') {
71 return onPressInner(store, navigation, sanitizeUrl(href), e)
72 }
73 },
74 [store, navigation, href],
75 )
76
77 if (noFeedback) {
78 if (isAndroid) {
79 // workaround for Android not working well with left/right swipe gestures and TouchableWithoutFeedback
80 // https://github.com/callstack/react-native-pager-view/issues/424
81 return (
82 <FixedTouchableHighlight
83 testID={testID}
84 onPress={onPress}
85 // @ts-ignore web only -prf
86 href={asAnchor ? sanitizeUrl(href) : undefined}
87 accessible={accessible}
88 accessibilityRole="link"
89 {...props}>
90 <View style={style}>
91 {children ? children : <Text>{title || 'link'}</Text>}
92 </View>
93 </FixedTouchableHighlight>
94 )
95 }
96 return (
97 <TouchableWithoutFeedback
98 testID={testID}
99 onPress={onPress}
100 accessible={accessible}
101 accessibilityRole="link"
102 {...props}>
103 {/* @ts-ignore web only -prf */}
104 <View style={style} href={anchorHref}>
105 {children ? children : <Text>{title || 'link'}</Text>}
106 </View>
107 </TouchableWithoutFeedback>
108 )
109 }
110
111 if (anchorNoUnderline) {
112 // @ts-ignore web only -prf
113 props.dataSet = props.dataSet || {}
114 // @ts-ignore web only -prf
115 props.dataSet.noUnderline = 1
116 }
117
118 if (title && !props.accessibilityLabel) {
119 props.accessibilityLabel = title
120 }
121
122 const Com = props.hoverStyle ? PressableWithHover : Pressable
123 return (
124 <Com
125 testID={testID}
126 style={style}
127 onPress={onPress}
128 accessible={accessible}
129 accessibilityRole="link"
130 // @ts-ignore web only -prf
131 href={anchorHref}
132 {...props}>
133 {children ? children : <Text>{title || 'link'}</Text>}
134 </Com>
135 )
136})
137
138export const TextLink = memo(function TextLink({
139 testID,
140 type = 'md',
141 style,
142 href,
143 text,
144 numberOfLines,
145 lineHeight,
146 dataSet,
147 title,
148 onPress,
149 warnOnMismatchingLabel,
150 ...orgProps
151}: {
152 testID?: string
153 type?: TypographyVariant
154 style?: StyleProp<TextStyle>
155 href: string
156 text: string | JSX.Element | React.ReactNode
157 numberOfLines?: number
158 lineHeight?: number
159 dataSet?: any
160 title?: string
161 warnOnMismatchingLabel?: boolean
162} & TextProps) {
163 const {...props} = useLinkProps({to: sanitizeUrl(href)})
164 const store = useStores()
165 const navigation = useNavigation<NavigationProp>()
166
167 if (warnOnMismatchingLabel && typeof text !== 'string') {
168 console.error('Unable to detect mismatching label')
169 }
170
171 props.onPress = React.useCallback(
172 (e?: Event) => {
173 const requiresWarning =
174 warnOnMismatchingLabel &&
175 linkRequiresWarning(href, typeof text === 'string' ? text : '')
176 if (requiresWarning) {
177 e?.preventDefault?.()
178 store.shell.openModal({
179 name: 'link-warning',
180 text: typeof text === 'string' ? text : '',
181 href,
182 })
183 }
184 if (onPress) {
185 e?.preventDefault?.()
186 // @ts-ignore function signature differs by platform -prf
187 return onPress()
188 }
189 return onPressInner(store, navigation, sanitizeUrl(href), e)
190 },
191 [onPress, store, navigation, href, text, warnOnMismatchingLabel],
192 )
193 const hrefAttrs = useMemo(() => {
194 const isExternal = isExternalUrl(href)
195 if (isExternal) {
196 return {
197 target: '_blank',
198 // rel: 'noopener noreferrer',
199 }
200 }
201 return {}
202 }, [href])
203
204 return (
205 <Text
206 testID={testID}
207 type={type}
208 style={style}
209 numberOfLines={numberOfLines}
210 lineHeight={lineHeight}
211 dataSet={dataSet}
212 title={title}
213 // @ts-ignore web only -prf
214 hrefAttrs={hrefAttrs} // hack to get open in new tab to work on safari. without this, safari will open in a new window
215 {...props}
216 {...orgProps}>
217 {text}
218 </Text>
219 )
220})
221
222/**
223 * Only acts as a link on desktop web
224 */
225interface DesktopWebTextLinkProps extends TextProps {
226 testID?: string
227 type?: TypographyVariant
228 style?: StyleProp<TextStyle>
229 href: string
230 text: string | JSX.Element
231 numberOfLines?: number
232 lineHeight?: number
233 accessible?: boolean
234 accessibilityLabel?: string
235 accessibilityHint?: string
236 title?: string
237}
238export const DesktopWebTextLink = memo(function DesktopWebTextLink({
239 testID,
240 type = 'md',
241 style,
242 href,
243 text,
244 numberOfLines,
245 lineHeight,
246 ...props
247}: DesktopWebTextLinkProps) {
248 const {isDesktop} = useWebMediaQueries()
249
250 if (isDesktop) {
251 return (
252 <TextLink
253 testID={testID}
254 type={type}
255 style={style}
256 href={href}
257 text={text}
258 numberOfLines={numberOfLines}
259 lineHeight={lineHeight}
260 title={props.title}
261 {...props}
262 />
263 )
264 }
265 return (
266 <Text
267 testID={testID}
268 type={type}
269 style={style}
270 numberOfLines={numberOfLines}
271 lineHeight={lineHeight}
272 title={props.title}
273 {...props}>
274 {text}
275 </Text>
276 )
277})
278
279// NOTE
280// we can't use the onPress given by useLinkProps because it will
281// match most paths to the HomeTab routes while we actually want to
282// preserve the tab the app is currently in
283//
284// we also have some additional behaviors - closing the current modal,
285// converting bsky urls, and opening http/s links in the system browser
286//
287// this method copies from the onPress implementation but adds our
288// needed customizations
289// -prf
290function onPressInner(
291 store: RootStoreModel,
292 navigation: NavigationProp,
293 href: string,
294 e?: Event,
295) {
296 let shouldHandle = false
297 const isLeftClick =
298 // @ts-ignore Web only -prf
299 Platform.OS === 'web' && (e.button == null || e.button === 0)
300 // @ts-ignore Web only -prf
301 const isMiddleClick = Platform.OS === 'web' && e.button === 1
302 const isMetaKey =
303 // @ts-ignore Web only -prf
304 Platform.OS === 'web' && (e.metaKey || e.altKey || e.ctrlKey || e.shiftKey)
305 const newTab = isMetaKey || isMiddleClick
306
307 if (Platform.OS !== 'web' || !e) {
308 shouldHandle = e ? !e.defaultPrevented : true
309 } else if (
310 !e.defaultPrevented && // onPress prevented default
311 (isLeftClick || isMiddleClick) && // ignore everything but left and middle clicks
312 // @ts-ignore Web only -prf
313 [undefined, null, '', 'self'].includes(e.currentTarget?.target) // let browser handle "target=_blank" etc.
314 ) {
315 e.preventDefault()
316 shouldHandle = true
317 }
318
319 if (shouldHandle) {
320 href = convertBskyAppUrlIfNeeded(href)
321 if (newTab || href.startsWith('http') || href.startsWith('mailto')) {
322 Linking.openURL(href)
323 } else {
324 store.shell.closeModal() // close any active modals
325
326 // @ts-ignore we're not able to type check on this one -prf
327 navigation.dispatch(StackActions.push(...router.matchPath(href)))
328 }
329 }
330}