From aa96bba0664d14f12ee742739c70847407062f35 Mon Sep 17 00:00:00 2001 From: Hailey Date: Mon, 15 Jan 2024 03:12:43 -0800 Subject: [PATCH] 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 --- modules/expo-selectable-text/README.md | 10 +- .../expo-module.config.json | 2 +- modules/expo-selectable-text/index.ts | 4 +- .../ios/ExpoSelectableTextModule.swift | 36 ---- .../ios/ExpoSelectableTextView.swift | 147 -------------- ...bleText.podspec => ExpoUITextView.podspec} | 6 +- .../ios/ExpoUITextView.swift | 192 ++++++++++++++++++ .../ios/ExpoUITextViewChild.swift | 7 + .../ios/ExpoUITextViewModule.swift | 32 +++ .../ios/TextSegment.swift | 18 +- .../expo-selectable-text/ios/TextStyle.swift | 20 +- ...electableTextUtil.swift => TextUtil.swift} | 2 +- .../src/ExpoSelectableText.types.ts | 44 ---- .../src/ExpoSelectableTextView.tsx | 159 --------------- .../src/ExpoUITextView.tsx | 79 +++++++ .../src/ExpoUITextView.types.ts | 16 ++ ...eTextModule.ts => ExpoUITextViewModule.ts} | 0 src/view/com/lightbox/Lightbox.tsx | 3 +- src/view/com/post-thread/PostThread.tsx | 3 +- src/view/com/post-thread/PostThreadItem.tsx | 5 +- src/view/com/util/Link.tsx | 90 +++----- src/view/com/util/text/RichText.tsx | 8 +- src/view/com/util/text/Text.tsx | 20 +- 23 files changed, 413 insertions(+), 490 deletions(-) delete mode 100644 modules/expo-selectable-text/ios/ExpoSelectableTextModule.swift delete mode 100644 modules/expo-selectable-text/ios/ExpoSelectableTextView.swift rename modules/expo-selectable-text/ios/{ExpoSelectableText.podspec => ExpoUITextView.podspec} (69%) create mode 100644 modules/expo-selectable-text/ios/ExpoUITextView.swift create mode 100644 modules/expo-selectable-text/ios/ExpoUITextViewChild.swift create mode 100644 modules/expo-selectable-text/ios/ExpoUITextViewModule.swift rename modules/expo-selectable-text/ios/{ExpoSelectableTextUtil.swift => TextUtil.swift} (99%) delete mode 100644 modules/expo-selectable-text/src/ExpoSelectableText.types.ts delete mode 100644 modules/expo-selectable-text/src/ExpoSelectableTextView.tsx create mode 100644 modules/expo-selectable-text/src/ExpoUITextView.tsx create mode 100644 modules/expo-selectable-text/src/ExpoUITextView.types.ts rename modules/expo-selectable-text/src/{ExpoSelectableTextModule.ts => ExpoUITextViewModule.ts} (100%) diff --git a/modules/expo-selectable-text/README.md b/modules/expo-selectable-text/README.md index c63594630d..a100867e51 100644 --- a/modules/expo-selectable-text/README.md +++ b/modules/expo-selectable-text/README.md @@ -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. diff --git a/modules/expo-selectable-text/expo-module.config.json b/modules/expo-selectable-text/expo-module.config.json index 343fbb79cb..5a6335eb82 100644 --- a/modules/expo-selectable-text/expo-module.config.json +++ b/modules/expo-selectable-text/expo-module.config.json @@ -1,6 +1,6 @@ { "platforms": ["ios"], "ios": { - "modules": ["ExpoSelectableTextModule"] + "modules": ["ExpoUITextViewModule", "ExpoUITextViewChildModule"] } } diff --git a/modules/expo-selectable-text/index.ts b/modules/expo-selectable-text/index.ts index 01075393ee..b3e74bda79 100644 --- a/modules/expo-selectable-text/index.ts +++ b/modules/expo-selectable-text/index.ts @@ -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} diff --git a/modules/expo-selectable-text/ios/ExpoSelectableTextModule.swift b/modules/expo-selectable-text/ios/ExpoSelectableTextModule.swift deleted file mode 100644 index 2050e3e9c4..0000000000 --- a/modules/expo-selectable-text/ios/ExpoSelectableTextModule.swift +++ /dev/null @@ -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) - } - } - } -} diff --git a/modules/expo-selectable-text/ios/ExpoSelectableTextView.swift b/modules/expo-selectable-text/ios/ExpoSelectableTextView.swift deleted file mode 100644 index 0fbc4dd441..0000000000 --- a/modules/expo-selectable-text/ios/ExpoSelectableTextView.swift +++ /dev/null @@ -1,147 +0,0 @@ -import ExpoModulesCore - -class ExpoSelectableTextView: ExpoView { - var textView: UITextView - var segments: Array = [] { - 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 - } -} diff --git a/modules/expo-selectable-text/ios/ExpoSelectableText.podspec b/modules/expo-selectable-text/ios/ExpoUITextView.podspec similarity index 69% rename from modules/expo-selectable-text/ios/ExpoSelectableText.podspec rename to modules/expo-selectable-text/ios/ExpoUITextView.podspec index dd25f6c62f..89c0fc9db9 100644 --- a/modules/expo-selectable-text/ios/ExpoSelectableText.podspec +++ b/modules/expo-selectable-text/ios/ExpoUITextView.podspec @@ -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' diff --git a/modules/expo-selectable-text/ios/ExpoUITextView.swift b/modules/expo-selectable-text/ios/ExpoUITextView.swift new file mode 100644 index 0000000000..000b14d8ef --- /dev/null +++ b/modules/expo-selectable-text/ios/ExpoUITextView.swift @@ -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() + } +} diff --git a/modules/expo-selectable-text/ios/ExpoUITextViewChild.swift b/modules/expo-selectable-text/ios/ExpoUITextViewChild.swift new file mode 100644 index 0000000000..d24c5b46b1 --- /dev/null +++ b/modules/expo-selectable-text/ios/ExpoUITextViewChild.swift @@ -0,0 +1,7 @@ +import ExpoModulesCore + +class ExpoUITextViewChild: ExpoView { + var text: String? + var style: TextStyle? + let onTextPress = EventDispatcher() +} diff --git a/modules/expo-selectable-text/ios/ExpoUITextViewModule.swift b/modules/expo-selectable-text/ios/ExpoUITextViewModule.swift new file mode 100644 index 0000000000..8e1ef09825 --- /dev/null +++ b/modules/expo-selectable-text/ios/ExpoUITextViewModule.swift @@ -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 + } + } + } +} diff --git a/modules/expo-selectable-text/ios/TextSegment.swift b/modules/expo-selectable-text/ios/TextSegment.swift index 8c410aaaab..3542cf73ea 100644 --- a/modules/expo-selectable-text/ios/TextSegment.swift +++ b/modules/expo-selectable-text/ios/TextSegment.swift @@ -1,13 +1,15 @@ import ExpoModulesCore -struct TextSegments: Decodable { - let segments: Array +struct TextSegments: Record { + @Field + var segments: Array } -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? } diff --git a/modules/expo-selectable-text/ios/TextStyle.swift b/modules/expo-selectable-text/ios/TextStyle.swift index e3ec6a01fa..9f751bccd6 100644 --- a/modules/expo-selectable-text/ios/TextStyle.swift +++ b/modules/expo-selectable-text/ios/TextStyle.swift @@ -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 } diff --git a/modules/expo-selectable-text/ios/ExpoSelectableTextUtil.swift b/modules/expo-selectable-text/ios/TextUtil.swift similarity index 99% rename from modules/expo-selectable-text/ios/ExpoSelectableTextUtil.swift rename to modules/expo-selectable-text/ios/TextUtil.swift index ad06434639..d83a6e4413 100644 --- a/modules/expo-selectable-text/ios/ExpoSelectableTextUtil.swift +++ b/modules/expo-selectable-text/ios/TextUtil.swift @@ -1,4 +1,4 @@ -public class ExpoSelectableTextUtil { +public class TextUtil { public static func hexToUIColor(hex: String?) -> UIColor { guard let hex else { return UIColor.black diff --git a/modules/expo-selectable-text/src/ExpoSelectableText.types.ts b/modules/expo-selectable-text/src/ExpoSelectableText.types.ts deleted file mode 100644 index 8dce1fd910..0000000000 --- a/modules/expo-selectable-text/src/ExpoSelectableText.types.ts +++ /dev/null @@ -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 -} diff --git a/modules/expo-selectable-text/src/ExpoSelectableTextView.tsx b/modules/expo-selectable-text/src/ExpoSelectableTextView.tsx deleted file mode 100644 index c2df72c98d..0000000000 --- a/modules/expo-selectable-text/src/ExpoSelectableTextView.tsx +++ /dev/null @@ -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 = - 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() - 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 or a . Check if the child is a string or a - // If it's a 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).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 ( - - - - ) -} diff --git a/modules/expo-selectable-text/src/ExpoUITextView.tsx b/modules/expo-selectable-text/src/ExpoUITextView.tsx new file mode 100644 index 0000000000..2201fffcd2 --- /dev/null +++ b/modules/expo-selectable-text/src/ExpoUITextView.tsx @@ -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 = + requireNativeViewManager('ExpoSelectableText') + +const NativeViewChild: React.ComponentType = + 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 ( + + + {React.Children.toArray(children).map((c, index) => { + if (React.isValidElement(c)) { + return c + } else if (typeof c === 'string') { + return ( + + ) + } + })} + + + ) + } else { + return ( + <> + {React.Children.toArray(children).map((c, index) => { + if (React.isValidElement(c)) { + return c + } else if (typeof c === 'string') { + return ( + + ) + } + })} + + ) + } +} diff --git a/modules/expo-selectable-text/src/ExpoUITextView.types.ts b/modules/expo-selectable-text/src/ExpoUITextView.types.ts new file mode 100644 index 0000000000..437283578b --- /dev/null +++ b/modules/expo-selectable-text/src/ExpoUITextView.types.ts @@ -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 +} diff --git a/modules/expo-selectable-text/src/ExpoSelectableTextModule.ts b/modules/expo-selectable-text/src/ExpoUITextViewModule.ts similarity index 100% rename from modules/expo-selectable-text/src/ExpoSelectableTextModule.ts rename to modules/expo-selectable-text/src/ExpoUITextViewModule.ts diff --git a/src/view/com/lightbox/Lightbox.tsx b/src/view/com/lightbox/Lightbox.tsx index 2271bb9fb1..988c445676 100644 --- a/src/view/com/lightbox/Lightbox.tsx +++ b/src/view/com/lightbox/Lightbox.tsx @@ -110,7 +110,8 @@ function LightboxFooter({imageIndex}: {imageIndex: number}) { accessibilityRole="button"> + numberOfLines={isAltExpanded ? undefined : 3} + selectable> {altText} diff --git a/src/view/com/post-thread/PostThread.tsx b/src/view/com/post-thread/PostThread.tsx index cb7fd3f410..3f4c14d319 100644 --- a/src/view/com/post-thread/PostThread.tsx +++ b/src/view/com/post-thread/PostThread.tsx @@ -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} /> ) } diff --git a/src/view/com/post-thread/PostThreadItem.tsx b/src/view/com/post-thread/PostThreadItem.tsx index 31f8cbbe91..5487f536d6 100644 --- a/src/view/com/post-thread/PostThreadItem.tsx +++ b/src/view/com/post-thread/PostThreadItem.tsx @@ -248,10 +248,9 @@ let PostThreadItemLoaded = ({ )} - @@ -446,7 +445,7 @@ let PostThreadItemLoaded = ({ /> - + ) diff --git a/src/view/com/util/Link.tsx b/src/view/com/util/Link.tsx index 1c9e73812c..4f898767d1 100644 --- a/src/view/com/util/Link.tsx +++ b/src/view/com/util/Link.tsx @@ -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 diff --git a/src/view/com/util/text/RichText.tsx b/src/view/com/util/text/RichText.tsx index 0a8d479936..910f5774be 100644 --- a/src/view/com/util/text/RichText.tsx +++ b/src/view/com/util/text/RichText.tsx @@ -44,7 +44,11 @@ export function RichText({ } return ( // @ts-ignore web only -prf - + {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 { diff --git a/src/view/com/util/text/Text.tsx b/src/view/com/util/text/Text.tsx index 91b960c57f..fc8ba5fe5f 100644 --- a/src/view/com/util/text/Text.tsx +++ b/src/view/com/util/text/Text.tsx @@ -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 ( - + {children} - + ) } @@ -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}