forked from Ivasoft/mattermost-mobile
Fix autocomplete not scrolling in search in Android (#6596)
This commit is contained in:
committed by
GitHub
parent
72d12ae1ac
commit
9ab4c935ef
@@ -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'
|
||||
/>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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
20
app/hooks/autocomplete.ts
Normal 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];
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
</>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user