Bluesky app fork with some witchin' additions 💫
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

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

+694 -24
+2 -2
.gitignore
··· 91 91 92 92 93 93 # Android & iOS folders 94 - android/ 95 - ios/ 94 + /android/ 95 + /ios/ 96 96 97 97 # environment variables 98 98 .env
+61
modules/react-native-ui-text-view/README.md
··· 1 + # React Native UITextView 2 + 3 + Drop in replacement for `<Text>` that renders a `UITextView`, support selection and native translation features on iOS. 4 + 5 + ## Installation 6 + 7 + In this project, no installation is required. The pod will be installed automatically during a `pod install`. 8 + 9 + In another project, clone the repo and copy the `modules/react-native-ui-text-view` directory to your own project 10 + directory. Afterward, run `pod install`. 11 + 12 + ## Usage 13 + 14 + Replace the outermost `<Text>` with `<UITextView>`. Styles and press events should be handled the same way they would 15 + with `<Text>`. Both `<UITextView>` and `<Text>` are supported as children of the root `<UITextView>`. 16 + 17 + ## Technical 18 + 19 + React Native's `Text` component allows for "infinite" nesting of further `Text` components. To make a true "drop-in", 20 + we want to do the same thing. 21 + 22 + To achieve this, we first need to handle determining if we are dealing with an ancestor or root `UITextView` component. 23 + 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). 24 + 25 + We create a context that contains a boolean to tell us if we have already rendered the root `UITextView`. We also store 26 + the root styles so that we can apply those styles if the ancestor `UITextView`s have not overwritten those styles. 27 + 28 + All of our children are placed into `RNUITextView`, which is the main native view that will display the iOS `UITextView`. 29 + 30 + 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 31 + it is, that means we have encountered what was once an RN `Text` component. RN doesn't let us pass plain text as 32 + children outside of `Text`, so we instead just pass the text into the `text` prop on `RNUITextViewChild`. We continue 33 + down the tree, until we run out of children. 34 + 35 + On the native side, we make use of the shadow view to calculate text container dimensions before the views are mounted. 36 + We cannot simply set the `UITextView` text first, since React will not have properly measured the layout before this 37 + occurs. 38 + 39 + 40 + As for `Text` props, the following props are implemented: 41 + 42 + - All accessibility props 43 + - `allowFontScaling` 44 + - `adjustsFontSizeToFit` 45 + - `ellipsizeMode` 46 + - `numberOfLines` 47 + - `onLayout` 48 + - `onPress` 49 + - `onTextLayout` 50 + - `selectable` 51 + 52 + All `ViewStyle` props will apply to the root `UITextView`. Individual children will respect these `TextStyle` styles: 53 + 54 + - `color` 55 + - `fontSize` 56 + - `fontStyle` 57 + - `fontWeight` 58 + - `fontVariant` 59 + - `letterSpacing` 60 + - `lineHeight` 61 + - `textDecorationLine`
+3
modules/react-native-ui-text-view/ios/RNUITextView-Bridging-Header.h
··· 1 + #import <React/RCTViewManager.h> 2 + #import <React/RCTBridge.h> 3 + #import <React/RCTBridge+Private.h>
+141
modules/react-native-ui-text-view/ios/RNUITextView.swift
··· 1 + class RNUITextView: UIView { 2 + var textView: UITextView 3 + 4 + @objc var numberOfLines: Int = 0 { 5 + didSet { 6 + textView.textContainer.maximumNumberOfLines = numberOfLines 7 + } 8 + } 9 + @objc var selectable: Bool = true { 10 + didSet { 11 + textView.isSelectable = selectable 12 + } 13 + } 14 + @objc var ellipsizeMode: String = "tail" { 15 + didSet { 16 + textView.textContainer.lineBreakMode = self.getLineBreakMode() 17 + } 18 + } 19 + @objc var onTextLayout: RCTDirectEventBlock? 20 + 21 + override init(frame: CGRect) { 22 + if #available(iOS 16.0, *) { 23 + textView = UITextView(usingTextLayoutManager: false) 24 + } else { 25 + textView = UITextView() 26 + } 27 + 28 + // Disable scrolling 29 + textView.isScrollEnabled = false 30 + // Remove all the padding 31 + textView.textContainerInset = .zero 32 + textView.textContainer.lineFragmentPadding = 0 33 + 34 + // Remove other properties 35 + textView.isEditable = false 36 + textView.backgroundColor = .clear 37 + 38 + // Init 39 + super.init(frame: frame) 40 + self.clipsToBounds = true 41 + 42 + // Add the view 43 + addSubview(textView) 44 + 45 + let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(callOnPress(_:))) 46 + tapGestureRecognizer.isEnabled = true 47 + textView.addGestureRecognizer(tapGestureRecognizer) 48 + } 49 + 50 + required init?(coder: NSCoder) { 51 + fatalError("init(coder:) has not been implemented") 52 + } 53 + 54 + // Resolves some animation issues 55 + override func reactSetFrame(_ frame: CGRect) { 56 + UIView.performWithoutAnimation { 57 + super.reactSetFrame(frame) 58 + } 59 + } 60 + 61 + func setText(string: NSAttributedString, size: CGSize, numberOfLines: Int) -> Void { 62 + self.textView.frame.size = size 63 + self.textView.textContainer.maximumNumberOfLines = numberOfLines 64 + self.textView.attributedText = string 65 + self.textView.selectedTextRange = nil 66 + 67 + if let onTextLayout = self.onTextLayout { 68 + var lines: [String] = [] 69 + textView.layoutManager.enumerateLineFragments( 70 + forGlyphRange: NSRange(location: 0, length: textView.attributedText.length)) 71 + { (rect, usedRect, textContainer, glyphRange, stop) in 72 + let characterRange = self.textView.layoutManager.characterRange(forGlyphRange: glyphRange, actualGlyphRange: nil) 73 + let line = (self.textView.text as NSString).substring(with: characterRange) 74 + lines.append(line) 75 + } 76 + 77 + onTextLayout([ 78 + "lines": lines 79 + ]) 80 + } 81 + } 82 + 83 + @IBAction func callOnPress(_ sender: UITapGestureRecognizer) -> Void { 84 + // If we find a child, then call onPress 85 + if let child = getPressed(sender) { 86 + if textView.selectedTextRange == nil, let onPress = child.onPress { 87 + onPress(["": ""]) 88 + } else { 89 + // Clear the selected text range if we are not pressing on a link 90 + textView.selectedTextRange = nil 91 + } 92 + } 93 + } 94 + 95 + // Try to get the pressed segment 96 + func getPressed(_ sender: UITapGestureRecognizer) -> RNUITextViewChild? { 97 + let layoutManager = textView.layoutManager 98 + var location = sender.location(in: textView) 99 + 100 + // Remove the padding 101 + location.x -= textView.textContainerInset.left 102 + location.y -= textView.textContainerInset.top 103 + 104 + // Get the index of the char 105 + let charIndex = layoutManager.characterIndex( 106 + for: location, 107 + in: textView.textContainer, 108 + fractionOfDistanceBetweenInsertionPoints: nil 109 + ) 110 + 111 + for child in self.reactSubviews() { 112 + if let child = child as? RNUITextViewChild, let childText = child.text { 113 + let fullText = self.textView.attributedText.string 114 + let range = fullText.range(of: childText) 115 + 116 + if let lowerBound = range?.lowerBound, let upperBound = range?.upperBound { 117 + if charIndex >= lowerBound.utf16Offset(in: fullText) && charIndex <= upperBound.utf16Offset(in: fullText) { 118 + return child 119 + } 120 + } 121 + } 122 + } 123 + 124 + return nil 125 + } 126 + 127 + func getLineBreakMode() -> NSLineBreakMode { 128 + switch self.ellipsizeMode { 129 + case "head": 130 + return .byTruncatingHead 131 + case "middle": 132 + return .byTruncatingMiddle 133 + case "tail": 134 + return .byTruncatingTail 135 + case "clip": 136 + return .byClipping 137 + default: 138 + return .byTruncatingTail 139 + } 140 + } 141 + }
+4
modules/react-native-ui-text-view/ios/RNUITextViewChild.swift
··· 1 + class RNUITextViewChild: UIView { 2 + @objc var text: String? 3 + @objc var onPress: RCTDirectEventBlock? 4 + }
+56
modules/react-native-ui-text-view/ios/RNUITextViewChildShadow.swift
··· 1 + // We want all of our props to be available in the child's shadow view so we 2 + // can create the attributed text before mount and calculate the needed size 3 + // for the view. 4 + class RNUITextViewChildShadow: RCTShadowView { 5 + @objc var text: String = "" 6 + @objc var color: UIColor = .black 7 + @objc var fontSize: CGFloat = 16.0 8 + @objc var fontStyle: String = "normal" 9 + @objc var fontWeight: String = "normal" 10 + @objc var letterSpacing: CGFloat = 0.0 11 + @objc var lineHeight: CGFloat = 0.0 12 + @objc var pointerEvents: NSString? 13 + 14 + override func isYogaLeafNode() -> Bool { 15 + return true 16 + } 17 + 18 + override func didSetProps(_ changedProps: [String]!) { 19 + guard let superview = self.superview as? RNUITextViewShadow else { 20 + return 21 + } 22 + 23 + if !YGNodeIsDirty(superview.yogaNode) { 24 + superview.setAttributedText() 25 + } 26 + } 27 + 28 + func getFontWeight() -> UIFont.Weight { 29 + switch self.fontWeight { 30 + case "bold": 31 + return .bold 32 + case "normal": 33 + return .regular 34 + case "100": 35 + return .ultraLight 36 + case "200": 37 + return .ultraLight 38 + case "300": 39 + return .light 40 + case "400": 41 + return .regular 42 + case "500": 43 + return .medium 44 + case "600": 45 + return .semibold 46 + case "700": 47 + return .semibold 48 + case "800": 49 + return .bold 50 + case "900": 51 + return .heavy 52 + default: 53 + return .regular 54 + } 55 + } 56 + }
+25
modules/react-native-ui-text-view/ios/RNUITextViewManager.m
··· 1 + #import <React/RCTViewManager.h> 2 + 3 + @interface RCT_EXTERN_MODULE(RNUITextViewManager, RCTViewManager) 4 + RCT_REMAP_SHADOW_PROPERTY(numberOfLines, numberOfLines, NSInteger) 5 + RCT_REMAP_SHADOW_PROPERTY(allowsFontScaling, allowsFontScaling, BOOL) 6 + 7 + RCT_EXPORT_VIEW_PROPERTY(onTextLayout, RCTDirectEventBlock) 8 + RCT_EXPORT_VIEW_PROPERTY(ellipsizeMode, NSString) 9 + RCT_EXPORT_VIEW_PROPERTY(selectable, BOOL) 10 + 11 + @end 12 + 13 + @interface RCT_EXTERN_MODULE(RNUITextViewChildManager, RCTViewManager) 14 + RCT_REMAP_SHADOW_PROPERTY(text, text, NSString) 15 + RCT_REMAP_SHADOW_PROPERTY(color, color, UIColor) 16 + RCT_REMAP_SHADOW_PROPERTY(fontSize, fontSize, CGFloat) 17 + RCT_REMAP_SHADOW_PROPERTY(fontStyle, fontStyle, NSString) 18 + RCT_REMAP_SHADOW_PROPERTY(fontWeight, fontWeight, NSString) 19 + RCT_REMAP_SHADOW_PROPERTY(letterSpacing, letterSpacing, CGFloat) 20 + RCT_REMAP_SHADOW_PROPERTY(lineHeight, lineHeight, CGFloat) 21 + RCT_REMAP_SHADOW_PROPERTY(pointerEvents, pointerEvents, NSString) 22 + 23 + RCT_EXPORT_VIEW_PROPERTY(text, NSString) 24 + RCT_EXPORT_VIEW_PROPERTY(onPress, RCTBubblingEventBlock) 25 + @end
+30
modules/react-native-ui-text-view/ios/RNUITextViewManager.swift
··· 1 + @objc(RNUITextViewManager) 2 + class RNUITextViewManager: RCTViewManager { 3 + override func view() -> (RNUITextView) { 4 + return RNUITextView() 5 + } 6 + 7 + @objc override static func requiresMainQueueSetup() -> Bool { 8 + return true 9 + } 10 + 11 + override func shadowView() -> RCTShadowView { 12 + // Pass the bridge to the shadow view 13 + return RNUITextViewShadow(bridge: self.bridge) 14 + } 15 + } 16 + 17 + @objc(RNUITextViewChildManager) 18 + class RNUITextViewChildManager: RCTViewManager { 19 + override func view() -> (RNUITextViewChild) { 20 + return RNUITextViewChild() 21 + } 22 + 23 + @objc override static func requiresMainQueueSetup() -> Bool { 24 + return true 25 + } 26 + 27 + override func shadowView() -> RCTShadowView { 28 + return RNUITextViewChildShadow() 29 + } 30 + }
+147
modules/react-native-ui-text-view/ios/RNUITextViewShadow.swift
··· 1 + class RNUITextViewShadow: RCTShadowView { 2 + // Props 3 + @objc var numberOfLines: Int = 0 { 4 + didSet { 5 + if !YGNodeIsDirty(self.yogaNode) { 6 + self.setAttributedText() 7 + } 8 + } 9 + } 10 + @objc var allowsFontScaling: Bool = true 11 + 12 + var attributedText: NSAttributedString = NSAttributedString() 13 + var frameSize: CGSize = CGSize() 14 + 15 + var lineHeight: CGFloat = 0 16 + 17 + var bridge: RCTBridge 18 + 19 + init(bridge: RCTBridge) { 20 + self.bridge = bridge 21 + super.init() 22 + 23 + // We need to set a custom measure func here to calculate the height correctly 24 + YGNodeSetMeasureFunc(self.yogaNode) { node, width, widthMode, height, heightMode in 25 + // Get the shadowview and determine the needed size to set 26 + let shadowView = Unmanaged<RNUITextViewShadow>.fromOpaque(YGNodeGetContext(node)).takeUnretainedValue() 27 + return shadowView.getNeededSize(maxWidth: width) 28 + } 29 + 30 + // Subscribe to ynamic type size changes 31 + NotificationCenter.default.addObserver( 32 + self, 33 + selector: #selector(preferredContentSizeChanged(_:)), 34 + name: UIContentSizeCategory.didChangeNotification, 35 + object: nil 36 + ) 37 + } 38 + 39 + @objc func preferredContentSizeChanged(_ notification: Notification) { 40 + self.setAttributedText() 41 + } 42 + 43 + // Tell yoga not to use flexbox 44 + override func isYogaLeafNode() -> Bool { 45 + return true 46 + } 47 + 48 + // We only need to insert text children 49 + override func insertReactSubview(_ subview: RCTShadowView!, at atIndex: Int) { 50 + if subview.isKind(of: RNUITextViewChildShadow.self) { 51 + super.insertReactSubview(subview, at: atIndex) 52 + } 53 + } 54 + 55 + // Whenever the subvies update, set the text 56 + override func didUpdateReactSubviews() { 57 + self.setAttributedText() 58 + } 59 + 60 + // Whenever we layout, update the UI 61 + override func layoutSubviews(with layoutContext: RCTLayoutContext) { 62 + // Don't do anything if the layout is dirty 63 + if(YGNodeIsDirty(self.yogaNode)) { 64 + return 65 + } 66 + 67 + // Update the text 68 + self.bridge.uiManager.addUIBlock { uiManager, viewRegistry in 69 + guard let textView = viewRegistry?[self.reactTag] as? RNUITextView else { 70 + return 71 + } 72 + textView.setText(string: self.attributedText, size: self.frameSize, numberOfLines: self.numberOfLines) 73 + } 74 + } 75 + 76 + override func dirtyLayout() { 77 + super.dirtyLayout() 78 + YGNodeMarkDirty(self.yogaNode) 79 + } 80 + 81 + // Update the attributed text whenever changes are made to the subviews. 82 + func setAttributedText() -> Void { 83 + // Create an attributed string to store each of the segments 84 + let finalAttributedString = NSMutableAttributedString() 85 + 86 + self.reactSubviews().forEach { child in 87 + guard let child = child as? RNUITextViewChildShadow else { 88 + return 89 + } 90 + let scaledFontSize = self.allowsFontScaling ? 91 + UIFontMetrics.default.scaledValue(for: child.fontSize) : child.fontSize 92 + let font = UIFont.systemFont(ofSize: scaledFontSize, weight: child.getFontWeight()) 93 + 94 + // Set some generic attributes that don't need ranges 95 + let attributes: [NSAttributedString.Key:Any] = [ 96 + .font: font, 97 + .foregroundColor: child.color, 98 + ] 99 + 100 + // Create the attributed string with the generic attributes 101 + let string = NSMutableAttributedString(string: child.text, attributes: attributes) 102 + 103 + // Set the paragraph style attributes if necessary 104 + let paragraphStyle = NSMutableParagraphStyle() 105 + if child.lineHeight != 0.0 { 106 + paragraphStyle.minimumLineHeight = child.lineHeight 107 + paragraphStyle.maximumLineHeight = child.lineHeight 108 + string.addAttribute( 109 + NSAttributedString.Key.paragraphStyle, 110 + value: paragraphStyle, 111 + range: NSMakeRange(0, string.length) 112 + ) 113 + 114 + // Store that height 115 + self.lineHeight = child.lineHeight 116 + } else { 117 + self.lineHeight = font.lineHeight 118 + } 119 + 120 + finalAttributedString.append(string) 121 + } 122 + 123 + self.attributedText = finalAttributedString 124 + self.dirtyLayout() 125 + } 126 + 127 + // Create a YGSize based on the max width 128 + func getNeededSize(maxWidth: Float) -> YGSize { 129 + // Create the max size and figure out the size of the entire text 130 + let maxSize = CGSize(width: CGFloat(maxWidth), height: CGFloat(MAXFLOAT)) 131 + let textSize = self.attributedText.boundingRect(with: maxSize, options: .usesLineFragmentOrigin, context: nil) 132 + 133 + // Figure out how many total lines there are 134 + let totalLines = Int(ceil(textSize.height / self.lineHeight)) 135 + 136 + // Default to the text size 137 + var neededSize: CGSize = textSize.size 138 + 139 + // If the total lines > max number, return size with the max 140 + if self.numberOfLines != 0, totalLines > self.numberOfLines { 141 + neededSize = CGSize(width: CGFloat(maxWidth), height: CGFloat(CGFloat(self.numberOfLines) * self.lineHeight)) 142 + } 143 + 144 + self.frameSize = neededSize 145 + return YGSize(width: Float(neededSize.width), height: Float(neededSize.height)) 146 + } 147 + }
+9
modules/react-native-ui-text-view/package.json
··· 1 + { 2 + "name": "react-native-ui-text-view", 3 + "version": "0.1.0", 4 + "description": "UITextView in React Native on iOS", 5 + "main": "src/index", 6 + "author": "haileyok", 7 + "license": "MIT", 8 + "homepage": "https://github.com/bluesky-social/social-app/modules/react-native-ui-text-view" 9 + }
+42
modules/react-native-ui-text-view/react-native-ui-text-view.podspec
··· 1 + require "json" 2 + 3 + package = JSON.parse(File.read(File.join(__dir__, "package.json"))) 4 + folly_compiler_flags = '-DFOLLY_NO_CONFIG -DFOLLY_MOBILE=1 -DFOLLY_USE_LIBCPP=1 -Wno-comma -Wno-shorten-64-to-32' 5 + 6 + Pod::Spec.new do |s| 7 + s.name = "react-native-ui-text-view" 8 + s.version = package["version"] 9 + s.summary = package["description"] 10 + s.homepage = package["homepage"] 11 + s.license = package["license"] 12 + s.authors = package["author"] 13 + 14 + s.platforms = { :ios => "11.0" } 15 + s.source = { :git => ".git", :tag => "#{s.version}" } 16 + 17 + s.source_files = "ios/**/*.{h,m,mm,swift}" 18 + 19 + # Use install_modules_dependencies helper to install the dependencies if React Native version >=0.71.0. 20 + # See https://github.com/facebook/react-native/blob/febf6b7f33fdb4904669f99d795eba4c0f95d7bf/scripts/cocoapods/new_architecture.rb#L79. 21 + if respond_to?(:install_modules_dependencies, true) 22 + install_modules_dependencies(s) 23 + else 24 + s.dependency "React-Core" 25 + 26 + # Don't install the dependencies when we run `pod install` in the old architecture. 27 + if ENV['RCT_NEW_ARCH_ENABLED'] == '1' then 28 + s.compiler_flags = folly_compiler_flags + " -DRCT_NEW_ARCH_ENABLED=1" 29 + s.pod_target_xcconfig = { 30 + "HEADER_SEARCH_PATHS" => "\"$(PODS_ROOT)/boost\"", 31 + "OTHER_CPLUSPLUSFLAGS" => "-DFOLLY_NO_CONFIG -DFOLLY_MOBILE=1 -DFOLLY_USE_LIBCPP=1", 32 + "CLANG_CXX_LANGUAGE_STANDARD" => "c++17" 33 + } 34 + s.dependency "React-RCTFabric" 35 + s.dependency "React-Codegen" 36 + s.dependency "RCT-Folly" 37 + s.dependency "RCTRequired" 38 + s.dependency "RCTTypeSafety" 39 + s.dependency "ReactCommon/turbomodule/core" 40 + end 41 + end 42 + end
+76
modules/react-native-ui-text-view/src/UITextView.tsx
··· 1 + import React from 'react' 2 + import {Platform, StyleSheet, TextProps, ViewStyle} from 'react-native' 3 + import {RNUITextView, RNUITextViewChild} from './index' 4 + 5 + const TextAncestorContext = React.createContext<[boolean, ViewStyle]>([ 6 + false, 7 + StyleSheet.create({}), 8 + ]) 9 + const useTextAncestorContext = () => React.useContext(TextAncestorContext) 10 + 11 + const textDefaults: TextProps = { 12 + allowFontScaling: true, 13 + selectable: true, 14 + } 15 + 16 + export function UITextView({style, children, ...rest}: TextProps) { 17 + const [isAncestor, rootStyle] = useTextAncestorContext() 18 + 19 + // Flatten the styles, and apply the root styles when needed 20 + const flattenedStyle = React.useMemo( 21 + () => StyleSheet.flatten([rootStyle, style]), 22 + [rootStyle, style], 23 + ) 24 + 25 + if (Platform.OS !== 'ios') { 26 + throw new Error('UITextView is only available on iOS') 27 + } 28 + 29 + if (!isAncestor) { 30 + return ( 31 + <TextAncestorContext.Provider value={[true, flattenedStyle]}> 32 + <RNUITextView 33 + {...textDefaults} 34 + {...rest} 35 + ellipsizeMode={rest.ellipsizeMode ?? rest.lineBreakMode ?? 'tail'} 36 + style={[{flex: 1}, flattenedStyle]} 37 + onPress={undefined} // We want these to go to the children only 38 + onLongPress={undefined}> 39 + {React.Children.toArray(children).map((c, index) => { 40 + if (React.isValidElement(c)) { 41 + return c 42 + } else if (typeof c === 'string') { 43 + return ( 44 + <RNUITextViewChild 45 + key={index} 46 + style={flattenedStyle} 47 + text={c} 48 + {...rest} 49 + /> 50 + ) 51 + } 52 + })} 53 + </RNUITextView> 54 + </TextAncestorContext.Provider> 55 + ) 56 + } else { 57 + return ( 58 + <> 59 + {React.Children.toArray(children).map((c, index) => { 60 + if (React.isValidElement(c)) { 61 + return c 62 + } else if (typeof c === 'string') { 63 + return ( 64 + <RNUITextViewChild 65 + key={index} 66 + style={flattenedStyle} 67 + text={c} 68 + {...rest} 69 + /> 70 + ) 71 + } 72 + })} 73 + </> 74 + ) 75 + } 76 + }
+42
modules/react-native-ui-text-view/src/index.tsx
··· 1 + import { 2 + requireNativeComponent, 3 + UIManager, 4 + Platform, 5 + type ViewStyle, 6 + TextProps, 7 + } from 'react-native' 8 + 9 + const LINKING_ERROR = 10 + `The package 'react-native-ui-text-view' doesn't seem to be linked. Make sure: \n\n` + 11 + Platform.select({ios: "- You have run 'pod install'\n", default: ''}) + 12 + '- You rebuilt the app after installing the package\n' + 13 + '- You are not using Expo Go\n' 14 + 15 + export interface RNUITextViewProps extends TextProps { 16 + children: React.ReactNode 17 + style: ViewStyle[] 18 + } 19 + 20 + export interface RNUITextViewChildProps extends TextProps { 21 + text: string 22 + onTextPress?: (...args: any[]) => void 23 + onTextLongPress?: (...args: any[]) => void 24 + } 25 + 26 + export const RNUITextView = 27 + UIManager.getViewManagerConfig && 28 + UIManager.getViewManagerConfig('RNUITextView') != null 29 + ? requireNativeComponent<RNUITextViewProps>('RNUITextView') 30 + : () => { 31 + throw new Error(LINKING_ERROR) 32 + } 33 + 34 + export const RNUITextViewChild = 35 + UIManager.getViewManagerConfig && 36 + UIManager.getViewManagerConfig('RNUITextViewChild') != null 37 + ? requireNativeComponent<RNUITextViewChildProps>('RNUITextViewChild') 38 + : () => { 39 + throw new Error(LINKING_ERROR) 40 + } 41 + 42 + export * from './UITextView'
+2 -1
package.json
··· 175 175 "tlds": "^1.234.0", 176 176 "use-deep-compare": "^1.1.0", 177 177 "zeego": "^1.6.2", 178 - "zod": "^3.20.2" 178 + "zod": "^3.20.2", 179 + "react-native-ui-text-view": "link:./modules/react-native-ui-text-view" 179 180 }, 180 181 "devDependencies": { 181 182 "@atproto/dev-env": "^0.2.19",
+16 -14
src/view/com/lightbox/Lightbox.tsx
··· 1 1 import React from 'react' 2 - import {StyleSheet, View, Pressable} from 'react-native' 2 + import {LayoutAnimation, StyleSheet, View} from 'react-native' 3 3 import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' 4 4 import ImageView from './ImageViewing' 5 5 import {shareImageModal, saveImageToMediaLibrary} from 'lib/media/manip' ··· 105 105 return ( 106 106 <View style={[styles.footer]}> 107 107 {altText ? ( 108 - <Pressable 109 - onPress={() => setAltExpanded(!isAltExpanded)} 110 - onLongPress={() => {}} 111 - accessibilityRole="button"> 112 - <View> 113 - <Text 114 - selectable 115 - style={[s.gray3, styles.footerText]} 116 - numberOfLines={isAltExpanded ? undefined : 3}> 117 - {altText} 118 - </Text> 119 - </View> 120 - </Pressable> 108 + <View accessibilityRole="button" style={styles.footerText}> 109 + <Text 110 + style={[s.gray3]} 111 + numberOfLines={isAltExpanded ? undefined : 3} 112 + selectable 113 + onPress={() => { 114 + LayoutAnimation.configureNext({ 115 + duration: 300, 116 + update: {type: 'spring', springDamping: 0.7}, 117 + }) 118 + setAltExpanded(prev => !prev) 119 + }}> 120 + {altText} 121 + </Text> 122 + </View> 121 123 ) : null} 122 124 <View style={styles.footerBtns}> 123 125 <Button
+2 -1
src/view/com/post-thread/PostThread.tsx
··· 40 40 usePreferencesQuery, 41 41 } from '#/state/queries/preferences' 42 42 import {useSession} from '#/state/session' 43 - import {isNative} from '#/platform/detection' 43 + import {isAndroid, isNative} from '#/platform/detection' 44 44 import {logger} from '#/logger' 45 45 import {moderatePost_wrapped as moderatePost} from '#/lib/moderatePost_wrapped' 46 46 ··· 400 400 style={s.hContentRegion} 401 401 // @ts-ignore our .web version only -prf 402 402 desktopFixedHeight 403 + removeClippedSubviews={isAndroid ? false : undefined} 403 404 /> 404 405 ) 405 406 }
+3 -3
src/view/com/post-thread/PostThreadItem.tsx
··· 248 248 </View> 249 249 )} 250 250 251 - <Link 251 + <View 252 252 testID={`postThreadItem-by-${post.author.handle}`} 253 253 style={[styles.outer, styles.outerHighlighted, pal.border, pal.view]} 254 - noFeedback 255 254 accessible={false}> 256 255 <PostSandboxWarning /> 257 256 <View style={styles.layout}> ··· 370 369 richText={richText} 371 370 lineHeight={1.3} 372 371 style={s.flex1} 372 + selectable 373 373 /> 374 374 </View> 375 375 ) : undefined} ··· 445 445 /> 446 446 </View> 447 447 </View> 448 - </Link> 448 + </View> 449 449 <WhoCanReply post={post} /> 450 450 </> 451 451 )
+13 -3
src/view/com/util/text/RichText.tsx
··· 17 17 lineHeight = 1.2, 18 18 style, 19 19 numberOfLines, 20 + selectable, 20 21 noLinks, 21 22 }: { 22 23 testID?: string ··· 25 26 lineHeight?: number 26 27 style?: StyleProp<TextStyle> 27 28 numberOfLines?: number 29 + selectable?: boolean 28 30 noLinks?: boolean 29 31 }) { 30 32 const theme = useTheme() ··· 44 46 } 45 47 return ( 46 48 // @ts-ignore web only -prf 47 - <Text testID={testID} style={[style, pal.text]} dataSet={WORD_WRAP}> 49 + <Text 50 + testID={testID} 51 + style={[style, pal.text]} 52 + dataSet={WORD_WRAP} 53 + selectable={selectable}> 48 54 {text} 49 55 </Text> 50 56 ) ··· 56 62 style={[style, pal.text, lineHeightStyle]} 57 63 numberOfLines={numberOfLines} 58 64 // @ts-ignore web only -prf 59 - dataSet={WORD_WRAP}> 65 + dataSet={WORD_WRAP} 66 + selectable={selectable}> 60 67 {text} 61 68 </Text> 62 69 ) ··· 85 92 href={`/profile/${mention.did}`} 86 93 style={[style, lineHeightStyle, pal.link, {pointerEvents: 'auto'}]} 87 94 dataSet={WORD_WRAP} 95 + selectable={selectable} 88 96 />, 89 97 ) 90 98 } else if (link && AppBskyRichtextFacet.validateLink(link).success) { ··· 100 108 style={[style, lineHeightStyle, pal.link, {pointerEvents: 'auto'}]} 101 109 dataSet={WORD_WRAP} 102 110 warnOnMismatchingLabel 111 + selectable={selectable} 103 112 />, 104 113 ) 105 114 } ··· 115 124 style={[style, pal.text, lineHeightStyle]} 116 125 numberOfLines={numberOfLines} 117 126 // @ts-ignore web only -prf 118 - dataSet={WORD_WRAP}> 127 + dataSet={WORD_WRAP} 128 + selectable={selectable}> 119 129 {els} 120 130 </Text> 121 131 )
+16
src/view/com/util/text/Text.tsx
··· 2 2 import {Text as RNText, TextProps} from 'react-native' 3 3 import {s, lh} from 'lib/styles' 4 4 import {useTheme, TypographyVariant} from 'lib/ThemeContext' 5 + import {isIOS} from 'platform/detection' 6 + import {UITextView} from 'react-native-ui-text-view' 5 7 6 8 export type CustomTextProps = TextProps & { 7 9 type?: TypographyVariant 8 10 lineHeight?: number 9 11 title?: string 10 12 dataSet?: Record<string, string | number> 13 + selectable?: boolean 11 14 } 12 15 13 16 export function Text({ ··· 17 20 style, 18 21 title, 19 22 dataSet, 23 + selectable, 20 24 ...props 21 25 }: React.PropsWithChildren<CustomTextProps>) { 22 26 const theme = useTheme() 23 27 const typography = theme.typography[type] 24 28 const lineHeightStyle = lineHeight ? lh(theme, type, lineHeight) : undefined 29 + 30 + if (selectable && isIOS) { 31 + return ( 32 + <UITextView 33 + style={[s.black, typography, lineHeightStyle, style]} 34 + {...props}> 35 + {children} 36 + </UITextView> 37 + ) 38 + } 39 + 25 40 return ( 26 41 <RNText 27 42 style={[s.black, typography, lineHeightStyle, style]} 28 43 // @ts-ignore web only -esb 29 44 dataSet={Object.assign({tooltip: title}, dataSet || {})} 45 + selectable={selectable} 30 46 {...props}> 31 47 {children} 32 48 </RNText>
+4
yarn.lock
··· 18370 18370 css-select "^5.1.0" 18371 18371 css-tree "^1.1.3" 18372 18372 18373 + "react-native-ui-text-view@link:./modules/react-native-ui-text-view": 18374 + version "0.0.0" 18375 + uid "" 18376 + 18373 18377 react-native-url-polyfill@^1.3.0: 18374 18378 version "1.3.0" 18375 18379 resolved "https://registry.yarnpkg.com/react-native-url-polyfill/-/react-native-url-polyfill-1.3.0.tgz#c1763de0f2a8c22cc3e959b654c8790622b6ef6a"