Checkpoint: switch back to monolith
This commit is contained in:
@@ -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,6 +287,7 @@ export function Input({
|
|||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
<View style={[a.relative, style]}>
|
<View style={[a.relative, style]}>
|
||||||
<View
|
<View
|
||||||
pointerEvents="none"
|
pointerEvents="none"
|
||||||
@@ -388,7 +299,7 @@ export function Input({
|
|||||||
previewScrollStyle,
|
previewScrollStyle,
|
||||||
]}>
|
]}>
|
||||||
<Text style={[textStyle, web({whiteSpace: 'pre-wrap'})]}>
|
<Text style={[textStyle, web({whiteSpace: 'pre-wrap'})]}>
|
||||||
{state.nodes.map((node, i) => {
|
{tapper.state.nodes.map((node, i) => {
|
||||||
switch (node.type) {
|
switch (node.type) {
|
||||||
case 'text':
|
case 'text':
|
||||||
return <Span key={i}>{node.value}</Span>
|
return <Span key={i}>{node.value}</Span>
|
||||||
@@ -399,7 +310,9 @@ export function Input({
|
|||||||
key={i}
|
key={i}
|
||||||
ref={IS_WEB ? sift.refs.setAnchor : undefined}
|
ref={IS_WEB ? sift.refs.setAnchor : undefined}
|
||||||
style={
|
style={
|
||||||
node.type === 'facet' && {color: t.palette.primary_500}
|
node.type === 'facet' && {
|
||||||
|
color: t.palette.primary_500,
|
||||||
|
}
|
||||||
}>
|
}>
|
||||||
{node.raw}
|
{node.raw}
|
||||||
</Span>
|
</Span>
|
||||||
@@ -476,39 +389,25 @@ export function Input({
|
|||||||
isComposing.current = false
|
isComposing.current = false
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{children}
|
||||||
</View>
|
</View>
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function Autocomplete() {
|
{activeFacet && (
|
||||||
const {tapper, sift} = useComposerContext()
|
|
||||||
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
|
<AutocompleteInner
|
||||||
sift={sift}
|
sift={sift}
|
||||||
activeFacet={activeFacet}
|
activeFacet={activeFacet}
|
||||||
onDismiss={() => setActiveFacet(null)}
|
onDismiss={() => setActiveFacet(null)}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* ─── Autocomplete (private) ───────────────────────────────────────────────────
|
||||||
|
*/
|
||||||
|
|
||||||
function AutocompleteInner({
|
function AutocompleteInner({
|
||||||
sift,
|
sift,
|
||||||
activeFacet,
|
activeFacet,
|
||||||
@@ -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,25 +107,14 @@ 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
|
|
||||||
internalApiRef={composerInternalApiRef}
|
|
||||||
initialText={text}
|
|
||||||
onChange={setText}
|
|
||||||
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()
|
|
||||||
}}>
|
|
||||||
<View
|
<View
|
||||||
// @ts-expect-error web only
|
// @ts-expect-error web only
|
||||||
onMouseEnter={onHoverIn}
|
onMouseEnter={onHoverIn}
|
||||||
onMouseLeave={onHoverOut}>
|
onMouseLeave={onHoverOut}>
|
||||||
<Composer.Input
|
<Composer
|
||||||
|
internalApiRef={composerInternalApiRef}
|
||||||
|
initialText={text}
|
||||||
|
onChange={setText}
|
||||||
editable={editable}
|
editable={editable}
|
||||||
autoFocus={IS_WEB}
|
autoFocus={IS_WEB}
|
||||||
label={l`Message input field`}
|
label={l`Message input field`}
|
||||||
@@ -161,8 +150,16 @@ export function MessageComposer({
|
|||||||
textStyle={[a.text_md, a.leading_snug]}
|
textStyle={[a.text_md, a.leading_snug]}
|
||||||
onFocus={onFocus}
|
onFocus={onFocus}
|
||||||
onBlur={onBlur}
|
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>
|
||||||
|
</Composer>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<Composer.Autocomplete />
|
|
||||||
</Composer.Root>
|
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{IS_WEB && (
|
{IS_WEB && (
|
||||||
|
|||||||
Reference in New Issue
Block a user