Bluesky app fork with some witchin' additions 馃挮 witchsky.app
bluesky fork client
119
fork

Configure Feed

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

at a876aae44ea07494ebea9727350aa060b81f317b 166 lines 4.9 kB view raw
1import {useMemo, useRef, useState} from 'react' 2import { 3 TextInput, 4 type TextInputContentSizeChangeEvent, 5 type TextInputProps, 6} from 'react-native' 7 8import {mergeRefs} from '#/lib/merge-refs' 9import {atoms as a, extractPadding, useAlf, web} from '#/alf' 10import {normalizeTextStyles} from '#/alf/typography' 11import {IS_ANDROID, IS_IOS, IS_WEB} from '#/env' 12 13export type AutosizedTextareaProps = Omit<TextInputProps, 'multiline'> & { 14 ref?: React.Ref<TextInput> 15 label: string 16 minRows?: number 17 maxRows?: number 18 onUpdateHeight?: (height: number) => void 19} 20 21export function AutosizedTextarea({ 22 ref, 23 label, 24 minRows = 1, 25 maxRows, 26 onUpdateHeight, 27 28 onChangeText: onChangeTextOuter, 29 onContentSizeChange: onContentSizeChangeOuter, 30 style: outerStyle, 31 ...rest 32}: AutosizedTextareaProps) { 33 const {theme: t, fonts} = useAlf() 34 const internalRef = useRef<TextInput>(null) 35 const {style, minInputHeight, maxInputHeight, verticalContentPadding} = 36 useMemo(() => { 37 const normalizedStyles = normalizeTextStyles( 38 [a.text_md, a.leading_snug, t.atoms.text, outerStyle], 39 { 40 fontScale: fonts.scaleMultiplier, 41 fontFamily: fonts.family, 42 flags: {}, 43 }, 44 ) 45 const lineHeight = normalizedStyles.lineHeight || 20 46 const {paddingTop, paddingBottom} = extractPadding(normalizedStyles ?? {}) 47 const verticalContentPadding = paddingTop + paddingBottom 48 const minInputHeight = lineHeight * minRows + verticalContentPadding 49 const maxInputHeight = maxRows 50 ? lineHeight * maxRows + verticalContentPadding 51 : Infinity 52 53 /* 54 * iOS: minHeight/maxHeight works fine natively. 55 * Web + Android: we set an explicit initial height and resize dynamically 56 * (web via DOM measurement, Android via onContentSizeChange state). 57 * 58 * iOS also seems to need 1px headroom to actually expand to the correct 59 * maxHeight 60 */ 61 const heightConstraints = IS_IOS 62 ? {minHeight: minInputHeight, maxHeight: maxInputHeight + 1} 63 : {height: minInputHeight} 64 65 return { 66 style: { 67 ...normalizedStyles, 68 ...heightConstraints, 69 }, 70 minInputHeight, 71 maxInputHeight, 72 verticalContentPadding, 73 } 74 }, [t, fonts, outerStyle, minRows, maxRows]) 75 76 /* 77 * Web handling 78 */ 79 const prevWebHeight = useRef(0) 80 const handleResizeWeb = () => { 81 const el = internalRef.current as unknown as HTMLTextAreaElement 82 if (!el) return 83 // collapse to get natural scroll height 84 el.style.height = '0px' 85 const scrollHeight = Math.ceil(el.scrollHeight) 86 const nextHeight = Math.min( 87 Math.max(scrollHeight, minInputHeight), 88 maxInputHeight, 89 ) 90 // immediately update height to prevent flicker 91 el.style.height = `${nextHeight}px` 92 el.style.overflowY = scrollHeight > maxInputHeight ? 'auto' : 'hidden' 93 if (nextHeight !== prevWebHeight.current) { 94 prevWebHeight.current = nextHeight 95 onUpdateHeight?.(nextHeight) 96 } 97 } 98 const onChangeText = (text: string) => { 99 if (IS_WEB) handleResizeWeb() 100 onChangeTextOuter?.(text) 101 } 102 103 /* 104 * Native handling 105 * 106 * We track the height as state on native, and on Android, we use this to 107 * directly drive the `height`. 108 */ 109 const [nativeHeight, setNativeHeight] = useState(minInputHeight) 110 const onContentSizeChange = (e: TextInputContentSizeChangeEvent) => { 111 const contentSize = Math.ceil(e.nativeEvent.contentSize.height) 112 // ios reports the content size without padding 113 const height = IS_IOS ? contentSize + verticalContentPadding : contentSize 114 const nextHeight = Math.min( 115 Math.max(height, minInputHeight), 116 maxInputHeight, 117 ) 118 119 if (nextHeight !== nativeHeight) { 120 setNativeHeight(nextHeight) 121 onUpdateHeight?.(nextHeight) 122 } 123 124 onContentSizeChangeOuter?.(e) 125 } 126 127 return ( 128 <TextInput 129 multiline 130 placeholderTextColor={t.palette.contrast_500} 131 accessibilityLabel={label} 132 accessibilityHint={label} 133 placeholder={label} 134 keyboardAppearance={t.scheme} 135 submitBehavior="newline" 136 scrollEnabled={nativeHeight >= maxInputHeight} 137 style={[ 138 a.relative, 139 a.border_0, 140 { 141 textAlignVertical: 'top', 142 includeFontPadding: false, 143 }, 144 web({ 145 resize: 'none', 146 outline: 'none', 147 whiteSpace: 'pre-wrap', 148 wordBreak: 'break-word', 149 }), 150 style, 151 IS_ANDROID ? {height: nativeHeight} : {}, 152 ]} 153 {...rest} 154 ref={mergeRefs([ 155 (node: TextInput | null) => { 156 internalRef.current = node 157 // bop resize on first render 158 if (IS_WEB && node) handleResizeWeb() 159 }, 160 ref, 161 ])} 162 onChangeText={onChangeText} 163 onContentSizeChange={onContentSizeChange} 164 /> 165 ) 166}