diff --git a/app/components/autocomplete/at_mention/at_mention.tsx b/app/components/autocomplete/at_mention/at_mention.tsx index 160d5c4455..7956a504f7 100644 --- a/app/components/autocomplete/at_mention/at_mention.tsx +++ b/app/components/autocomplete/at_mention/at_mention.tsx @@ -3,7 +3,7 @@ import {debounce} from 'lodash'; import React, {useCallback, useEffect, useMemo, useState} from 'react'; -import {Platform, SectionList, SectionListData, SectionListRenderItemInfo} from 'react-native'; +import {Platform, SectionList, SectionListData, SectionListRenderItemInfo, StyleProp, ViewStyle} from 'react-native'; import {searchGroupsByName, searchGroupsByNameInChannel, searchGroupsByNameInTeam} from '@actions/local/group'; import {searchUsers} from '@actions/remote/user'; @@ -13,12 +13,10 @@ import AutocompleteSectionHeader from '@components/autocomplete/autocomplete_sec import SpecialMentionItem from '@components/autocomplete/special_mention_item'; import {AT_MENTION_REGEX, AT_MENTION_SEARCH_REGEX} from '@constants/autocomplete'; import {useServerUrl} from '@context/server'; -import {useTheme} from '@context/theme'; import DatabaseManager from '@database/manager'; import {t} from '@i18n'; import {queryAllUsers} from '@queries/servers/user'; import {hasTrailingSpaces} from '@utils/helpers'; -import {makeStyleSheetFromTheme} from '@utils/theme'; import type GroupModel from '@typings/database/models/servers/group'; import type UserModel from '@typings/database/models/servers/user'; @@ -182,7 +180,6 @@ type Props = { teamId?: string; cursorPosition: number; isSearch: boolean; - maxListHeight: number; updateValue: (v: string) => void; onShowingChange: (c: boolean) => void; value: string; @@ -191,17 +188,9 @@ type Props = { useGroupMentions: boolean; isChannelConstrained: boolean; isTeamConstrained: boolean; + listStyle: StyleProp; } -const getStyleFromTheme = makeStyleSheetFromTheme((theme) => { - return { - listView: { - backgroundColor: theme.centerChannelBg, - borderRadius: 4, - }, - }; -}); - const emptyUserlList: Array = []; const emptySectionList: UserMentionSections = []; const emptyGroupList: GroupModel[] = []; @@ -220,7 +209,6 @@ const AtMention = ({ teamId, cursorPosition, isSearch, - maxListHeight, updateValue, onShowingChange, value, @@ -229,10 +217,9 @@ const AtMention = ({ useGroupMentions, isChannelConstrained, isTeamConstrained, + listStyle, }: Props) => { const serverUrl = useServerUrl(); - const theme = useTheme(); - const style = getStyleFromTheme(theme); const [sections, setSections] = useState(emptySectionList); const [usersInChannel, setUsersInChannel] = useState>(emptyUserlList); @@ -459,7 +446,7 @@ const AtMention = ({ removeClippedSubviews={Platform.OS === 'android'} renderItem={renderItem} renderSectionHeader={renderSectionHeader} - style={[style.listView, {maxHeight: maxListHeight}]} + style={listStyle} sections={sections} testID='autocomplete.at_mention.section_list' /> diff --git a/app/components/autocomplete/autocomplete.tsx b/app/components/autocomplete/autocomplete.tsx index bbced252b3..4cd5a962c3 100644 --- a/app/components/autocomplete/autocomplete.tsx +++ b/app/components/autocomplete/autocomplete.tsx @@ -2,7 +2,8 @@ // See LICENSE.txt for license information. import React, {useMemo, useState} from 'react'; -import {Platform, useWindowDimensions, View} from 'react-native'; +import {Platform, StyleProp, useWindowDimensions, ViewStyle} from 'react-native'; +import Animated, {SharedValue, useAnimatedStyle, useDerivedValue} from 'react-native-reanimated'; import {MAX_LIST_HEIGHT, MAX_LIST_TABLET_DIFF} from '@constants/autocomplete'; import {useTheme} from '@context/theme'; @@ -19,8 +20,8 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => { return { base: { left: 8, - position: 'absolute', right: 8, + position: 'absolute', }, borders: { borderWidth: 1, @@ -29,16 +30,6 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => { borderRadius: 8, elevation: 3, }, - searchContainer: { - ...Platform.select({ - android: { - top: 42, - }, - ios: { - top: 55, - }, - }), - }, shadow: { shadowColor: '#000', shadowOpacity: 0.12, @@ -48,12 +39,16 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => { height: 6, }, }, + listStyle: { + backgroundColor: theme.centerChannelBg, + borderRadius: 4, + }, }; }); type Props = { cursorPosition: number; - position: number; + position: SharedValue; rootId?: string; channelId?: string; isSearch?: boolean; @@ -63,9 +58,10 @@ type Props = { nestedScrollEnabled?: boolean; updateValue: (v: string) => void; hasFilesAttached?: boolean; - availableSpace: number; + availableSpace: SharedValue; inPost?: boolean; growDown?: boolean; + containerStyle?: StyleProp; } const Autocomplete = ({ @@ -84,6 +80,7 @@ const Autocomplete = ({ hasFilesAttached, inPost = false, growDown = false, + containerStyle, }: Props) => { const theme = useTheme(); const isTablet = useIsTablet(); @@ -102,75 +99,73 @@ const Autocomplete = ({ const appsTakeOver = showingAppCommand; const showCommands = !(showingChannelMention || showingEmoji || showingAtMention); - const wrapperStyles = useMemo(() => { - const s = []; - if (Platform.OS === 'ios') { - s.push(style.shadow); - } - return s; - }, [style]); - - const containerStyles = useMemo(() => { - const s = [style.base]; - if (growDown) { - s.push({top: -position}); - } else { - s.push({bottom: position}); - } - if (hasElements) { - s.push(style.borders); - } - return s; - }, [hasElements, position, growDown, style]); - const isLandscape = dimensions.width > dimensions.height; const maxHeightAdjust = (isTablet && isLandscape) ? MAX_LIST_TABLET_DIFF : 0; const defaultMaxHeight = MAX_LIST_HEIGHT - maxHeightAdjust; - const maxListHeight = Math.min(availableSpace, defaultMaxHeight); + const maxHeight = useDerivedValue(() => { + return Math.min(availableSpace.value, defaultMaxHeight); + }, [defaultMaxHeight]); + + const containerAnimatedStyle = useAnimatedStyle(() => { + return growDown ? + {top: position.value, bottom: Platform.OS === 'ios' ? 'auto' : undefined, maxHeight: maxHeight.value} : + {top: Platform.OS === 'ios' ? 'auto' : undefined, bottom: position.value, maxHeight: maxHeight.value}; + }, [growDown, position]); + + const containerStyles = useMemo(() => { + const s = [style.base, containerAnimatedStyle]; + if (hasElements) { + s.push(style.borders); + } + if (Platform.OS === 'ios') { + s.push(style.shadow); + } + if (containerStyle) { + s.push(containerStyle); + } + return s; + }, [hasElements, style, containerStyle, containerAnimatedStyle]); return ( - - - {isAppsEnabled && channelId && ( - - )} - {(!appsTakeOver || !isAppsEnabled) && (<> - - - {!isSearch && + {isAppsEnabled && channelId && ( + + )} + {(!appsTakeOver || !isAppsEnabled) && (<> + + + {!isSearch && - } - {showCommands && channelId && + } + {showCommands && channelId && - } - {/* {(isSearch && enableDateSuggestion) && + } + {/* {(isSearch && enableDateSuggestion) && } */} - )} - - + )} + ); }; diff --git a/app/components/autocomplete/channel_mention/channel_mention.tsx b/app/components/autocomplete/channel_mention/channel_mention.tsx index c8991bb338..c3073759e1 100644 --- a/app/components/autocomplete/channel_mention/channel_mention.tsx +++ b/app/components/autocomplete/channel_mention/channel_mention.tsx @@ -3,7 +3,7 @@ import {debounce} from 'lodash'; import React, {useCallback, useEffect, useMemo, useState} from 'react'; -import {Platform, SectionList, SectionListData, SectionListRenderItemInfo} from 'react-native'; +import {Platform, SectionList, SectionListData, SectionListRenderItemInfo, StyleProp, ViewStyle} from 'react-native'; import {searchChannels} from '@actions/remote/channel'; import AutocompleteSectionHeader from '@components/autocomplete/autocomplete_section_header'; @@ -11,14 +11,12 @@ import ChannelMentionItem from '@components/autocomplete/channel_mention_item'; import {General} from '@constants'; import {CHANNEL_MENTION_REGEX, CHANNEL_MENTION_SEARCH_REGEX} from '@constants/autocomplete'; import {useServerUrl} from '@context/server'; -import {useTheme} from '@context/theme'; import DatabaseManager from '@database/manager'; import useDidUpdate from '@hooks/did_update'; import {t} from '@i18n'; import {queryAllChannelsForTeam} from '@queries/servers/channel'; import {getCurrentTeamId} from '@queries/servers/system'; import {hasTrailingSpaces} from '@utils/helpers'; -import {makeStyleSheetFromTheme} from '@utils/theme'; import type ChannelModel from '@typings/database/models/servers/channel'; import type MyChannelModel from '@typings/database/models/servers/my_channel'; @@ -160,23 +158,14 @@ const filterResults = (channels: Array, term: string) => type Props = { cursorPosition: number; isSearch: boolean; - maxListHeight: number; myMembers: MyChannelModel[]; updateValue: (v: string) => void; onShowingChange: (c: boolean) => void; value: string; nestedScrollEnabled: boolean; + listStyle: StyleProp; } -const getStyleFromTheme = makeStyleSheetFromTheme((theme) => { - return { - listView: { - backgroundColor: theme.centerChannelBg, - borderRadius: 4, - }, - }; -}); - const getAllChannels = async (serverUrl: string) => { const database = DatabaseManager.serverDatabases[serverUrl]?.database; if (!database) { @@ -193,16 +182,14 @@ const emptyChannels: Array = []; const ChannelMention = ({ cursorPosition, isSearch, - maxListHeight, myMembers, updateValue, onShowingChange, value, nestedScrollEnabled, + listStyle, }: Props) => { const serverUrl = useServerUrl(); - const theme = useTheme(); - const style = getStyleFromTheme(theme); const [sections, setSections] = useState>>(emptySections); const [channels, setChannels] = useState>(emptyChannels); @@ -213,10 +200,6 @@ const ChannelMention = ({ const [localChannels, setlocalChannels] = useState(); const [filteredLocalChannels, setFilteredLocalChannels] = useState(emptyChannels); - const listStyle = useMemo(() => - [style.listView, {maxHeight: maxListHeight}] - , [style, maxListHeight]); - const runSearch = useMemo(() => debounce(async (sUrl: string, term: string) => { setLoading(true); const {channels: receivedChannels, error} = await searchChannels(sUrl, term, isSearch); diff --git a/app/components/autocomplete/emoji_suggestion/emoji_suggestion.tsx b/app/components/autocomplete/emoji_suggestion/emoji_suggestion.tsx index 9c02abbe9a..1b38f442d9 100644 --- a/app/components/autocomplete/emoji_suggestion/emoji_suggestion.tsx +++ b/app/components/autocomplete/emoji_suggestion/emoji_suggestion.tsx @@ -4,7 +4,7 @@ import Fuse from 'fuse.js'; import {debounce} from 'lodash'; import React, {useCallback, useEffect, useMemo} from 'react'; -import {FlatList, Platform, Text, View} from 'react-native'; +import {FlatList, Platform, StyleProp, Text, View, ViewStyle} from 'react-native'; import {useSafeAreaInsets} from 'react-native-safe-area-context'; import {searchCustomEmojis} from '@actions/remote/custom_emoji'; @@ -48,8 +48,6 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => { }, listView: { paddingTop: 16, - backgroundColor: theme.centerChannelBg, - borderRadius: 8, }, row: { flexDirection: 'row', @@ -67,7 +65,6 @@ const keyExtractor = (item: string) => item; type Props = { cursorPosition: number; customEmojis: CustomEmojiModel[]; - maxListHeight: number; updateValue: (v: string) => void; onShowingChange: (c: boolean) => void; rootId?: string; @@ -76,11 +73,11 @@ type Props = { skinTone: string; hasFilesAttached?: boolean; inPost: boolean; + listStyle: StyleProp; } const EmojiSuggestion = ({ cursorPosition, customEmojis = [], - maxListHeight, updateValue, onShowingChange, rootId, @@ -89,14 +86,13 @@ const EmojiSuggestion = ({ skinTone, hasFilesAttached = false, inPost, + listStyle, }: Props) => { const insets = useSafeAreaInsets(); const theme = useTheme(); const style = getStyleFromTheme(theme); const serverUrl = useServerUrl(); - const flatListStyle = useMemo(() => - [style.listView, {maxHeight: maxListHeight}] - , [style, maxListHeight]); + const containerStyle = useMemo(() => ({paddingBottom: insets.bottom + 12}) , [insets.bottom]); @@ -219,7 +215,7 @@ const EmojiSuggestion = ({ return ( void; onShowingChange: (c: boolean) => void; value: string; @@ -31,21 +29,11 @@ export type Props = { rootId?: string; channelId: string; isAppsEnabled: boolean; + listStyle: StyleProp; }; const keyExtractor = (item: ExtendedAutocompleteSuggestion): string => item.Suggestion + item.type + item.item; -const getStyleFromTheme = makeStyleSheetFromTheme((theme: Theme) => { - return { - listView: { - flex: 1, - backgroundColor: theme.centerChannelBg, - paddingTop: 8, - borderRadius: 4, - }, - }; -}); - const emptySuggestonList: AutocompleteSuggestion[] = []; const AppSlashSuggestion = ({ @@ -54,10 +42,10 @@ const AppSlashSuggestion = ({ rootId, value = '', isAppsEnabled, - maxListHeight, nestedScrollEnabled, updateValue, onShowingChange, + listStyle, }: Props) => { const intl = useIntl(); const theme = useTheme(); @@ -65,11 +53,8 @@ const AppSlashSuggestion = ({ const appCommandParser = useRef(new AppCommandParser(serverUrl, intl, channelId, currentTeamId, rootId, theme)); const [dataSource, setDataSource] = useState(emptySuggestonList); const active = isAppsEnabled && Boolean(dataSource.length); - const style = getStyleFromTheme(theme); const mounted = useRef(false); - const listStyle = useMemo(() => [style.listView, {maxHeight: maxListHeight}], [maxListHeight, style]); - const fetchAndShowAppCommandSuggestions = useMemo(() => debounce(async (pretext: string, cId: string, tId = '', rId?: string) => { appCommandParser.current.setChannelContext(cId, tId, rId); const suggestions = await appCommandParser.current.getSuggestions(pretext); diff --git a/app/components/autocomplete/slash_suggestion/slash_suggestion.tsx b/app/components/autocomplete/slash_suggestion/slash_suggestion.tsx index 33e898ec66..65d5bf929c 100644 --- a/app/components/autocomplete/slash_suggestion/slash_suggestion.tsx +++ b/app/components/autocomplete/slash_suggestion/slash_suggestion.tsx @@ -7,6 +7,8 @@ import {useIntl} from 'react-intl'; import { FlatList, Platform, + StyleProp, + ViewStyle, } from 'react-native'; import {fetchSuggestions} from '@actions/remote/command'; @@ -14,7 +16,6 @@ import {useServerUrl} from '@context/server'; import {useTheme} from '@context/theme'; import analytics from '@managers/analytics'; import IntegrationsManager from '@managers/integrations_manager'; -import {makeStyleSheetFromTheme} from '@utils/theme'; import {AppCommandParser} from './app_command_parser/app_command_parser'; import SlashSuggestionItem from './slash_suggestion_item'; @@ -27,17 +28,6 @@ const COMMANDS_TO_HIDE_ON_MOBILE = new Set([...COMMANDS_TO_IMPLEMENT_LATER, ...N const commandFilter = (v: Command) => !COMMANDS_TO_HIDE_ON_MOBILE.has(v.trigger); -const getStyleFromTheme = makeStyleSheetFromTheme((theme: Theme) => { - return { - listView: { - flex: 1, - backgroundColor: theme.centerChannelBg, - paddingTop: 8, - borderRadius: 4, - }, - }; -}); - const filterCommands = (matchTerm: string, commands: Command[]): AutocompleteSuggestion[] => { const data = commands.filter((command) => { if (!command.auto_complete) { @@ -63,7 +53,6 @@ const keyExtractor = (item: Command & AutocompleteSuggestion): string => item.id type Props = { currentTeamId: string; - maxListHeight?: number; updateValue: (text: string) => void; onShowingChange: (c: boolean) => void; value: string; @@ -71,6 +60,7 @@ type Props = { rootId?: string; channelId: string; isAppsEnabled: boolean; + listStyle: StyleProp; }; const emptyCommandList: Command[] = []; @@ -82,14 +72,13 @@ const SlashSuggestion = ({ rootId, onShowingChange, isAppsEnabled, - maxListHeight, nestedScrollEnabled, updateValue, value = '', + listStyle, }: Props) => { const intl = useIntl(); const theme = useTheme(); - const style = getStyleFromTheme(theme); const serverUrl = useServerUrl(); const appCommandParser = useRef(new AppCommandParser(serverUrl, intl, channelId, currentTeamId, rootId, theme)); const mounted = useRef(false); @@ -100,8 +89,6 @@ const SlashSuggestion = ({ const active = Boolean(dataSource.length); - const listStyle = useMemo(() => [style.listView, {maxHeight: maxListHeight}], [maxListHeight, style]); - const updateSuggestions = useCallback((matches: AutocompleteSuggestion[]) => { setDataSource(matches); onShowingChange(Boolean(matches.length)); diff --git a/app/components/post_draft/post_draft.tsx b/app/components/post_draft/post_draft.tsx index e632c199f4..950a7a67b6 100644 --- a/app/components/post_draft/post_draft.tsx +++ b/app/components/post_draft/post_draft.tsx @@ -2,12 +2,13 @@ // See LICENSE.txt for license information. import React, {RefObject, useEffect, useState} from 'react'; -import {Platform, View} from 'react-native'; +import {Platform} from 'react-native'; import {KeyboardTrackingView, KeyboardTrackingViewRef} from 'react-native-keyboard-tracking-view'; import {useSafeAreaInsets} from 'react-native-safe-area-context'; import Autocomplete from '@components/autocomplete'; import {View as ViewConstants} from '@constants'; +import {useAutocompleteDefaultAnimatedValues} from '@hooks/autocomplete'; import {useIsTablet, useKeyboardHeight} from '@hooks/device'; import {useDefaultHeaderHeight} from '@hooks/header'; @@ -67,6 +68,16 @@ function PostDraft({ setCursorPosition(message.length); }, [channelId, rootId]); + const keyboardAdjustment = (isTablet && isChannelScreen) ? KEYBOARD_TRACKING_OFFSET : 0; + const insetsAdjustment = (isTablet && isChannelScreen) ? 0 : insets.bottom; + const autocompletePosition = AUTOCOMPLETE_ADJUST + Platform.select({ + ios: (keyboardHeight ? keyboardHeight - keyboardAdjustment : (postInputTop + insetsAdjustment)), + default: postInputTop + insetsAdjustment, + }); + const autocompleteAvailableSpace = containerHeight - autocompletePosition - (isChannelScreen ? headerHeight + insets.top : 0); + + const [animatedAutocompletePosition, animatedAutocompleteAvailableSpace] = useAutocompleteDefaultAnimatedValues(autocompletePosition, autocompleteAvailableSpace); + if (channelIsArchived || deactivatedChannel) { const archivedTestID = `${testID}.archived`; @@ -102,17 +113,9 @@ function PostDraft({ /> ); - const keyboardAdjustment = (isTablet && isChannelScreen) ? KEYBOARD_TRACKING_OFFSET : 0; - const insetsAdjustment = (isTablet && isChannelScreen) ? 0 : insets.bottom; - const autocompletePosition = AUTOCOMPLETE_ADJUST + Platform.select({ - ios: (keyboardHeight ? keyboardHeight - keyboardAdjustment : (postInputTop + insetsAdjustment)), - default: postInputTop + insetsAdjustment, - }); - const autocompleteAvailableSpace = containerHeight - autocompletePosition - (isChannelScreen ? headerHeight + insets.top : 0); - const autoComplete = ( ); @@ -144,9 +147,7 @@ function PostDraft({ > {draftHandler} - - {autoComplete} - + {autoComplete} ); } diff --git a/app/hooks/autocomplete.ts b/app/hooks/autocomplete.ts new file mode 100644 index 0000000000..0e4e234b19 --- /dev/null +++ b/app/hooks/autocomplete.ts @@ -0,0 +1,20 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {useEffect} from 'react'; +import {useSharedValue} from 'react-native-reanimated'; + +export const useAutocompleteDefaultAnimatedValues = (position: number, availableSpace: number) => { + const animatedPosition = useSharedValue(position); + const animatedAvailableSpace = useSharedValue(availableSpace); + + useEffect(() => { + animatedPosition.value = position; + }, [position]); + + useEffect(() => { + animatedAvailableSpace.value = availableSpace; + }, [availableSpace]); + + return [animatedPosition, animatedAvailableSpace]; +}; diff --git a/app/screens/create_or_edit_channel/channel_info_form.tsx b/app/screens/create_or_edit_channel/channel_info_form.tsx index 9b1e4d12ff..ca984e1fbf 100644 --- a/app/screens/create_or_edit_channel/channel_info_form.tsx +++ b/app/screens/create_or_edit_channel/channel_info_form.tsx @@ -25,6 +25,7 @@ import Loading from '@components/loading'; import OptionItem from '@components/option_item'; import {General, Channel} from '@constants'; import {useTheme} from '@context/theme'; +import {useAutocompleteDefaultAnimatedValues} from '@hooks/autocomplete'; import {useIsTablet, useKeyboardHeight, useModalPosition} from '@hooks/device'; import {t} from '@i18n'; import { @@ -212,6 +213,34 @@ export default function ChannelInfoForm({ setWrapperHeight(e.nativeEvent.layout.height); }, []); + const bottomSpace = (dimensions.height - wrapperHeight - modalPosition); + const otherElementsSize = LIST_PADDING + errorHeight + + (showSelector ? makePrivateHeight + MAKE_PRIVATE_MARGIN_BOTTOM : 0) + + (displayHeaderOnly ? 0 : purposeFieldHeight + FIELD_MARGIN_BOTTOM + displayNameFieldHeight + FIELD_MARGIN_BOTTOM); + + const keyboardOverlap = Platform.select({ + ios: isTablet ? + Math.max(0, keyboardHeight - bottomSpace) : + keyboardHeight || insets.bottom, + default: 0}); + const workingSpace = wrapperHeight - keyboardOverlap; + const spaceOnTop = otherElementsSize - scrollPosition - AUTOCOMPLETE_ADJUST; + const spaceOnBottom = (workingSpace + scrollPosition) - (otherElementsSize + headerFieldHeight + BOTTOM_AUTOCOMPLETE_SEPARATION); + const insetsAdjust = keyboardHeight || insets.bottom; + const keyboardAdjust = Platform.select({ + ios: isTablet ? + keyboardOverlap : + insetsAdjust, + default: 0, + }); + const autocompletePosition = spaceOnBottom > spaceOnTop ? + (otherElementsSize + headerFieldHeight) - scrollPosition : + (workingSpace + scrollPosition + AUTOCOMPLETE_ADJUST + keyboardAdjust) - otherElementsSize; + const autocompleteAvailableSpace = spaceOnBottom > spaceOnTop ? spaceOnBottom : spaceOnTop; + const growDown = spaceOnBottom > spaceOnTop; + + const [animatedAutocompletePosition, animatedAutocompleteAvailableSpace] = useAutocompleteDefaultAnimatedValues(autocompletePosition, autocompleteAvailableSpace); + if (saving) { return ( @@ -243,30 +272,6 @@ export default function ChannelInfoForm({ ); } - const bottomSpace = (dimensions.height - wrapperHeight - modalPosition); - const otherElementsSize = LIST_PADDING + errorHeight + - (showSelector ? makePrivateHeight + MAKE_PRIVATE_MARGIN_BOTTOM : 0) + - (displayHeaderOnly ? 0 : purposeFieldHeight + FIELD_MARGIN_BOTTOM + displayNameFieldHeight + FIELD_MARGIN_BOTTOM); - - const keyboardOverlap = Platform.select({ - ios: isTablet ? - Math.max(0, keyboardHeight - bottomSpace) : - keyboardHeight || insets.bottom, - default: 0}); - const workingSpace = wrapperHeight - keyboardOverlap; - const spaceOnTop = otherElementsSize - scrollPosition - AUTOCOMPLETE_ADJUST; - const spaceOnBottom = (workingSpace + scrollPosition) - (otherElementsSize + headerFieldHeight + BOTTOM_AUTOCOMPLETE_SEPARATION); - const insetsAdjust = keyboardHeight - keyboardHeight ? insets.bottom : 0; - const keyboardAdjust = Platform.select({ - ios: isTablet ? - keyboardOverlap : - insetsAdjust, - default: 0, - }); - const autocompletePosition = spaceOnTop > spaceOnBottom ? (workingSpace + scrollPosition + AUTOCOMPLETE_ADJUST + keyboardAdjust) - otherElementsSize : (workingSpace + scrollPosition + keyboardAdjust) - (otherElementsSize + headerFieldHeight); - const autocompleteAvailableSpace = spaceOnTop > spaceOnBottom ? spaceOnTop : spaceOnBottom; - const growUp = spaceOnTop > spaceOnBottom; - return ( - - - + ); } diff --git a/app/screens/edit_post/edit_post.tsx b/app/screens/edit_post/edit_post.tsx index a8efe5bc99..4036b608fc 100644 --- a/app/screens/edit_post/edit_post.tsx +++ b/app/screens/edit_post/edit_post.tsx @@ -11,6 +11,7 @@ import Autocomplete from '@components/autocomplete'; import Loading from '@components/loading'; import {useServerUrl} from '@context/server'; import {useTheme} from '@context/theme'; +import {useAutocompleteDefaultAnimatedValues} from '@hooks/autocomplete'; import {useIsTablet, useKeyboardHeight, useModalPosition} from '@hooks/device'; import useDidUpdate from '@hooks/did_update'; import useNavButtonPressed from '@hooks/navigation_button_pressed'; @@ -197,14 +198,6 @@ const EditPost = ({componentId, maxPostSize, post, closeButtonId, hasFilesAttach useNavButtonPressed(RIGHT_BUTTON.id, componentId, onSavePostMessage, [postMessage]); useNavButtonPressed(closeButtonId, componentId, onClose, []); - if (isUpdating) { - return ( - - - - ); - } - const bottomSpace = (dimensions.height - containerHeight - modalPosition); const autocompletePosition = Platform.select({ ios: isTablet ? @@ -213,6 +206,16 @@ const EditPost = ({componentId, maxPostSize, post, closeButtonId, hasFilesAttach default: 0}) + AUTOCOMPLETE_SEPARATION; const autocompleteAvailableSpace = containerHeight - autocompletePosition; + const [animatedAutocompletePosition, animatedAutocompleteAvailableSpace] = useAutocompleteDefaultAnimatedValues(autocompletePosition, autocompleteAvailableSpace); + + if (isUpdating) { + return ( + + + + ); + } + return ( <> diff --git a/app/screens/home/search/search.tsx b/app/screens/home/search/search.tsx index b8fbb1b6ef..137cb7e123 100644 --- a/app/screens/home/search/search.tsx +++ b/app/screens/home/search/search.tsx @@ -5,7 +5,7 @@ import {useIsFocused, useNavigation} from '@react-navigation/native'; import React, {useCallback, useMemo, useState} from 'react'; import {useIntl} from 'react-intl'; import {FlatList, LayoutChangeEvent, Platform, StyleSheet, ViewProps} from 'react-native'; -import Animated, {useAnimatedStyle, withTiming} from 'react-native-reanimated'; +import Animated, {useAnimatedStyle, useDerivedValue, withTiming} from 'react-native-reanimated'; import {Edge, SafeAreaView, useSafeAreaInsets} from 'react-native-safe-area-context'; import {getPosts} from '@actions/local/post'; @@ -40,7 +40,6 @@ const emptyChannelIds: string[] = []; const dummyData = [1]; const AutocompletePaddingTop = 4; -const AutocompleteZindex = 11; type Props = { teamId: string; @@ -54,6 +53,9 @@ const styles = StyleSheet.create({ flex: 1, justifyContent: 'center', }, + autocompleteContainer: { + zIndex: 11, + }, }); const getSearchParams = (terms: string, filterValue?: FileFilter) => { @@ -240,12 +242,16 @@ const SearchScreen = ({teamId}: Props) => { ); } - const autocompleteRemoveFromHeight = headerHeight.value + Platform.select({ - ios: keyboardHeight ? keyboardHeight - BOTTOM_TAB_HEIGHT : insets.bottom, - default: 0, - }); - const autocompleteMaxHeight = containerHeight - autocompleteRemoveFromHeight; - const autocompletePosition = AutocompletePaddingTop; + const autocompleteMaxHeight = useDerivedValue(() => { + const iosAdjust = keyboardHeight ? keyboardHeight - BOTTOM_TAB_HEIGHT : insets.bottom; + const autocompleteRemoveFromHeight = headerHeight.value + (Platform.OS === 'ios' ? iosAdjust : 0); + return containerHeight - autocompleteRemoveFromHeight; + }, [keyboardHeight, insets.bottom, containerHeight]); + + const autocompletePosition = useDerivedValue(() => { + return headerHeight.value - AutocompletePaddingTop; + }, [containerHeight]); + const autocomplete = useMemo(() => ( { availableSpace={autocompleteMaxHeight} position={autocompletePosition} growDown={true} + containerStyle={styles.autocompleteContainer} /> ), [cursorPosition, handleTextChange, searchValue, autocompleteMaxHeight, autocompletePosition]); @@ -276,9 +283,6 @@ const SearchScreen = ({teamId}: Props) => { onCancel={handleCancelAndClearSearch} defaultValue={searchValue} /> - - {autocomplete} - { } + {autocomplete} ); }; diff --git a/patches/react-native-reanimated+2.9.1.patch b/patches/react-native-reanimated+2.9.1.patch index de8f16675b..dc9ad61327 100644 --- a/patches/react-native-reanimated+2.9.1.patch +++ b/patches/react-native-reanimated+2.9.1.patch @@ -22,7 +22,7 @@ index 5ae42ec..da59568 100644 export const getAnimatedStyle = (received) => { return getCurrentStyle(received); diff --git a/node_modules/react-native-reanimated/src/createAnimatedComponent.tsx b/node_modules/react-native-reanimated/src/createAnimatedComponent.tsx -index 1cf0c3f..a334889 100644 +index 1cf0c3f..a242e4b 100644 --- a/node_modules/react-native-reanimated/src/createAnimatedComponent.tsx +++ b/node_modules/react-native-reanimated/src/createAnimatedComponent.tsx @@ -195,6 +195,7 @@ export default function createAnimatedComponent( @@ -33,7 +33,7 @@ index 1cf0c3f..a334889 100644 sv: SharedValue> | null; _propsAnimated?: PropsAnimated; _component: ComponentRef | null = null; -@@ -580,17 +581,24 @@ export default function createAnimatedComponent( +@@ -580,16 +581,33 @@ export default function createAnimatedComponent( _filterNonAnimatedStyle(inputStyle: StyleProps) { const style: StyleProps = {}; @@ -50,20 +50,20 @@ index 1cf0c3f..a334889 100644 + changed = changed || style[key] !== this._lastSentStyle?.[key]; } } -- return style; -+ if (changed) { -+ return style; -+ } else { -+ return this._lastSentStyle; ++ if (!changed) { ++ const inputKeys = new Set(Object.keys(inputStyle)); ++ let equalKeys = true; ++ for (const key in this._lastSentStyle) { ++ if (!inputKeys.has(key)) { ++ equalKeys = false; ++ break; ++ } ++ } ++ if (equalKeys) { ++ return this._lastSentStyle; ++ } + } ++ this._lastSentStyle = style; + return style; } - _filterNonAnimatedProps( -@@ -620,6 +628,7 @@ export default function createAnimatedComponent( - props[key] = this._filterNonAnimatedStyle( - StyleSheet.flatten(processedStyle) - ); -+ this._lastSentStyle = props[key] - } else if (key === 'animatedProps') { - const animatedProp = inputProps.animatedProps as Partial< - AnimatedComponentProps