mattermost-mobile/app/components/autocomplete/autocomplete.tsx
Rajat Dabade e39b27be7e
New keyboard library Setup (#9278)
* Initial setup for new keyboard

* fix the offset calculation onMove by adding isKeyboardFullyOpened

* Done with the keyboard handling implementation

* Handled keyboard focus and blured state using context

* Added default height for input container

* Android support

* Tablet state handling

* Fix for refreshing offset in list

* Created a default context for mention post list

* Fix linter errors

* Fix tests

* Minor

* Fix the height issue for tablet view

* Review comments

* Dependency fix

* Reveiw comment

* keyboard animation only enabled with screen on top navigation

* added tests

* Added extra keyboard component for emoji picker (#9328)

* Added extra keyboard component

* handled swipe geature for extra keyboard

* scroll to bottom visible on emoji picker

* Check for stale event for keyboard geature area

* fix keyboard stale event for mid-gesture change swipe direction

* Closing emoji picker when message priority is opened

* changing to thread view closes emoji picker

* Close emoij picker and keyboard when attachment icons are clicked

* handle android emoji picker behaviour

* Remove emoji picker code

* fix tests

* Address reviev comments

* Test fixes

* usePreventDoubleTab for race condition and corrected comment on isInputAccessoryViewMode flag

* File attachments option in bottom sheet quick action (#9331)

* Added extra keyboard component

* handled swipe geature for extra keyboard

* File attachments option in bottom sheet quick action

* Updated tests

* i18n

* fix test

* Added tests

* Review comment

* fix tests

* Integrated Emoji picker to Extra keyboard component.  (#9339)

* Added extra keyboard component

* handled swipe geature for extra keyboard

* scroll to bottom visible on emoji picker

* Fix the post input container height change issue

* Added emoji picker with search functionality

* keyboard height not recorded fallback to default height, set search to false closing emoji picker

* fix search funcitonality in android

* scroll to end bottom dismissed with pressed

* Fixed the scroll issue for android

* fix the flickering post input issue

* Wired up the emoji picker

* Added keyboard and spaceback in emoji picker

* intl extract

* initial cursor position and simple calculation review comment

* separated grapheme to utils file

* Review comments

* nitpick

* Fix the bottom safe area and navigation stack restore fix

* Fix test

* disabled emoji picker in edit post screen

* change the name of the variable to name it clarier

* fix the tab gap issue in andriod

* disabled the emoji skin tone change on andriod

* fix the input container jump issue

* fix the failing test

* UX review

* added white space after the attachment icon

* When @ or / is press open keyboard on andriod also fix the scroll issue when keyboard is open

* back bottom closes emoji picker and opening keyboard jump fix

* reverted code

* fixed the flickering issue of input and scroll position fix

* Fix the gap issue in thread

* fix the warning reaminated issue when opening a channel

* Fix the extra space issue between input and emoij picker

* Minor fix for extra space between input and emoji picker

* Fix thread view bottom safe area and gap between keyboard and searched emojis

* Fix the keyboard issue in tablet

* Fix the warning issue react-native-keyboard-controller

* Fix tests

* Fix the tablet search hight issue

* Replaced the ExtraKeyboardProvider with KeyboardProvider from rnkc for permalink

* Bottom sheet jump issue resolved

* Search do not close after selecting emoji in search list

* Added the bottom inset height in search emoji for android

* fix buid issue

* Fix the cancel state to emoji picker

* use portals for autocomplete

* fix permalink extra space in post list

* Portal only for android

---------

Co-authored-by: Mattermost Build <build@mattermost.com>
Co-authored-by: Rahim Rahman <rahim.rahman@mattermost.com>
Co-authored-by: Harshil Sharma <harshilsharma63@gmail.com>
Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
2026-01-13 22:38:35 +05:30

242 lines
8.6 KiB
TypeScript

// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {Portal} from '@gorhom/portal';
import React, {useMemo, useState} from 'react';
import {Platform, type StyleProp, useWindowDimensions, type ViewStyle} from 'react-native';
import Animated, {type SharedValue, useAnimatedStyle, useDerivedValue} from 'react-native-reanimated';
import {MAX_LIST_HEIGHT, MAX_LIST_TABLET_DIFF} from '@constants/autocomplete';
import {useTheme} from '@context/theme';
import {useIsTablet} from '@hooks/device';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
import AtMention from './at_mention/';
import ChannelMention from './channel_mention/';
import EmojiSuggestion from './emoji_suggestion/';
import SlashSuggestion from './slash_suggestion/';
import AppSlashSuggestion from './slash_suggestion/app_slash_suggestion/';
const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
return {
base: {
position: 'absolute',
},
borders: {
borderWidth: 1,
borderColor: changeOpacity(theme.centerChannelColor, 0.2),
overflow: 'hidden',
borderRadius: 8,
elevation: 3,
},
shadow: {
backgroundColor: theme.centerChannelBg,
shadowColor: '#000',
shadowOpacity: 1,
shadowRadius: 6,
shadowOffset: {
width: 0,
height: 6,
},
},
listStyle: {
backgroundColor: theme.centerChannelBg,
borderRadius: 4,
paddingHorizontal: 16,
},
};
});
type Props = {
cursorPosition: number;
position: SharedValue<number>;
rootId?: string;
channelId?: string;
isSearch?: boolean;
value: string;
enableDateSuggestion?: boolean;
isAppsEnabled: boolean;
nestedScrollEnabled?: boolean;
updateValue: (v: string) => void;
shouldDirectlyReact?: boolean;
availableSpace: SharedValue<number>;
growDown?: boolean;
teamId?: string;
containerStyle?: StyleProp<ViewStyle>;
autocompleteProviders?: AutocompleteProviders;
useAllAvailableSpace?: boolean;
horizontalPadding?: number;
usePortal?: boolean;
}
type AutocompleteProviders = {
user: boolean;
channel: boolean;
emoji: boolean;
slash: boolean;
}
const defaultAutocompleteProviders: AutocompleteProviders = {
user: true,
channel: true,
emoji: true,
slash: true,
};
const Autocomplete = ({
cursorPosition,
position,
rootId,
channelId,
isSearch = false,
value,
availableSpace,
//enableDateSuggestion = false,
isAppsEnabled,
nestedScrollEnabled = false,
updateValue,
shouldDirectlyReact = false,
growDown = false,
containerStyle,
teamId,
autocompleteProviders = defaultAutocompleteProviders,
useAllAvailableSpace = false,
horizontalPadding = 8,
usePortal = false,
}: Props) => {
const theme = useTheme();
const isTablet = useIsTablet();
const style = getStyleFromTheme(theme);
const dimensions = useWindowDimensions();
const [showingAtMention, setShowingAtMention] = useState(false);
const [showingChannelMention, setShowingChannelMention] = useState(false);
const [showingEmoji, setShowingEmoji] = useState(false);
const [showingCommand, setShowingCommand] = useState(false);
const [showingAppCommand, setShowingAppCommand] = useState(false);
// const [showingDate, setShowingDate] = useState(false);
const hasElements = showingChannelMention || showingEmoji || showingAtMention || showingCommand || showingAppCommand; // || showingDate;
const appsTakeOver = showingAppCommand;
const showCommands = !(showingChannelMention || showingEmoji || showingAtMention);
const isLandscape = dimensions.width > dimensions.height;
const maxHeightAdjust = (isTablet && isLandscape) ? MAX_LIST_TABLET_DIFF : 0;
const defaultMaxHeight = MAX_LIST_HEIGHT - maxHeightAdjust;
const maxHeight = useDerivedValue(() => {
return useAllAvailableSpace ? availableSpace.value : Math.min(availableSpace.value, defaultMaxHeight);
}, [defaultMaxHeight, useAllAvailableSpace, availableSpace]);
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: StyleProp<ViewStyle> = [style.base, {left: horizontalPadding, right: horizontalPadding}, containerAnimatedStyle];
if (hasElements) {
s.push(style.borders);
}
if (Platform.OS === 'ios') {
s.push(style.shadow);
}
if (containerStyle) {
s.push(containerStyle);
}
return s;
}, [style.base, style.borders, style.shadow, horizontalPadding, containerAnimatedStyle, hasElements, containerStyle]);
const component = (
<Animated.View
testID='autocomplete'
style={containerStyles}
>
{isAppsEnabled && channelId && autocompleteProviders.slash && (
<AppSlashSuggestion
listStyle={style.listStyle}
updateValue={updateValue}
onShowingChange={setShowingAppCommand}
value={value || ''}
nestedScrollEnabled={nestedScrollEnabled}
channelId={channelId}
rootId={rootId}
/>
)}
{(!appsTakeOver || !isAppsEnabled) && (<>
{autocompleteProviders.user && (
<AtMention
cursorPosition={cursorPosition}
listStyle={style.listStyle}
updateValue={updateValue}
onShowingChange={setShowingAtMention}
value={value || ''}
nestedScrollEnabled={nestedScrollEnabled}
isSearch={isSearch}
channelId={channelId}
teamId={teamId}
/>
)}
{autocompleteProviders.channel && (
<ChannelMention
cursorPosition={cursorPosition}
listStyle={style.listStyle}
updateValue={updateValue}
onShowingChange={setShowingChannelMention}
value={value || ''}
nestedScrollEnabled={nestedScrollEnabled}
isSearch={isSearch}
channelId={channelId}
teamId={teamId}
/>
)}
{!isSearch && autocompleteProviders.emoji && (
<EmojiSuggestion
cursorPosition={cursorPosition}
listStyle={style.listStyle}
updateValue={updateValue}
onShowingChange={setShowingEmoji}
value={value || ''}
nestedScrollEnabled={nestedScrollEnabled}
rootId={rootId}
shouldDirectlyReact={shouldDirectlyReact}
/>
)}
{showCommands && channelId && autocompleteProviders.slash && (
<SlashSuggestion
listStyle={style.listStyle}
updateValue={updateValue}
onShowingChange={setShowingCommand}
value={value || ''}
nestedScrollEnabled={nestedScrollEnabled}
channelId={channelId}
rootId={rootId}
isAppsEnabled={isAppsEnabled}
/>
)}
{/* {(isSearch && enableDateSuggestion) &&
<DateSuggestion
cursorPosition={cursorPosition}
updateValue={updateValue}
onResultCountChange={setShowingDate}
value={value || ''}
/>
} */}
</>)}
</Animated.View>
);
if (usePortal && Platform.OS === 'android') {
return (
<Portal>
{component}
</Portal>
);
}
return component;
};
export default Autocomplete;