diff --git a/package.json b/package.json
index e5561f1739..c8fb86c3ef 100644
--- a/package.json
+++ b/package.json
@@ -88,8 +88,8 @@
"@bsky.app/expo-image-crop-tool": "^0.5.0",
"@bsky.app/expo-translate-text": "^0.2.9",
"@bsky.app/react-native-mmkv": "2.12.5",
- "@bsky.app/sift": "^0.2.6",
- "@bsky.app/tapper": "^0.4.1",
+ "@bsky.app/sift": "^0.2.8",
+ "@bsky.app/tapper": "^0.4.2",
"@discord/bottom-sheet": "bluesky-social/react-native-bottom-sheet",
"@emoji-mart/data": "^1.2.1",
"@emoji-mart/react": "^1.1.1",
diff --git a/src/components/Autocomplete/AutocompleteItemEmoji.tsx b/src/components/Autocomplete/AutocompleteItemEmoji.tsx
index f8dc65e11e..7fd2f82622 100644
--- a/src/components/Autocomplete/AutocompleteItemEmoji.tsx
+++ b/src/components/Autocomplete/AutocompleteItemEmoji.tsx
@@ -19,11 +19,14 @@ export function AutocompleteItemEmoji({
[
- a.px_md,
- a.py_sm,
+ {paddingVertical: 6, paddingHorizontal: 10},
+ a.flex_row,
+ a.align_center,
+ a.gap_sm,
active || s.hovered || s.pressed ? [t.atoms.bg_contrast_25] : [],
]}>
- {item.value}
+ {item.value}
+ :{item.emoji.id}:
)
}
diff --git a/src/components/Autocomplete/types.ts b/src/components/Autocomplete/types.ts
index a02d4d9326..1bf31bad76 100644
--- a/src/components/Autocomplete/types.ts
+++ b/src/components/Autocomplete/types.ts
@@ -1,4 +1,5 @@
import {type Sift} from '@bsky.app/sift'
+import {type Emoji} from '@emoji-mart/data'
import type * as bsky from '#/types/bsky'
@@ -20,7 +21,7 @@ export type AutocompleteEmoji = {
key: string
type: 'emoji'
value: string
- emoji: string
+ emoji: Emoji
}
export type AutocompleteItem =
diff --git a/src/components/Autocomplete/useAutocomplete.ts b/src/components/Autocomplete/useAutocomplete.ts
index 65726b0d09..20ce2aa808 100644
--- a/src/components/Autocomplete/useAutocomplete.ts
+++ b/src/components/Autocomplete/useAutocomplete.ts
@@ -92,7 +92,7 @@ export function useAutocomplete({
key: result.item.id,
type: 'emoji' as const,
value: result.item.skins[0].native,
- emoji: result.item.skins[0].native,
+ emoji: result.item,
}))
}
diff --git a/src/components/Composer/index.tsx b/src/components/Composer/index.tsx
index 2bc72fb71a..c260f58757 100644
--- a/src/components/Composer/index.tsx
+++ b/src/components/Composer/index.tsx
@@ -1,9 +1,3 @@
-/**
- * TODO
- *
- * Native
- * - make sure we limit the height of autocomplete so it doesn't go off screen
- */
import {
useCallback,
useEffect,
@@ -14,7 +8,6 @@ import {
} from 'react'
import {
type TextInput,
- type TextInputProps,
type TextInputSubmitEditingEvent,
View,
} from 'react-native'
@@ -22,8 +15,10 @@ import Animated, {
useAnimatedStyle,
useSharedValue,
} from 'react-native-reanimated'
+import {useSafeAreaInsets} from 'react-native-safe-area-context'
import {useSift, type UseSiftReturn} from '@bsky.app/sift'
import {
+ facets,
type TapperActiveFacet,
type TapperFacet,
useTapper,
@@ -45,7 +40,10 @@ import {
parseAutocompleteItemType,
useAutocomplete,
} from '#/components/Autocomplete'
-import {AutosizedTextarea} from '#/components/forms/AutosizedTextarea'
+import {
+ AutosizedTextarea,
+ type AutosizedTextareaProps,
+} from '#/components/forms/AutosizedTextarea'
import {useOnKeyboard} from '#/components/hooks/useOnKeyboard'
import {Span, Text} from '#/components/Typography'
import {IS_IOS, IS_WEB, IS_WEB_TOUCH_DEVICE} from '#/env'
@@ -71,6 +69,7 @@ export type ComposerInternalApi = {
input?: ReturnType['input']
clear: () => void
insert(text: string): void
+ setAnchorRef(ref: View | null): void
}
export function useComposerInternalApiRef() {
@@ -82,7 +81,7 @@ export function useComposerInternalApiRef() {
*/
export type ComposerProps = Omit<
- TextInputProps,
+ AutosizedTextareaProps,
| 'value'
| 'onChange'
| 'onChangeText'
@@ -91,57 +90,65 @@ export type ComposerProps = Omit<
| 'style'
| 'onSubmitEditing'
> & {
- children?: React.ReactNode
label: string
ref?: React.Ref
- style?: ViewStyleProp['style']
- padding?: {
+ internalApiRef?: React.Ref
+ outerStyle?: ViewStyleProp['style']
+ contentTextStyle?: TextStyleProp['style']
+ contentPaddingStyle?: {
paddingTop?: number
paddingBottom?: number
paddingLeft?: number
paddingRight?: number
}
- textStyle?: TextStyleProp['style']
- maxNumberOfLines?: number
- initialText?: string
onChange?: (text: string) => void
onActiveFacet?: (activeFacet: TapperActiveFacet | null) => void
onFacetCommitted?: (facet: TapperFacet) => void
onRequestSubmit?: (request: SubmitRequest) => void
- internalApiRef?: React.Ref
autocompletePlacement?: Exclude<
Parameters[0],
undefined
>['placement']
+ disableEmojiFacets?: boolean
}
export function Composer({
- children,
label,
- placeholder,
- style,
- padding,
- textStyle: rawTextStyle,
- maxNumberOfLines,
- initialText,
+ ref,
+ internalApiRef,
+ outerStyle,
+ contentTextStyle,
+ contentPaddingStyle,
onChange: onChangeOuter,
onActiveFacet: onActiveFacetOuter,
onFacetCommitted: onFacetCommittedOuter,
onRequestSubmit,
- internalApiRef,
autocompletePlacement,
+ defaultValue,
+ disableEmojiFacets = !IS_WEB,
...rest
}: ComposerProps) {
const {theme: t, fonts} = useAlf()
+ const insets = useSafeAreaInsets()
/*
* Meat and potatoes
*/
- const tapper = useTapper({initialText})
+ const tapper = useTapper({
+ initialText: defaultValue ?? '',
+ facets: disableEmojiFacets
+ ? {
+ mention: facets.mention,
+ tag: facets.tag,
+ url: facets.url,
+ }
+ : facets,
+ })
const sift = useSift({
offset: a.p_sm.padding,
placement: autocompletePlacement,
dynamicWidth: IS_WEB,
+ insets,
})
/*
@@ -166,8 +173,11 @@ export function Composer({
inputScrollSharedValue.value = 0
},
insert: tapper.insert,
+ setAnchorRef: (ref: View | null) => {
+ sift.refs.setAnchor(ref)
+ },
}),
- [tapper.input, tapper.insert, inputScrollSharedValue],
+ [tapper.input, tapper.insert, inputScrollSharedValue, sift.refs],
)
/*
@@ -220,7 +230,7 @@ export function Composer({
}))
const textStyle = useMemo(() => {
const ts = normalizeTextStyles(
- [a.leading_snug, rawTextStyle, t.atoms.text],
+ [a.leading_snug, t.atoms.text, contentTextStyle],
{
fontScale: fonts.scaleMultiplier,
fontFamily: fonts.family,
@@ -240,7 +250,7 @@ export function Composer({
delete ts.lineHeight
}
return ts
- }, [rawTextStyle, fonts])
+ }, [contentTextStyle, fonts])
/*
* Web keyboard handling
@@ -282,13 +292,13 @@ export function Composer({
return (
<>
-
+
@@ -317,8 +327,6 @@ export function Composer({
{
rest.onBlur?.(e)
setActiveFacet(null)
@@ -368,11 +372,9 @@ export function Composer({
}}
onUpdateHeight={updateAutocompletePosition}
/>
-
- {children}
- {activeFacet && (
+ {activeFacet && activeFacet.type !== 'url' && (
& {
+ ref?: React.Ref
+ label: string
+ minRows?: number
+ maxRows?: number
+ onUpdateHeight?: (height: number) => void
+}
+
export function AutosizedTextarea({
ref,
label,
@@ -21,13 +29,7 @@ export function AutosizedTextarea({
onContentSizeChange: onContentSizeChangeOuter,
style: outerStyle,
...rest
-}: Omit & {
- ref?: React.Ref
- label: string
- minRows?: number
- maxRows?: number
- onUpdateHeight?: (height: number) => void
-}) {
+}: AutosizedTextareaProps) {
const {theme: t, fonts} = useAlf()
const internalRef = useRef(null)
const {style, minInputHeight, maxInputHeight, verticalContentPadding} =
diff --git a/src/screens/Messages/components/MessageComposer.tsx b/src/screens/Messages/components/MessageComposer.tsx
index 8717c16a61..623839f704 100644
--- a/src/screens/Messages/components/MessageComposer.tsx
+++ b/src/screens/Messages/components/MessageComposer.tsx
@@ -23,7 +23,7 @@ import {useInteractionState} from '#/components/hooks/useInteractionState'
import {EmojiArc_Stroke2_Corner0_Rounded as EmojiSmile} from '#/components/icons/Emoji'
import {PaperPlane_Stroke2_Corner0_Rounded as PaperPlane} from '#/components/icons/PaperPlane'
import * as Toast from '#/components/Toast'
-import {IS_ANDROID, IS_WEB} from '#/env'
+import {IS_WEB} from '#/env'
export function MessageComposer({
onSendMessage,
@@ -107,25 +107,84 @@ export function MessageComposer({
<>
{children}
+
{
+ if (!IS_WEB && node) {
+ composerInternalApiRef.current?.setAnchorRef(node)
+ }
+ }}
// @ts-expect-error web only
onMouseEnter={onHoverIn}
- onMouseLeave={onHoverOut}>
+ onMouseLeave={onHoverOut}
+ style={[a.w_full, a.flex_row, a.gap_sm]}>
+ {IS_WEB && (
+ {
+ e.currentTarget.measure((_fx, _fy, _width, _height, px, py) => {
+ openEmojiPicker?.({
+ top: py,
+ left: px,
+ right: px,
+ bottom: py,
+ nextFocusRef: {
+ current: composerInternalApiRef.current?.input?.element,
+ },
+ })
+ })
+ }}
+ style={[
+ a.overflow_hidden,
+ a.absolute,
+ a.rounded_full,
+ a.align_center,
+ a.justify_center,
+ a.z_30,
+ {
+ height: 30,
+ width: 30,
+ top: 8,
+ left: 8,
+ },
+ ]}
+ accessibilityLabel={l`Open emoji picker`}
+ accessibilityHint="">
+ {state => (
+
+
+
+ )}
+
+ )}
+
{
if (facet.type === 'url' && isBskyPostUrl(facet.value)) {
setEmbed(facet.value)
@@ -154,77 +214,25 @@ export function MessageComposer({
if (req.platform === 'web' && req.shiftKey) return
req.nativeEvent.preventDefault()
onSubmit()
- }}>
- {IS_WEB && (
- {
- e.currentTarget.measure(
- (_fx, _fy, _width, _height, px, py) => {
- openEmojiPicker?.({
- top: py,
- left: px,
- right: px,
- bottom: py,
- nextFocusRef: {
- current:
- composerInternalApiRef.current?.input?.element,
- },
- })
- },
- )
- }}
- style={[
- a.overflow_hidden,
- a.absolute,
- a.rounded_full,
- a.align_center,
- a.justify_center,
- a.z_30,
- {
- height: 30,
- width: 30,
- top: 7,
- left: 7,
- },
- ]}
- accessibilityLabel={l`Open emoji picker`}
- accessibilityHint="">
- {state => (
-
-
-
- )}
-
- )}
+ }}
+ />
+
+ {focused || text.length ? (
-
+ ) : null}
diff --git a/yarn.lock b/yarn.lock
index a2e190d891..577c214816 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2413,15 +2413,15 @@
resolved "https://registry.yarnpkg.com/@bsky.app/react-native-mmkv/-/react-native-mmkv-2.12.5.tgz#eb17d31a6158c74393f617a1763ac223ff3f83a6"
integrity sha512-3vUz1nQY1DiKIPAWRkpp5ZGxH5f2G6Ui0UuQuEYjYv81xx1qFcSzS9KQ2sHcOKYdkOM9amWV2Q8TQCxt1lrAHg==
-"@bsky.app/sift@^0.2.6":
- version "0.2.6"
- resolved "https://registry.yarnpkg.com/@bsky.app/sift/-/sift-0.2.6.tgz#9ceeebd67e5cda693fbac666c424b2025b01d366"
- integrity sha512-AC6YXP24tusGtHXYT5rEcRVJkW5zAe9GksR4agi5XoCqcZ+Z2jf8QTPbU6wFQxybjmKUxW36WLF7uMMGo4+PaQ==
+"@bsky.app/sift@^0.2.8":
+ version "0.2.8"
+ resolved "https://registry.yarnpkg.com/@bsky.app/sift/-/sift-0.2.8.tgz#58cb8ed82e6402061a1e2315a03e1aaec66e6718"
+ integrity sha512-CVBo0fp8kpSPhiGYO1H0J8aaEREQ87fzoXdL2qaH64GgX+tqFJT9BlS4EvEyhJoyMHdRWinGgTTubf4Gkvcg/Q==
-"@bsky.app/tapper@^0.4.1":
- version "0.4.1"
- resolved "https://registry.yarnpkg.com/@bsky.app/tapper/-/tapper-0.4.1.tgz#f8f68f411fbd5ce421cf482a03078b18ac23ecac"
- integrity sha512-1ET0T34VKkKmaKj6WY6Gnmx0UC7FByPwGZ2SiLJUwJP6Ott5gBfk9xhaag3pdf3OZsZ3O1uIs99J7pdGcQVrew==
+"@bsky.app/tapper@^0.4.2":
+ version "0.4.2"
+ resolved "https://registry.yarnpkg.com/@bsky.app/tapper/-/tapper-0.4.2.tgz#a01355f87bb17c4767cb66930a9d1e65904b298a"
+ integrity sha512-64+EI5zzIFAMnLFlHFRuS0L26ANCLnMQS1Q4GszOo6/vhWko6opQA0yYowizEFHCQ73uiquaup2Oc8m7pDSxTA==
"@crowdin/cli@^4.14.1":
version "4.14.1"