diff --git a/app/components/autocomplete/slash_suggestion/index.js b/app/components/autocomplete/slash_suggestion/index.js
index a05e54526..958c4b89e 100644
--- a/app/components/autocomplete/slash_suggestion/index.js
+++ b/app/components/autocomplete/slash_suggestion/index.js
@@ -5,8 +5,8 @@ import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
import {createSelector} from 'reselect';
-import {getAutocompleteCommands} from '@mm-redux/actions/integrations';
-import {getAutocompleteCommandsList} from '@mm-redux/selectors/entities/integrations';
+import {getAutocompleteCommands, getCommandAutocompleteSuggestions} from '@mm-redux/actions/integrations';
+import {getAutocompleteCommandsList, getCommandAutocompleteSuggestionsList} from '@mm-redux/selectors/entities/integrations';
import {getTheme} from '@mm-redux/selectors/entities/preferences';
import {getCurrentTeamId} from '@mm-redux/selectors/entities/teams';
import {isLandscape} from 'app/selectors/device';
@@ -32,6 +32,7 @@ function mapStateToProps(state) {
currentTeamId: getCurrentTeamId(state),
theme: getTheme(state),
isLandscape: isLandscape(state),
+ suggestions: getCommandAutocompleteSuggestionsList(state),
};
}
@@ -39,8 +40,9 @@ function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
getAutocompleteCommands,
+ getCommandAutocompleteSuggestions,
}, dispatch),
};
}
-export default connect(mapStateToProps, mapDispatchToProps)(SlashSuggestion);
\ No newline at end of file
+export default connect(mapStateToProps, mapDispatchToProps)(SlashSuggestion);
diff --git a/app/components/autocomplete/slash_suggestion/slash_suggestion.js b/app/components/autocomplete/slash_suggestion/slash_suggestion.js
index c16b80bf6..39a104105 100644
--- a/app/components/autocomplete/slash_suggestion/slash_suggestion.js
+++ b/app/components/autocomplete/slash_suggestion/slash_suggestion.js
@@ -12,14 +12,16 @@ import AutocompleteDivider from 'app/components/autocomplete/autocomplete_divide
import {makeStyleSheetFromTheme} from 'app/utils/theme';
import SlashSuggestionItem from './slash_suggestion_item';
+import {Client4} from '@mm-redux/client';
+import {isMinimumServerVersion} from '@mm-redux/utils/helpers';
-const SLASH_REGEX = /(^\/)([a-zA-Z-]*)$/;
const TIME_BEFORE_NEXT_COMMAND_REQUEST = 1000 * 60 * 5;
export default class SlashSuggestion extends PureComponent {
static propTypes = {
actions: PropTypes.shape({
getAutocompleteCommands: PropTypes.func.isRequired,
+ getCommandAutocompleteSuggestions: PropTypes.func.isRequired,
}).isRequired,
currentTeamId: PropTypes.string.isRequired,
commands: PropTypes.array,
@@ -31,6 +33,9 @@ export default class SlashSuggestion extends PureComponent {
value: PropTypes.string,
isLandscape: PropTypes.bool.isRequired,
nestedScrollEnabled: PropTypes.bool,
+ suggestions: PropTypes.array,
+ rootId: PropTypes.string,
+ channelId: PropTypes.string,
};
static defaultProps = {
@@ -40,13 +45,13 @@ export default class SlashSuggestion extends PureComponent {
state = {
active: false,
- suggestionComplete: false,
dataSource: [],
lastCommandRequest: 0,
};
componentWillReceiveProps(nextProps) {
- if (nextProps.isSearch) {
+ if ((nextProps.value === this.props.value && nextProps.suggestions === this.props.suggestions) ||
+ nextProps.isSearch || nextProps.value.startsWith('//') || !nextProps.channelId) {
return;
}
@@ -55,49 +60,73 @@ export default class SlashSuggestion extends PureComponent {
commands: nextCommands,
currentTeamId: nextTeamId,
value: nextValue,
+ suggestions: nextSuggestions,
} = nextProps;
- if (currentTeamId !== nextTeamId) {
- this.setState({
- lastCommandRequest: 0,
- });
- }
-
- const match = nextValue.match(SLASH_REGEX);
-
- if (!match || this.state.suggestionComplete) {
+ if (nextValue[0] !== '/') {
this.setState({
active: false,
- matchTerm: null,
- suggestionComplete: false,
});
this.props.onResultCountChange(0);
return;
}
- const dataIsStale = Date.now() - this.state.lastCommandRequest > TIME_BEFORE_NEXT_COMMAND_REQUEST;
+ if (nextValue.indexOf(' ') === -1) { // return suggestions for a top level cached commands
+ if (currentTeamId !== nextTeamId) {
+ this.setState({
+ lastCommandRequest: 0,
+ });
+ }
- if ((!nextCommands.length || dataIsStale)) {
- this.props.actions.getAutocompleteCommands(nextProps.currentTeamId);
+ const dataIsStale = Date.now() - this.state.lastCommandRequest > TIME_BEFORE_NEXT_COMMAND_REQUEST;
+
+ if ((!nextCommands.length || dataIsStale)) {
+ this.props.actions.getAutocompleteCommands(nextProps.currentTeamId);
+ this.setState({
+ lastCommandRequest: Date.now(),
+ });
+ }
+
+ const matches = this.filterSlashSuggestions(nextValue.substring(1), nextCommands);
+ this.updateSuggestions(matches);
+ } else if (isMinimumServerVersion(Client4.getServerVersion(), 5, 24)) {
+ if (nextProps.suggestions === this.props.suggestions) {
+ const args = {
+ channel_id: this.props.channelId,
+ ...(this.props.rootId && {root_id: this.props.rootId, parent_id: this.props.rootId}),
+ };
+ this.props.actions.getCommandAutocompleteSuggestions(nextProps.value, nextProps.currentTeamId, args);
+ } else {
+ const matches = [];
+ nextSuggestions.forEach((sug) => {
+ if (!this.contains(matches, '/' + sug.Complete)) {
+ matches.push({
+ Complete: sug.Complete,
+ Suggestion: sug.Suggestion,
+ Hint: sug.Hint,
+ Description: sug.Description,
+ });
+ }
+ });
+ this.updateSuggestions(matches);
+ }
+ } else {
this.setState({
- lastCommandRequest: Date.now(),
+ active: false,
});
}
+ }
- const matchTerm = match[2];
-
- const data = this.filterSlashSuggestions(matchTerm, nextCommands);
-
+ updateSuggestions = (matches) => {
this.setState({
- active: data.length,
- dataSource: data,
+ active: matches.length,
+ dataSource: matches,
});
-
- this.props.onResultCountChange(data.length);
+ this.props.onResultCountChange(matches.length);
}
filterSlashSuggestions = (matchTerm, commands) => {
- return commands.filter((command) => {
+ const data = commands.filter((command) => {
if (!command.auto_complete) {
return false;
} else if (!matchTerm) {
@@ -106,6 +135,18 @@ export default class SlashSuggestion extends PureComponent {
return command.display_name.startsWith(matchTerm) || command.trigger.startsWith(matchTerm);
});
+ return data.map((item) => {
+ return {
+ Complete: item.trigger,
+ Suggestion: '/' + item.trigger,
+ Hint: item.auto_complete_hint,
+ Description: item.auto_complete_desc,
+ };
+ });
+ }
+
+ contains = (matches, complete) => {
+ return matches.findIndex((match) => match.complete === complete) !== -1;
}
completeSuggestion = (command) => {
@@ -128,21 +169,23 @@ export default class SlashSuggestion extends PureComponent {
});
}
- this.setState({
- active: false,
- suggestionComplete: true,
- });
+ if (!isMinimumServerVersion(Client4.getServerVersion(), 5, 24)) {
+ this.setState({
+ active: false,
+ });
+ }
};
- keyExtractor = (item) => item.id || item.trigger;
+ keyExtractor = (item) => item.id || item.Suggestion;
renderItem = ({item}) => (
)
diff --git a/app/components/autocomplete/slash_suggestion/slash_suggestion_item.js b/app/components/autocomplete/slash_suggestion/slash_suggestion_item.js
index 922ef4570..00adc7ad8 100644
--- a/app/components/autocomplete/slash_suggestion/slash_suggestion_item.js
+++ b/app/components/autocomplete/slash_suggestion/slash_suggestion_item.js
@@ -15,13 +15,14 @@ export default class SlashSuggestionItem extends PureComponent {
hint: PropTypes.string,
onPress: PropTypes.func.isRequired,
theme: PropTypes.object.isRequired,
- trigger: PropTypes.string,
+ suggestion: PropTypes.string,
+ complete: PropTypes.string,
isLandscape: PropTypes.bool.isRequired,
};
completeSuggestion = () => {
- const {onPress, trigger} = this.props;
- onPress(trigger);
+ const {onPress, complete} = this.props;
+ onPress(complete);
};
render() {
@@ -29,7 +30,7 @@ export default class SlashSuggestionItem extends PureComponent {
description,
hint,
theme,
- trigger,
+ suggestion,
isLandscape,
} = this.props;
@@ -41,7 +42,7 @@ export default class SlashSuggestionItem extends PureComponent {
style={[style.row, padding(isLandscape)]}
type={'opacity'}
>
- {`/${trigger} ${hint}`}
+ {`${suggestion} ${hint}`}
{description}
);
diff --git a/app/components/post_draft/post_draft.js b/app/components/post_draft/post_draft.js
index 0798ab8ff..c0740c5b8 100644
--- a/app/components/post_draft/post_draft.js
+++ b/app/components/post_draft/post_draft.js
@@ -679,6 +679,8 @@ export default class PostDraft extends PureComponent {
maxHeight={Math.min(this.state.top - AUTOCOMPLETE_MARGIN, AUTOCOMPLETE_MAX_HEIGHT)}
onChangeText={this.handleInputQuickAction}
valueEvent={valueEvent}
+ rootId={rootId}
+ channelId={channelId}
/>
}
{
+ return this.doFetch(
+ `${this.getTeamRoute(teamId)}/commands/autocomplete_suggestions${buildQueryString({...commandArgs, user_input: userInput})}`,
+ {method: 'get'},
+ );
+ };
+
// Groups
linkGroupSyncable = async (groupID: string, syncableID: string, syncableType: string, patch: SyncablePatch) => {
diff --git a/app/mm-redux/reducers/entities/integrations.ts b/app/mm-redux/reducers/entities/integrations.ts
index 76828e8f5..22dd436d8 100644
--- a/app/mm-redux/reducers/entities/integrations.ts
+++ b/app/mm-redux/reducers/entities/integrations.ts
@@ -4,7 +4,7 @@
import {combineReducers} from 'redux';
import {IntegrationTypes, ChannelTypes} from '@mm-redux/action_types';
import {GenericAction} from '@mm-redux/types/actions';
-import {Command, IncomingWebhook, OutgoingWebhook, OAuthApp} from '@mm-redux/types/integrations';
+import {Command, IncomingWebhook, OutgoingWebhook, OAuthApp, AutocompleteSuggestion} from '@mm-redux/types/integrations';
import {Dictionary, IDMappedObjects} from '@mm-redux/types/utilities';
function incomingHooks(state: IDMappedObjects = {}, action: GenericAction) {
@@ -159,6 +159,17 @@ function systemCommands(state: IDMappedObjects = {}, action: GenericAct
}
}
+function commandAutocompleteSuggestions(state: Array = [], action: GenericAction) {
+ switch (action.type) {
+ case IntegrationTypes.RECEIVED_COMMAND_SUGGESTIONS:
+ return action.data;
+ case IntegrationTypes.RECEIVED_COMMAND_SUGGESTIONS_FAILURE:
+ return [];
+ default:
+ return state;
+ }
+}
+
function oauthApps(state: IDMappedObjects = {}, action: GenericAction) {
switch (action.type) {
case IntegrationTypes.RECEIVED_OAUTH_APPS: {
@@ -224,4 +235,7 @@ export default combineReducers({
// data for an interactive dialog to display
dialog,
+
+ // object represents slash command autocomplete suggestions
+ commandAutocompleteSuggestions,
});
diff --git a/app/mm-redux/selectors/entities/integrations.ts b/app/mm-redux/selectors/entities/integrations.ts
index ca1ca2519..6b64ac95e 100644
--- a/app/mm-redux/selectors/entities/integrations.ts
+++ b/app/mm-redux/selectors/entities/integrations.ts
@@ -26,6 +26,10 @@ export function getSystemCommands(state: types.store.GlobalState) {
return state.entities.integrations.systemCommands;
}
+export function getCommandAutocompleteSuggestionsList(state: types.store.GlobalState) {
+ return state.entities.integrations.commandAutocompleteSuggestions;
+}
+
/**
* get outgoing hooks in current team
*/
diff --git a/app/mm-redux/types/integrations.ts b/app/mm-redux/types/integrations.ts
index 92dd8755e..9e0f2f1e3 100644
--- a/app/mm-redux/types/integrations.ts
+++ b/app/mm-redux/types/integrations.ts
@@ -52,6 +52,15 @@ export type Command = {
'description': string;
'url': string;
};
+
+// AutocompleteSuggestion represents a single suggestion downloaded from the server.
+export type AutocompleteSuggestion = {
+ Complete: string;
+ Suggestion: string;
+ Hint: string;
+ Description: string;
+ IconData: string;
+};
export type OAuthApp = {
'id': string;
'creator_id': string;
@@ -71,6 +80,7 @@ export type IntegrationsState = {
oauthApps: IDMappedObjects;
systemCommands: IDMappedObjects;
commands: IDMappedObjects;
+ commandAutocompleteSuggestions: Array;
};
export type DialogSubmission = {
url: string;
diff --git a/app/screens/channel/channel.ios.js b/app/screens/channel/channel.ios.js
index 81d325206..d71ce39a5 100644
--- a/app/screens/channel/channel.ios.js
+++ b/app/screens/channel/channel.ios.js
@@ -71,6 +71,7 @@ export default class ChannelIOS extends ChannelBase {
onChangeText={this.handleAutoComplete}
cursorPositionEvent={CHANNEL_POST_TEXTBOX_CURSOR_CHANGE}
valueEvent={CHANNEL_POST_TEXTBOX_VALUE_CHANGE}
+ channelId={currentChannelId}
/>
{LocalConfig.EnableMobileClientUpgrade && }
diff --git a/app/screens/thread/__snapshots__/thread.ios.test.js.snap b/app/screens/thread/__snapshots__/thread.ios.test.js.snap
index 54bc86d92..12b2c1689 100644
--- a/app/screens/thread/__snapshots__/thread.ios.test.js.snap
+++ b/app/screens/thread/__snapshots__/thread.ios.test.js.snap
@@ -52,6 +52,7 @@ exports[`thread should match snapshot, has root post 1`] = `
nativeID="threadAccessoriesContainer"
>
>