diff --git a/app/components/autocomplete/slash_suggestion/app_command_parser/app_command_parser.ts b/app/components/autocomplete/slash_suggestion/app_command_parser/app_command_parser.ts
index 94a6656cd1..4f69940b7a 100644
--- a/app/components/autocomplete/slash_suggestion/app_command_parser/app_command_parser.ts
+++ b/app/components/autocomplete/slash_suggestion/app_command_parser/app_command_parser.ts
@@ -9,6 +9,7 @@ import {fetchChannelById, fetchChannelByName, searchChannels} from '@actions/rem
import {fetchUsersByIds, fetchUsersByUsernames, searchUsers} from '@actions/remote/user';
import {AppCallResponseTypes, AppCallTypes, AppFieldTypes, COMMAND_SUGGESTION_ERROR} from '@constants/apps';
import DatabaseManager from '@database/manager';
+import IntegrationsManager from '@init/integrations_manager';
import {getChannelById, queryChannelsByNames} from '@queries/servers/channel';
import {getCurrentTeamId} from '@queries/servers/system';
import {getUserById, queryUsersByUsername} from '@queries/servers/user';
@@ -48,26 +49,6 @@ interface Intl {
formatMessage(config: {id: string; defaultMessage: string}, values?: {[name: string]: any}): string;
}
-// TODO: Implemnet app bindings
-const getCommandBindings = () => {
- return [];
-};
-const getRHSCommandBindings = () => {
- return [];
-};
-const getAppRHSCommandForm = (key: string) => {// eslint-disable-line @typescript-eslint/no-unused-vars
- return undefined;
-};
-const getAppCommandForm = (key: string) => {// eslint-disable-line @typescript-eslint/no-unused-vars
- return undefined;
-};
-const setAppRHSCommandForm = (key: string, form: AppForm) => {// eslint-disable-line @typescript-eslint/no-unused-vars
- return undefined;
-};
-const setAppCommandForm = (key: string, form: AppForm) => {// eslint-disable-line @typescript-eslint/no-unused-vars
- return undefined;
-};
-
// Common dependencies with Webapp. Due to the big change of removing redux, we may have to rethink how to deal with this.
const getExecuteSuggestion = (parsed: ParsedCommand) => null; // eslint-disable-line @typescript-eslint/no-unused-vars
export const EXECUTE_CURRENT_COMMAND_ITEM_ID = '_execute_current_command';
@@ -961,10 +942,11 @@ export class AppCommandParser {
// getCommandBindings returns the commands in the redux store.
// They are grouped by app id since each app has one base command
private getCommandBindings = (): AppBinding[] => {
+ const manager = IntegrationsManager.getManager(this.serverUrl);
if (this.rootPostID) {
- return getRHSCommandBindings();
+ return manager.getRHSCommandBindings();
}
- return getCommandBindings();
+ return manager.getCommandBindings();
};
// getChannel gets the channel in which the user is typing the command
@@ -1067,9 +1049,10 @@ export class AppCommandParser {
};
public getForm = async (location: string, binding: AppBinding): Promise<{form?: AppForm; error?: string} | undefined> => {
+ const manager = IntegrationsManager.getManager(this.serverUrl);
const rootID = this.rootPostID || '';
const key = `${this.channelID}-${rootID}-${location}`;
- const form = this.rootPostID ? getAppRHSCommandForm(key) : getAppCommandForm(key);
+ const form = this.rootPostID ? manager.getAppRHSCommandForm(key) : manager.getAppCommandForm(key);
if (form) {
return {form};
}
@@ -1077,9 +1060,9 @@ export class AppCommandParser {
const fetched = await this.fetchForm(binding);
if (fetched?.form) {
if (this.rootPostID) {
- setAppRHSCommandForm(key, fetched.form);
+ manager.setAppRHSCommandForm(key, fetched.form);
} else {
- setAppCommandForm(key, fetched.form);
+ manager.setAppCommandForm(key, fetched.form);
}
}
return fetched;
diff --git a/app/components/autocomplete/slash_suggestion/app_slash_suggestion/app_slash_suggestion.tsx b/app/components/autocomplete/slash_suggestion/app_slash_suggestion/app_slash_suggestion.tsx
index eb755caac1..920d846f48 100644
--- a/app/components/autocomplete/slash_suggestion/app_slash_suggestion/app_slash_suggestion.tsx
+++ b/app/components/autocomplete/slash_suggestion/app_slash_suggestion/app_slash_suggestion.tsx
@@ -2,7 +2,7 @@
// See LICENSE.txt for license information.
import {debounce} from 'lodash';
-import React, {useEffect, useMemo, useRef, useState} from 'react';
+import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react';
import {useIntl} from 'react-intl';
import {
FlatList,
@@ -70,6 +70,8 @@ const AppSlashSuggestion = ({
const style = getStyleFromTheme(theme);
const mounted = useRef(false);
+ const listStyle = useMemo(() => [style.listView, {maxHeight: maxListHeight}], [maxListHeight, style]);
+
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);
@@ -84,7 +86,7 @@ const AppSlashSuggestion = ({
onShowingChange(Boolean(matches.length));
};
- const completeSuggestion = (command: string) => {
+ 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
@@ -103,21 +105,15 @@ const AppSlashSuggestion = ({
updateValue(completedDraft.replace(`//${command} `, `/${command} `));
});
}
- };
+ }, [serverUrl, updateValue]);
- const completeUserSuggestion = (base: string): (username: string) => void => {
+ const completeIgnoringSuggestion = useCallback((base: string): (toIgnore: string) => void => {
return () => {
completeSuggestion(base);
};
- };
+ }, [completeSuggestion]);
- const completeChannelMention = (base: string): (channelName: string) => void => {
- return () => {
- completeSuggestion(base);
- };
- };
-
- const renderItem = ({item}: {item: ExtendedAutocompleteSuggestion}) => {
+ const renderItem = useCallback(({item}: {item: ExtendedAutocompleteSuggestion}) => {
switch (item.type) {
case COMMAND_SUGGESTION_USER:
if (!item.item) {
@@ -126,7 +122,7 @@ const AppSlashSuggestion = ({
return (
);
@@ -137,7 +133,7 @@ const AppSlashSuggestion = ({
return (
);
@@ -153,7 +149,7 @@ const AppSlashSuggestion = ({
/>
);
}
- };
+ }, [completeSuggestion, completeIgnoringSuggestion]);
const isAppCommand = (pretext: string, channelID: string, teamID = '', rootID?: string) => {
appCommandParser.current.setChannelContext(channelID, teamID, rootID);
@@ -199,7 +195,7 @@ const AppSlashSuggestion = ({
({
- currentTeamId: observeCurrentChannelId(database),
+ currentTeamId: observeCurrentTeamId(database),
isAppsEnabled: observeConfigBooleanValue(database, 'FeatureFlagAppsEnabled'),
}));
diff --git a/app/components/autocomplete/slash_suggestion/index.ts b/app/components/autocomplete/slash_suggestion/index.ts
index bad9e4ee7c..2a65f39795 100644
--- a/app/components/autocomplete/slash_suggestion/index.ts
+++ b/app/components/autocomplete/slash_suggestion/index.ts
@@ -4,14 +4,14 @@
import {withDatabase} from '@nozbe/watermelondb/DatabaseProvider';
import withObservables from '@nozbe/with-observables';
-import {observeConfigBooleanValue, observeCurrentChannelId} from '@queries/servers/system';
+import {observeConfigBooleanValue, observeCurrentTeamId} from '@queries/servers/system';
import SlashSuggestion from './slash_suggestion';
import type {WithDatabaseArgs} from '@typings/database/database';
const enhanced = withObservables([], ({database}: WithDatabaseArgs) => ({
- currentTeamId: observeCurrentChannelId(database),
+ currentTeamId: observeCurrentTeamId(database),
isAppsEnabled: observeConfigBooleanValue(database, 'FeatureFlagAppsEnabled'),
}));
diff --git a/app/components/autocomplete/slash_suggestion/slash_suggestion.tsx b/app/components/autocomplete/slash_suggestion/slash_suggestion.tsx
index 5dd4e0ee8f..5b96de6a28 100644
--- a/app/components/autocomplete/slash_suggestion/slash_suggestion.tsx
+++ b/app/components/autocomplete/slash_suggestion/slash_suggestion.tsx
@@ -9,7 +9,8 @@ import {
Platform,
} from 'react-native';
-import {fetchCommands, fetchSuggestions} from '@actions/remote/command';
+import {fetchSuggestions} from '@actions/remote/command';
+import IntegrationsManager from '@app/init/integrations_manager';
import {useServerUrl} from '@context/server';
import {useTheme} from '@context/theme';
import analytics from '@init/analytics';
@@ -99,6 +100,8 @@ const SlashSuggestion = ({
const active = Boolean(dataSource.length);
+ const listStyle = useMemo(() => [style.listView, {maxHeight: maxListHeight}], [maxListHeight, style]);
+
const updateSuggestions = useCallback((matches: AutocompleteSuggestion[]) => {
setDataSource(matches);
onShowingChange(Boolean(matches.length));
@@ -190,11 +193,11 @@ const SlashSuggestion = ({
}
if (!commands) {
- fetchCommands(serverUrl, currentTeamId).then((res) => {
- if (res.error) {
- setCommands(emptyCommandList);
+ IntegrationsManager.getManager(serverUrl).fetchCommands(currentTeamId).then((res) => {
+ if (res.length) {
+ setCommands(res.filter(commandFilter));
} else {
- setCommands(res.commands.filter(commandFilter));
+ setCommands(emptyCommandList);
}
});
return;
@@ -231,7 +234,7 @@ const SlashSuggestion = ({
{
+ const iconAsSource = useMemo(() => {
+ return {uri: icon};
+ }, [icon]);
+
+ const touchableStyle = useMemo(() => {
+ return {marginLeft: insets.left, marginRight: insets.right};
+ }, [insets]);
+
+ const completeSuggestion = useCallback(() => {
onPress(complete);
- };
+ }, [onPress, complete]);
let suggestionText = suggestion;
if (suggestionText?.[0] === '/' && complete.split(' ').length === 1) {
@@ -113,7 +122,7 @@ const SlashSuggestionItem = ({
} else if (icon.startsWith('http')) {
image = (
);
@@ -121,21 +130,21 @@ const SlashSuggestionItem = ({
if (icon.startsWith('data:image/svg+xml')) {
let xml = '';
try {
- xml = Buffer.from(icon.substring('data:image/svg+xml;base64,'.length), 'base64').toString();
- } catch {
+ xml = base64.decode(icon.substring('data:image/svg+xml;base64,'.length));
+ image = (
+
+ );
+ } catch (error) {
// Do nothing
}
- image = (
-
- );
} else {
image = (
);
@@ -145,7 +154,7 @@ const SlashSuggestionItem = ({
return (
diff --git a/app/init/integrations_manager.ts b/app/init/integrations_manager.ts
new file mode 100644
index 0000000000..86859775d9
--- /dev/null
+++ b/app/init/integrations_manager.ts
@@ -0,0 +1,86 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+import {fetchCommands} from '@actions/remote/command';
+
+const TIME_TO_REFETCH_COMMANDS = 60000; // 1 minute
+class ServerIntegrationsManager {
+ private serverUrl: string;
+ private commandsLastFetched: {[teamId: string]: number | undefined} = {};
+ private commands: {[teamId: string]: Command[] | undefined} = {};
+
+ private triggerId = '';
+
+ private bindings: AppBinding[] = [];
+ private rhsBindings: AppBinding[] = [];
+
+ private commandForms: {[key: string]: AppForm | undefined} = {};
+ private rhsCommandForms: {[key: string]: AppForm | undefined} = {};
+
+ constructor(serverUrl: string) {
+ this.serverUrl = serverUrl;
+ }
+
+ public async fetchCommands(teamId: string) {
+ const lastFetched = this.commandsLastFetched[teamId] || 0;
+ const lastCommands = this.commands[teamId];
+ if (lastCommands && lastFetched + TIME_TO_REFETCH_COMMANDS > Date.now()) {
+ return lastCommands;
+ }
+
+ try {
+ const res = await fetchCommands(this.serverUrl, teamId);
+ if (res.error) {
+ return [];
+ }
+ this.commands[teamId] = res.commands;
+ this.commandsLastFetched[teamId] = Date.now();
+ return res.commands;
+ } catch {
+ return [];
+ }
+ }
+
+ public getCommandBindings() {
+ // TODO filter bindings
+ return this.bindings;
+ }
+
+ public getRHSCommandBindings() {
+ // TODO filter bindings
+ return this.rhsBindings;
+ }
+
+ public getAppRHSCommandForm(key: string) {
+ return this.rhsCommandForms[key];
+ }
+ public getAppCommandForm(key: string) {
+ return this.commandForms[key];
+ }
+ public setAppRHSCommandForm(key: string, form: AppForm) {
+ this.rhsCommandForms[key] = form;
+ }
+ public setAppCommandForm(key: string, form: AppForm) {
+ this.commandForms[key] = form;
+ }
+
+ public getTriggerId() {
+ return this.triggerId;
+ }
+ public setTriggerId(id: string) {
+ this.triggerId = id;
+ }
+}
+
+class IntegrationsManager {
+ private serverManagers: {[serverUrl: string]: ServerIntegrationsManager | undefined} = {};
+ public getManager(serverUrl: string): ServerIntegrationsManager {
+ if (!this.serverManagers[serverUrl]) {
+ this.serverManagers[serverUrl] = new ServerIntegrationsManager(serverUrl);
+ }
+
+ return this.serverManagers[serverUrl]!;
+ }
+}
+
+export default new IntegrationsManager();
diff --git a/package-lock.json b/package-lock.json
index d6f4a29f4e..6a0df0afc8 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -35,6 +35,7 @@
"@rudderstack/rudder-sdk-react-native": "1.2.1",
"@sentry/react-native": "3.2.13",
"@stream-io/flat-list-mvcp": "0.10.1",
+ "base-64": "1.0.0",
"commonmark": "github:mattermost/commonmark.js#90a62d97ed2dbd2d4711a5adda327128f5827983",
"commonmark-react-renderer": "github:mattermost/commonmark-react-renderer#4e52e1725c0ef5b1e2ecfe9883220ec36c2eb67d",
"deep-equal": "2.0.5",
@@ -109,6 +110,7 @@
"@babel/runtime": "7.17.2",
"@react-native-community/eslint-config": "3.0.1",
"@testing-library/react-native": "9.0.0",
+ "@types/base-64": "1.0.0",
"@types/commonmark": "0.27.5",
"@types/commonmark-react-renderer": "4.3.1",
"@types/deep-equal": "1.0.1",
@@ -5708,6 +5710,12 @@
"@babel/types": "^7.3.0"
}
},
+ "node_modules/@types/base-64": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/@types/base-64/-/base-64-1.0.0.tgz",
+ "integrity": "sha512-AvCJx/HrfYHmOQRFdVvgKMplXfzTUizmh0tz9GFTpDePWgCY4uoKll84zKlaRoeiYiCr7c9ZnqSTzkl0BUVD6g==",
+ "dev": true
+ },
"node_modules/@types/commonmark": {
"version": "0.27.5",
"resolved": "https://registry.npmjs.org/@types/commonmark/-/commonmark-0.27.5.tgz",
@@ -7609,6 +7617,11 @@
"node": ">=0.10.0"
}
},
+ "node_modules/base-64": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/base-64/-/base-64-1.0.0.tgz",
+ "integrity": "sha512-kwDPIFCGx0NZHog36dj+tHiwP4QMzsZ3AgMViUBKI0+V5n4U0ufTCUMhnQ04diaRI8EX/QcPfql7zlhZ7j4zgg=="
+ },
"node_modules/base/node_modules/define-property": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz",
@@ -28525,6 +28538,12 @@
"@babel/types": "^7.3.0"
}
},
+ "@types/base-64": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/@types/base-64/-/base-64-1.0.0.tgz",
+ "integrity": "sha512-AvCJx/HrfYHmOQRFdVvgKMplXfzTUizmh0tz9GFTpDePWgCY4uoKll84zKlaRoeiYiCr7c9ZnqSTzkl0BUVD6g==",
+ "dev": true
+ },
"@types/commonmark": {
"version": "0.27.5",
"resolved": "https://registry.npmjs.org/@types/commonmark/-/commonmark-0.27.5.tgz",
@@ -30051,6 +30070,11 @@
}
}
},
+ "base-64": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/base-64/-/base-64-1.0.0.tgz",
+ "integrity": "sha512-kwDPIFCGx0NZHog36dj+tHiwP4QMzsZ3AgMViUBKI0+V5n4U0ufTCUMhnQ04diaRI8EX/QcPfql7zlhZ7j4zgg=="
+ },
"base64-js": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
diff --git a/package.json b/package.json
index d9e1e28517..bf630838a1 100644
--- a/package.json
+++ b/package.json
@@ -32,6 +32,7 @@
"@rudderstack/rudder-sdk-react-native": "1.2.1",
"@sentry/react-native": "3.2.13",
"@stream-io/flat-list-mvcp": "0.10.1",
+ "base-64": "1.0.0",
"commonmark": "github:mattermost/commonmark.js#90a62d97ed2dbd2d4711a5adda327128f5827983",
"commonmark-react-renderer": "github:mattermost/commonmark-react-renderer#4e52e1725c0ef5b1e2ecfe9883220ec36c2eb67d",
"deep-equal": "2.0.5",
@@ -106,6 +107,7 @@
"@babel/runtime": "7.17.2",
"@react-native-community/eslint-config": "3.0.1",
"@testing-library/react-native": "9.0.0",
+ "@types/base-64": "1.0.0",
"@types/commonmark": "0.27.5",
"@types/commonmark-react-renderer": "4.3.1",
"@types/deep-equal": "1.0.1",