Fix autocomplete not scrolling in search in Android (#6596)

This commit is contained in:
Daniel Espino García
2022-09-08 09:39:15 +02:00
committed by GitHub
parent 72d12ae1ac
commit 9ab4c935ef
12 changed files with 215 additions and 251 deletions

View File

@@ -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<ViewStyle>;
}
const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
return {
listView: {
backgroundColor: theme.centerChannelBg,
borderRadius: 4,
},
};
});
const emptyUserlList: Array<UserModel | UserProfile> = [];
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<UserMentionSections>(emptySectionList);
const [usersInChannel, setUsersInChannel] = useState<Array<UserProfile | UserModel>>(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'
/>

View File

@@ -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<number>;
rootId?: string;
channelId?: string;
isSearch?: boolean;
@@ -63,9 +58,10 @@ type Props = {
nestedScrollEnabled?: boolean;
updateValue: (v: string) => void;
hasFilesAttached?: boolean;
availableSpace: number;
availableSpace: SharedValue<number>;
inPost?: boolean;
growDown?: boolean;
containerStyle?: StyleProp<ViewStyle>;
}
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 (
<View
style={wrapperStyles}
<Animated.View
testID='autocomplete'
style={containerStyles}
>
<View
testID='autocomplete'
style={containerStyles}
>
{isAppsEnabled && channelId && (
<AppSlashSuggestion
maxListHeight={maxListHeight}
updateValue={updateValue}
onShowingChange={setShowingAppCommand}
value={value || ''}
nestedScrollEnabled={nestedScrollEnabled}
channelId={channelId}
rootId={rootId}
/>
)}
{(!appsTakeOver || !isAppsEnabled) && (<>
<AtMention
cursorPosition={cursorPosition}
maxListHeight={maxListHeight}
updateValue={updateValue}
onShowingChange={setShowingAtMention}
value={value || ''}
nestedScrollEnabled={nestedScrollEnabled}
isSearch={isSearch}
channelId={channelId}
/>
<ChannelMention
cursorPosition={cursorPosition}
maxListHeight={maxListHeight}
updateValue={updateValue}
onShowingChange={setShowingChannelMention}
value={value || ''}
nestedScrollEnabled={nestedScrollEnabled}
isSearch={isSearch}
/>
{!isSearch &&
{isAppsEnabled && channelId && (
<AppSlashSuggestion
listStyle={style.listStyle}
updateValue={updateValue}
onShowingChange={setShowingAppCommand}
value={value || ''}
nestedScrollEnabled={nestedScrollEnabled}
channelId={channelId}
rootId={rootId}
/>
)}
{(!appsTakeOver || !isAppsEnabled) && (<>
<AtMention
cursorPosition={cursorPosition}
listStyle={style.listStyle}
updateValue={updateValue}
onShowingChange={setShowingAtMention}
value={value || ''}
nestedScrollEnabled={nestedScrollEnabled}
isSearch={isSearch}
channelId={channelId}
/>
<ChannelMention
cursorPosition={cursorPosition}
listStyle={style.listStyle}
updateValue={updateValue}
onShowingChange={setShowingChannelMention}
value={value || ''}
nestedScrollEnabled={nestedScrollEnabled}
isSearch={isSearch}
/>
{!isSearch &&
<EmojiSuggestion
cursorPosition={cursorPosition}
maxListHeight={maxListHeight}
listStyle={style.listStyle}
updateValue={updateValue}
onShowingChange={setShowingEmoji}
value={value || ''}
@@ -179,10 +174,10 @@ const Autocomplete = ({
hasFilesAttached={hasFilesAttached}
inPost={inPost}
/>
}
{showCommands && channelId &&
}
{showCommands && channelId &&
<SlashSuggestion
maxListHeight={maxListHeight}
listStyle={style.listStyle}
updateValue={updateValue}
onShowingChange={setShowingCommand}
value={value || ''}
@@ -190,8 +185,8 @@ const Autocomplete = ({
channelId={channelId}
rootId={rootId}
/>
}
{/* {(isSearch && enableDateSuggestion) &&
}
{/* {(isSearch && enableDateSuggestion) &&
<DateSuggestion
cursorPosition={cursorPosition}
updateValue={updateValue}
@@ -199,9 +194,8 @@ const Autocomplete = ({
value={value || ''}
/>
} */}
</>)}
</View>
</View>
</>)}
</Animated.View>
);
};

View File

@@ -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<Channel | ChannelModel>, 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<ViewStyle>;
}
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<Channel | ChannelModel> = [];
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<Array<SectionListData<(Channel | ChannelModel)>>>(emptySections);
const [channels, setChannels] = useState<Array<ChannelModel | Channel>>(emptyChannels);
@@ -213,10 +200,6 @@ const ChannelMention = ({
const [localChannels, setlocalChannels] = useState<ChannelModel[]>();
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);

View File

@@ -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<ViewStyle>;
}
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 (
<FlatList
keyboardShouldPersistTaps='always'
style={flatListStyle}
style={[style.listView, listStyle]}
data={data}
keyExtractor={keyExtractor}
removeClippedSubviews={true}

View File

@@ -4,7 +4,7 @@
import {debounce} from 'lodash';
import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react';
import {useIntl} from 'react-intl';
import {FlatList, Platform} from 'react-native';
import {FlatList, Platform, StyleProp, ViewStyle} from 'react-native';
import AtMentionItem from '@components/autocomplete/at_mention_item';
import ChannelMentionItem from '@components/autocomplete/channel_mention_item';
@@ -12,7 +12,6 @@ import {COMMAND_SUGGESTION_CHANNEL, COMMAND_SUGGESTION_USER} from '@constants/ap
import {useServerUrl} from '@context/server';
import {useTheme} from '@context/theme';
import analytics from '@managers/analytics';
import {makeStyleSheetFromTheme} from '@utils/theme';
import {AppCommandParser, ExtendedAutocompleteSuggestion} from '../app_command_parser/app_command_parser';
import SlashSuggestionItem from '../slash_suggestion_item';
@@ -23,7 +22,6 @@ import type UserModel from '@typings/database/models/servers/user';
export type Props = {
currentTeamId: string;
isSearch?: boolean;
maxListHeight?: number;
updateValue: (text: string) => void;
onShowingChange: (c: boolean) => void;
value: string;
@@ -31,21 +29,11 @@ export type Props = {
rootId?: string;
channelId: string;
isAppsEnabled: boolean;
listStyle: StyleProp<ViewStyle>;
};
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<AppCommandParser>(new AppCommandParser(serverUrl, intl, channelId, currentTeamId, rootId, theme));
const [dataSource, setDataSource] = useState<AutocompleteSuggestion[]>(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);

View File

@@ -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<ViewStyle>;
};
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<AppCommandParser>(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));

View File

@@ -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 = (
<Autocomplete
position={autocompletePosition}
position={animatedAutocompletePosition}
updateValue={setValue}
rootId={rootId}
channelId={channelId}
@@ -121,7 +124,7 @@ function PostDraft({
isSearch={isSearch}
hasFilesAttached={Boolean(files?.length)}
inPost={true}
availableSpace={autocompleteAvailableSpace}
availableSpace={animatedAutocompleteAvailableSpace}
/>
);
@@ -144,9 +147,7 @@ function PostDraft({
>
{draftHandler}
</KeyboardTrackingView>
<View>
{autoComplete}
</View>
{autoComplete}
</>
);
}

20
app/hooks/autocomplete.ts Normal file
View File

@@ -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];
};

View File

@@ -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 (
<View style={styles.container}>
@@ -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 (
<SafeAreaView
edges={['bottom', 'left', 'right']}
@@ -390,18 +395,16 @@ export default function ChannelInfoForm({
</View>
</TouchableWithoutFeedback>
</KeyboardAwareScrollView>
<View>
<Autocomplete
position={autocompletePosition}
updateValue={onHeaderChange}
cursorPosition={header.length}
value={header}
nestedScrollEnabled={true}
availableSpace={autocompleteAvailableSpace}
inPost={false}
growDown={!growUp}
/>
</View>
<Autocomplete
position={animatedAutocompletePosition}
updateValue={onHeaderChange}
cursorPosition={header.length}
value={header}
nestedScrollEnabled={true}
availableSpace={animatedAutocompleteAvailableSpace}
inPost={false}
growDown={growDown}
/>
</SafeAreaView>
);
}

View File

@@ -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 (
<View style={styles.loader}>
<Loading color={theme.buttonBg}/>
</View>
);
}
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 (
<View style={styles.loader}>
<Loading color={theme.buttonBg}/>
</View>
);
}
return (
<>
<SafeAreaView
@@ -248,8 +251,8 @@ const EditPost = ({componentId, maxPostSize, post, closeButtonId, hasFilesAttach
updateValue={onChangeText}
value={postMessage}
cursorPosition={cursorPosition}
position={autocompletePosition}
availableSpace={autocompleteAvailableSpace}
position={animatedAutocompletePosition}
availableSpace={animatedAutocompleteAvailableSpace}
inPost={false}
/>
</>

View File

@@ -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(() => (
<Autocomplete
updateValue={handleTextChange}
@@ -256,6 +262,7 @@ const SearchScreen = ({teamId}: Props) => {
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}
/>
<Animated.View style={[top, {zIndex: AutocompleteZindex}]}>
{autocomplete}
</Animated.View>
<SafeAreaView
style={styles.flex}
edges={EDGES}
@@ -319,6 +323,7 @@ const SearchScreen = ({teamId}: Props) => {
}
</Animated.View>
</SafeAreaView>
{autocomplete}
</FreezeScreen>
);
};

View File

@@ -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 | Record<string, unknown>> | 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<AnimatedProps>