Checkpoint: switch back to monolith
This commit is contained in:
+176
-269
@@ -1,7 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
createContext,
|
|
||||||
useCallback,
|
useCallback,
|
||||||
useContext,
|
|
||||||
useEffect,
|
useEffect,
|
||||||
useImperativeHandle,
|
useImperativeHandle,
|
||||||
useMemo,
|
useMemo,
|
||||||
@@ -15,7 +13,6 @@ import {
|
|||||||
View,
|
View,
|
||||||
} from 'react-native'
|
} from 'react-native'
|
||||||
import Animated, {
|
import Animated, {
|
||||||
type SharedValue,
|
|
||||||
useAnimatedStyle,
|
useAnimatedStyle,
|
||||||
useSharedValue,
|
useSharedValue,
|
||||||
} from 'react-native-reanimated'
|
} from 'react-native-reanimated'
|
||||||
@@ -23,7 +20,6 @@ import {useSift, type UseSiftReturn} from '@bsky.app/sift'
|
|||||||
import {
|
import {
|
||||||
type TapperActiveFacet,
|
type TapperActiveFacet,
|
||||||
type TapperFacet,
|
type TapperFacet,
|
||||||
type TapperSnapshot,
|
|
||||||
useTapper,
|
useTapper,
|
||||||
} from '@bsky.app/tapper'
|
} from '@bsky.app/tapper'
|
||||||
|
|
||||||
@@ -80,57 +76,27 @@ export function useComposerInternalApiRef() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* ─── Contexts ─────────────────────────────────────────────────────────────────
|
* ─── Composer ─────────────────────────────────────────────────────────────────
|
||||||
*/
|
*/
|
||||||
|
|
||||||
type ComposerContextValue = {
|
export type ComposerProps = Omit<
|
||||||
tapper: {
|
TextInputProps,
|
||||||
on: ReturnType<typeof useTapper>['on']
|
| 'value'
|
||||||
insert: ReturnType<typeof useTapper>['insert']
|
| 'onChange'
|
||||||
input: ReturnType<typeof useTapper>['input']
|
| 'onChangeText'
|
||||||
inputProps: ReturnType<typeof useTapper>['inputProps']
|
| 'onSelectionChange'
|
||||||
}
|
| 'selection'
|
||||||
sift: UseSiftReturn
|
| 'style'
|
||||||
inputScrollSharedValue: SharedValue<number>
|
| 'onSubmitEditing'
|
||||||
onRequestSubmit?: (request: SubmitRequest) => void
|
> & {
|
||||||
}
|
children?: React.ReactNode
|
||||||
|
label: string
|
||||||
const ComposerContext = createContext<ComposerContextValue | null>(null)
|
ref?: React.Ref<TextInput>
|
||||||
ComposerContext.displayName = 'ComposerContext'
|
style?: ViewStyleProp['style']
|
||||||
|
padding?: Parameters<typeof extractPadding>[0]
|
||||||
export function useComposerContext() {
|
textStyle?: TextStyleProp['style']
|
||||||
const ctx = useContext(ComposerContext)
|
initialNumberOfLines?: number
|
||||||
if (!ctx) {
|
maxNumberOfLines?: number
|
||||||
throw new Error('useComposerContext must be used within a Composer.Root')
|
|
||||||
}
|
|
||||||
return ctx
|
|
||||||
}
|
|
||||||
|
|
||||||
type ComposerStateContextValue = {
|
|
||||||
state: TapperSnapshot
|
|
||||||
}
|
|
||||||
|
|
||||||
const ComposerStateContext = createContext<ComposerStateContextValue | null>(
|
|
||||||
null,
|
|
||||||
)
|
|
||||||
ComposerStateContext.displayName = 'ComposerStateContext'
|
|
||||||
|
|
||||||
export function useComposerStateContext() {
|
|
||||||
const ctx = useContext(ComposerStateContext)
|
|
||||||
if (!ctx) {
|
|
||||||
throw new Error(
|
|
||||||
'useComposerStateContext must be used within a Composer.Root',
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return ctx
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* ─── Root ─────────────────────────────────────────────────────────────────────
|
|
||||||
*/
|
|
||||||
|
|
||||||
export type RootProps = {
|
|
||||||
children: React.ReactNode
|
|
||||||
initialText?: string
|
initialText?: string
|
||||||
onChange?: (text: string) => void
|
onChange?: (text: string) => void
|
||||||
onActiveFacet?: (activeFacet: TapperActiveFacet | null) => void
|
onActiveFacet?: (activeFacet: TapperActiveFacet | null) => void
|
||||||
@@ -139,34 +105,42 @@ export type RootProps = {
|
|||||||
internalApiRef?: React.Ref<ComposerInternalApi>
|
internalApiRef?: React.Ref<ComposerInternalApi>
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Root({
|
export function Composer({
|
||||||
children,
|
children,
|
||||||
|
label,
|
||||||
|
placeholder,
|
||||||
|
style,
|
||||||
|
padding,
|
||||||
|
textStyle: rawTextStyle,
|
||||||
|
initialNumberOfLines = 1,
|
||||||
|
maxNumberOfLines,
|
||||||
initialText,
|
initialText,
|
||||||
onChange: onChangeOuter,
|
onChange: onChangeOuter,
|
||||||
onActiveFacet: onActiveFacetOuter,
|
onActiveFacet: onActiveFacetOuter,
|
||||||
onFacetCommitted: onFacetCommittedOuter,
|
onFacetCommitted: onFacetCommittedOuter,
|
||||||
onRequestSubmit,
|
onRequestSubmit,
|
||||||
internalApiRef,
|
internalApiRef,
|
||||||
}: RootProps) {
|
...rest
|
||||||
const tapper = useTapper({
|
}: ComposerProps) {
|
||||||
initialText,
|
const {theme: t, fonts} = useAlf()
|
||||||
})
|
const textInputRef = useRef<TextInput>(null)
|
||||||
|
|
||||||
|
const tapper = useTapper({initialText})
|
||||||
const sift = useSift({
|
const sift = useSift({
|
||||||
offset: a.p_sm.padding,
|
offset: a.p_sm.padding,
|
||||||
placement: 'top-start',
|
placement: 'top-start',
|
||||||
dynamicWidth: IS_WEB,
|
dynamicWidth: IS_WEB,
|
||||||
})
|
})
|
||||||
const inputScrollSharedValue = useSharedValue(0)
|
const inputScrollSharedValue = useSharedValue(0)
|
||||||
|
const [activeFacet, setActiveFacet] = useState<TapperActiveFacet | null>(null)
|
||||||
|
|
||||||
const callbackRefs = useRef({
|
const callbackRefs = useRef({
|
||||||
onActiveFacetOuter,
|
onActiveFacetOuter,
|
||||||
onFacetCommittedOuter,
|
onFacetCommittedOuter,
|
||||||
focus: tapper.input.focus,
|
|
||||||
})
|
})
|
||||||
callbackRefs.current = {
|
callbackRefs.current = {
|
||||||
onActiveFacetOuter,
|
onActiveFacetOuter,
|
||||||
onFacetCommittedOuter,
|
onFacetCommittedOuter,
|
||||||
focus: tapper.input.focus,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
useImperativeHandle(
|
useImperativeHandle(
|
||||||
@@ -197,95 +171,23 @@ export function Root({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const offActiveFacet = tapper.on('activeFacet', facet => {
|
const offActiveFacet = tapper.on('activeFacet', facet => {
|
||||||
|
setActiveFacet(facet)
|
||||||
callbackRefs.current.onActiveFacetOuter?.(facet)
|
callbackRefs.current.onActiveFacetOuter?.(facet)
|
||||||
})
|
})
|
||||||
const offFacetCommitted = tapper.on('facetCommitted', facet => {
|
const offFacetCommitted = tapper.on('facetCommitted', facet => {
|
||||||
callbackRefs.current.onFacetCommittedOuter?.(facet)
|
callbackRefs.current.onFacetCommittedOuter?.(facet)
|
||||||
})
|
})
|
||||||
const offAfterInsert = tapper.on('afterInsert', () => {
|
const offAfterInsert = tapper.on('afterInsert', () => {
|
||||||
callbackRefs.current?.focus()
|
tapper.input.focus()
|
||||||
})
|
})
|
||||||
return () => {
|
return () => {
|
||||||
offActiveFacet()
|
offActiveFacet()
|
||||||
offFacetCommitted()
|
offFacetCommitted()
|
||||||
offAfterInsert()
|
offAfterInsert()
|
||||||
}
|
}
|
||||||
}, [tapper.on])
|
}, [tapper.on, tapper.input])
|
||||||
|
|
||||||
const composerCtx = useMemo<ComposerContextValue>(
|
// ─── Text style computation ───────────────────────────────────────────
|
||||||
() => ({
|
|
||||||
tapper: {
|
|
||||||
on: tapper.on,
|
|
||||||
insert: tapper.insert,
|
|
||||||
input: tapper.input,
|
|
||||||
inputProps: tapper.inputProps,
|
|
||||||
},
|
|
||||||
sift,
|
|
||||||
inputScrollSharedValue,
|
|
||||||
onRequestSubmit,
|
|
||||||
}),
|
|
||||||
[
|
|
||||||
tapper.on,
|
|
||||||
tapper.insert,
|
|
||||||
tapper.input,
|
|
||||||
tapper.inputProps,
|
|
||||||
sift,
|
|
||||||
inputScrollSharedValue,
|
|
||||||
onRequestSubmit,
|
|
||||||
],
|
|
||||||
)
|
|
||||||
|
|
||||||
const stateCtx = useMemo<ComposerStateContextValue>(
|
|
||||||
() => ({state: tapper.state}),
|
|
||||||
[tapper.state],
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ComposerContext.Provider value={composerCtx}>
|
|
||||||
<ComposerStateContext.Provider value={stateCtx}>
|
|
||||||
{children}
|
|
||||||
</ComposerStateContext.Provider>
|
|
||||||
</ComposerContext.Provider>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* ─── Input ────────────────────────────────────────────────────────────────────
|
|
||||||
*/
|
|
||||||
|
|
||||||
export type InputProps = Omit<
|
|
||||||
TextInputProps,
|
|
||||||
| 'value'
|
|
||||||
| 'onChangeText'
|
|
||||||
| 'onSelectionChange'
|
|
||||||
| 'selection'
|
|
||||||
| 'style'
|
|
||||||
| 'onSubmitEditing'
|
|
||||||
> & {
|
|
||||||
label: string
|
|
||||||
ref?: React.Ref<TextInput>
|
|
||||||
style?: ViewStyleProp['style']
|
|
||||||
padding?: Parameters<typeof extractPadding>[0]
|
|
||||||
textStyle?: TextStyleProp['style']
|
|
||||||
initialNumberOfLines?: number
|
|
||||||
maxNumberOfLines?: number
|
|
||||||
}
|
|
||||||
|
|
||||||
export function Input({
|
|
||||||
label,
|
|
||||||
placeholder,
|
|
||||||
style,
|
|
||||||
padding,
|
|
||||||
textStyle: rawTextStyle,
|
|
||||||
initialNumberOfLines = 1,
|
|
||||||
maxNumberOfLines,
|
|
||||||
...rest
|
|
||||||
}: InputProps) {
|
|
||||||
const {theme: t, fonts} = useAlf()
|
|
||||||
const {tapper, sift, inputScrollSharedValue, onRequestSubmit} =
|
|
||||||
useComposerContext()
|
|
||||||
const {state} = useComposerStateContext()
|
|
||||||
const textInputRef = useRef<TextInput>(null)
|
|
||||||
|
|
||||||
const {textStyle, textAreaStyle, minHeight, maxHeight} = useMemo(() => {
|
const {textStyle, textAreaStyle, minHeight, maxHeight} = useMemo(() => {
|
||||||
const ts = normalizeTextStyles(
|
const ts = normalizeTextStyles(
|
||||||
@@ -309,11 +211,6 @@ export function Input({
|
|||||||
? {height: lineHeight + verticalSpace}
|
? {height: lineHeight + verticalSpace}
|
||||||
: {minHeight: mh, maxHeight: xh}
|
: {minHeight: mh, maxHeight: xh}
|
||||||
|
|
||||||
/*
|
|
||||||
* On iOS especially, TextInput and Text line height does not render the
|
|
||||||
* same way, but setting this to undefined and using the default font
|
|
||||||
* metrics works fine.
|
|
||||||
*/
|
|
||||||
if (!IS_WEB) {
|
if (!IS_WEB) {
|
||||||
delete ts.lineHeight
|
delete ts.lineHeight
|
||||||
}
|
}
|
||||||
@@ -321,6 +218,15 @@ export function Input({
|
|||||||
return {textStyle: ts, textAreaStyle: tas, minHeight: mh, maxHeight: xh}
|
return {textStyle: ts, textAreaStyle: tas, minHeight: mh, maxHeight: xh}
|
||||||
}, [t, fonts, padding, rawTextStyle, initialNumberOfLines, maxNumberOfLines])
|
}, [t, fonts, padding, rawTextStyle, initialNumberOfLines, maxNumberOfLines])
|
||||||
|
|
||||||
|
// ─── Height auto-resize + sift positioning ────────────────────────────
|
||||||
|
|
||||||
|
const updateAutocompletePosition = useCallback(() => {
|
||||||
|
sift.updatePosition()
|
||||||
|
}, [sift])
|
||||||
|
|
||||||
|
useOnKeyboard('keyboardDidShow', updateAutocompletePosition)
|
||||||
|
useOnKeyboard('keyboardDidHide', updateAutocompletePosition)
|
||||||
|
|
||||||
const prevHeight = useRef(0)
|
const prevHeight = useRef(0)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (IS_WEB) {
|
if (IS_WEB) {
|
||||||
@@ -333,7 +239,7 @@ export function Input({
|
|||||||
el.style.overflowY = scrollHeight > maxHeight ? 'auto' : 'hidden'
|
el.style.overflowY = scrollHeight > maxHeight ? 'auto' : 'hidden'
|
||||||
if (nextHeight !== prevHeight.current) {
|
if (nextHeight !== prevHeight.current) {
|
||||||
prevHeight.current = nextHeight
|
prevHeight.current = nextHeight
|
||||||
sift.updatePosition()
|
updateAutocompletePosition()
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -341,15 +247,19 @@ export function Input({
|
|||||||
textInputRef.current?.measure((_x, _y, _w, h) => {
|
textInputRef.current?.measure((_x, _y, _w, h) => {
|
||||||
if (h !== prevHeight.current) {
|
if (h !== prevHeight.current) {
|
||||||
prevHeight.current = h
|
prevHeight.current = h
|
||||||
sift.updatePosition()
|
updateAutocompletePosition()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}, [state.text, minHeight, maxHeight, sift])
|
}, [tapper.state.text, minHeight, maxHeight, updateAutocompletePosition])
|
||||||
|
|
||||||
|
// ─── Scroll sync ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
const previewScrollStyle = useAnimatedStyle(() => ({
|
const previewScrollStyle = useAnimatedStyle(() => ({
|
||||||
transform: [{translateY: -inputScrollSharedValue.value}],
|
transform: [{translateY: -inputScrollSharedValue.value}],
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
// ─── Web keyboard handling ────────────────────────────────────────────
|
||||||
|
|
||||||
const isComposing = useRef(false)
|
const isComposing = useRef(false)
|
||||||
const onKeyPressWeb = useCallback(
|
const onKeyPressWeb = useCallback(
|
||||||
(e: React.KeyboardEvent | any) => {
|
(e: React.KeyboardEvent | any) => {
|
||||||
@@ -377,137 +287,126 @@ export function Input({
|
|||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={[a.relative, style]}>
|
<>
|
||||||
<View
|
<View style={[a.relative, style]}>
|
||||||
pointerEvents="none"
|
<View
|
||||||
style={[a.absolute, a.inset_0, a.z_10, {overflow: 'hidden'}]}>
|
pointerEvents="none"
|
||||||
<Animated.View
|
style={[a.absolute, a.inset_0, a.z_10, {overflow: 'hidden'}]}>
|
||||||
|
<Animated.View
|
||||||
|
style={[
|
||||||
|
padding,
|
||||||
|
{position: 'absolute', left: 0, right: 0},
|
||||||
|
previewScrollStyle,
|
||||||
|
]}>
|
||||||
|
<Text style={[textStyle, web({whiteSpace: 'pre-wrap'})]}>
|
||||||
|
{tapper.state.nodes.map((node, i) => {
|
||||||
|
switch (node.type) {
|
||||||
|
case 'text':
|
||||||
|
return <Span key={i}>{node.value}</Span>
|
||||||
|
case 'trigger':
|
||||||
|
case 'facet':
|
||||||
|
return (
|
||||||
|
<Span
|
||||||
|
key={i}
|
||||||
|
ref={IS_WEB ? sift.refs.setAnchor : undefined}
|
||||||
|
style={
|
||||||
|
node.type === 'facet' && {
|
||||||
|
color: t.palette.primary_500,
|
||||||
|
}
|
||||||
|
}>
|
||||||
|
{node.raw}
|
||||||
|
</Span>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})}
|
||||||
|
</Text>
|
||||||
|
</Animated.View>
|
||||||
|
</View>
|
||||||
|
<TextInput
|
||||||
|
dirName="ltr"
|
||||||
|
autoCapitalize="none"
|
||||||
|
autoCorrect={false}
|
||||||
|
multiline={true}
|
||||||
|
hitSlop={HITSLOP_10}
|
||||||
|
placeholder={placeholder}
|
||||||
|
placeholderTextColor={t.palette.contrast_500}
|
||||||
|
accessibilityLabel={label}
|
||||||
|
accessibilityHint={label}
|
||||||
|
keyboardAppearance={t.scheme}
|
||||||
|
submitBehavior="newline"
|
||||||
|
onSubmitEditing={e => {
|
||||||
|
onRequestSubmit?.({platform: 'native', nativeEvent: e})
|
||||||
|
}}
|
||||||
style={[
|
style={[
|
||||||
|
textStyle,
|
||||||
padding,
|
padding,
|
||||||
{position: 'absolute', left: 0, right: 0},
|
a.relative,
|
||||||
previewScrollStyle,
|
a.z_20,
|
||||||
]}>
|
a.border_0,
|
||||||
<Text style={[textStyle, web({whiteSpace: 'pre-wrap'})]}>
|
{
|
||||||
{state.nodes.map((node, i) => {
|
color: 'transparent',
|
||||||
switch (node.type) {
|
background: 'transparent',
|
||||||
case 'text':
|
textAlignVertical: 'top',
|
||||||
return <Span key={i}>{node.value}</Span>
|
includeFontPadding: false,
|
||||||
case 'trigger':
|
},
|
||||||
case 'facet':
|
textAreaStyle,
|
||||||
return (
|
web({
|
||||||
<Span
|
resize: 'none',
|
||||||
key={i}
|
outline: 'none',
|
||||||
ref={IS_WEB ? sift.refs.setAnchor : undefined}
|
caretColor: textStyle.color ?? 'black',
|
||||||
style={
|
whiteSpace: 'pre-wrap',
|
||||||
node.type === 'facet' && {color: t.palette.primary_500}
|
wordBreak: 'break-word',
|
||||||
}>
|
overscrollBehavior: 'none',
|
||||||
{node.raw}
|
...textAreaStyle,
|
||||||
</Span>
|
}),
|
||||||
)
|
]}
|
||||||
}
|
{...rest}
|
||||||
})}
|
{...tapper.inputProps}
|
||||||
</Text>
|
{...sift.targetProps}
|
||||||
</Animated.View>
|
ref={mergeRefs([
|
||||||
|
textInputRef,
|
||||||
|
rest.ref,
|
||||||
|
tapper.inputProps.ref,
|
||||||
|
sift.targetProps.ref,
|
||||||
|
])}
|
||||||
|
onBlur={e => {
|
||||||
|
rest.onBlur?.(e)
|
||||||
|
}}
|
||||||
|
onKeyPress={IS_WEB ? onKeyPressWeb : undefined}
|
||||||
|
onScroll={e => {
|
||||||
|
if (IS_WEB) {
|
||||||
|
inputScrollSharedValue.value = (e.target as any).scrollTop
|
||||||
|
} else {
|
||||||
|
inputScrollSharedValue.value = e.nativeEvent.contentOffset.y
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
// @ts-ignore web only
|
||||||
|
onCompositionStart={() => {
|
||||||
|
isComposing.current = true
|
||||||
|
}}
|
||||||
|
// @ts-ignore web only
|
||||||
|
onCompositionEnd={() => {
|
||||||
|
isComposing.current = false
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{children}
|
||||||
</View>
|
</View>
|
||||||
<TextInput
|
|
||||||
dirName="ltr"
|
{activeFacet && (
|
||||||
autoCapitalize="none"
|
<AutocompleteInner
|
||||||
autoCorrect={false}
|
sift={sift}
|
||||||
multiline={true}
|
activeFacet={activeFacet}
|
||||||
hitSlop={HITSLOP_10}
|
onDismiss={() => setActiveFacet(null)}
|
||||||
placeholder={placeholder}
|
/>
|
||||||
placeholderTextColor={t.palette.contrast_500}
|
)}
|
||||||
accessibilityLabel={label}
|
</>
|
||||||
accessibilityHint={label}
|
|
||||||
keyboardAppearance={t.scheme}
|
|
||||||
submitBehavior="newline"
|
|
||||||
onSubmitEditing={e => {
|
|
||||||
onRequestSubmit?.({platform: 'native', nativeEvent: e})
|
|
||||||
}}
|
|
||||||
style={[
|
|
||||||
textStyle,
|
|
||||||
padding,
|
|
||||||
a.relative,
|
|
||||||
a.z_20,
|
|
||||||
a.border_0,
|
|
||||||
{
|
|
||||||
color: 'transparent',
|
|
||||||
background: 'transparent',
|
|
||||||
textAlignVertical: 'top',
|
|
||||||
includeFontPadding: false,
|
|
||||||
},
|
|
||||||
textAreaStyle,
|
|
||||||
web({
|
|
||||||
resize: 'none',
|
|
||||||
outline: 'none',
|
|
||||||
caretColor: textStyle.color ?? 'black',
|
|
||||||
whiteSpace: 'pre-wrap',
|
|
||||||
wordBreak: 'break-word',
|
|
||||||
overscrollBehavior: 'none',
|
|
||||||
...textAreaStyle,
|
|
||||||
}),
|
|
||||||
]}
|
|
||||||
{...rest}
|
|
||||||
{...tapper.inputProps}
|
|
||||||
{...sift.targetProps}
|
|
||||||
ref={mergeRefs([
|
|
||||||
textInputRef,
|
|
||||||
rest.ref,
|
|
||||||
tapper.inputProps.ref,
|
|
||||||
sift.targetProps.ref,
|
|
||||||
])}
|
|
||||||
onBlur={e => {
|
|
||||||
rest.onBlur?.(e)
|
|
||||||
}}
|
|
||||||
onKeyPress={IS_WEB ? onKeyPressWeb : undefined}
|
|
||||||
onScroll={e => {
|
|
||||||
if (IS_WEB) {
|
|
||||||
inputScrollSharedValue.value = (e.target as any).scrollTop
|
|
||||||
} else {
|
|
||||||
inputScrollSharedValue.value = e.nativeEvent.contentOffset.y
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
// @ts-ignore web only
|
|
||||||
onCompositionStart={() => {
|
|
||||||
isComposing.current = true
|
|
||||||
}}
|
|
||||||
// @ts-ignore web only
|
|
||||||
onCompositionEnd={() => {
|
|
||||||
isComposing.current = false
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Autocomplete() {
|
/*
|
||||||
const {tapper, sift} = useComposerContext()
|
* ─── Autocomplete (private) ───────────────────────────────────────────────────
|
||||||
const [activeFacet, setActiveFacet] = useState<TapperActiveFacet | null>(null)
|
*/
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const off = tapper.on('activeFacet', facet => {
|
|
||||||
setActiveFacet(facet)
|
|
||||||
})
|
|
||||||
return off
|
|
||||||
}, [tapper.on])
|
|
||||||
|
|
||||||
const updatePosition = useCallback(() => {
|
|
||||||
sift.updatePosition()
|
|
||||||
}, [sift])
|
|
||||||
|
|
||||||
useOnKeyboard('keyboardDidShow', updatePosition)
|
|
||||||
useOnKeyboard('keyboardDidHide', updatePosition)
|
|
||||||
|
|
||||||
if (!activeFacet) return null
|
|
||||||
|
|
||||||
return (
|
|
||||||
<AutocompleteInner
|
|
||||||
sift={sift}
|
|
||||||
activeFacet={activeFacet}
|
|
||||||
onDismiss={() => setActiveFacet(null)}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function AutocompleteInner({
|
function AutocompleteInner({
|
||||||
sift,
|
sift,
|
||||||
@@ -523,6 +422,13 @@ function AutocompleteInner({
|
|||||||
query: activeFacet.value,
|
query: activeFacet.value,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const updatePosition = useCallback(() => {
|
||||||
|
sift.updatePosition()
|
||||||
|
}, [sift])
|
||||||
|
|
||||||
|
useOnKeyboard('keyboardDidShow', updatePosition)
|
||||||
|
useOnKeyboard('keyboardDidHide', updatePosition)
|
||||||
|
|
||||||
return data && data.length ? (
|
return data && data.length ? (
|
||||||
<AutocompleteBase
|
<AutocompleteBase
|
||||||
sift={sift}
|
sift={sift}
|
||||||
@@ -535,6 +441,7 @@ function AutocompleteInner({
|
|||||||
}}
|
}}
|
||||||
onSelect={item => {
|
onSelect={item => {
|
||||||
activeFacet.replace(item.value)
|
activeFacet.replace(item.value)
|
||||||
|
onDismiss()
|
||||||
}}
|
}}
|
||||||
onDismiss={onDismiss}
|
onDismiss={onDismiss}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import {
|
|||||||
type EmojiPickerState,
|
type EmojiPickerState,
|
||||||
} from '#/view/com/composer/text-input/web/EmojiPicker'
|
} from '#/view/com/composer/text-input/web/EmojiPicker'
|
||||||
import {atoms as a, useTheme} from '#/alf'
|
import {atoms as a, useTheme} from '#/alf'
|
||||||
import * as Composer from '#/components/Composer'
|
import {Composer, useComposerInternalApiRef} from '#/components/Composer'
|
||||||
import {useInteractionState} from '#/components/hooks/useInteractionState'
|
import {useInteractionState} from '#/components/hooks/useInteractionState'
|
||||||
import {EmojiArc_Stroke2_Corner0_Rounded as EmojiSmile} from '#/components/icons/Emoji'
|
import {EmojiArc_Stroke2_Corner0_Rounded as EmojiSmile} from '#/components/icons/Emoji'
|
||||||
import {PaperPlane_Stroke2_Corner0_Rounded as PaperPlane} from '#/components/icons/PaperPlane'
|
import {PaperPlane_Stroke2_Corner0_Rounded as PaperPlane} from '#/components/icons/PaperPlane'
|
||||||
@@ -46,7 +46,7 @@ export function MessageComposer({
|
|||||||
isOpen: false,
|
isOpen: false,
|
||||||
pos: {top: 0, left: 0, right: 0, bottom: 0, nextFocusRef: null},
|
pos: {top: 0, left: 0, right: 0, bottom: 0, nextFocusRef: null},
|
||||||
})
|
})
|
||||||
const composerInternalApiRef = Composer.useComposerInternalApiRef()
|
const composerInternalApiRef = useComposerInternalApiRef()
|
||||||
|
|
||||||
const {state: focused, onIn: onFocus, onOut: onBlur} = useInteractionState()
|
const {state: focused, onIn: onFocus, onOut: onBlur} = useInteractionState()
|
||||||
const {
|
const {
|
||||||
@@ -107,62 +107,59 @@ export function MessageComposer({
|
|||||||
<>
|
<>
|
||||||
<View style={[a.px_md, a.pb_sm, a.pt_xs]}>
|
<View style={[a.px_md, a.pb_sm, a.pt_xs]}>
|
||||||
{children}
|
{children}
|
||||||
<Composer.Root
|
<View
|
||||||
internalApiRef={composerInternalApiRef}
|
// @ts-expect-error web only
|
||||||
initialText={text}
|
onMouseEnter={onHoverIn}
|
||||||
onChange={setText}
|
onMouseLeave={onHoverOut}>
|
||||||
onFacetCommitted={facet => {
|
<Composer
|
||||||
if (facet.type === 'url' && isBskyPostUrl(facet.value)) {
|
internalApiRef={composerInternalApiRef}
|
||||||
setEmbed(facet.value)
|
initialText={text}
|
||||||
}
|
onChange={setText}
|
||||||
}}
|
editable={editable}
|
||||||
onRequestSubmit={req => {
|
autoFocus={IS_WEB}
|
||||||
if (req.platform === 'web' && req.shiftKey) return
|
label={l`Message input field`}
|
||||||
req.nativeEvent.preventDefault()
|
placeholder={l`Write a message`}
|
||||||
onSubmit()
|
maxNumberOfLines={12}
|
||||||
}}>
|
style={[
|
||||||
<View
|
t.atoms.bg_contrast_25,
|
||||||
// @ts-expect-error web only
|
{
|
||||||
onMouseEnter={onHoverIn}
|
borderWidth: 1,
|
||||||
onMouseLeave={onHoverOut}>
|
borderColor: 'transparent',
|
||||||
<Composer.Input
|
borderRadius: 25,
|
||||||
editable={editable}
|
},
|
||||||
autoFocus={IS_WEB}
|
editable &&
|
||||||
label={l`Message input field`}
|
hovered && {
|
||||||
placeholder={l`Write a message`}
|
borderColor: t.atoms.border_contrast_medium.borderColor,
|
||||||
maxNumberOfLines={12}
|
|
||||||
style={[
|
|
||||||
t.atoms.bg_contrast_25,
|
|
||||||
{
|
|
||||||
borderWidth: 1,
|
|
||||||
borderColor: 'transparent',
|
|
||||||
borderRadius: 25,
|
|
||||||
},
|
},
|
||||||
editable &&
|
editable &&
|
||||||
hovered && {
|
focused && {
|
||||||
borderColor: t.atoms.border_contrast_medium.borderColor,
|
borderColor: t.palette.primary_500,
|
||||||
},
|
|
||||||
editable &&
|
|
||||||
focused && {
|
|
||||||
borderColor: t.palette.primary_500,
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
padding={[
|
|
||||||
a.p_md,
|
|
||||||
{
|
|
||||||
paddingRight: 35 + a.p_sm.padding,
|
|
||||||
},
|
},
|
||||||
IS_WEB
|
]}
|
||||||
? {
|
padding={[
|
||||||
paddingLeft: 30 + a.p_sm.padding,
|
a.p_md,
|
||||||
}
|
{
|
||||||
: {},
|
paddingRight: 35 + a.p_sm.padding,
|
||||||
]}
|
},
|
||||||
textStyle={[a.text_md, a.leading_snug]}
|
IS_WEB
|
||||||
onFocus={onFocus}
|
? {
|
||||||
onBlur={onBlur}
|
paddingLeft: 30 + a.p_sm.padding,
|
||||||
/>
|
}
|
||||||
|
: {},
|
||||||
|
]}
|
||||||
|
textStyle={[a.text_md, a.leading_snug]}
|
||||||
|
onFocus={onFocus}
|
||||||
|
onBlur={onBlur}
|
||||||
|
onFacetCommitted={facet => {
|
||||||
|
if (facet.type === 'url' && isBskyPostUrl(facet.value)) {
|
||||||
|
setEmbed(facet.value)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onRequestSubmit={req => {
|
||||||
|
if (req.platform === 'web' && req.shiftKey) return
|
||||||
|
req.nativeEvent.preventDefault()
|
||||||
|
onSubmit()
|
||||||
|
}}>
|
||||||
{IS_WEB && (
|
{IS_WEB && (
|
||||||
<Pressable
|
<Pressable
|
||||||
onPress={e => {
|
onPress={e => {
|
||||||
@@ -242,10 +239,8 @@ export function MessageComposer({
|
|||||||
style={[a.relative, {left: 1}]}
|
style={[a.relative, {left: 1}]}
|
||||||
/>
|
/>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
</View>
|
</Composer>
|
||||||
|
</View>
|
||||||
<Composer.Autocomplete />
|
|
||||||
</Composer.Root>
|
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{IS_WEB && (
|
{IS_WEB && (
|
||||||
|
|||||||
Reference in New Issue
Block a user