mattermost-mobile/app/components/autocomplete/slash_suggestion/slash_suggestion.tsx
Daniel Espino García 86658edc30
Refactor errors around the app (#7306)
* Refactor errors around the app

* Fix recursive function

* Fix tests
2023-05-03 13:08:55 +02:00

232 lines
7.7 KiB
TypeScript

// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {debounce} from 'lodash';
import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react';
import {useIntl} from 'react-intl';
import {
FlatList,
Platform,
type StyleProp,
type ViewStyle,
} from 'react-native';
import {fetchSuggestions} from '@actions/remote/command';
import {useServerUrl} from '@context/server';
import analytics from '@managers/analytics';
import IntegrationsManager from '@managers/integrations_manager';
import {AppCommandParser} from './app_command_parser/app_command_parser';
import SlashSuggestionItem from './slash_suggestion_item';
// TODO: Remove when all below commands have been implemented https://mattermost.atlassian.net/browse/MM-43478
const COMMANDS_TO_IMPLEMENT_LATER = ['collapse', 'expand', 'logout'];
const NON_MOBILE_COMMANDS = ['shortcuts', 'search', 'settings'];
const COMMANDS_TO_HIDE_ON_MOBILE = new Set([...COMMANDS_TO_IMPLEMENT_LATER, ...NON_MOBILE_COMMANDS]);
const commandFilter = (v: Command) => !COMMANDS_TO_HIDE_ON_MOBILE.has(v.trigger);
const filterCommands = (matchTerm: string, commands: Command[]): AutocompleteSuggestion[] => {
const data = commands.filter((command) => {
if (!command.auto_complete) {
return false;
} else if (!matchTerm) {
return true;
}
return command.display_name.startsWith(matchTerm) || command.trigger.startsWith(matchTerm);
});
return data.map((command) => {
return {
Complete: command.trigger,
Suggestion: '/' + command.trigger,
Hint: command.auto_complete_hint,
Description: command.auto_complete_desc,
IconData: command.icon_url || command.autocomplete_icon_data || '',
};
});
};
const keyExtractor = (item: Command & AutocompleteSuggestion): string => item.id || item.Suggestion;
type Props = {
currentTeamId: string;
updateValue: (text: string) => void;
onShowingChange: (c: boolean) => void;
value: string;
nestedScrollEnabled?: boolean;
rootId?: string;
channelId: string;
isAppsEnabled: boolean;
listStyle: StyleProp<ViewStyle>;
};
const emptyCommandList: Command[] = [];
const emptySuggestionList: AutocompleteSuggestion[] = [];
const SlashSuggestion = ({
channelId,
currentTeamId,
rootId,
onShowingChange,
isAppsEnabled,
nestedScrollEnabled,
updateValue,
value = '',
listStyle,
}: Props) => {
const intl = useIntl();
const serverUrl = useServerUrl();
const appCommandParser = useRef<AppCommandParser>(new AppCommandParser(serverUrl, intl, channelId, currentTeamId, rootId));
const mounted = useRef(false);
const [noResultsTerm, setNoResultsTerm] = useState<string|null>(null);
const [dataSource, setDataSource] = useState<AutocompleteSuggestion[]>(emptySuggestionList);
const [commands, setCommands] = useState<Command[]>();
const active = Boolean(dataSource.length);
const updateSuggestions = useCallback((matches: AutocompleteSuggestion[]) => {
setDataSource(matches);
onShowingChange(Boolean(matches.length));
}, [onShowingChange]);
const runFetch = useMemo(() => debounce(async (sUrl: string, term: string, tId: string, cId: string, rId?: string) => {
try {
const res = await fetchSuggestions(sUrl, term, tId, cId, rId);
if (!mounted.current) {
return;
}
if ('error' in res) {
updateSuggestions(emptySuggestionList);
} else if (res.suggestions.length === 0) {
updateSuggestions(emptySuggestionList);
setNoResultsTerm(term);
} else {
updateSuggestions(res.suggestions);
}
} catch {
updateSuggestions(emptySuggestionList);
}
}, 200), [updateSuggestions]);
const getAppBaseCommandSuggestions = (pretext: string): AutocompleteSuggestion[] => {
appCommandParser.current.setChannelContext(channelId, currentTeamId, rootId);
const suggestions = appCommandParser.current.getSuggestionsBase(pretext);
return suggestions;
};
const showBaseCommands = (text: string) => {
let matches: AutocompleteSuggestion[] = [];
if (isAppsEnabled) {
const appCommands = getAppBaseCommandSuggestions(text);
matches = matches.concat(appCommands);
}
matches = matches.concat(filterCommands(text.substring(1), commands!));
matches.sort((match1, match2) => {
if (match1.Suggestion === match2.Suggestion) {
return 0;
}
return match1.Suggestion > match2.Suggestion ? 1 : -1;
});
updateSuggestions(matches);
};
const completeSuggestion = useCallback((command: string) => {
analytics.get(serverUrl)?.trackCommand('complete_suggestion', `/${command} `);
// 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
let completedDraft = `/${command} `;
if (Platform.OS === 'ios') {
completedDraft = `//${command} `;
}
updateValue(completedDraft);
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(`//${command} `, `/${command} `));
});
}
}, [updateValue, serverUrl]);
const renderItem = useCallback(({item}: {item: AutocompleteSuggestion}) => (
<SlashSuggestionItem
description={item.Description}
hint={item.Hint}
onPress={completeSuggestion}
suggestion={item.Suggestion}
complete={item.Complete}
icon={item.IconData}
/>
), [completeSuggestion]);
useEffect(() => {
if (value[0] !== '/') {
runFetch.cancel();
setNoResultsTerm(null);
updateSuggestions(emptySuggestionList);
return;
}
if (!commands) {
IntegrationsManager.getManager(serverUrl).fetchCommands(currentTeamId).then((res) => {
if (res.length) {
setCommands(res.filter(commandFilter));
} else {
setCommands(emptyCommandList);
}
});
return;
}
if (value.indexOf(' ') === -1) {
runFetch.cancel();
showBaseCommands(value);
setNoResultsTerm(null);
return;
}
if (noResultsTerm && value.startsWith(noResultsTerm)) {
return;
}
runFetch(serverUrl, value, currentTeamId, channelId, rootId);
}, [value, commands]);
useEffect(() => {
mounted.current = true;
return () => {
mounted.current = false;
};
}, []);
if (!active) {
// If we are not in an active state return null so nothing is rendered
// other components are not blocked.
return null;
}
return (
<FlatList
keyboardShouldPersistTaps='always'
style={listStyle}
data={dataSource}
keyExtractor={keyExtractor}
removeClippedSubviews={true}
renderItem={renderItem}
nestedScrollEnabled={nestedScrollEnabled}
testID='autocomplete.slash_suggestion.flat_list'
/>
);
};
export default SlashSuggestion;