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 0d9b6954472bb89f63be479d79986bb6d8b7e735
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 3c381f94700167367b8519cb5d56360c51cea131
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 f951015637132d99d3523c1d93279b6b0b728293
Author: Hailey <me@haileyok.com>
Date: Thu Jan 18 23:46:25 2024 -0800
update readme
commit aa9b8b06eda6c4a00f7e4b0bcd5f7e5205c9b166
Author: Hailey <me@haileyok.com>
Date: Thu Jan 18 23:37:49 2024 -0800
calculate line height
commit 9fe479630c763fe3fe5dd7b8a5a6d82803f1ad06
Author: Hailey <me@haileyok.com>
Date: Thu Jan 18 23:19:31 2024 -0800
improve height calculation, render on prop changes
commit 209caffa7df30af933eff10ab16bf32d53b26df4
Author: Hailey <me@haileyok.com>
Date: Thu Jan 18 22:53:08 2024 -0800
presses
commit 384c8ec3a8774b075d0dca665d01de82ff9d19bd
Author: Hailey <me@haileyok.com>
Date: Thu Jan 18 21:57:56 2024 -0800
line break mode
commit adfcf05fe498b5ab6554e9b3fd399d7dd3ade79b
Author: Hailey <me@haileyok.com>
Date: Thu Jan 18 21:50:21 2024 -0800
onTextLayout event
commit e9ba104e6f12eb8144ee752335cdeecdfbf3d8e5
Author: Hailey <me@haileyok.com>
Date: Thu Jan 18 21:34:35 2024 -0800
better naming
commit e335f5ab7f813ec0d458476eeb91d0070fde0933
Author: Hailey <me@haileyok.com>
Date: Thu Jan 18 21:31:38 2024 -0800
remove android
commit 9e197934ba996a422ab03a204255a1b0b40d2d25
Author: Hailey <me@haileyok.com>
Date: Thu Jan 18 21:28:28 2024 -0800
remove expo module
commit 99882c7e3976a0cb59648e67f0eb4916f93f6830
Author: Hailey <me@haileyok.com>
Date: Thu Jan 18 21:27:43 2024 -0800
handle presses
commit 18f818649efcd1e18c810aaf4ea1a4cb93ddd111
Author: Hailey <me@haileyok.com>
Date: Thu Jan 18 21:14:38 2024 -0800
make use of rctshadowview
commit 7134e1106e338013555c984607d51124727b9264
Author: Hailey <me@haileyok.com>
Date: Wed Jan 17 20:38:39 2024 -0800
stop unnecessary layouts, resize container before setting text
commit 340b84f053d48e45a5e4e9648ac4f87fc00e5f4a
Author: Hailey <me@haileyok.com>
Date: Wed Jan 17 11:17:36 2024 -0800
handle prop changes for both children and root views
commit d906fe4fcfa4a919dbb66f4ec3f17e8f8be8bf02
Author: Hailey <me@haileyok.com>
Date: Tue Jan 16 18:42:22 2024 -0800
handle onpress better
commit b6b096416894893973be54793f4d3e3f08974293
Author: Hailey <me@haileyok.com>
Date: Tue Jan 16 16:57:31 2024 -0800
resolve animation issue, animate alt text expansion
commit daedd1f671fc933af27e2953b52b3a08eddb7c92
Author: Hailey <me@haileyok.com>
Date: Tue Jan 16 15:47:24 2024 -0800
move getChildren to didMoveToWindow
commit 87d44e4b576cce56a12a1f887e1b9605db1427aa
Author: Hailey <me@haileyok.com>
Date: Mon Jan 15 18:48:36 2024 -0800
simplify getPressed
commit d92584bad7db7179d95f155bd480854df8fae17f
Author: Hailey <me@haileyok.com>
Date: Mon Jan 15 17:56:43 2024 -0800
just more cleanup
commit d39f7a937dc8b47b98d120469db35d697bcf74be
Author: Hailey <me@haileyok.com>
Date: Mon Jan 15 17:03:19 2024 -0800
remove unnecessary property for gesture recognizer
commit a35513a1d236bcd94aab0e7c5ac1cd0907f61762
Author: Hailey <me@haileyok.com>
Date: Mon Jan 15 16:55:36 2024 -0800
remove debug line
commit 788956aa01d2b46783ad0d0a45949fc5ca9e0aab
Author: Hailey <me@haileyok.com>
Date: Mon Jan 15 16:33:44 2024 -0800
typo
commit a3ba6e782542a8e9ca09b5b49b1043ba046dcc70
Author: Hailey <me@haileyok.com>
Date: Mon Jan 15 13:42:25 2024 -0800
make alt text selectable
commit e5472a13da277ef7cccb870d62197dd86b9c3e86
Author: Hailey <me@haileyok.com>
Date: Mon Jan 15 05:27:15 2024 -0800
re-render on numberOfLines change
commit 9f5b7602c11a92cb83704feb3946fe6b4f584fa5
Author: Hailey <me@haileyok.com>
Date: Mon Jan 15 04:57:35 2024 -0800
more implementations
commit aa96bba0664d14f12ee742739c70847407062f35
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 ec8c05f3f05949b6e3ae8be2e4d153d7d51b18f9
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 2435a25257c4a3b12c38949b1928848a0acf1a97
Author: Hailey <me@haileyok.com>
Date: Fri Jan 12 23:30:13 2024 -0800
cleanup
commit fdf75927f6fc176a390a11cba56e462c6fe48bdf
Author: Hailey <me@haileyok.com>
Date: Fri Jan 12 23:25:23 2024 -0800
remove debug
commit 36d8cd82ef57483dcf3740c803c6524bc76e87c9
Author: Hailey <me@haileyok.com>
Date: Fri Jan 12 23:25:17 2024 -0800
reset text selection on text update
commit b8f7bc23c2df8532941af8b62a4d36a4814c5965
Author: Hailey <me@haileyok.com>
Date: Fri Jan 12 23:24:43 2024 -0800
use textkit 1
commit 5216464458f4ffd1d6384a1d15ca7be5e8a96d5d
Author: Hailey <me@haileyok.com>
Date: Fri Jan 12 22:50:15 2024 -0800
properly handle link press events
commit 2802902c69f5d68140c3b573115e8e73638ce9b5
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 860610e63ab15cfa9b18da317243137b35a6bf6d
Author: Hailey <me@haileyok.com>
Date: Fri Jan 12 19:17:51 2024 -0800
always make sure we use the latest styles
commit 7f05d0141b6355aa4f521f91056edc06ffc2f5ba
Author: Hailey <me@haileyok.com>
Date: Fri Jan 12 16:57:08 2024 -0800
update readme with tech info
commit b8318446a34d07fb0fc37029c3143d0b81eb2b29
Author: Hailey <me@haileyok.com>
Date: Fri Jan 12 16:34:35 2024 -0800
remove all uitextview padding
commit 0f0b6aa131a1e68e0e4eeb456157c866ebc85de3
Author: Hailey <me@haileyok.com>
Date: Fri Jan 12 16:34:28 2024 -0800
cleanup imports
commit c9f0064836d5fe26c55ce571b5d1abf5678ca3a5
Author: Hailey <me@haileyok.com>
Date: Fri Jan 12 16:18:08 2024 -0800
update interface
commit 7dcac644baeedb506f91f1f4dcaf80dbfb46f610
Author: Hailey <me@haileyok.com>
Date: Fri Jan 12 16:13:49 2024 -0800
remove useless struct
commit 5174744213c97cb74ca7fe3a513a3abc108fe83d
Author: Hailey <me@haileyok.com>
Date: Fri Jan 12 16:13:34 2024 -0800
adjust deps
commit ce8b9ed62bcf484ad498b0de05998d8986b132ac
Author: Hailey <me@haileyok.com>
Date: Thu Jan 11 22:15:50 2024 -0800
add readme, update info
commit 33c6e3b15c64bcb952b62d1f5c3100c517a64c57
Author: Hailey <me@haileyok.com>
Date: Thu Jan 11 22:04:53 2024 -0800
remove unnecessary android/web stuff
commit fbca531bdfeff90bd2a99214482e102f2601c453
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 648552eafbc3bf861567ca160c6e84295eec26f8
Author: Hailey <me@haileyok.com>
Date: Thu Jan 11 02:01:20 2024 -0800
wip
commit c6d2e54923e779180f456bef3ba275dcb2f74d5d
Author: Hailey <me@haileyok.com>
Date: Thu Jan 11 00:38:47 2024 -0800
selectable text experiment
This commit is contained in:
parent
cda4fe4a7f
commit
a2b58852e7
20 changed files with 694 additions and 24 deletions
4
.gitignore
vendored
4
.gitignore
vendored
|
|
@ -91,8 +91,8 @@ web-build/
|
||||||
|
|
||||||
|
|
||||||
# Android & iOS folders
|
# Android & iOS folders
|
||||||
android/
|
/android/
|
||||||
ios/
|
/ios/
|
||||||
|
|
||||||
# environment variables
|
# environment variables
|
||||||
.env
|
.env
|
||||||
|
|
|
||||||
61
modules/react-native-ui-text-view/README.md
Normal file
61
modules/react-native-ui-text-view/README.md
Normal file
|
|
@ -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>
|
||||||
141
modules/react-native-ui-text-view/ios/RNUITextView.swift
Normal file
141
modules/react-native-ui-text-view/ios/RNUITextView.swift
Normal file
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
25
modules/react-native-ui-text-view/ios/RNUITextViewManager.m
Normal file
25
modules/react-native-ui-text-view/ios/RNUITextViewManager.m
Normal file
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
147
modules/react-native-ui-text-view/ios/RNUITextViewShadow.swift
Normal file
147
modules/react-native-ui-text-view/ios/RNUITextViewShadow.swift
Normal file
|
|
@ -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))
|
||||||
|
}
|
||||||
|
}
|
||||||
9
modules/react-native-ui-text-view/package.json
Normal file
9
modules/react-native-ui-text-view/package.json
Normal file
|
|
@ -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
|
||||||
76
modules/react-native-ui-text-view/src/UITextView.tsx
Normal file
76
modules/react-native-ui-text-view/src/UITextView.tsx
Normal file
|
|
@ -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}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
42
modules/react-native-ui-text-view/src/index.tsx
Normal file
42
modules/react-native-ui-text-view/src/index.tsx
Normal file
|
|
@ -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'
|
||||||
|
|
@ -175,7 +175,8 @@
|
||||||
"tlds": "^1.234.0",
|
"tlds": "^1.234.0",
|
||||||
"use-deep-compare": "^1.1.0",
|
"use-deep-compare": "^1.1.0",
|
||||||
"zeego": "^1.6.2",
|
"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": {
|
"devDependencies": {
|
||||||
"@atproto/dev-env": "^0.2.19",
|
"@atproto/dev-env": "^0.2.19",
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import React from 'react'
|
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 {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
||||||
import ImageView from './ImageViewing'
|
import ImageView from './ImageViewing'
|
||||||
import {shareImageModal, saveImageToMediaLibrary} from 'lib/media/manip'
|
import {shareImageModal, saveImageToMediaLibrary} from 'lib/media/manip'
|
||||||
|
|
@ -105,19 +105,21 @@ function LightboxFooter({imageIndex}: {imageIndex: number}) {
|
||||||
return (
|
return (
|
||||||
<View style={[styles.footer]}>
|
<View style={[styles.footer]}>
|
||||||
{altText ? (
|
{altText ? (
|
||||||
<Pressable
|
<View accessibilityRole="button" style={styles.footerText}>
|
||||||
onPress={() => setAltExpanded(!isAltExpanded)}
|
<Text
|
||||||
onLongPress={() => {}}
|
style={[s.gray3]}
|
||||||
accessibilityRole="button">
|
numberOfLines={isAltExpanded ? undefined : 3}
|
||||||
<View>
|
selectable
|
||||||
<Text
|
onPress={() => {
|
||||||
selectable
|
LayoutAnimation.configureNext({
|
||||||
style={[s.gray3, styles.footerText]}
|
duration: 300,
|
||||||
numberOfLines={isAltExpanded ? undefined : 3}>
|
update: {type: 'spring', springDamping: 0.7},
|
||||||
{altText}
|
})
|
||||||
</Text>
|
setAltExpanded(prev => !prev)
|
||||||
</View>
|
}}>
|
||||||
</Pressable>
|
{altText}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
) : null}
|
) : null}
|
||||||
<View style={styles.footerBtns}>
|
<View style={styles.footerBtns}>
|
||||||
<Button
|
<Button
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,7 @@ import {
|
||||||
usePreferencesQuery,
|
usePreferencesQuery,
|
||||||
} from '#/state/queries/preferences'
|
} from '#/state/queries/preferences'
|
||||||
import {useSession} from '#/state/session'
|
import {useSession} from '#/state/session'
|
||||||
import {isNative} from '#/platform/detection'
|
import {isAndroid, isNative} from '#/platform/detection'
|
||||||
import {logger} from '#/logger'
|
import {logger} from '#/logger'
|
||||||
import {moderatePost_wrapped as moderatePost} from '#/lib/moderatePost_wrapped'
|
import {moderatePost_wrapped as moderatePost} from '#/lib/moderatePost_wrapped'
|
||||||
|
|
||||||
|
|
@ -400,6 +400,7 @@ function PostThreadLoaded({
|
||||||
style={s.hContentRegion}
|
style={s.hContentRegion}
|
||||||
// @ts-ignore our .web version only -prf
|
// @ts-ignore our .web version only -prf
|
||||||
desktopFixedHeight
|
desktopFixedHeight
|
||||||
|
removeClippedSubviews={isAndroid ? false : undefined}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -248,10 +248,9 @@ let PostThreadItemLoaded = ({
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Link
|
<View
|
||||||
testID={`postThreadItem-by-${post.author.handle}`}
|
testID={`postThreadItem-by-${post.author.handle}`}
|
||||||
style={[styles.outer, styles.outerHighlighted, pal.border, pal.view]}
|
style={[styles.outer, styles.outerHighlighted, pal.border, pal.view]}
|
||||||
noFeedback
|
|
||||||
accessible={false}>
|
accessible={false}>
|
||||||
<PostSandboxWarning />
|
<PostSandboxWarning />
|
||||||
<View style={styles.layout}>
|
<View style={styles.layout}>
|
||||||
|
|
@ -370,6 +369,7 @@ let PostThreadItemLoaded = ({
|
||||||
richText={richText}
|
richText={richText}
|
||||||
lineHeight={1.3}
|
lineHeight={1.3}
|
||||||
style={s.flex1}
|
style={s.flex1}
|
||||||
|
selectable
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
) : undefined}
|
) : undefined}
|
||||||
|
|
@ -445,7 +445,7 @@ let PostThreadItemLoaded = ({
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</Link>
|
</View>
|
||||||
<WhoCanReply post={post} />
|
<WhoCanReply post={post} />
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ export function RichText({
|
||||||
lineHeight = 1.2,
|
lineHeight = 1.2,
|
||||||
style,
|
style,
|
||||||
numberOfLines,
|
numberOfLines,
|
||||||
|
selectable,
|
||||||
noLinks,
|
noLinks,
|
||||||
}: {
|
}: {
|
||||||
testID?: string
|
testID?: string
|
||||||
|
|
@ -25,6 +26,7 @@ export function RichText({
|
||||||
lineHeight?: number
|
lineHeight?: number
|
||||||
style?: StyleProp<TextStyle>
|
style?: StyleProp<TextStyle>
|
||||||
numberOfLines?: number
|
numberOfLines?: number
|
||||||
|
selectable?: boolean
|
||||||
noLinks?: boolean
|
noLinks?: boolean
|
||||||
}) {
|
}) {
|
||||||
const theme = useTheme()
|
const theme = useTheme()
|
||||||
|
|
@ -44,7 +46,11 @@ export function RichText({
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
// @ts-ignore web only -prf
|
// @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}
|
||||||
</Text>
|
</Text>
|
||||||
)
|
)
|
||||||
|
|
@ -56,7 +62,8 @@ export function RichText({
|
||||||
style={[style, pal.text, lineHeightStyle]}
|
style={[style, pal.text, lineHeightStyle]}
|
||||||
numberOfLines={numberOfLines}
|
numberOfLines={numberOfLines}
|
||||||
// @ts-ignore web only -prf
|
// @ts-ignore web only -prf
|
||||||
dataSet={WORD_WRAP}>
|
dataSet={WORD_WRAP}
|
||||||
|
selectable={selectable}>
|
||||||
{text}
|
{text}
|
||||||
</Text>
|
</Text>
|
||||||
)
|
)
|
||||||
|
|
@ -85,6 +92,7 @@ export function RichText({
|
||||||
href={`/profile/${mention.did}`}
|
href={`/profile/${mention.did}`}
|
||||||
style={[style, lineHeightStyle, pal.link, {pointerEvents: 'auto'}]}
|
style={[style, lineHeightStyle, pal.link, {pointerEvents: 'auto'}]}
|
||||||
dataSet={WORD_WRAP}
|
dataSet={WORD_WRAP}
|
||||||
|
selectable={selectable}
|
||||||
/>,
|
/>,
|
||||||
)
|
)
|
||||||
} else if (link && AppBskyRichtextFacet.validateLink(link).success) {
|
} else if (link && AppBskyRichtextFacet.validateLink(link).success) {
|
||||||
|
|
@ -100,6 +108,7 @@ export function RichText({
|
||||||
style={[style, lineHeightStyle, pal.link, {pointerEvents: 'auto'}]}
|
style={[style, lineHeightStyle, pal.link, {pointerEvents: 'auto'}]}
|
||||||
dataSet={WORD_WRAP}
|
dataSet={WORD_WRAP}
|
||||||
warnOnMismatchingLabel
|
warnOnMismatchingLabel
|
||||||
|
selectable={selectable}
|
||||||
/>,
|
/>,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -115,7 +124,8 @@ export function RichText({
|
||||||
style={[style, pal.text, lineHeightStyle]}
|
style={[style, pal.text, lineHeightStyle]}
|
||||||
numberOfLines={numberOfLines}
|
numberOfLines={numberOfLines}
|
||||||
// @ts-ignore web only -prf
|
// @ts-ignore web only -prf
|
||||||
dataSet={WORD_WRAP}>
|
dataSet={WORD_WRAP}
|
||||||
|
selectable={selectable}>
|
||||||
{els}
|
{els}
|
||||||
</Text>
|
</Text>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -2,12 +2,15 @@ import React from 'react'
|
||||||
import {Text as RNText, TextProps} from 'react-native'
|
import {Text as RNText, TextProps} from 'react-native'
|
||||||
import {s, lh} from 'lib/styles'
|
import {s, lh} from 'lib/styles'
|
||||||
import {useTheme, TypographyVariant} from 'lib/ThemeContext'
|
import {useTheme, TypographyVariant} from 'lib/ThemeContext'
|
||||||
|
import {isIOS} from 'platform/detection'
|
||||||
|
import {UITextView} from 'react-native-ui-text-view'
|
||||||
|
|
||||||
export type CustomTextProps = TextProps & {
|
export type CustomTextProps = TextProps & {
|
||||||
type?: TypographyVariant
|
type?: TypographyVariant
|
||||||
lineHeight?: number
|
lineHeight?: number
|
||||||
title?: string
|
title?: string
|
||||||
dataSet?: Record<string, string | number>
|
dataSet?: Record<string, string | number>
|
||||||
|
selectable?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Text({
|
export function Text({
|
||||||
|
|
@ -17,16 +20,29 @@ export function Text({
|
||||||
style,
|
style,
|
||||||
title,
|
title,
|
||||||
dataSet,
|
dataSet,
|
||||||
|
selectable,
|
||||||
...props
|
...props
|
||||||
}: React.PropsWithChildren<CustomTextProps>) {
|
}: React.PropsWithChildren<CustomTextProps>) {
|
||||||
const theme = useTheme()
|
const theme = useTheme()
|
||||||
const typography = theme.typography[type]
|
const typography = theme.typography[type]
|
||||||
const lineHeightStyle = lineHeight ? lh(theme, type, lineHeight) : undefined
|
const lineHeightStyle = lineHeight ? lh(theme, type, lineHeight) : undefined
|
||||||
|
|
||||||
|
if (selectable && isIOS) {
|
||||||
|
return (
|
||||||
|
<UITextView
|
||||||
|
style={[s.black, typography, lineHeightStyle, style]}
|
||||||
|
{...props}>
|
||||||
|
{children}
|
||||||
|
</UITextView>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RNText
|
<RNText
|
||||||
style={[s.black, typography, lineHeightStyle, style]}
|
style={[s.black, typography, lineHeightStyle, style]}
|
||||||
// @ts-ignore web only -esb
|
// @ts-ignore web only -esb
|
||||||
dataSet={Object.assign({tooltip: title}, dataSet || {})}
|
dataSet={Object.assign({tooltip: title}, dataSet || {})}
|
||||||
|
selectable={selectable}
|
||||||
{...props}>
|
{...props}>
|
||||||
{children}
|
{children}
|
||||||
</RNText>
|
</RNText>
|
||||||
|
|
|
||||||
|
|
@ -18370,6 +18370,10 @@ react-native-svg@14.1.0:
|
||||||
css-select "^5.1.0"
|
css-select "^5.1.0"
|
||||||
css-tree "^1.1.3"
|
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:
|
react-native-url-polyfill@^1.3.0:
|
||||||
version "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"
|
resolved "https://registry.yarnpkg.com/react-native-url-polyfill/-/react-native-url-polyfill-1.3.0.tgz#c1763de0f2a8c22cc3e959b654c8790622b6ef6a"
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue