From 8e026c20acf6c8a36f036d6b683fc6e01060ed40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Espino=20Garc=C3=ADa?= Date: Thu, 3 Mar 2022 11:01:12 +0100 Subject: [PATCH] [Gekidou] [MM-39682] Add channel autocomplete (#5998) * Add channel autocomplete * Fix autocomplete position and shadow effect * Fix feedback issues * Fix autocomplete on iOS and piggyback removal of section borders * Fixes the channel autocomplete showing up after completion * Address feedback Co-authored-by: Elias Nahum --- app/components/autocomplete/autocomplete.tsx | 73 ++-- .../autocomplete_section_header.tsx | 74 ++++ .../channel_mention/channel_mention.tsx | 315 ++++++++++++++++++ .../autocomplete/channel_mention/index.ts | 21 ++ .../channel_mention_item.tsx | 129 +++++++ .../channel_mention_item/index.ts | 39 +++ .../emoji_suggestion/emoji_suggestion.tsx | 7 +- .../post_draft/draft_input/index.tsx | 2 +- app/components/post_draft/post_draft.tsx | 9 +- .../post_draft/post_input/post_input.tsx | 1 - app/constants/autocomplete.ts | 13 + 11 files changed, 629 insertions(+), 54 deletions(-) create mode 100644 app/components/autocomplete/autocomplete_section_header.tsx create mode 100644 app/components/autocomplete/channel_mention/channel_mention.tsx create mode 100644 app/components/autocomplete/channel_mention/index.ts create mode 100644 app/components/autocomplete/channel_mention_item/channel_mention_item.tsx create mode 100644 app/components/autocomplete/channel_mention_item/index.ts diff --git a/app/components/autocomplete/autocomplete.tsx b/app/components/autocomplete/autocomplete.tsx index 0bc196f31e..cb445712cd 100644 --- a/app/components/autocomplete/autocomplete.tsx +++ b/app/components/autocomplete/autocomplete.tsx @@ -1,14 +1,15 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import React, {useEffect, useMemo, useState} from 'react'; -import {Keyboard, KeyboardEvent, Platform, useWindowDimensions, View} from 'react-native'; +import React, {useMemo, useState} from 'react'; +import {Platform, useWindowDimensions, View} from 'react-native'; +import {LIST_BOTTOM, MAX_LIST_DIFF, MAX_LIST_HEIGHT, MAX_LIST_TABLET_DIFF, OFFSET_TABLET} from '@constants/autocomplete'; import {useTheme} from '@context/theme'; import {useIsTablet} from '@hooks/device'; -import useHeaderHeight from '@hooks/header'; import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme'; +import ChannelMention from './channel_mention/'; import EmojiSuggestion from './emoji_suggestion/'; const getStyleFromTheme = makeStyleSheetFromTheme((theme) => { @@ -22,7 +23,8 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => { borderWidth: 1, borderColor: changeOpacity(theme.centerChannelColor, 0.2), overflow: 'hidden', - borderRadius: 4, + borderRadius: 8, + elevation: 3, }, hidden: { display: 'none', @@ -40,10 +42,10 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => { shadow: { shadowColor: '#000', shadowOpacity: 0.12, - shadowRadius: 8, + shadowRadius: 6, shadowOffset: { width: 0, - height: 8, + height: 6, }, }, }; @@ -58,15 +60,11 @@ type Props = { value: string; enableDateSuggestion?: boolean; isAppsEnabled: boolean; - offsetY?: number; nestedScrollEnabled?: boolean; updateValue: (v: string) => void; hasFilesAttached: boolean; } -const OFFSET_IPAD = 60; -const AUTOCOMPLETE_MARGIN = 20; - const Autocomplete = ({ cursorPosition, postInputTop, @@ -78,52 +76,46 @@ const Autocomplete = ({ //enableDateSuggestion = false, isAppsEnabled, - offsetY = 60, nestedScrollEnabled = false, updateValue, hasFilesAttached, }: Props) => { const theme = useTheme(); const isTablet = useIsTablet(); + const dimensions = useWindowDimensions(); const style = getStyleFromTheme(theme); - const {height: deviceHeight} = useWindowDimensions(); - const {defaultHeight: headerHeight} = useHeaderHeight(false, true, false); - const [keyboardHeight, setKeyboardHeight] = useState(0); // const [showingAtMention, setShowingAtMention] = useState(false); - // const [showingChannelMention, setShowingChannelMention] = 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 = showingEmoji; // || showingAtMention || showingChannelMention || showingCommand || showingAppCommand || showingDate; + const hasElements = showingChannelMention || showingEmoji; // || showingAtMention || showingCommand || showingAppCommand || showingDate; const appsTakeOver = false; // showingAppCommand; const maxListHeight = useMemo(() => { - const postInputHeight = deviceHeight - postInputTop; - let offset = 0; - if (Platform.OS === 'ios' && isTablet) { - offset = OFFSET_IPAD; + const isLandscape = dimensions.width > dimensions.height; + const offset = isTablet && isLandscape ? OFFSET_TABLET : 0; + let postInputDiff = 0; + if (isTablet && postInputTop && isLandscape) { + postInputDiff = MAX_LIST_TABLET_DIFF; + } else if (postInputTop) { + postInputDiff = MAX_LIST_DIFF; } - - if (keyboardHeight) { - return (deviceHeight - (postInputHeight + headerHeight + AUTOCOMPLETE_MARGIN + offset)); - } - return (deviceHeight - (postInputHeight + headerHeight + AUTOCOMPLETE_MARGIN + offset)) / 2; - }, [postInputTop, deviceHeight, headerHeight, isTablet]); // We don't depend on keyboardHeight to avoid visual artifacts due to postInputTop and keyboardHeight not being updated in the same render. + return MAX_LIST_HEIGHT - postInputDiff - offset; + }, [postInputTop, isTablet, dimensions.width]); const wrapperStyles = useMemo(() => { const s = []; if (Platform.OS === 'ios') { s.push(style.shadow); } - if (isSearch) { s.push(style.base, style.searchContainer, {height: maxListHeight}); } - if (!hasElements) { s.push(style.hidden); } @@ -133,26 +125,14 @@ const Autocomplete = ({ const containerStyles = useMemo(() => { const s = [style.borders]; if (!isSearch) { - s.push(style.base, {bottom: offsetY}); + const offset = isTablet ? -OFFSET_TABLET : 0; + s.push(style.base, {bottom: postInputTop + LIST_BOTTOM + offset}); } if (!hasElements) { s.push(style.hidden); } return s; - }, [!isSearch && offsetY, hasElements]); - - useEffect(() => { - const keyboardEvent = (event: KeyboardEvent) => { - setKeyboardHeight(event.endCoordinates.height); - }; - const shown = Keyboard.addListener('keyboardDidShow', keyboardEvent); - const hidden = Keyboard.addListener('keyboardDidHide', keyboardEvent); - - return () => { - shown.remove(); - hidden.remove(); - }; - }, []); + }, [!isSearch, isTablet, hasElements, postInputTop]); return ( + /> */} */} + isSearch={isSearch} + /> {!isSearch && { + return { + section: { + justifyContent: 'center', + position: 'relative', + top: -1, + flexDirection: 'row', + }, + sectionText: { + fontSize: 12, + fontWeight: '600', + textTransform: 'uppercase', + color: changeOpacity(theme.centerChannelColor, 0.56), + paddingTop: 16, + paddingBottom: 8, + paddingHorizontal: 16, + flex: 1, + }, + sectionWrapper: { + backgroundColor: theme.centerChannelBg, + }, + }; +}); + +type Props = { + defaultMessage: string; + id: string; + loading: boolean; +} + +const AutocompleteSectionHeader = ({ + defaultMessage, + id, + loading, +}: Props) => { + const insets = useSafeAreaInsets(); + const theme = useTheme(); + const style = getStyleFromTheme(theme); + + const sectionStyles = useMemo(() => { + return [style.section, {marginLeft: insets.left, marginRight: insets.right}]; + }, [style, insets]); + + return ( + + + + {loading && + + } + + + ); +}; + +export default AutocompleteSectionHeader; diff --git a/app/components/autocomplete/channel_mention/channel_mention.tsx b/app/components/autocomplete/channel_mention/channel_mention.tsx new file mode 100644 index 0000000000..bc6dcaa64e --- /dev/null +++ b/app/components/autocomplete/channel_mention/channel_mention.tsx @@ -0,0 +1,315 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {debounce} from 'lodash'; +import React, {useCallback, useEffect, useMemo, useState} from 'react'; +import {Platform, SectionList, SectionListData} from 'react-native'; + +import {searchChannels} from '@actions/remote/channel'; +import AutocompleteSectionHeader from '@components/autocomplete/autocomplete_section_header'; +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 useDidUpdate from '@hooks/did_update'; +import {t} from '@i18n'; +import {makeStyleSheetFromTheme} from '@utils/theme'; + +import type MyChannelModel from '@typings/database/models/servers/my_channel'; + +const keyExtractor = (item: Channel) => { + return item.id; +}; + +const getMatchTermForChannelMention = (() => { + let lastMatchTerm: string | null = null; + let lastValue: string; + let lastIsSearch: boolean; + return (value: string, isSearch: boolean) => { + if (value !== lastValue || isSearch !== lastIsSearch) { + const regex = isSearch ? CHANNEL_MENTION_SEARCH_REGEX : CHANNEL_MENTION_REGEX; + const match = value.match(regex); + lastValue = value; + lastIsSearch = isSearch; + if (match) { + if (isSearch) { + lastMatchTerm = match[1].toLowerCase(); + } else if (match.index && match.index > 0 && value[match.index - 1] === '~') { + lastMatchTerm = null; + } else { + lastMatchTerm = match[2].toLowerCase(); + } + } else { + lastMatchTerm = null; + } + } + return lastMatchTerm; + }; +})(); + +const reduceChannelsForSearch = (channels: Channel[], members: MyChannelModel[]) => { + return channels.reduce(([pubC, priC, dms], c) => { + switch (c.type) { + case General.OPEN_CHANNEL: + if (members.find((m) => c.id === m.id)) { + pubC.push(c); + } + break; + case General.PRIVATE_CHANNEL: + priC.push(c); + break; + case General.DM_CHANNEL: + case General.GM_CHANNEL: + dms.push(c); + } + return [pubC, priC, dms]; + }, [[], [], []]); +}; + +const reduceChannelsForAutocomplete = (channels: Channel[], members: MyChannelModel[]) => { + return channels.reduce(([myC, otherC], c) => { + if (members.find((m) => c.id === m.id)) { + myC.push(c); + } else { + otherC.push(c); + } + return [myC, otherC]; + }, [[], []]); +}; + +const makeSections = (channels: Channel[], myMembers: MyChannelModel[], isSearch = false) => { + const newSections = []; + if (isSearch) { + const [publicChannels, privateChannels, directAndGroupMessages] = reduceChannelsForSearch(channels, myMembers); + if (publicChannels.length) { + newSections.push({ + id: t('suggestion.search.public'), + defaultMessage: 'Public Channels', + data: publicChannels, + key: 'publicChannels', + hideLoadingIndicator: true, + }); + } + + if (privateChannels.length) { + newSections.push({ + id: t('suggestion.search.private'), + defaultMessage: 'Private Channels', + data: privateChannels, + key: 'privateChannels', + hideLoadingIndicator: true, + }); + } + + if (directAndGroupMessages.length) { + newSections.push({ + id: t('suggestion.search.direct'), + defaultMessage: 'Direct Messages', + data: directAndGroupMessages, + key: 'directAndGroupMessages', + hideLoadingIndicator: true, + }); + } + } else { + const [myChannels, otherChannels] = reduceChannelsForAutocomplete(channels, myMembers); + if (myChannels.length) { + newSections.push({ + id: t('suggestion.mention.channels'), + defaultMessage: 'My Channels', + data: myChannels, + key: 'myChannels', + hideLoadingIndicator: true, + }); + } + + if (otherChannels.length) { + newSections.push({ + id: t('suggestion.mention.morechannels'), + defaultMessage: 'Other Channels', + data: otherChannels, + key: 'otherChannels', + hideLoadingIndicator: true, + }); + } + } + + const nSections = newSections.length; + if (nSections) { + newSections[nSections - 1].hideLoadingIndicator = false; + } + + return newSections; +}; +type Props = { + cursorPosition: number; + isSearch: boolean; + maxListHeight: number; + myMembers: MyChannelModel[]; + updateValue: (v: string) => void; + onShowingChange: (c: boolean) => void; + value: string; + nestedScrollEnabled: boolean; +} + +const getStyleFromTheme = makeStyleSheetFromTheme((theme) => { + return { + listView: { + backgroundColor: theme.centerChannelBg, + borderRadius: 4, + }, + }; +}); + +const ChannelMention = ({ + cursorPosition, + isSearch, + maxListHeight, + myMembers, + updateValue, + onShowingChange, + value, + nestedScrollEnabled, +}: Props) => { + const serverUrl = useServerUrl(); + const theme = useTheme(); + const style = getStyleFromTheme(theme); + + const [sections, setSections] = useState>>([]); + const [channels, setChannels] = useState([]); + const [loading, setLoading] = useState(false); + const [noResultsTerm, setNoResultsTerm] = useState(null); + const [localCursorPosition, setLocalCursorPosition] = useState(cursorPosition); // To avoid errors due to delay between value changes and cursor position changes. + + 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); + if (!error) { + setChannels(receivedChannels!); + } + setLoading(false); + }, 200), []); + + const matchTerm = getMatchTermForChannelMention(value.substring(0, localCursorPosition), isSearch); + const resetState = () => { + setChannels([]); + setSections([]); + runSearch.cancel(); + }; + + const completeMention = useCallback((mention: string) => { + const mentionPart = value.substring(0, localCursorPosition); + + let completedDraft: string; + if (isSearch) { + const channelOrIn = mentionPart.includes('in:') ? 'in:' : 'channel:'; + completedDraft = mentionPart.replace(CHANNEL_MENTION_SEARCH_REGEX, `${channelOrIn} ${mention} `); + } else if (Platform.OS === 'ios') { + // We are going to set a double ~ on iOS to prevent the auto correct from taking over and replacing it + // with the wrong value, this is a hack but I could not found another way to solve it + completedDraft = mentionPart.replace(CHANNEL_MENTION_REGEX, `~~${mention} `); + } else { + completedDraft = mentionPart.replace(CHANNEL_MENTION_REGEX, `~${mention} `); + } + + const newCursorPosition = completedDraft.length - 1; + + if (value.length > localCursorPosition) { + completedDraft += value.substring(localCursorPosition); + } + + updateValue(completedDraft); + setLocalCursorPosition(newCursorPosition); + + if (Platform.OS === 'ios') { + // This is the second part of the hack were we replace the double ~ with just one + // after the auto correct vanished + setTimeout(() => { + updateValue(completedDraft.replace(`~~${mention} `, `~${mention} `)); + }); + } + + onShowingChange(false); + setNoResultsTerm(mention); + setSections([]); + }, [value, localCursorPosition, isSearch]); + + const renderItem = useCallback(({item}) => { + return ( + + ); + }, [completeMention]); + + const renderSectionHeader = useCallback(({section}) => { + return ( + + ); + }, [loading]); + + useEffect(() => { + if (localCursorPosition !== cursorPosition) { + setLocalCursorPosition(cursorPosition); + } + }, [cursorPosition]); + + useEffect(() => { + if (matchTerm === null) { + resetState(); + onShowingChange(false); + return; + } + + if (noResultsTerm != null && matchTerm.startsWith(noResultsTerm)) { + return; + } + + setNoResultsTerm(null); + runSearch(serverUrl, matchTerm); + }, [matchTerm]); + + useDidUpdate(() => { + const newSections = makeSections(channels, myMembers, isSearch); + const nSections = newSections.length; + + if (!loading && !nSections && noResultsTerm == null) { + setNoResultsTerm(matchTerm); + } + setSections(newSections); + onShowingChange(Boolean(nSections)); + }, [channels, myMembers, loading]); + + if (sections.length === 0 || noResultsTerm != null) { + // If we are not in an active state or the mention has been completed return null so nothing is rendered + // other components are not blocked. + return null; + } + + return ( + + ); +}; + +export default ChannelMention; diff --git a/app/components/autocomplete/channel_mention/index.ts b/app/components/autocomplete/channel_mention/index.ts new file mode 100644 index 0000000000..42fe679107 --- /dev/null +++ b/app/components/autocomplete/channel_mention/index.ts @@ -0,0 +1,21 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {withDatabase} from '@nozbe/watermelondb/DatabaseProvider'; +import withObservables from '@nozbe/with-observables'; + +import {MM_TABLES} from '@constants/database'; + +import ChannelMention from './channel_mention'; + +import type {WithDatabaseArgs} from '@typings/database/database'; +import type MyChannelModel from '@typings/database/models/servers/my_channel'; + +const {SERVER: {MY_CHANNEL}} = MM_TABLES; +const enhanced = withObservables([], ({database}: WithDatabaseArgs) => { + return { + myMembers: database.get(MY_CHANNEL).query().observe(), + }; +}); + +export default withDatabase(enhanced(ChannelMention)); diff --git a/app/components/autocomplete/channel_mention_item/channel_mention_item.tsx b/app/components/autocomplete/channel_mention_item/channel_mention_item.tsx new file mode 100644 index 0000000000..aa0a741306 --- /dev/null +++ b/app/components/autocomplete/channel_mention_item/channel_mention_item.tsx @@ -0,0 +1,129 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React, {useMemo} from 'react'; +import {Text, View} from 'react-native'; +import {useSafeAreaInsets} from 'react-native-safe-area-context'; + +import ChannelIcon from '@app/components/channel_icon'; +import {BotTag, GuestTag} from '@components/tag'; +import TouchableWithFeedback from '@components/touchable_with_feedback'; +import {General} from '@constants'; +import {useTheme} from '@context/theme'; +import {makeStyleSheetFromTheme, changeOpacity} from '@utils/theme'; + +const getStyleFromTheme = makeStyleSheetFromTheme((theme: Theme) => { + return { + icon: { + marginRight: 11, + opacity: 0.56, + }, + row: { + paddingHorizontal: 16, + height: 40, + flexDirection: 'row', + alignItems: 'center', + }, + rowDisplayName: { + fontSize: 15, + color: theme.centerChannelColor, + }, + rowName: { + fontSize: 15, + color: theme.centerChannelColor, + opacity: 0.56, + }, + }; +}); + +type Props = { + channel: Channel; + displayName?: string; + isBot: boolean; + isGuest: boolean; + onPress: (name?: string) => void; + testID?: string; +}; + +const ChannelMentionItem = ({ + channel, + displayName, + isBot, + isGuest, + onPress, + testID, +}: Props) => { + const insets = useSafeAreaInsets(); + const theme = useTheme(); + + const completeMention = () => { + if (channel.type === General.DM_CHANNEL || channel.type === General.GM_CHANNEL) { + onPress('@' + displayName?.replace(/ /g, '')); + } else { + onPress(channel.name); + } + }; + + const style = getStyleFromTheme(theme); + const margins = useMemo(() => { + return {marginLeft: insets.left, marginRight: insets.right}; + }, [insets]); + const rowStyle = useMemo(() => { + return [style.row, margins]; + }, [margins, style]); + + let component; + + if (channel.type === General.DM_CHANNEL || channel.type === General.GM_CHANNEL) { + if (!displayName) { + return null; + } + + component = ( + + {'@' + displayName} + + + + ); + } else { + component = ( + + + 0} + size={18} + style={style.icon} + /> + {displayName} + {` ~${channel.name}`} + + + ); + } + + return component; +}; + +export default ChannelMentionItem; diff --git a/app/components/autocomplete/channel_mention_item/index.ts b/app/components/autocomplete/channel_mention_item/index.ts new file mode 100644 index 0000000000..8231897d2a --- /dev/null +++ b/app/components/autocomplete/channel_mention_item/index.ts @@ -0,0 +1,39 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {withDatabase} from '@nozbe/watermelondb/DatabaseProvider'; +import withObservables from '@nozbe/with-observables'; +import {of as of$} from 'rxjs'; +import {switchMap} from 'rxjs/operators'; + +import {General} from '@constants'; +import {MM_TABLES} from '@constants/database'; + +import ChannelMentionItem from './channel_mention_item'; + +import type {WithDatabaseArgs} from '@typings/database/database'; +import type UserModel from '@typings/database/models/servers/user'; + +type OwnProps = { + channel: Channel; +} + +const {SERVER: {USER}} = MM_TABLES; +const enhanced = withObservables([], ({database, channel}: WithDatabaseArgs & OwnProps) => { + let user = of$(undefined); + if (channel.type === General.DM_CHANNEL) { + user = database.get(USER).findAndObserve(channel.teammate_id!); + } + + const isBot = user.pipe(switchMap((u) => of$(u ? u.isBot : false))); + const isGuest = user.pipe(switchMap((u) => of$(u ? u.isGuest : false))); + const displayName = user.pipe(switchMap((u) => of$(u ? u.username : channel.display_name))); + + return { + isBot, + isGuest, + displayName, + }; +}); + +export default withDatabase(enhanced(ChannelMentionItem)); diff --git a/app/components/autocomplete/emoji_suggestion/emoji_suggestion.tsx b/app/components/autocomplete/emoji_suggestion/emoji_suggestion.tsx index f0eaf86535..d08f114c38 100644 --- a/app/components/autocomplete/emoji_suggestion/emoji_suggestion.tsx +++ b/app/components/autocomplete/emoji_suggestion/emoji_suggestion.tsx @@ -49,7 +49,7 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => { listView: { paddingTop: 16, backgroundColor: theme.centerChannelBg, - borderRadius: 4, + borderRadius: 8, }, row: { flexDirection: 'row', @@ -167,6 +167,7 @@ const EmojiSuggestion = ({ const renderItem = useCallback(({item}: {item: string}) => { const completeItemSuggestion = () => completeSuggestion(item); + return ( { - updatePostInputTop(e.nativeEvent.layout.y); + updatePostInputTop(e.nativeEvent.layout.height); }, []); // Render diff --git a/app/components/post_draft/post_draft.tsx b/app/components/post_draft/post_draft.tsx index 8b44128101..288ebd9ee3 100644 --- a/app/components/post_draft/post_draft.tsx +++ b/app/components/post_draft/post_draft.tsx @@ -112,7 +112,6 @@ export default function PostDraft({ updateValue={setValue} rootId={rootId} channelId={channelId} - offsetY={0} cursorPosition={cursorPosition} value={value} isSearch={isSearch} @@ -123,17 +122,14 @@ export default function PostDraft({ if (Platform.OS === 'android') { return ( <> - {autoComplete} {draftHandler} + {autoComplete} ); } return ( <> - - {autoComplete} - {draftHandler} + + {autoComplete} + ); } diff --git a/app/components/post_draft/post_input/post_input.tsx b/app/components/post_draft/post_input/post_input.tsx index 0489dbdf76..f508d72c7a 100644 --- a/app/components/post_draft/post_input/post_input.tsx +++ b/app/components/post_draft/post_input/post_input.tsx @@ -265,7 +265,6 @@ export default function PostInput({ // May change when we implement Fabric input.current?.setNativeProps({ text: value, - selection: {start: cursorPosition}, }); lastNativeValue.current = value; } diff --git a/app/constants/autocomplete.ts b/app/constants/autocomplete.ts index 5b99e31781..c6897ef4e4 100644 --- a/app/constants/autocomplete.ts +++ b/app/constants/autocomplete.ts @@ -1,6 +1,8 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. +import {Platform} from 'react-native'; + export const AT_MENTION_REGEX = /\B(@([^@\r\n]*))$/i; export const AT_MENTION_REGEX_GLOBAL = /\B(@([^@\r\n]*))/gi; @@ -17,6 +19,13 @@ export const ALL_SEARCH_FLAGS_REGEX = /\b\w+:/g; export const CODE_REGEX = /(`+)([^`]|[^`][\s\S]*?[^`])\1(?!`)| *(`{3,}|~{3,})[ .]*(\S+)? *\n([\s\S]*?\s*)\3 *(?:\n+|$)/g; +export const LIST_BOTTOM = Platform.select({ios: 30, default: -5}); + +export const MAX_LIST_HEIGHT = 280; +export const MAX_LIST_DIFF = 50; +export const MAX_LIST_TABLET_DIFF = 140; +export const OFFSET_TABLET = 35; + export default { ALL_SEARCH_FLAGS_REGEX, AT_MENTION_REGEX, @@ -26,4 +35,8 @@ export default { CHANNEL_MENTION_SEARCH_REGEX, CODE_REGEX, DATE_MENTION_SEARCH_REGEX, + LIST_BOTTOM, + MAX_LIST_HEIGHT, + MAX_LIST_DIFF, + OFFSET_TABLET, };