Merge PR #2495 by haileyok

Squashed commit of the following:

commit 9d9c46ced116079add8ae1beaed854b38962d608
Author: Paul Frazee <pfrazee@gmail.com>
Date:   Tue Jan 23 14:12:32 2024 -0800

    Fix reference error on the web build

commit 1981621c5b6f2b63b3e3875b68721161487d7df0
Merge: cda4fe4a 0d9b6954
Author: Paul Frazee <pfrazee@gmail.com>
Date:   Tue Jan 23 12:43:51 2024 -0800

    Merge branch 'feat/selectable-text' of https://github.com/haileyok/social-app into haileyok-feat/selectable-text

commit 0d9b695447
Merge: 3c381f94 f1a7a571
Author: Hailey <153161762+haileyok@users.noreply.github.com>
Date:   Fri Jan 19 16:42:13 2024 -0800

    Merge branch 'main' into feat/selectable-text

commit 3c381f9470
Merge: f9510156 fb596e7f
Author: Hailey <153161762+haileyok@users.noreply.github.com>
Date:   Thu Jan 18 23:48:10 2024 -0800

    Merge branch 'main' into feat/selectable-text

commit f951015637
Author: Hailey <me@haileyok.com>
Date:   Thu Jan 18 23:46:25 2024 -0800

    update readme

commit aa9b8b06ed
Author: Hailey <me@haileyok.com>
Date:   Thu Jan 18 23:37:49 2024 -0800

    calculate line height

commit 9fe479630c
Author: Hailey <me@haileyok.com>
Date:   Thu Jan 18 23:19:31 2024 -0800

    improve height calculation, render on prop changes

commit 209caffa7d
Author: Hailey <me@haileyok.com>
Date:   Thu Jan 18 22:53:08 2024 -0800

    presses

commit 384c8ec3a8
Author: Hailey <me@haileyok.com>
Date:   Thu Jan 18 21:57:56 2024 -0800

    line break mode

commit adfcf05fe4
Author: Hailey <me@haileyok.com>
Date:   Thu Jan 18 21:50:21 2024 -0800

    onTextLayout event

commit e9ba104e6f
Author: Hailey <me@haileyok.com>
Date:   Thu Jan 18 21:34:35 2024 -0800

    better naming

commit e335f5ab7f
Author: Hailey <me@haileyok.com>
Date:   Thu Jan 18 21:31:38 2024 -0800

    remove android

commit 9e197934ba
Author: Hailey <me@haileyok.com>
Date:   Thu Jan 18 21:28:28 2024 -0800

    remove expo module

commit 99882c7e39
Author: Hailey <me@haileyok.com>
Date:   Thu Jan 18 21:27:43 2024 -0800

    handle presses

commit 18f818649e
Author: Hailey <me@haileyok.com>
Date:   Thu Jan 18 21:14:38 2024 -0800

    make use of rctshadowview

commit 7134e1106e
Author: Hailey <me@haileyok.com>
Date:   Wed Jan 17 20:38:39 2024 -0800

    stop unnecessary layouts, resize container before setting text

commit 340b84f053
Author: Hailey <me@haileyok.com>
Date:   Wed Jan 17 11:17:36 2024 -0800

    handle prop changes for both children and root views

commit d906fe4fcf
Author: Hailey <me@haileyok.com>
Date:   Tue Jan 16 18:42:22 2024 -0800

    handle onpress better

commit b6b0964168
Author: Hailey <me@haileyok.com>
Date:   Tue Jan 16 16:57:31 2024 -0800

    resolve animation issue, animate alt text expansion

commit daedd1f671
Author: Hailey <me@haileyok.com>
Date:   Tue Jan 16 15:47:24 2024 -0800

    move getChildren to didMoveToWindow

commit 87d44e4b57
Author: Hailey <me@haileyok.com>
Date:   Mon Jan 15 18:48:36 2024 -0800

    simplify getPressed

commit d92584bad7
Author: Hailey <me@haileyok.com>
Date:   Mon Jan 15 17:56:43 2024 -0800

    just more cleanup

commit d39f7a937d
Author: Hailey <me@haileyok.com>
Date:   Mon Jan 15 17:03:19 2024 -0800

    remove unnecessary property for gesture recognizer

commit a35513a1d2
Author: Hailey <me@haileyok.com>
Date:   Mon Jan 15 16:55:36 2024 -0800

    remove debug line

commit 788956aa01
Author: Hailey <me@haileyok.com>
Date:   Mon Jan 15 16:33:44 2024 -0800

    typo

commit a3ba6e7825
Author: Hailey <me@haileyok.com>
Date:   Mon Jan 15 13:42:25 2024 -0800

    make alt text selectable

commit e5472a13da
Author: Hailey <me@haileyok.com>
Date:   Mon Jan 15 05:27:15 2024 -0800

    re-render on numberOfLines change

commit 9f5b7602c1
Author: Hailey <me@haileyok.com>
Date:   Mon Jan 15 04:57:35 2024 -0800

    more implementations

commit aa96bba066
Author: Hailey <me@haileyok.com>
Date:   Mon Jan 15 03:12:43 2024 -0800

    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

commit ec8c05f3f0
Merge: 2435a252 12a0ceee
Author: Hailey <me@haileyok.com>
Date:   Fri Jan 12 23:41:10 2024 -0800

    Merge branch 'main' into feat/selectable-text

    # Conflicts:
    #	src/view/com/util/Link.tsx

commit 2435a25257
Author: Hailey <me@haileyok.com>
Date:   Fri Jan 12 23:30:13 2024 -0800

    cleanup

commit fdf75927f6
Author: Hailey <me@haileyok.com>
Date:   Fri Jan 12 23:25:23 2024 -0800

    remove debug

commit 36d8cd82ef
Author: Hailey <me@haileyok.com>
Date:   Fri Jan 12 23:25:17 2024 -0800

    reset text selection on text update

commit b8f7bc23c2
Author: Hailey <me@haileyok.com>
Date:   Fri Jan 12 23:24:43 2024 -0800

    use textkit 1

commit 5216464458
Author: Hailey <me@haileyok.com>
Date:   Fri Jan 12 22:50:15 2024 -0800

    properly handle link press events

commit 2802902c69
Author: Hailey <me@haileyok.com>
Date:   Fri Jan 12 22:49:47 2024 -0800

    modify Link so that we can create the TextLink press handler outside

commit 860610e63a
Author: Hailey <me@haileyok.com>
Date:   Fri Jan 12 19:17:51 2024 -0800

    always make sure we use the latest styles

commit 7f05d0141b
Author: Hailey <me@haileyok.com>
Date:   Fri Jan 12 16:57:08 2024 -0800

    update readme with tech info

commit b8318446a3
Author: Hailey <me@haileyok.com>
Date:   Fri Jan 12 16:34:35 2024 -0800

    remove all uitextview padding

commit 0f0b6aa131
Author: Hailey <me@haileyok.com>
Date:   Fri Jan 12 16:34:28 2024 -0800

    cleanup imports

commit c9f0064836
Author: Hailey <me@haileyok.com>
Date:   Fri Jan 12 16:18:08 2024 -0800

    update interface

commit 7dcac644ba
Author: Hailey <me@haileyok.com>
Date:   Fri Jan 12 16:13:49 2024 -0800

    remove useless struct

commit 5174744213
Author: Hailey <me@haileyok.com>
Date:   Fri Jan 12 16:13:34 2024 -0800

    adjust deps

commit ce8b9ed62b
Author: Hailey <me@haileyok.com>
Date:   Thu Jan 11 22:15:50 2024 -0800

    add readme, update info

commit 33c6e3b15c
Author: Hailey <me@haileyok.com>
Date:   Thu Jan 11 22:04:53 2024 -0800

    remove unnecessary android/web stuff

commit fbca531bdf
Author: Hailey <me@haileyok.com>
Date:   Thu Jan 11 22:02:30 2024 -0800

    simplify cast of string.index to int before i forget

commit 648552eafb
Author: Hailey <me@haileyok.com>
Date:   Thu Jan 11 02:01:20 2024 -0800

    wip

commit c6d2e54923
Author: Hailey <me@haileyok.com>
Date:   Thu Jan 11 00:38:47 2024 -0800

    selectable text experiment
This commit is contained in:
Paul Frazee
2024-01-23 14:14:46 -08:00
parent cda4fe4a7f
commit a2b58852e7
20 changed files with 694 additions and 24 deletions
+2 -2
View File
@@ -91,8 +91,8 @@ web-build/
# Android & iOS folders
android/
ios/
/android/
/ios/
# environment variables
.env
@@ -0,0 +1,61 @@
# React Native UITextView
Drop in replacement for `<Text>` that renders a `UITextView`, support selection and native translation features on iOS.
## Installation
In this project, no installation is required. The pod will be installed automatically during a `pod install`.
In another project, clone the repo and copy the `modules/react-native-ui-text-view` directory to your own project
directory. Afterward, run `pod install`.
## Usage
Replace the outermost `<Text>` with `<UITextView>`. Styles and press events should be handled the same way they would
with `<Text>`. Both `<UITextView>` and `<Text>` are supported as children of the root `<UITextView>`.
## Technical
React Native's `Text` component allows for "infinite" nesting of further `Text` components. To make a true "drop-in",
we want to do the same thing.
To achieve this, we first need to handle determining if we are dealing with an ancestor or root `UITextView` component.
We can implement similar logic to the `Text` component [see Text.js](https://github.com/facebook/react-native/blob/7f2529de7bc9ab1617eaf571e950d0717c3102a6/packages/react-native/Libraries/Text/Text.js).
We create a context that contains a boolean to tell us if we have already rendered the root `UITextView`. We also store
the root styles so that we can apply those styles if the ancestor `UITextView`s have not overwritten those styles.
All of our children are placed into `RNUITextView`, which is the main native view that will display the iOS `UITextView`.
We next map each child into the view. We have to be careful here to check if the child's `children` prop is a string. If
it is, that means we have encountered what was once an RN `Text` component. RN doesn't let us pass plain text as
children outside of `Text`, so we instead just pass the text into the `text` prop on `RNUITextViewChild`. We continue
down the tree, until we run out of children.
On the native side, we make use of the shadow view to calculate text container dimensions before the views are mounted.
We cannot simply set the `UITextView` text first, since React will not have properly measured the layout before this
occurs.
As for `Text` props, the following props are implemented:
- All accessibility props
- `allowFontScaling`
- `adjustsFontSizeToFit`
- `ellipsizeMode`
- `numberOfLines`
- `onLayout`
- `onPress`
- `onTextLayout`
- `selectable`
All `ViewStyle` props will apply to the root `UITextView`. Individual children will respect these `TextStyle` styles:
- `color`
- `fontSize`
- `fontStyle`
- `fontWeight`
- `fontVariant`
- `letterSpacing`
- `lineHeight`
- `textDecorationLine`
@@ -0,0 +1,3 @@
#import <React/RCTViewManager.h>
#import <React/RCTBridge.h>
#import <React/RCTBridge+Private.h>
@@ -0,0 +1,141 @@
class RNUITextView: UIView {
var textView: UITextView
@objc var numberOfLines: Int = 0 {
didSet {
textView.textContainer.maximumNumberOfLines = numberOfLines
}
}
@objc var selectable: Bool = true {
didSet {
textView.isSelectable = selectable
}
}
@objc var ellipsizeMode: String = "tail" {
didSet {
textView.textContainer.lineBreakMode = self.getLineBreakMode()
}
}
@objc var onTextLayout: RCTDirectEventBlock?
override init(frame: CGRect) {
if #available(iOS 16.0, *) {
textView = UITextView(usingTextLayoutManager: false)
} else {
textView = UITextView()
}
// Disable scrolling
textView.isScrollEnabled = false
// Remove all the padding
textView.textContainerInset = .zero
textView.textContainer.lineFragmentPadding = 0
// Remove other properties
textView.isEditable = false
textView.backgroundColor = .clear
// Init
super.init(frame: frame)
self.clipsToBounds = true
// Add the view
addSubview(textView)
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(callOnPress(_:)))
tapGestureRecognizer.isEnabled = true
textView.addGestureRecognizer(tapGestureRecognizer)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
// Resolves some animation issues
override func reactSetFrame(_ frame: CGRect) {
UIView.performWithoutAnimation {
super.reactSetFrame(frame)
}
}
func setText(string: NSAttributedString, size: CGSize, numberOfLines: Int) -> Void {
self.textView.frame.size = size
self.textView.textContainer.maximumNumberOfLines = numberOfLines
self.textView.attributedText = string
self.textView.selectedTextRange = nil
if let onTextLayout = self.onTextLayout {
var lines: [String] = []
textView.layoutManager.enumerateLineFragments(
forGlyphRange: NSRange(location: 0, length: textView.attributedText.length))
{ (rect, usedRect, textContainer, glyphRange, stop) in
let characterRange = self.textView.layoutManager.characterRange(forGlyphRange: glyphRange, actualGlyphRange: nil)
let line = (self.textView.text as NSString).substring(with: characterRange)
lines.append(line)
}
onTextLayout([
"lines": lines
])
}
}
@IBAction func callOnPress(_ sender: UITapGestureRecognizer) -> Void {
// If we find a child, then call onPress
if let child = getPressed(sender) {
if textView.selectedTextRange == nil, let onPress = child.onPress {
onPress(["": ""])
} 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) -> RNUITextViewChild? {
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
)
for child in self.reactSubviews() {
if let child = child as? RNUITextViewChild, let childText = child.text {
let fullText = self.textView.attributedText.string
let range = fullText.range(of: childText)
if let lowerBound = range?.lowerBound, let upperBound = range?.upperBound {
if charIndex >= lowerBound.utf16Offset(in: fullText) && charIndex <= upperBound.utf16Offset(in: fullText) {
return child
}
}
}
}
return nil
}
func getLineBreakMode() -> NSLineBreakMode {
switch self.ellipsizeMode {
case "head":
return .byTruncatingHead
case "middle":
return .byTruncatingMiddle
case "tail":
return .byTruncatingTail
case "clip":
return .byClipping
default:
return .byTruncatingTail
}
}
}
@@ -0,0 +1,4 @@
class RNUITextViewChild: UIView {
@objc var text: String?
@objc var onPress: RCTDirectEventBlock?
}
@@ -0,0 +1,56 @@
// We want all of our props to be available in the child's shadow view so we
// can create the attributed text before mount and calculate the needed size
// for the view.
class RNUITextViewChildShadow: RCTShadowView {
@objc var text: String = ""
@objc var color: UIColor = .black
@objc var fontSize: CGFloat = 16.0
@objc var fontStyle: String = "normal"
@objc var fontWeight: String = "normal"
@objc var letterSpacing: CGFloat = 0.0
@objc var lineHeight: CGFloat = 0.0
@objc var pointerEvents: NSString?
override func isYogaLeafNode() -> Bool {
return true
}
override func didSetProps(_ changedProps: [String]!) {
guard let superview = self.superview as? RNUITextViewShadow else {
return
}
if !YGNodeIsDirty(superview.yogaNode) {
superview.setAttributedText()
}
}
func getFontWeight() -> UIFont.Weight {
switch self.fontWeight {
case "bold":
return .bold
case "normal":
return .regular
case "100":
return .ultraLight
case "200":
return .ultraLight
case "300":
return .light
case "400":
return .regular
case "500":
return .medium
case "600":
return .semibold
case "700":
return .semibold
case "800":
return .bold
case "900":
return .heavy
default:
return .regular
}
}
}
@@ -0,0 +1,25 @@
#import <React/RCTViewManager.h>
@interface RCT_EXTERN_MODULE(RNUITextViewManager, RCTViewManager)
RCT_REMAP_SHADOW_PROPERTY(numberOfLines, numberOfLines, NSInteger)
RCT_REMAP_SHADOW_PROPERTY(allowsFontScaling, allowsFontScaling, BOOL)
RCT_EXPORT_VIEW_PROPERTY(onTextLayout, RCTDirectEventBlock)
RCT_EXPORT_VIEW_PROPERTY(ellipsizeMode, NSString)
RCT_EXPORT_VIEW_PROPERTY(selectable, BOOL)
@end
@interface RCT_EXTERN_MODULE(RNUITextViewChildManager, RCTViewManager)
RCT_REMAP_SHADOW_PROPERTY(text, text, NSString)
RCT_REMAP_SHADOW_PROPERTY(color, color, UIColor)
RCT_REMAP_SHADOW_PROPERTY(fontSize, fontSize, CGFloat)
RCT_REMAP_SHADOW_PROPERTY(fontStyle, fontStyle, NSString)
RCT_REMAP_SHADOW_PROPERTY(fontWeight, fontWeight, NSString)
RCT_REMAP_SHADOW_PROPERTY(letterSpacing, letterSpacing, CGFloat)
RCT_REMAP_SHADOW_PROPERTY(lineHeight, lineHeight, CGFloat)
RCT_REMAP_SHADOW_PROPERTY(pointerEvents, pointerEvents, NSString)
RCT_EXPORT_VIEW_PROPERTY(text, NSString)
RCT_EXPORT_VIEW_PROPERTY(onPress, RCTBubblingEventBlock)
@end
@@ -0,0 +1,30 @@
@objc(RNUITextViewManager)
class RNUITextViewManager: RCTViewManager {
override func view() -> (RNUITextView) {
return RNUITextView()
}
@objc override static func requiresMainQueueSetup() -> Bool {
return true
}
override func shadowView() -> RCTShadowView {
// Pass the bridge to the shadow view
return RNUITextViewShadow(bridge: self.bridge)
}
}
@objc(RNUITextViewChildManager)
class RNUITextViewChildManager: RCTViewManager {
override func view() -> (RNUITextViewChild) {
return RNUITextViewChild()
}
@objc override static func requiresMainQueueSetup() -> Bool {
return true
}
override func shadowView() -> RCTShadowView {
return RNUITextViewChildShadow()
}
}
@@ -0,0 +1,147 @@
class RNUITextViewShadow: RCTShadowView {
// Props
@objc var numberOfLines: Int = 0 {
didSet {
if !YGNodeIsDirty(self.yogaNode) {
self.setAttributedText()
}
}
}
@objc var allowsFontScaling: Bool = true
var attributedText: NSAttributedString = NSAttributedString()
var frameSize: CGSize = CGSize()
var lineHeight: CGFloat = 0
var bridge: RCTBridge
init(bridge: RCTBridge) {
self.bridge = bridge
super.init()
// We need to set a custom measure func here to calculate the height correctly
YGNodeSetMeasureFunc(self.yogaNode) { node, width, widthMode, height, heightMode in
// Get the shadowview and determine the needed size to set
let shadowView = Unmanaged<RNUITextViewShadow>.fromOpaque(YGNodeGetContext(node)).takeUnretainedValue()
return shadowView.getNeededSize(maxWidth: width)
}
// Subscribe to ynamic type size changes
NotificationCenter.default.addObserver(
self,
selector: #selector(preferredContentSizeChanged(_:)),
name: UIContentSizeCategory.didChangeNotification,
object: nil
)
}
@objc func preferredContentSizeChanged(_ notification: Notification) {
self.setAttributedText()
}
// Tell yoga not to use flexbox
override func isYogaLeafNode() -> Bool {
return true
}
// We only need to insert text children
override func insertReactSubview(_ subview: RCTShadowView!, at atIndex: Int) {
if subview.isKind(of: RNUITextViewChildShadow.self) {
super.insertReactSubview(subview, at: atIndex)
}
}
// Whenever the subvies update, set the text
override func didUpdateReactSubviews() {
self.setAttributedText()
}
// Whenever we layout, update the UI
override func layoutSubviews(with layoutContext: RCTLayoutContext) {
// Don't do anything if the layout is dirty
if(YGNodeIsDirty(self.yogaNode)) {
return
}
// Update the text
self.bridge.uiManager.addUIBlock { uiManager, viewRegistry in
guard let textView = viewRegistry?[self.reactTag] as? RNUITextView else {
return
}
textView.setText(string: self.attributedText, size: self.frameSize, numberOfLines: self.numberOfLines)
}
}
override func dirtyLayout() {
super.dirtyLayout()
YGNodeMarkDirty(self.yogaNode)
}
// Update the attributed text whenever changes are made to the subviews.
func setAttributedText() -> Void {
// Create an attributed string to store each of the segments
let finalAttributedString = NSMutableAttributedString()
self.reactSubviews().forEach { child in
guard let child = child as? RNUITextViewChildShadow else {
return
}
let scaledFontSize = self.allowsFontScaling ?
UIFontMetrics.default.scaledValue(for: child.fontSize) : child.fontSize
let font = UIFont.systemFont(ofSize: scaledFontSize, weight: child.getFontWeight())
// Set some generic attributes that don't need ranges
let attributes: [NSAttributedString.Key:Any] = [
.font: font,
.foregroundColor: child.color,
]
// Create the attributed string with the generic attributes
let string = NSMutableAttributedString(string: child.text, attributes: attributes)
// Set the paragraph style attributes if necessary
let paragraphStyle = NSMutableParagraphStyle()
if child.lineHeight != 0.0 {
paragraphStyle.minimumLineHeight = child.lineHeight
paragraphStyle.maximumLineHeight = child.lineHeight
string.addAttribute(
NSAttributedString.Key.paragraphStyle,
value: paragraphStyle,
range: NSMakeRange(0, string.length)
)
// Store that height
self.lineHeight = child.lineHeight
} else {
self.lineHeight = font.lineHeight
}
finalAttributedString.append(string)
}
self.attributedText = finalAttributedString
self.dirtyLayout()
}
// Create a YGSize based on the max width
func getNeededSize(maxWidth: Float) -> YGSize {
// Create the max size and figure out the size of the entire text
let maxSize = CGSize(width: CGFloat(maxWidth), height: CGFloat(MAXFLOAT))
let textSize = self.attributedText.boundingRect(with: maxSize, options: .usesLineFragmentOrigin, context: nil)
// Figure out how many total lines there are
let totalLines = Int(ceil(textSize.height / self.lineHeight))
// Default to the text size
var neededSize: CGSize = textSize.size
// If the total lines > max number, return size with the max
if self.numberOfLines != 0, totalLines > self.numberOfLines {
neededSize = CGSize(width: CGFloat(maxWidth), height: CGFloat(CGFloat(self.numberOfLines) * self.lineHeight))
}
self.frameSize = neededSize
return YGSize(width: Float(neededSize.width), height: Float(neededSize.height))
}
}
@@ -0,0 +1,9 @@
{
"name": "react-native-ui-text-view",
"version": "0.1.0",
"description": "UITextView in React Native on iOS",
"main": "src/index",
"author": "haileyok",
"license": "MIT",
"homepage": "https://github.com/bluesky-social/social-app/modules/react-native-ui-text-view"
}
@@ -0,0 +1,42 @@
require "json"
package = JSON.parse(File.read(File.join(__dir__, "package.json")))
folly_compiler_flags = '-DFOLLY_NO_CONFIG -DFOLLY_MOBILE=1 -DFOLLY_USE_LIBCPP=1 -Wno-comma -Wno-shorten-64-to-32'
Pod::Spec.new do |s|
s.name = "react-native-ui-text-view"
s.version = package["version"]
s.summary = package["description"]
s.homepage = package["homepage"]
s.license = package["license"]
s.authors = package["author"]
s.platforms = { :ios => "11.0" }
s.source = { :git => ".git", :tag => "#{s.version}" }
s.source_files = "ios/**/*.{h,m,mm,swift}"
# Use install_modules_dependencies helper to install the dependencies if React Native version >=0.71.0.
# See https://github.com/facebook/react-native/blob/febf6b7f33fdb4904669f99d795eba4c0f95d7bf/scripts/cocoapods/new_architecture.rb#L79.
if respond_to?(:install_modules_dependencies, true)
install_modules_dependencies(s)
else
s.dependency "React-Core"
# Don't install the dependencies when we run `pod install` in the old architecture.
if ENV['RCT_NEW_ARCH_ENABLED'] == '1' then
s.compiler_flags = folly_compiler_flags + " -DRCT_NEW_ARCH_ENABLED=1"
s.pod_target_xcconfig = {
"HEADER_SEARCH_PATHS" => "\"$(PODS_ROOT)/boost\"",
"OTHER_CPLUSPLUSFLAGS" => "-DFOLLY_NO_CONFIG -DFOLLY_MOBILE=1 -DFOLLY_USE_LIBCPP=1",
"CLANG_CXX_LANGUAGE_STANDARD" => "c++17"
}
s.dependency "React-RCTFabric"
s.dependency "React-Codegen"
s.dependency "RCT-Folly"
s.dependency "RCTRequired"
s.dependency "RCTTypeSafety"
s.dependency "ReactCommon/turbomodule/core"
end
end
end
@@ -0,0 +1,76 @@
import React from 'react'
import {Platform, StyleSheet, TextProps, ViewStyle} from 'react-native'
import {RNUITextView, RNUITextViewChild} from './index'
const TextAncestorContext = React.createContext<[boolean, ViewStyle]>([
false,
StyleSheet.create({}),
])
const useTextAncestorContext = () => React.useContext(TextAncestorContext)
const textDefaults: TextProps = {
allowFontScaling: true,
selectable: true,
}
export function UITextView({style, children, ...rest}: TextProps) {
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 (Platform.OS !== 'ios') {
throw new Error('UITextView is only available on iOS')
}
if (!isAncestor) {
return (
<TextAncestorContext.Provider value={[true, flattenedStyle]}>
<RNUITextView
{...textDefaults}
{...rest}
ellipsizeMode={rest.ellipsizeMode ?? rest.lineBreakMode ?? 'tail'}
style={[{flex: 1}, flattenedStyle]}
onPress={undefined} // We want these to go to the children only
onLongPress={undefined}>
{React.Children.toArray(children).map((c, index) => {
if (React.isValidElement(c)) {
return c
} else if (typeof c === 'string') {
return (
<RNUITextViewChild
key={index}
style={flattenedStyle}
text={c}
{...rest}
/>
)
}
})}
</RNUITextView>
</TextAncestorContext.Provider>
)
} else {
return (
<>
{React.Children.toArray(children).map((c, index) => {
if (React.isValidElement(c)) {
return c
} else if (typeof c === 'string') {
return (
<RNUITextViewChild
key={index}
style={flattenedStyle}
text={c}
{...rest}
/>
)
}
})}
</>
)
}
}
@@ -0,0 +1,42 @@
import {
requireNativeComponent,
UIManager,
Platform,
type ViewStyle,
TextProps,
} from 'react-native'
const LINKING_ERROR =
`The package 'react-native-ui-text-view' doesn't seem to be linked. Make sure: \n\n` +
Platform.select({ios: "- You have run 'pod install'\n", default: ''}) +
'- You rebuilt the app after installing the package\n' +
'- You are not using Expo Go\n'
export interface RNUITextViewProps extends TextProps {
children: React.ReactNode
style: ViewStyle[]
}
export interface RNUITextViewChildProps extends TextProps {
text: string
onTextPress?: (...args: any[]) => void
onTextLongPress?: (...args: any[]) => void
}
export const RNUITextView =
UIManager.getViewManagerConfig &&
UIManager.getViewManagerConfig('RNUITextView') != null
? requireNativeComponent<RNUITextViewProps>('RNUITextView')
: () => {
throw new Error(LINKING_ERROR)
}
export const RNUITextViewChild =
UIManager.getViewManagerConfig &&
UIManager.getViewManagerConfig('RNUITextViewChild') != null
? requireNativeComponent<RNUITextViewChildProps>('RNUITextViewChild')
: () => {
throw new Error(LINKING_ERROR)
}
export * from './UITextView'
+2 -1
View File
@@ -175,7 +175,8 @@
"tlds": "^1.234.0",
"use-deep-compare": "^1.1.0",
"zeego": "^1.6.2",
"zod": "^3.20.2"
"zod": "^3.20.2",
"react-native-ui-text-view": "link:./modules/react-native-ui-text-view"
},
"devDependencies": {
"@atproto/dev-env": "^0.2.19",
+16 -14
View File
@@ -1,5 +1,5 @@
import React from 'react'
import {StyleSheet, View, Pressable} from 'react-native'
import {LayoutAnimation, StyleSheet, View} from 'react-native'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import ImageView from './ImageViewing'
import {shareImageModal, saveImageToMediaLibrary} from 'lib/media/manip'
@@ -105,19 +105,21 @@ function LightboxFooter({imageIndex}: {imageIndex: number}) {
return (
<View style={[styles.footer]}>
{altText ? (
<Pressable
onPress={() => setAltExpanded(!isAltExpanded)}
onLongPress={() => {}}
accessibilityRole="button">
<View>
<Text
selectable
style={[s.gray3, styles.footerText]}
numberOfLines={isAltExpanded ? undefined : 3}>
{altText}
</Text>
</View>
</Pressable>
<View accessibilityRole="button" style={styles.footerText}>
<Text
style={[s.gray3]}
numberOfLines={isAltExpanded ? undefined : 3}
selectable
onPress={() => {
LayoutAnimation.configureNext({
duration: 300,
update: {type: 'spring', springDamping: 0.7},
})
setAltExpanded(prev => !prev)
}}>
{altText}
</Text>
</View>
) : null}
<View style={styles.footerBtns}>
<Button
+2 -1
View File
@@ -40,7 +40,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'
import {moderatePost_wrapped as moderatePost} from '#/lib/moderatePost_wrapped'
@@ -400,6 +400,7 @@ function PostThreadLoaded({
style={s.hContentRegion}
// @ts-ignore our .web version only -prf
desktopFixedHeight
removeClippedSubviews={isAndroid ? false : undefined}
/>
)
}
+3 -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}>
@@ -370,6 +369,7 @@ let PostThreadItemLoaded = ({
richText={richText}
lineHeight={1.3}
style={s.flex1}
selectable
/>
</View>
) : undefined}
@@ -445,7 +445,7 @@ let PostThreadItemLoaded = ({
/>
</View>
</View>
</Link>
</View>
<WhoCanReply post={post} />
</>
)
+13 -3
View File
@@ -17,6 +17,7 @@ export function RichText({
lineHeight = 1.2,
style,
numberOfLines,
selectable,
noLinks,
}: {
testID?: string
@@ -25,6 +26,7 @@ export function RichText({
lineHeight?: number
style?: StyleProp<TextStyle>
numberOfLines?: number
selectable?: boolean
noLinks?: boolean
}) {
const theme = useTheme()
@@ -44,7 +46,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>
)
@@ -56,7 +62,8 @@ export function RichText({
style={[style, pal.text, lineHeightStyle]}
numberOfLines={numberOfLines}
// @ts-ignore web only -prf
dataSet={WORD_WRAP}>
dataSet={WORD_WRAP}
selectable={selectable}>
{text}
</Text>
)
@@ -85,6 +92,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) {
@@ -100,6 +108,7 @@ export function RichText({
style={[style, lineHeightStyle, pal.link, {pointerEvents: 'auto'}]}
dataSet={WORD_WRAP}
warnOnMismatchingLabel
selectable={selectable}
/>,
)
}
@@ -115,7 +124,8 @@ export function RichText({
style={[style, pal.text, lineHeightStyle]}
numberOfLines={numberOfLines}
// @ts-ignore web only -prf
dataSet={WORD_WRAP}>
dataSet={WORD_WRAP}
selectable={selectable}>
{els}
</Text>
)
+16
View File
@@ -2,12 +2,15 @@ import React from 'react'
import {Text as RNText, TextProps} from 'react-native'
import {s, lh} from 'lib/styles'
import {useTheme, TypographyVariant} from 'lib/ThemeContext'
import {isIOS} from 'platform/detection'
import {UITextView} from 'react-native-ui-text-view'
export type CustomTextProps = TextProps & {
type?: TypographyVariant
lineHeight?: number
title?: string
dataSet?: Record<string, string | number>
selectable?: boolean
}
export function Text({
@@ -17,16 +20,29 @@ export function Text({
style,
title,
dataSet,
selectable,
...props
}: React.PropsWithChildren<CustomTextProps>) {
const theme = useTheme()
const typography = theme.typography[type]
const lineHeightStyle = lineHeight ? lh(theme, type, lineHeight) : undefined
if (selectable && isIOS) {
return (
<UITextView
style={[s.black, typography, lineHeightStyle, style]}
{...props}>
{children}
</UITextView>
)
}
return (
<RNText
style={[s.black, typography, lineHeightStyle, style]}
// @ts-ignore web only -esb
dataSet={Object.assign({tooltip: title}, dataSet || {})}
selectable={selectable}
{...props}>
{children}
</RNText>
+4
View File
@@ -18370,6 +18370,10 @@ react-native-svg@14.1.0:
css-select "^5.1.0"
css-tree "^1.1.3"
"react-native-ui-text-view@link:./modules/react-native-ui-text-view":
version "0.0.0"
uid ""
react-native-url-polyfill@^1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/react-native-url-polyfill/-/react-native-url-polyfill-1.3.0.tgz#c1763de0f2a8c22cc3e959b654c8790622b6ef6a"