merge main in

what are you doing there? go away

fix recognizer to clear selected text on tap

remove jank/hacks

update readme

remove android stuff

(?) don't remove clipped subview on android for selection
enable selection of alt text

add numberOfLines
properly apply container styles

handle both selection and expand press events in alt text

far better implementation

revert link changes

revert lightbox changes for now

fix file name
This commit is contained in:
Hailey
2024-01-15 03:12:43 -08:00
parent ec8c05f3f0
commit aa96bba066
23 changed files with 413 additions and 490 deletions
+1 -9
View File
@@ -35,11 +35,7 @@ podspec inside of this directory.
## Technical
Whenever we use the SelectableText component, we then loop over each of the children (or just the text if the child
is typeof Text). We create "segments" of text based on this and encode them to JSON* to pass to the native side.
/* Expo Modules doesn't currently support passing an array over the bridge, but even if it did, it would be parsing
sending JSON over the bridge anyway, so this is not an issue. I want to make sure this is the case (on expo support) but
regardless there's no real perf concern.
is typeof Text). We create "segments" of text based on this.
The native side renders a *single* UITextView and adds each of the text segments to the view. Using NSAttributedStrings,
we can add the styles for each segment individually (falling back to the root styles).
@@ -54,7 +50,3 @@ As such, we create a gesture recognizer for the entire UITextView. The recognize
the press, determines which segment it is, and sends an `onTextPress` event to the JS side along with the index of the
segment. On the JS side, we have our array of segments (that included the `onPress` event from the `Text`/`SelectableText`)
that we can now call based on the index from the native event.
We can't reliably modify the size of the view's container on the native side (*don't take my word for this, I might be -
and likely am - wrong about this). Therefore, we send an `onTextLayout` event to the JS thread that we can use to resize
the container's height based on the required height to display the text. This only gets called once per text update.
@@ -1,6 +1,6 @@
{
"platforms": ["ios"],
"ios": {
"modules": ["ExpoSelectableTextModule"]
"modules": ["ExpoUITextViewModule", "ExpoUITextViewChildModule"]
}
}
+2 -2
View File
@@ -1,4 +1,4 @@
import ExpoSelectableTextView from './src/ExpoSelectableTextView'
import {ExpoProTextViewProps} from './src/ExpoSelectableText.types'
import ExpoSelectableTextView from './src/ExpoUITextView'
import {ExpoProTextViewProps} from './src/ExpoUITextView.types'
export {ExpoSelectableTextView as SelectableText, type ExpoProTextViewProps}
@@ -1,36 +0,0 @@
import ExpoModulesCore
public class ExpoSelectableTextModule: Module {
public func definition() -> ModuleDefinition {
Name("ExpoSelectableText")
View(ExpoSelectableTextView.self) {
Events("onTextPress", "onTextLongPress", "onTextLayout")
Prop("segments") { (view: ExpoSelectableTextView, prop: String) in
// Convert the JSON to segments
if let data = prop.data(using: .utf8) {
if let segments = try? JSONDecoder().decode(TextSegments.self, from: data) {
view.segments = segments.segments
}
}
}
Prop("rootStyle") { (view: ExpoSelectableTextView, prop: String) in
if let data = prop.data(using: .utf8) {
if let style = try? JSONDecoder().decode(TextStyle.self, from: data) {
view.style = style
}
}
}
Prop("selectable") { (view: ExpoSelectableTextView, prop: Bool) in
view.textView.isSelectable = prop
}
Prop("children") { (view: ExpoSelectableTextView, prop: JavaScriptValue) in
print(prop)
}
}
}
}
@@ -1,147 +0,0 @@
import ExpoModulesCore
class ExpoSelectableTextView: ExpoView {
var textView: UITextView
var segments: Array<TextSegment> = [] {
didSet {
// We don't want to set the text if the root style has not been set yet
self.setText()
}
}
var style: TextStyle? {
didSet {
// If the text has not been set and there are segments, set the text
self.setText()
}
}
let onTextPress = EventDispatcher()
let onTextLongPress = EventDispatcher()
let onTextLayout = EventDispatcher()
public required init(appContext: AppContext? = nil) {
if #available(iOS 16.0, *) {
textView = UITextView(usingTextLayoutManager: false)
} else {
textView = UITextView()
}
super.init(appContext: appContext)
// Configure default appearance
textView.scrollsToTop = false
textView.isEditable = false
textView.isScrollEnabled = false
textView.backgroundColor = .clear
// Remove all of the padding from the view
textView.textContainerInset = UIEdgeInsets.zero
textView.textContainer.lineFragmentPadding = 0
// Add the text view to the root view
self.addSubview(textView)
// Configure the press recognizer
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(callOnPress(_:)))
textView.addGestureRecognizer(tapGestureRecognizer)
}
override func layoutSubviews() -> Void {
// Set the textView's frame on layout
self.setSize()
}
@IBAction func callOnPress(_ sender: UITapGestureRecognizer) -> Void {
if let segment = getPressedSegment(sender), segment.handlePress {
if textView.selectedTextRange == nil {
onTextPress([
"index": segment.index
])
} else {
// Clear the selected text range if we are not pressing on a link
textView.selectedTextRange = nil
}
}
}
func setSize() -> Void {
// Figure out the height of our text and create a CGRect
let maxWidth = bounds.width
let sizeThatFits = textView.sizeThatFits(CGSize(width: maxWidth, height: CGFloat(MAXFLOAT)))
let size = CGSize(width: maxWidth, height: sizeThatFits.height)
textView.frame.size = size
self.onTextLayout([
"height": sizeThatFits.height,
"width": maxWidth
])
}
func setText() -> Void {
let finalAttributedString = NSMutableAttributedString()
self.segments.forEach { segment in
// Set some generic attributes that don't need ranges
let attributes: [NSAttributedString.Key:Any] = [
.font: UIFont.systemFont(ofSize: segment.style?.fontSize ?? self.style?.fontSize ?? 12.0, weight: segment.style?.fontWeight?.toFontWeight() ?? self.style?.fontWeight?.toFontWeight() ?? .regular),
.foregroundColor: ExpoSelectableTextUtil.hexToUIColor(hex: segment.style?.color),
]
// Create the attributed string with the generic attributes
let string = NSMutableAttributedString(string: segment.text, attributes: attributes)
// Set the paragraph style attributes if necessary
if let lineHeight = segment.style?.lineHeight {
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.minimumLineHeight = lineHeight
paragraphStyle.maximumLineHeight = lineHeight
string.addAttribute(NSAttributedString.Key.paragraphStyle, value: paragraphStyle, range: NSMakeRange(0, string.length))
}
if let textDecorationLine = segment.style?.textDecorationLine {
if textDecorationLine == .underline || textDecorationLine == .underlineLineThrough {
string.addAttribute(NSAttributedString.Key.underlineStyle, value: NSUnderlineStyle.single.rawValue, range: NSMakeRange(0, string.length))
}
if textDecorationLine == .lineThrough || textDecorationLine == .underlineLineThrough {
string.addAttribute(NSAttributedString.Key.strikethroughStyle, value: NSUnderlineStyle.single.rawValue, range: NSMakeRange(0, string.length))
}
}
finalAttributedString.append(string)
}
textView.attributedText = finalAttributedString
textView.selectedTextRange = nil
self.setNeedsLayout()
}
func getPressedSegment(_ sender: UITapGestureRecognizer) -> TextSegment? {
let layoutManager = textView.layoutManager
var location = sender.location(in: textView)
// Remove the padding
location.x -= textView.textContainerInset.left
location.y -= textView.textContainerInset.top
// Get the index of the char
let charIndex = layoutManager.characterIndex(for: location, in: textView.textContainer, fractionOfDistanceBetweenInsertionPoints: nil)
let text = textView.attributedText.string
var foundSegment: TextSegment?
// Check each segment
self.segments.forEach { segment in
let range = text.range(of: segment.text)
// Figure out the bounds
if let lowerBound = range?.lowerBound, let upperBound = range?.upperBound {
if charIndex >= lowerBound.utf16Offset(in: text), charIndex <= upperBound.utf16Offset(in: text) {
foundSegment = segment
}
}
}
return foundSegment
}
}
@@ -1,8 +1,8 @@
Pod::Spec.new do |s|
s.name = 'ExpoSelectableText'
s.name = 'ExpoUITextView'
s.version = '1.0.0'
s.summary = 'Simple wrapper for RN Text to use UITextView instead of UILabel'
s.description = 'Simple wrapper for RN Text to use UITextView instead of UILabel'
s.summary = 'Drop in replacement for RN Text that uses UITextView'
s.description = 'Drop in replacement for RN Text that uses UITextView'
s.author = ''
s.homepage = 'https://github.com/bluesky-social/social-app/modules/expo-selectable-text'
s.platform = :ios, '13.0'
@@ -0,0 +1,192 @@
import ExpoModulesCore
class ExpoUITextView: ExpoView {
var textView: UITextView
var textChildren: [ExpoUITextViewChild] = []
var tapGestureRecognizer: UITapGestureRecognizer?
public required init(appContext: AppContext? = nil) {
if #available(iOS 16.0, *) {
textView = UITextView(usingTextLayoutManager: false)
} else {
textView = UITextView()
}
// Configure default appearance
textView.scrollsToTop = false
textView.isEditable = false
textView.isScrollEnabled = false
textView.backgroundColor = .clear
textView.textContainer.lineBreakMode = .byTruncatingTail
textView.isSelectable = true
// Remove all of the padding from the view
textView.textContainerInset = UIEdgeInsets.zero
textView.textContainer.lineFragmentPadding = 0
super.init(appContext: appContext)
addSubview(textView)
// Configure the tap gesture recognizer
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(callOnPress(_:)))
tapGestureRecognizer.isEnabled = true
self.tapGestureRecognizer = tapGestureRecognizer
textView.addGestureRecognizer(tapGestureRecognizer)
}
// Update children whenever new react subviews are added
override func insertReactSubview(_ subview: UIView!, at atIndex: Int) {
if subview.isKind(of: ExpoUITextViewChild.self) {
insertSubview(subview, at: atIndex)
self.getTextChildren()
}
}
// Do the same whenever subviews are removed
override func removeReactSubview(_ subview: UIView!) {
if subview.isKind(of: ExpoUITextViewChild.self) {
subview.removeFromSuperview()
}
}
override func reactSubviews() -> [UIView]! {
return subviews
}
override func layoutSubviews() {
// Get the width from the bounds
let maxWidth = bounds.width
// Calculate the size of the text
let sizeThatFits = textView.sizeThatFits(CGSize(width: maxWidth, height: CGFloat(MAXFLOAT)))
let size = CGSize(width: maxWidth, height: sizeThatFits.height)
// Set the textview's frame
textView.frame.size = size
self.appContext?.reactBridge?.uiManager.setSize(size, for: self)
}
@IBAction func callOnPress(_ sender: UITapGestureRecognizer) -> Void {
// If we find a child, then call onPress
if let child = getPressed(sender) {
if textView.selectedTextRange == nil {
child.onTextPress()
} else {
// Clear the selected text range if we are not pressing on a link
textView.selectedTextRange = nil
}
}
}
// Try to get the pressed segment
func getPressed(_ sender: UITapGestureRecognizer) -> ExpoUITextViewChild? {
let layoutManager = textView.layoutManager
var location = sender.location(in: textView)
// Remove the padding
location.x -= textView.textContainerInset.left
location.y -= textView.textContainerInset.top
// Get the index of the char
let charIndex = layoutManager.characterIndex(
for: location,
in: textView.textContainer,
fractionOfDistanceBetweenInsertionPoints: nil
)
let text = textView.attributedText.string
var foundChild: ExpoUITextViewChild?
// Check each segment
self.textChildren.forEach { child in
let range = text.range(of: child.text ?? "")
// Figure out the bounds
if let lowerBound = range?.lowerBound, let upperBound = range?.upperBound {
if charIndex >= lowerBound.utf16Offset(in: text), charIndex <= upperBound.utf16Offset(in: text) {
foundChild = child
}
}
}
return foundChild
}
// Get the children. Always use getTextChildren() so that we ensure the correct order of views
func getTextChildren() -> Void {
var children: [ExpoUITextViewChild] = []
self.reactSubviews().forEach { view in
if view.isKind(of: ExpoUITextViewChild.self) {
children.append(view as! ExpoUITextViewChild)
}
}
// Save the children for our onPress handler
self.textChildren = children
// Update the UITextView with the styled text
self.setText()
}
func setText() -> Void {
// Create an attributed string to store each of the segments
let finalAttributedString = NSMutableAttributedString()
self.textChildren.forEach { child in
// If we don't have any text in this child, move to the next one
guard let text = child.text else {
return
}
// Set some generic attributes that don't need ranges
let attributes: [NSAttributedString.Key:Any] = [
.font: UIFont.systemFont(
ofSize: child.style?.fontSize ?? 12.0,
weight: child.style?.fontWeight?.toFontWeight() ?? .regular
),
.foregroundColor: TextUtil.hexToUIColor(hex: child.style?.color),
]
// Create the attributed string with the generic attributes
let string = NSMutableAttributedString(string: text, attributes: attributes)
// Set the paragraph style attributes if necessary
if let lineHeight = child.style?.lineHeight {
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.minimumLineHeight = lineHeight
paragraphStyle.maximumLineHeight = lineHeight
string.addAttribute(
NSAttributedString.Key.paragraphStyle,
value: paragraphStyle,
range: NSMakeRange(0, string.length)
)
}
if let textDecorationLine = child.style?.textDecorationLine {
if textDecorationLine == .underline || textDecorationLine == .underlineLineThrough {
string.addAttribute(
NSAttributedString.Key.underlineStyle,
value: NSUnderlineStyle.single.rawValue,
range: NSMakeRange(0, string.length)
)
}
if textDecorationLine == .lineThrough || textDecorationLine == .underlineLineThrough {
string.addAttribute(
NSAttributedString.Key.strikethroughStyle,
value: NSUnderlineStyle.single.rawValue,
range: NSMakeRange(0, string.length)
)
}
}
finalAttributedString.append(string)
}
textView.attributedText = finalAttributedString
textView.selectedTextRange = nil
self.setNeedsLayout()
}
}
@@ -0,0 +1,7 @@
import ExpoModulesCore
class ExpoUITextViewChild: ExpoView {
var text: String?
var style: TextStyle?
let onTextPress = EventDispatcher()
}
@@ -0,0 +1,32 @@
import ExpoModulesCore
public class ExpoUITextViewModule: Module {
public func definition() -> ModuleDefinition {
Name("ExpoSelectableText")
View(ExpoUITextView.self) {
}
}
}
/**
Children should have parity with React Native text props. The difference is that we will use the text prop for the text rather than getting it from children.
*/
public class ExpoUITextViewChildModule: Module {
public func definition() -> ModuleDefinition {
Name("ExpoTextChild")
View(ExpoUITextViewChild.self) {
Events("onTextPress")
Prop("text") { (view: ExpoUITextViewChild, prop: String) in
view.text = prop
}
Prop("textStyle") { (view: ExpoUITextViewChild, prop: TextStyle) in
view.style = prop
}
}
}
}
@@ -1,13 +1,15 @@
import ExpoModulesCore
struct TextSegments: Decodable {
let segments: Array<TextSegment>
struct TextSegments: Record {
@Field
var segments: Array<TextSegment>
}
struct TextSegment: Decodable {
let index: Int
let text: String
let style: TextStyle?
let handlePress: Bool
let handleLongPress: Bool
struct TextSegment: Record {
@Field
var index: Int
@Field
var text: String
@Field
var style: TextStyle?
}
@@ -1,16 +1,29 @@
struct TextStyle: Decodable {
import ExpoModulesCore
struct TextStyle: Record {
@Field
var color: String? = "black"
@Field
var fontSize: CGFloat? = 12
@Field
var fontStyle: String? = "normal"
@Field
var fontWeight: FontWeight?
@Field
var letterSpacing: Double?
@Field
var textAlign: String? = "auto"
@Field
var lineHeight: Double?
@Field
var textDecorationLine: TextDecorationLine?
@Field
var flex: Int?
@Field
var pointerEvents: String?
}
enum FontWeight: String, Decodable {
enum FontWeight: String, Enumerable {
case bold
case normal
case one = "100"
@@ -51,10 +64,9 @@ enum FontWeight: String, Decodable {
}
}
enum TextDecorationLine: String, Decodable {
enum TextDecorationLine: String, Enumerable {
case underline
case lineThrough = "line-through"
case underlineLineThrough = "underline line-through"
case normal
}
@@ -1,4 +1,4 @@
public class ExpoSelectableTextUtil {
public class TextUtil {
public static func hexToUIColor(hex: String?) -> UIColor {
guard let hex else {
return UIColor.black
@@ -1,44 +0,0 @@
import React from 'react'
import {TextStyle, ViewStyle} from 'react-native'
interface ExpoProTextViewCommonProps {
selectable?: boolean
}
export interface ExpoProTextViewProps extends ExpoProTextViewCommonProps {
children: React.ReactNode
style?: TextStyle
onPress?: () => void
onLongPress?: () => void
}
export interface ExpoProTextNativeViewProps extends ExpoProTextViewCommonProps {
segments: string
textStyle?: TextStyle
onTextPress?: (event: ExpoProTextPressEvent) => void
onTextLongPress?: (event: ExpoProTextPressEvent) => void
onTextLayout?: (event: ExpoProTextLayoutEvent) => void
disableLongPress: boolean
style: ViewStyle
rootStyle?: string
}
export interface ExpoProTextLayoutEvent {
nativeEvent: {
height: number
}
}
export interface ExpoProTextPressEvent {
nativeEvent: {
index: number
}
}
export interface ExpoProTextSegment {
index: number
text: string
style?: TextStyle
handlePress?: boolean
handleLongPress?: boolean
}
@@ -1,159 +0,0 @@
import {requireNativeViewManager} from 'expo-modules-core'
import * as React from 'react'
import {
ExpoProTextLayoutEvent,
ExpoProTextNativeViewProps,
ExpoProTextPressEvent,
ExpoProTextSegment,
ExpoProTextViewProps,
} from './ExpoSelectableText.types'
import {StyleSheet, View} from 'react-native'
import {onTextLinkPress, TextLink} from 'view/com/util/Link'
import {useNavigation} from '@react-navigation/native'
import {NavigationProp} from 'lib/routes/types'
import {useModalControls} from 'state/modals'
const NativeView: React.ComponentType<ExpoProTextNativeViewProps> =
requireNativeViewManager('ExpoSelectableText')
export default function ExpoSelectableTextView({
style,
children,
selectable = true,
onPress,
onLongPress,
}: ExpoProTextViewProps) {
// Dimensions based on the native view's text height
const [dims, setDims] = React.useState({height: 0})
// Needed for navigation on link presses
const navigation = useNavigation<NavigationProp>()
const {openModal, closeModal} = useModalControls()
// Store the callbacks for onPress and onLongPress events
const segmentPressCallbacks = React.useRef<
Array<{index: number; onPress: () => void}>
>([])
const segmentLongPressCallbacks = React.useRef<
Array<{index: number; onLongPress: () => void}>
>([])
// The root style, stringified
const rootStyle = React.useMemo(() => {
return style ? JSON.stringify(style) : undefined
}, [style])
// The text segments, stringified
const textSegments = React.useMemo(() => {
const segments: ExpoProTextSegment[] = []
for (const [index, child] of React.Children.toArray(children).entries()) {
// Most of our children will be strings. Simply add them to the segments array.
if (typeof child === 'string') {
segments.push({
index,
text: child,
style: style,
handlePress: onPress !== undefined,
handleLongPress: onLongPress !== undefined,
})
} else if (React.isValidElement(child)) {
// If it is a child, it is either a nested <Text> or a <TextLink>. Check if the child is a string or a <TextLink>
// If it's a <TextLink> we need to create on the onPress handler (it won't be created in the component since the
// component never actually gets rendered)
const {
children: innerChildren,
onLongPress: innerOnLongPress,
style: innerStyle,
text: innerText,
href,
navigationAction,
warnOnMismatchingLabel,
} = child.props
let innerOnPress = child.props.onPress
const type = (child as React.ReactElement<any>).type
if (typeof innerChildren === 'string' || type === TextLink) {
if (type === TextLink) {
// Set the onPress handler
innerOnPress = () => {
onTextLinkPress({
openModal,
closeModal,
text: innerText,
navigation,
href,
navigationAction,
warnOnMismatchingLabel,
})
}
}
// Add the segment to the array
segments.push({
index,
text: innerText ?? innerChildren,
style: StyleSheet.flatten(innerStyle),
handlePress: innerOnPress !== undefined,
handleLongPress: innerOnLongPress !== undefined,
})
// If we have press events, push them in
if (innerOnPress !== undefined) {
segmentPressCallbacks.current.push({
index,
onPress: innerOnPress,
})
}
if (onLongPress !== undefined) {
segmentLongPressCallbacks.current.push({
index,
onLongPress: innerOnLongPress,
})
}
}
}
}
return segments
}, [children, closeModal, navigation, onLongPress, onPress, openModal, style])
const segmentsJson = React.useMemo(() => {
return JSON.stringify({segments: textSegments})
}, [textSegments])
const onTextLayout = React.useCallback((e: ExpoProTextLayoutEvent) => {
setDims({
height: e.nativeEvent.height,
})
}, [])
const onTextPress = React.useCallback(
(e: ExpoProTextPressEvent) => {
const onPressSegment =
segmentPressCallbacks.current.find(s => s.index === e.nativeEvent.index)
?.onPress ?? onPress
onPressSegment?.()
},
[onPress],
)
return (
<View style={[dims, {width: '100%'}]}>
<NativeView
segments={segmentsJson}
selectable={selectable}
onTextPress={onTextPress}
onTextLongPress={onLongPress}
onTextLayout={onTextLayout}
disableLongPress={onLongPress !== undefined}
style={{flex: 1}}
rootStyle={rootStyle}
/>
</View>
)
}
@@ -0,0 +1,79 @@
import React from 'react'
import {requireNativeViewManager} from 'expo-modules-core'
import {StyleSheet, ViewStyle} from 'react-native'
import {
ExpoUITextViewChildNativeProps,
ExpoUITextViewNativeProps,
ExpoUITextViewProps,
} from './ExpoUITextView.types'
const NativeView: React.ComponentType<ExpoUITextViewNativeProps> =
requireNativeViewManager('ExpoSelectableText')
const NativeViewChild: React.ComponentType<ExpoUITextViewChildNativeProps> =
requireNativeViewManager('ExpoTextChild')
const TextAncestorContext = React.createContext<[boolean, ViewStyle]>([
false,
StyleSheet.create({}),
])
const useTextAncestorContext = () => React.useContext(TextAncestorContext)
export default function ExpoUITextView({
style,
children,
onPress,
...rest
}: ExpoUITextViewProps) {
const [isAncestor, rootStyle] = useTextAncestorContext()
// Flatten the styles, and apply the root styles when needed
const flattenedStyle = React.useMemo(
() => StyleSheet.flatten([rootStyle, style]),
[rootStyle, style],
)
if (!isAncestor) {
return (
<TextAncestorContext.Provider value={[true, flattenedStyle]}>
<NativeView style={{flex: 1}}>
{React.Children.toArray(children).map((c, index) => {
if (React.isValidElement(c)) {
return c
} else if (typeof c === 'string') {
return (
<NativeViewChild
key={index}
textStyle={flattenedStyle}
text={c}
onTextPress={onPress}
{...rest}
/>
)
}
})}
</NativeView>
</TextAncestorContext.Provider>
)
} else {
return (
<>
{React.Children.toArray(children).map((c, index) => {
if (React.isValidElement(c)) {
return c
} else if (typeof c === 'string') {
return (
<NativeViewChild
key={index}
textStyle={flattenedStyle}
text={c}
onTextPress={onPress}
{...rest}
/>
)
}
})}
</>
)
}
}
@@ -0,0 +1,16 @@
import React from 'react'
import {TextProps, TextStyle, ViewStyle} from 'react-native'
export interface ExpoUITextViewProps extends TextProps {}
export interface ExpoUITextViewNativeProps {
children: React.ReactNode
style: ViewStyle
}
export interface ExpoUITextViewChildNativeProps extends ExpoUITextViewProps {
text: string
textStyle: TextStyle
onTextPress?: (...args: any[]) => void
onTextLongPress?: (...args: any[]) => void
}
+2 -1
View File
@@ -110,7 +110,8 @@ function LightboxFooter({imageIndex}: {imageIndex: number}) {
accessibilityRole="button">
<Text
style={[s.gray3, styles.footerText]}
numberOfLines={isAltExpanded ? undefined : 3}>
numberOfLines={isAltExpanded ? undefined : 3}
selectable>
{altText}
</Text>
</Pressable>
+2 -1
View File
@@ -39,7 +39,7 @@ import {
usePreferencesQuery,
} from '#/state/queries/preferences'
import {useSession} from '#/state/session'
import {isNative} from '#/platform/detection'
import {isAndroid, isNative} from '#/platform/detection'
import {logger} from '#/logger'
const MAINTAIN_VISIBLE_CONTENT_POSITION = {minIndexForVisible: 2}
@@ -358,6 +358,7 @@ function PostThreadLoaded({
style={s.hContentRegion}
// @ts-ignore our .web version only -prf
desktopFixedHeight
removeClippedSubviews={isAndroid ? false : undefined}
/>
)
}
+2 -3
View File
@@ -248,10 +248,9 @@ let PostThreadItemLoaded = ({
</View>
)}
<Link
<View
testID={`postThreadItem-by-${post.author.handle}`}
style={[styles.outer, styles.outerHighlighted, pal.border, pal.view]}
noFeedback
accessible={false}>
<PostSandboxWarning />
<View style={styles.layout}>
@@ -446,7 +445,7 @@ let PostThreadItemLoaded = ({
/>
</View>
</View>
</Link>
</View>
<WhoCanReply post={post} />
</>
)
+33 -57
View File
@@ -70,14 +70,14 @@ export const Link = memo(function Link({
const onPress = React.useCallback(
(e?: Event) => {
if (typeof href === 'string') {
return onTextLinkPress({
return onPressInner(
closeModal,
navigation,
href: sanitizeUrl(href),
sanitizeUrl(href),
navigationAction,
openLink,
e,
})
)
}
},
[closeModal, navigation, navigationAction, href, openLink],
@@ -182,28 +182,40 @@ export const TextLink = memo(function TextLink({
props.onPress = React.useCallback(
(e?: Event) => {
onTextLinkPress({
onPress,
e,
openModal,
const requiresWarning =
warnOnMismatchingLabel &&
linkRequiresWarning(href, typeof text === 'string' ? text : '')
if (requiresWarning) {
e?.preventDefault?.()
openModal({
name: 'link-warning',
text: typeof text === 'string' ? text : '',
href,
})
}
if (onPress) {
e?.preventDefault?.()
// @ts-ignore function signature differs by platform -prf
return onPress()
}
return onPressInner(
closeModal,
navigation,
href,
text,
sanitizeUrl(href),
navigationAction,
warnOnMismatchingLabel,
openLink,
})
e,
)
},
[
onPress,
openModal,
closeModal,
openModal,
navigation,
href,
text,
navigationAction,
warnOnMismatchingLabel,
navigationAction,
openLink,
],
)
@@ -294,9 +306,6 @@ export const TextLinkOnWebOnly = memo(function DesktopWebTextLink({
)
})
// Moving all of this logic into a separate function. Becuase we need to be able to use this function from
// SelectableText, it's easier to move it outside the component instead of duplicating logic
// NOTE
// we can't use the onPress given by useLinkProps because it will
// match most paths to the HomeTab routes while we actually want to
@@ -308,47 +317,14 @@ export const TextLinkOnWebOnly = memo(function DesktopWebTextLink({
// this method copies from the onPress implementation but adds our
// needed customizations
// -prf
export const onTextLinkPress = ({
onPress,
e,
openModal,
closeModal,
navigation,
href,
text,
navigationAction = 'push',
warnOnMismatchingLabel,
openLink,
}: {
onPress?: (e: GestureResponderEvent) => void
e?: Event
openModal?: any
closeModal: any
navigation: NavigationProp
href: string
text?: any
navigationAction?: 'push' | 'replace' | 'navigate'
warnOnMismatchingLabel?: boolean
openLink: (href: string) => void
}) => {
const requiresWarning =
warnOnMismatchingLabel &&
linkRequiresWarning(href, typeof text === 'string' ? text : '')
if (requiresWarning) {
e?.preventDefault?.()
openModal({
name: 'link-warning',
text: typeof text === 'string' ? text : '',
href,
})
}
if (onPress) {
e?.preventDefault?.()
// @ts-ignore function signature differs by platform -prf
onPress()
return
}
function onPressInner(
closeModal = () => {},
navigation: NavigationProp,
href: string,
navigationAction: 'push' | 'replace' | 'navigate' = 'push',
openLink: (href: string) => void,
e?: Event,
) {
let shouldHandle = false
const isLeftClick =
// @ts-ignore Web only -prf
+7 -1
View File
@@ -44,7 +44,11 @@ export function RichText({
}
return (
// @ts-ignore web only -prf
<Text testID={testID} style={[style, pal.text]} dataSet={WORD_WRAP}>
<Text
testID={testID}
style={[style, pal.text]}
dataSet={WORD_WRAP}
selectable={selectable}>
{text}
</Text>
)
@@ -82,6 +86,7 @@ export function RichText({
href={`/profile/${mention.did}`}
style={[style, lineHeightStyle, pal.link, {pointerEvents: 'auto'}]}
dataSet={WORD_WRAP}
selectable={selectable}
/>,
)
} else if (link && AppBskyRichtextFacet.validateLink(link).success) {
@@ -94,6 +99,7 @@ export function RichText({
style={[style, lineHeightStyle, pal.link, {pointerEvents: 'auto'}]}
dataSet={WORD_WRAP}
warnOnMismatchingLabel
selectable={selectable}
/>,
)
} else {
+7 -13
View File
@@ -1,9 +1,9 @@
import React from 'react'
import {StyleSheet, Text as RNText, TextProps} from 'react-native'
import {Text as RNText, TextProps} from 'react-native'
import {s, lh} from 'lib/styles'
import {useTheme, TypographyVariant} from 'lib/ThemeContext'
import {SelectableText} from '../../../../../modules/expo-selectable-text'
import {isIOS} from 'platform/detection'
import ExpoSelectableTextView from '../../../../../modules/expo-selectable-text/src/ExpoUITextView'
export type CustomTextProps = TextProps & {
type?: TypographyVariant
@@ -27,20 +27,13 @@ export function Text({
const typography = theme.typography[type]
const lineHeightStyle = lineHeight ? lh(theme, type, lineHeight) : undefined
// if (false) {
// TODO remove
if (selectable && isIOS) {
return (
<SelectableText
selectable
style={StyleSheet.flatten([
s.black,
typography,
lineHeightStyle,
style,
])}>
<ExpoSelectableTextView
style={[s.black, typography, lineHeightStyle, style]}
{...props}>
{children}
</SelectableText>
</ExpoSelectableTextView>
)
}
@@ -49,6 +42,7 @@ export function Text({
style={[s.black, typography, lineHeightStyle, style]}
// @ts-ignore web only -esb
dataSet={Object.assign({tooltip: title}, dataSet || {})}
selectable={selectable}
{...props}>
{children}
</RNText>