This commit is contained in:
Hailey
2024-01-11 02:01:20 -08:00
parent c6d2e54923
commit 648552eafb
15 changed files with 132 additions and 78 deletions
+3 -25
View File
@@ -1,26 +1,4 @@
import { NativeModulesProxy, EventEmitter, Subscription } from 'expo-modules-core'; import ExpoSelectableTextView from './src/ExpoSelectableTextView'
import {ExpoProTextViewProps} from './src/ExpoSelectableText.types'
// Import the native module. On web, it will be resolved to ExpoSelectableText.web.ts export {ExpoSelectableTextView as SelectableText, type ExpoProTextViewProps}
// and on native platforms to ExpoSelectableText.ts
import ExpoSelectableTextModule from './src/ExpoSelectableTextModule';
import ExpoSelectableTextView from './src/ExpoSelectableTextView';
import { ChangeEventPayload, ExpoSelectableTextViewProps } from './src/ExpoSelectableText.types';
// Get the native constant value.
export const PI = ExpoSelectableTextModule.PI;
export function hello(): string {
return ExpoSelectableTextModule.hello();
}
export async function setValueAsync(value: string) {
return await ExpoSelectableTextModule.setValueAsync(value);
}
const emitter = new EventEmitter(ExpoSelectableTextModule ?? NativeModulesProxy.ExpoSelectableText);
export function addChangeListener(listener: (event: ChangeEventPayload) => void): Subscription {
return emitter.addListener<ChangeEventPayload>('onChange', listener);
}
export { ExpoSelectableTextView, ExpoSelectableTextViewProps, ChangeEventPayload };
@@ -1,26 +0,0 @@
import ExpoModulesCore
public class ExpoProTextModule: Module {
public func definition() -> ModuleDefinition {
Name("ExpoProText")
View(ExpoProTextView.self) {
Events("onTextPress", "onTextLongPress")
Prop("segments") { (view: ExpoProTextView, prop: String) in
// Convert the JSON to segments
if let data = prop.data(using: .utf8) {
print("yes")
if let segments = try? JSONDecoder().decode(TextSegments.self, from: data) {
print("yes2")
view.segments = segments.segments
}
}
}
Prop("selectable") { (view: ExpoProTextView, prop: Bool) in
view.textView.isSelectable = prop
}
}
}
}
@@ -1,5 +1,5 @@
Pod::Spec.new do |s| Pod::Spec.new do |s|
s.name = 'ExpoProText' s.name = 'ExpoSelectableText'
s.version = '1.0.0' s.version = '1.0.0'
s.summary = 'A sample project summary' s.summary = 'A sample project summary'
s.description = 'A sample project description' s.description = 'A sample project description'
@@ -16,6 +16,6 @@ Pod::Spec.new do |s|
'DEFINES_MODULE' => 'YES', 'DEFINES_MODULE' => 'YES',
'SWIFT_COMPILATION_MODE' => 'wholemodule' 'SWIFT_COMPILATION_MODE' => 'wholemodule'
} }
s.source_files = "**/*.{h,m,mm,swift,hpp,cpp}" s.source_files = "**/*.{h,m,mm,swift,hpp,cpp}"
end end
@@ -0,0 +1,32 @@
import ExpoModulesCore
public class ExpoSelectableTextModule: Module {
public func definition() -> ModuleDefinition {
Name("ExpoSelectableText")
View(ExpoSelectableTextView.self) {
Events("onTextPress", "onTextLongPress", "onTextLayout")
Prop("segments") { (view: ExpoSelectableTextView, prop: String) in
// Convert the JSON to segments
if let data = prop.data(using: .utf8) {
if let segments = try? JSONDecoder().decode(TextSegments.self, from: data) {
view.segments = segments.segments
}
}
}
Prop("selectable") { (view: ExpoSelectableTextView, prop: Bool) in
view.textView.isSelectable = prop
}
Prop("rootStyle") { (view: ExpoSelectableTextView, prop: String) in
if let data = prop.data(using: .utf8) {
if let style = try? JSONDecoder().decode(TextStyle.self, from: data) {
view.style = style
}
}
}
}
}
}
@@ -1,4 +1,4 @@
public class ExpoProTextUtil { public class ExpoSelectableTextUtil {
public static func hexToUIColor(hex: String?) -> UIColor { public static func hexToUIColor(hex: String?) -> UIColor {
guard let hex else { guard let hex else {
return UIColor.black return UIColor.black
@@ -1,13 +1,18 @@
import Foundation import Foundation
import ExpoModulesCore import ExpoModulesCore
class ExpoProTextView: ExpoView { class ExpoSelectableTextView: ExpoView {
let textView: UITextView = UITextView() let textView: UITextView = UITextView()
var segments: Array<TextSegment> = [] { var segments: Array<TextSegment> = [] {
didSet { didSet {
setText() setText()
} }
} }
var style: TextStyle? {
didSet {
self.setNeedsLayout()
}
}
let onTextPress = EventDispatcher() let onTextPress = EventDispatcher()
let onTextLongPress = EventDispatcher() let onTextLongPress = EventDispatcher()
@@ -22,6 +27,8 @@ class ExpoProTextView: ExpoView {
textView.isScrollEnabled = false textView.isScrollEnabled = false
textView.backgroundColor = .clear textView.backgroundColor = .clear
clipsToBounds = true
// Add the text view to the root view // Add the text view to the root view
addSubview(textView) addSubview(textView)
@@ -71,8 +78,8 @@ class ExpoProTextView: ExpoView {
segments.forEach { segment in segments.forEach { segment in
// Set some generic attributes that don't need ranges // Set some generic attributes that don't need ranges
let attributes: [NSAttributedString.Key:Any] = [ let attributes: [NSAttributedString.Key:Any] = [
.font: UIFont.systemFont(ofSize: segment.style?.fontSize ?? 12.0, weight: segment.style?.fontWeight?.toFontWeight() ?? .regular), .font: UIFont.systemFont(ofSize: segment.style?.fontSize ?? self.style?.fontSize ?? 12.0, weight: segment.style?.fontWeight?.toFontWeight() ?? self.style?.fontWeight?.toFontWeight() ?? .regular),
.foregroundColor: ExpoProTextUtil.hexToUIColor(hex: segment.style?.color), .foregroundColor: ExpoSelectableTextUtil.hexToUIColor(hex: segment.style?.color),
] ]
// Create the attributed string with the generic attributes // Create the attributed string with the generic attributes
@@ -81,7 +88,8 @@ class ExpoProTextView: ExpoView {
// Set the paragraph style attributes if necessary // Set the paragraph style attributes if necessary
if let lineHeight = segment.style?.lineHeight { if let lineHeight = segment.style?.lineHeight {
let paragraphStyle = NSMutableParagraphStyle() let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.lineSpacing = lineHeight paragraphStyle.minimumLineHeight = lineHeight
paragraphStyle.maximumLineHeight = lineHeight
string.addAttribute(NSAttributedString.Key.paragraphStyle, value: paragraphStyle, range: NSMakeRange(0, string.length)) string.addAttribute(NSAttributedString.Key.paragraphStyle, value: paragraphStyle, range: NSMakeRange(0, string.length))
} }
@@ -7,11 +7,21 @@ struct TextStyle: Decodable {
var textAlign: String? = "auto" var textAlign: String? = "auto"
var lineHeight: Double? var lineHeight: Double?
var textDecorationLine: TextDecorationLine? var textDecorationLine: TextDecorationLine?
var flex: Int?
} }
enum FontWeight: String, Decodable { enum FontWeight: String, Decodable {
case bold case bold
case normal case normal
case one = "100"
case two = "200"
case three = "300"
case four = "400"
case five = "500"
case six = "600"
case seven = "700"
case eight = "800"
case nine = "900"
func toFontWeight() -> UIFont.Weight { func toFontWeight() -> UIFont.Weight {
switch self { switch self {
@@ -19,6 +29,24 @@ enum FontWeight: String, Decodable {
return .bold return .bold
case .normal: case .normal:
return .regular return .regular
case .one:
return .ultraLight
case .two:
return .ultraLight
case .three:
return .light
case .four:
return .regular
case .five:
return .medium
case .six:
return .semibold
case .seven:
return .semibold
case .eight:
return .bold
case .nine:
return .heavy
} }
} }
} }
@@ -27,5 +55,6 @@ enum TextDecorationLine: String, Decodable {
case underline case underline
case lineThrough = "line-through" case lineThrough = "line-through"
case underlineLineThrough = "underline line-through" case underlineLineThrough = "underline line-through"
case normal case normal
} }
@@ -1,5 +1,5 @@
import React from 'react' import React from 'react'
import { ColorValue } from 'react-native' import {ColorValue, TextStyle, ViewStyle} from 'react-native'
interface ExpoProTextViewCommonProps { interface ExpoProTextViewCommonProps {
selectable?: boolean selectable?: boolean
@@ -7,7 +7,7 @@ interface ExpoProTextViewCommonProps {
export interface ExpoProTextViewProps extends ExpoProTextViewCommonProps { export interface ExpoProTextViewProps extends ExpoProTextViewCommonProps {
children: React.ReactNode children: React.ReactNode
style?: ExpoProTextStyle style?: TextStyle
onPress?: () => void onPress?: () => void
onLongPress?: () => void onLongPress?: () => void
} }
@@ -19,6 +19,8 @@ export interface ExpoProTextNativeViewProps extends ExpoProTextViewCommonProps {
onTextLongPress?: (event: ExpoProTextPressEvent) => void onTextLongPress?: (event: ExpoProTextPressEvent) => void
onTextLayout?: (event: ExpoProTextLayoutEvent) => void onTextLayout?: (event: ExpoProTextLayoutEvent) => void
disableLongPress: boolean disableLongPress: boolean
style: ViewStyle
rootStyle?: string
} }
interface ExpoProTextStyle { interface ExpoProTextStyle {
@@ -1,6 +1,6 @@
import * as React from 'react' import * as React from 'react'
import { ExpoProTextViewProps } from './ExpoProText.types' import {ExpoProTextViewProps} from './ExpoSelectableText.types'
export default function ExpoProTextView(props: ExpoProTextViewProps) { export default function ExpoProTextView(props: ExpoProTextViewProps) {
return ( return (
@@ -1,4 +1,4 @@
import { requireNativeViewManager } from 'expo-modules-core' import {requireNativeViewManager} from 'expo-modules-core'
import * as React from 'react' import * as React from 'react'
import { import {
@@ -7,37 +7,37 @@ import {
ExpoProTextPressEvent, ExpoProTextPressEvent,
ExpoProTextSegment, ExpoProTextSegment,
ExpoProTextViewProps, ExpoProTextViewProps,
} from './ExpoProText.types' } from './ExpoSelectableText.types'
import { Text, View } from 'react-native' import {StyleSheet, Text, View} from 'react-native'
const NativeView: React.ComponentType<ExpoProTextNativeViewProps> = const NativeView: React.ComponentType<ExpoProTextNativeViewProps> =
requireNativeViewManager('ExpoProText') requireNativeViewManager('ExpoSelectableText')
export default function ExpoProTextView({ export default function ExpoSelectableTextView({
style, style,
children, children,
selectable = false, selectable = false,
onPress, onPress,
onLongPress, onLongPress,
}: ExpoProTextViewProps) { }: ExpoProTextViewProps) {
const [dims, setDims] = React.useState({ height: 0 }) const [dims, setDims] = React.useState({height: 0})
const segmentPressCallbacks = React.useRef< const segmentPressCallbacks = React.useRef<
Array<{ index: number; onPress: () => void }> Array<{index: number; onPress: () => void}>
>([]) >([])
const segmentLongPressCallbacks = React.useRef< const segmentLongPressCallbacks = React.useRef<
Array<{ index: number, onLongPress: () => void, }> Array<{index: number; onLongPress: () => void}>
>([]) >([])
const onTextLayout = React.useCallback((e: ExpoProTextLayoutEvent) => { const onTextLayout = React.useCallback((e: ExpoProTextLayoutEvent) => {
console.log('layout')
setDims({ setDims({
height: e.nativeEvent.height, height: e.nativeEvent.height,
}) })
}, []) }, [])
const onTextPress = React.useCallback((e: ExpoProTextPressEvent) => { const onTextPress = React.useCallback((e: ExpoProTextPressEvent) => {
console.log(e.nativeEvent)
const onPressSegment = segmentPressCallbacks.current.find( const onPressSegment = segmentPressCallbacks.current.find(
(s) => s.index === e.nativeEvent.index, s => s.index === e.nativeEvent.index,
) )
onPressSegment?.onPress() onPressSegment?.onPress()
}, []) }, [])
@@ -58,7 +58,7 @@ export default function ExpoProTextView({
React.isValidElement(child) && React.isValidElement(child) &&
(child as React.ReactElement<any>).type === Text (child as React.ReactElement<any>).type === Text
) { ) {
const { onPress, onLongPress, children, style } = child.props const {onPress, onLongPress, children, style} = child.props
segments.push({ segments.push({
index, index,
@@ -88,21 +88,25 @@ export default function ExpoProTextView({
}, [children]) }, [children])
const segmentsJson = React.useMemo(() => { const segmentsJson = React.useMemo(() => {
const json = JSON.stringify({ segments: textSegments }) const json = JSON.stringify({segments: textSegments})
console.log(json)
return json return json
}, [textSegments]) }, [textSegments])
const rootStyle = React.useMemo(() => {
return style ? JSON.stringify(style) : undefined
}, [style])
return ( return (
<View style={dims}> <View style={[dims, {width: '100%'}]}>
<NativeView <NativeView
textStyle={style}
segments={segmentsJson} segments={segmentsJson}
selectable={selectable} selectable={selectable}
onTextPress={onTextPress} onTextPress={onTextPress}
onTextLongPress={onLongPress} onTextLongPress={onLongPress}
onTextLayout={onTextLayout} onTextLayout={onTextLayout}
disableLongPress={onLongPress !== undefined} disableLongPress={onLongPress !== undefined}
style={{flex: 1}}
rootStyle={rootStyle}
/> />
</View> </View>
) )
@@ -369,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}
+4 -1
View File
@@ -17,6 +17,7 @@ export function RichText({
lineHeight = 1.2, lineHeight = 1.2,
style, style,
numberOfLines, numberOfLines,
selectable,
}: { }: {
testID?: string testID?: string
type?: TypographyVariant type?: TypographyVariant
@@ -24,6 +25,7 @@ export function RichText({
lineHeight?: number lineHeight?: number
style?: StyleProp<TextStyle> style?: StyleProp<TextStyle>
numberOfLines?: number numberOfLines?: number
selectable?: boolean
}) { }) {
const theme = useTheme() const theme = useTheme()
const pal = usePalette('default') const pal = usePalette('default')
@@ -54,7 +56,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>
) )
+24 -1
View File
@@ -1,13 +1,17 @@
import React from 'react' import React from 'react'
import {Text as RNText, TextProps} from 'react-native' import {StyleSheet, 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 {SelectableText} from '../../../../../modules/expo-selectable-text'
import {isIOS} from 'platform/detection'
import {fontSize} from '#/alf/tokens'
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,11 +21,30 @@ 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
// {"color":"#000000","fontSize":20,"letterSpacing":0.2,"fontWeight":"400","flex":1,"lineHeight":26}
if (selectable && isIOS) {
return (
<SelectableText
selectable
style={StyleSheet.flatten([
s.black,
typography,
lineHeightStyle,
style,
])}>
{children}
</SelectableText>
)
}
return ( return (
<RNText <RNText
style={[s.black, typography, lineHeightStyle, style]} style={[s.black, typography, lineHeightStyle, style]}