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"