diff --git a/app/components/autocomplete/autocomplete.tsx b/app/components/autocomplete/autocomplete.tsx
index 5b82aeb9b..440a6f202 100644
--- a/app/components/autocomplete/autocomplete.tsx
+++ b/app/components/autocomplete/autocomplete.tsx
@@ -12,6 +12,8 @@ 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 {
@@ -92,13 +94,14 @@ const Autocomplete = ({
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 [showingCommand, setShowingCommand] = useState(false);
- // const [showingAppCommand, setShowingAppCommand] = useState(false);
// const [showingDate, setShowingDate] = useState(false);
- const hasElements = showingChannelMention || showingEmoji || showingAtMention; // || showingCommand || showingAppCommand || showingDate;
- const appsTakeOver = false; // showingAppCommand;
+ const hasElements = showingChannelMention || showingEmoji || showingAtMention || showingCommand || showingAppCommand; // || showingDate;
+ const appsTakeOver = showingAppCommand;
+ const showCommands = !(showingChannelMention || showingEmoji || showingAtMention);
const maxListHeight = useMemo(() => {
if (maxHeightOverride) {
@@ -151,15 +154,17 @@ const Autocomplete = ({
testID='autocomplete'
style={containerStyles}
>
- {/* {isAppsEnabled && (
+ {isAppsEnabled && (
- )} */}
+ )}
{(!appsTakeOver || !isAppsEnabled) && (<>
}
- {/*
- {(isSearch && enableDateSuggestion) &&
+ }
+ {/* {(isSearch && enableDateSuggestion) &&
{
return [{
- complete: '',
- suggestion: '',
- hint: this.intl.formatMessage({
+ Complete: '',
+ Suggestion: '',
+ Hint: this.intl.formatMessage({
id: 'apps.suggestion.no_suggestion',
defaultMessage: 'No matching suggestions.',
}),
- iconData: COMMAND_SUGGESTION_ERROR,
- description: '',
+ IconData: COMMAND_SUGGESTION_ERROR,
+ Description: '',
}];
};
getErrorSuggestion = (parsed: ParsedCommand): AutocompleteSuggestion[] => {
return [{
- complete: '',
- suggestion: '',
- hint: this.intl.formatMessage({
+ Complete: '',
+ Suggestion: '',
+ Hint: this.intl.formatMessage({
id: 'apps.suggestion.errors.parser_error',
defaultMessage: 'Parsing error',
}),
- iconData: COMMAND_SUGGESTION_ERROR,
- description: parsed.error,
+ IconData: COMMAND_SUGGESTION_ERROR,
+ Description: parsed.error,
}];
};
@@ -940,22 +940,21 @@ export class AppCommandParser {
// decorateSuggestionComplete applies the necessary modifications for a suggestion to be processed
private decorateSuggestionComplete = (parsed: ParsedCommand, choice: AutocompleteSuggestion): AutocompleteSuggestion => {
- if (choice.complete && choice.complete.endsWith(EXECUTE_CURRENT_COMMAND_ITEM_ID)) {
+ if (choice.Complete && choice.Complete.endsWith(EXECUTE_CURRENT_COMMAND_ITEM_ID)) {
return choice as AutocompleteSuggestion;
}
let goBackSpace = 0;
- if (choice.complete === '') {
+ if (choice.Complete === '') {
goBackSpace = 1;
}
let complete = parsed.command.substring(0, parsed.incompleteStart - goBackSpace);
- complete += choice.complete === undefined ? choice.suggestion : choice.complete;
- choice.hint = choice.hint || '';
+ complete += choice.Complete === undefined ? choice.Suggestion : choice.Complete;
complete = complete.substring(1);
return {
...choice,
- complete,
+ Complete: complete,
};
};
@@ -1097,11 +1096,11 @@ export class AppCommandParser {
bindings.forEach((b) => {
if (b.label.toLowerCase().startsWith(parsed.incomplete.toLowerCase())) {
result.push({
- complete: b.label,
- suggestion: b.label,
- description: b.description || '',
- hint: b.hint || '',
- iconData: b.icon || '',
+ Complete: b.label,
+ Suggestion: b.label,
+ Description: b.description || '',
+ Hint: b.hint || '',
+ IconData: b.icon || '',
});
}
});
@@ -1187,11 +1186,11 @@ export class AppCommandParser {
if (applicable) {
return applicable.map((f) => {
return {
- complete: prefix + (f.label || f.name),
- suggestion: '--' + (f.label || f.name),
- description: f.description || '',
- hint: f.hint || '',
- iconData: parsed.binding?.icon || '',
+ Complete: prefix + (f.label || f.name),
+ Suggestion: '--' + (f.label || f.name),
+ Description: f.description || '',
+ Hint: f.hint || '',
+ IconData: parsed.binding?.icon || '',
};
});
}
@@ -1232,11 +1231,11 @@ export class AppCommandParser {
const fieldName = parsed.field.modal_label || parsed.field.label || parsed.field.name;
return [{
- complete,
- suggestion: `${fieldName}: ${delimiter || '"'}${parsed.incomplete}${delimiter || '"'}`,
- description: f.description || '',
- hint: '',
- iconData: parsed.binding?.icon || '',
+ Complete: complete,
+ Suggestion: `${fieldName}: ${delimiter || '"'}${parsed.incomplete}${delimiter || '"'}`,
+ Description: f.description || '',
+ Hint: '',
+ IconData: parsed.binding?.icon || '',
}];
};
@@ -1246,14 +1245,14 @@ export class AppCommandParser {
const opts = f.options?.filter((opt) => opt.label.toLowerCase().startsWith(parsed.incomplete.toLowerCase()));
if (!opts?.length) {
return [{
- complete: '',
- suggestion: '',
- hint: this.intl.formatMessage({
+ Complete: '',
+ Suggestion: '',
+ Hint: this.intl.formatMessage({
id: 'apps.suggestion.no_static',
defaultMessage: 'No matching options.',
}),
- description: '',
- iconData: COMMAND_SUGGESTION_ERROR,
+ Description: '',
+ IconData: COMMAND_SUGGESTION_ERROR,
}];
}
return opts.map((opt) => {
@@ -1264,11 +1263,11 @@ export class AppCommandParser {
complete = '`' + complete + '`';
}
return {
- complete,
- suggestion: opt.label,
- hint: f.hint || '',
- description: f.description || '',
- iconData: opt.icon_data || parsed.binding?.icon || '',
+ Complete: complete,
+ Suggestion: opt.label,
+ Hint: f.hint || '',
+ Description: f.description || '',
+ IconData: opt.icon_data || parsed.binding?.icon || '',
};
});
};
@@ -1331,14 +1330,14 @@ export class AppCommandParser {
items = items?.filter(filterEmptyOptions);
if (!items?.length) {
return [{
- complete: '',
- suggestion: '',
- hint: this.intl.formatMessage({
+ Complete: '',
+ Suggestion: '',
+ Hint: this.intl.formatMessage({
id: 'apps.suggestion.no_static',
defaultMessage: 'No matching options.',
}),
- iconData: '',
- description: this.intl.formatMessage({
+ IconData: '',
+ Description: this.intl.formatMessage({
id: 'apps.suggestion.no_dynamic',
defaultMessage: 'No data was returned for dynamic suggestions',
}),
@@ -1353,11 +1352,11 @@ export class AppCommandParser {
complete = '`' + complete + '`';
}
return ({
- complete,
- description: s.label || s.value,
- suggestion: s.value,
- hint: '',
- iconData: s.icon_data || parsed.binding?.icon || '',
+ Complete: complete,
+ Description: s.label || s.value,
+ Suggestion: s.value,
+ Hint: '',
+ IconData: s.icon_data || parsed.binding?.icon || '',
});
});
};
@@ -1370,14 +1369,14 @@ export class AppCommandParser {
error: message,
});
return [{
- complete: '',
- suggestion: '',
- hint: this.intl.formatMessage({
+ Complete: '',
+ Suggestion: '',
+ Hint: this.intl.formatMessage({
id: 'apps.suggestion.dynamic.error',
defaultMessage: 'Dynamic select error',
}),
- iconData: COMMAND_SUGGESTION_ERROR,
- description: errMsg,
+ IconData: COMMAND_SUGGESTION_ERROR,
+ Description: errMsg,
}];
};
@@ -1405,20 +1404,20 @@ export class AppCommandParser {
if ('true'.startsWith(parsed.incomplete)) {
suggestions.push({
- complete: 'true',
- suggestion: 'true',
- description: parsed.field?.description || '',
- hint: parsed.field?.hint || '',
- iconData: parsed.binding?.icon || '',
+ Complete: 'true',
+ Suggestion: 'true',
+ Description: parsed.field?.description || '',
+ Hint: parsed.field?.hint || '',
+ IconData: parsed.binding?.icon || '',
});
}
if ('false'.startsWith(parsed.incomplete)) {
suggestions.push({
- complete: 'false',
- suggestion: 'false',
- description: parsed.field?.description || '',
- hint: parsed.field?.hint || '',
- iconData: parsed.binding?.icon || '',
+ Complete: 'false',
+ Suggestion: 'false',
+ Description: parsed.field?.description || '',
+ Hint: parsed.field?.hint || '',
+ IconData: parsed.binding?.icon || '',
});
}
return suggestions;
diff --git a/app/components/autocomplete/slash_suggestion/app_command_parser/mentions.ts b/app/components/autocomplete/slash_suggestion/app_command_parser/mentions.ts
index a11f71082..a1577c5ab 100644
--- a/app/components/autocomplete/slash_suggestion/app_command_parser/mentions.ts
+++ b/app/components/autocomplete/slash_suggestion/app_command_parser/mentions.ts
@@ -13,11 +13,11 @@ export async function inTextMentionSuggestions(serverUrl: string, pretext: strin
const res = await searchUsers(serverUrl, lastWord.substring(1), channelID);
const users = await getUserSuggestions(res.users);
users.forEach((u) => {
- let complete = incompleteLessLastWord ? incompleteLessLastWord + ' ' + u.complete : u.complete;
+ let complete = incompleteLessLastWord ? incompleteLessLastWord + ' ' + u.Complete : u.Complete;
if (delimiter) {
complete = delimiter + complete;
}
- u.complete = complete;
+ u.Complete = complete;
});
return users;
}
@@ -26,11 +26,11 @@ export async function inTextMentionSuggestions(serverUrl: string, pretext: strin
const res = await searchChannels(serverUrl, lastWord.substring(1));
const channels = await getChannelSuggestions(res.channels);
channels.forEach((c) => {
- let complete = incompleteLessLastWord ? incompleteLessLastWord + ' ' + c.complete : c.complete;
+ let complete = incompleteLessLastWord ? incompleteLessLastWord + ' ' + c.Complete : c.Complete;
if (delimiter) {
complete = delimiter + complete;
}
- c.complete = complete;
+ c.Complete = complete;
});
return channels;
}
@@ -40,11 +40,11 @@ export async function inTextMentionSuggestions(serverUrl: string, pretext: strin
export async function getUserSuggestions(usersAutocomplete?: {users: UserProfile[]; out_of_channel?: UserProfile[]}): Promise {
const notFoundSuggestions = [{
- complete: '',
- suggestion: '',
- description: 'No user found',
- hint: '',
- iconData: '',
+ Complete: '',
+ Suggestion: '',
+ Description: 'No user found',
+ Hint: '',
+ IconData: '',
}];
if (!usersAutocomplete) {
return notFoundSuggestions;
@@ -67,11 +67,11 @@ export async function getUserSuggestions(usersAutocomplete?: {users: UserProfile
export async function getChannelSuggestions(channels?: Channel[]): Promise {
const notFoundSuggestion = [{
- complete: '',
- suggestion: '',
- description: 'No channel found',
- hint: '',
- iconData: '',
+ Complete: '',
+ Suggestion: '',
+ Description: 'No channel found',
+ Hint: '',
+ IconData: '',
}];
if (!channels) {
return notFoundSuggestion;
@@ -82,11 +82,11 @@ export async function getChannelSuggestions(channels?: Channel[]): Promise {
return {
- complete: '~' + c.name,
- suggestion: '',
- description: '',
- hint: '',
- iconData: '',
+ Complete: '~' + c.name,
+ Suggestion: '',
+ Description: '',
+ Hint: '',
+ IconData: '',
type: COMMAND_SUGGESTION_CHANNEL,
item: c,
};
@@ -97,11 +97,11 @@ export async function getChannelSuggestions(channels?: Channel[]): Promise void;
- onResultCountChange: (count: number) => void;
+ updateValue: (text: string) => void;
+ onShowingChange: (c: boolean) => void;
value: string;
nestedScrollEnabled?: boolean;
rootId?: string;
channelId: string;
- appsEnabled: boolean;
+ isAppsEnabled: boolean;
};
-const keyExtractor = (item: ExtendedAutocompleteSuggestion): string => (item.suggestion || '') + item.type + item.item;
+const keyExtractor = (item: ExtendedAutocompleteSuggestion): string => item.Suggestion + item.type + item.item;
const getStyleFromTheme = makeStyleSheetFromTheme((theme: Theme) => {
return {
@@ -47,28 +48,40 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme: Theme) => {
};
});
+const emptySuggestonList: AutocompleteSuggestion[] = [];
+
const AppSlashSuggestion = ({
channelId,
currentTeamId,
rootId,
value = '',
- appsEnabled,
+ isAppsEnabled,
maxListHeight,
nestedScrollEnabled,
- onChangeText,
- onResultCountChange,
+ updateValue,
+ onShowingChange,
}: Props) => {
const intl = useIntl();
const theme = useTheme();
const serverUrl = useServerUrl();
const appCommandParser = useRef(new AppCommandParser(serverUrl, intl, channelId, currentTeamId, rootId, theme));
- const [dataSource, setDataSource] = useState([]);
- const active = appsEnabled && Boolean(dataSource.length);
+ const [dataSource, setDataSource] = useState(emptySuggestonList);
+ const active = isAppsEnabled && Boolean(dataSource.length);
const style = getStyleFromTheme(theme);
+ const mounted = useRef(false);
+
+ 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);
+ if (!mounted.current) {
+ return;
+ }
+ updateSuggestions(suggestions);
+ }), []);
const updateSuggestions = (matches: ExtendedAutocompleteSuggestion[]) => {
setDataSource(matches);
- onResultCountChange(matches.length);
+ onShowingChange(Boolean(matches.length));
};
const completeSuggestion = (command: string) => {
@@ -81,13 +94,13 @@ const AppSlashSuggestion = ({
completedDraft = `//${command} `;
}
- onChangeText(completedDraft);
+ 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(() => {
- onChangeText(completedDraft.replace(`//${command} `, `/${command} `));
+ updateValue(completedDraft.replace(`//${command} `, `/${command} `));
});
}
};
@@ -113,7 +126,7 @@ const AppSlashSuggestion = ({
return (
);
@@ -124,19 +137,19 @@ const AppSlashSuggestion = ({
return (
);
default:
return (
);
}
@@ -147,31 +160,35 @@ const AppSlashSuggestion = ({
return appCommandParser.current.isAppCommand(pretext);
};
- const fetchAndShowAppCommandSuggestions = async (pretext: string, channelID: string, teamID = '', rootID?: string) => {
- appCommandParser.current.setChannelContext(channelID, teamID, rootID);
- const suggestions = await appCommandParser.current.getSuggestions(pretext);
- updateSuggestions(suggestions);
- };
-
useEffect(() => {
if (value[0] !== '/') {
- setDataSource([]);
- onResultCountChange(0);
+ fetchAndShowAppCommandSuggestions.cancel();
+ updateSuggestions(emptySuggestonList);
return;
}
if (value.indexOf(' ') === -1) {
- setDataSource([]);
+ // Let slash command suggestions handle base commands.
+ fetchAndShowAppCommandSuggestions.cancel();
+ updateSuggestions(emptySuggestonList);
return;
}
if (!isAppCommand(value, channelId, currentTeamId, rootId)) {
- setDataSource([]);
+ fetchAndShowAppCommandSuggestions.cancel();
+ updateSuggestions(emptySuggestonList);
return;
}
fetchAndShowAppCommandSuggestions(value, channelId, currentTeamId, rootId);
}, [value]);
+ 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.
diff --git a/app/components/autocomplete/slash_suggestion/slash_suggestion.tsx b/app/components/autocomplete/slash_suggestion/slash_suggestion.tsx
index e54a3296a..80d20838b 100644
--- a/app/components/autocomplete/slash_suggestion/slash_suggestion.tsx
+++ b/app/components/autocomplete/slash_suggestion/slash_suggestion.tsx
@@ -47,30 +47,29 @@ const filterCommands = (matchTerm: string, commands: Command[]): AutocompleteSug
return command.display_name.startsWith(matchTerm) || command.trigger.startsWith(matchTerm);
});
- return data.map((item) => {
+ return data.map((command) => {
return {
- complete: item.trigger,
- suggestion: '/' + item.trigger,
- hint: item.auto_complete_hint,
- description: item.auto_complete_desc,
- iconData: item.icon_url || item.autocomplete_icon_data || '',
+ 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 || '';
+const keyExtractor = (item: Command & AutocompleteSuggestion): string => item.id || item.Suggestion;
type Props = {
currentTeamId: string;
- commands: Command[];
maxListHeight?: number;
- onChangeText: (text: string) => void;
- onResultCountChange: (count: number) => void;
+ updateValue: (text: string) => void;
+ onShowingChange: (c: boolean) => void;
value: string;
nestedScrollEnabled?: boolean;
rootId?: string;
channelId: string;
- appsEnabled: boolean;
+ isAppsEnabled: boolean;
};
const emptyCommandList: Command[] = [];
@@ -80,11 +79,11 @@ const SlashSuggestion = ({
channelId,
currentTeamId,
rootId,
- onResultCountChange,
- appsEnabled,
+ onShowingChange,
+ isAppsEnabled,
maxListHeight,
nestedScrollEnabled,
- onChangeText,
+ updateValue,
value = '',
}: Props) => {
const intl = useIntl();
@@ -92,6 +91,7 @@ const SlashSuggestion = ({
const style = getStyleFromTheme(theme);
const serverUrl = useServerUrl();
const appCommandParser = useRef(new AppCommandParser(serverUrl, intl, channelId, currentTeamId, rootId, theme));
+ const mounted = useRef(false);
const [dataSource, setDataSource] = useState(emptySuggestionList);
const [commands, setCommands] = useState();
@@ -100,12 +100,15 @@ const SlashSuggestion = ({
const updateSuggestions = useCallback((matches: AutocompleteSuggestion[]) => {
setDataSource(matches);
- onResultCountChange(matches.length);
- }, [onResultCountChange]);
+ 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 (res.error) {
updateSuggestions(emptySuggestionList);
} else if (res.suggestions.length === 0) {
@@ -127,7 +130,7 @@ const SlashSuggestion = ({
const showBaseCommands = (text: string) => {
let matches: AutocompleteSuggestion[] = [];
- if (appsEnabled) {
+ if (isAppsEnabled) {
const appCommands = getAppBaseCommandSuggestions(text);
matches = matches.concat(appCommands);
}
@@ -135,10 +138,10 @@ const SlashSuggestion = ({
matches = matches.concat(filterCommands(text.substring(1), commands!));
matches.sort((match1, match2) => {
- if (match1.suggestion === match2.suggestion) {
+ if (match1.Suggestion === match2.Suggestion) {
return 0;
}
- return match1.suggestion > match2.suggestion ? 1 : -1;
+ return match1.Suggestion > match2.Suggestion ? 1 : -1;
});
updateSuggestions(matches);
@@ -154,30 +157,31 @@ const SlashSuggestion = ({
completedDraft = `//${command} `;
}
- onChangeText(completedDraft);
+ 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(() => {
- onChangeText(completedDraft.replace(`//${command} `, `/${command} `));
+ updateValue(completedDraft.replace(`//${command} `, `/${command} `));
});
}
- }, [onChangeText, serverUrl]);
+ }, [updateValue, serverUrl]);
const renderItem = useCallback(({item}: {item: AutocompleteSuggestion}) => (
), [completeSuggestion]);
useEffect(() => {
if (value[0] !== '/') {
+ runFetch.cancel();
updateSuggestions(emptySuggestionList);
return;
}
@@ -194,6 +198,7 @@ const SlashSuggestion = ({
}
if (value.indexOf(' ') === -1) {
+ runFetch.cancel();
showBaseCommands(value);
return;
}
@@ -201,6 +206,13 @@ const SlashSuggestion = ({
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.
diff --git a/app/components/autocomplete/slash_suggestion/slash_suggestion_item.tsx b/app/components/autocomplete/slash_suggestion/slash_suggestion_item.tsx
index c5aaafaca..bf0c35132 100644
--- a/app/components/autocomplete/slash_suggestion/slash_suggestion_item.tsx
+++ b/app/components/autocomplete/slash_suggestion/slash_suggestion_item.tsx
@@ -59,11 +59,11 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme: Theme) => {
type Props = {
complete: string;
- description?: string;
- hint?: string;
+ description: string;
+ hint: string;
onPress: (complete: string) => void;
- suggestion?: string;
- icon?: string;
+ suggestion: string;
+ icon: string;
}
const SlashSuggestionItem = ({
@@ -112,15 +112,16 @@ const SlashSuggestionItem = ({
source={bangIcon}
/>
);
- } else if (icon && icon.startsWith('http')) {
+ } else if (icon.startsWith('http')) {
image = (
);
- } else if (icon && icon.startsWith('data:')) {
+ } else if (icon.startsWith('data:')) {
if (icon.startsWith('data:image/svg+xml')) {
+ // TODO: What base64 library should we use? Security implications on doing things like this?
const xml = ''; // base64.decode(icon.substring('data:image/svg+xml;base64,'.length));
image = (