mattermost-mobile-upstream/app/actions/remote/command.ts
Elias Nahum b6fc85385e
Migrate from RNN to Expo router (#9402)
* Expo-Router partial migration

* Full migration to ExpoRouter

* i18n

* add tests

* Merge branch 'main' into expo-router

* align package-lock.json

* i18n

* feedback review

* feedback review

* android 16kb patch

* Refactored emoji picker keyboard using a state machine

* set android launch_screen in drawables instead of layout

* no conditional hooks

* remove duplicate call in test

* address feedback

* fix tests and address more feedback

* small corrections

* small corrections

* small corrections

* fix draft test

* fix draft test 2

* remove navigateToScreen in draff test

* fix nit in draft test

* remove doc file

* Feedback review

* fix snapshots

* improve test coverage

* fix i18n

* Upgrade t o RN 0.83, other dependencies and new architecture (#9737)

* library hardware-keyboard android new arch support fix

* library rnshare android new arch support fix

* library rnutils android new arch support fix

* library secure-pdf-viewer add new arch support

* Enable new arch on iOS and Android

* update emm and network client with new-arch

* update intune to use new-arch

* performance improvements

* fix tests

* fixes post input on new-arch

* fix android emoji picker

* remove doc

* run tests in parallel

* upgrade to React Native 0.83.9, Expo 55, React 19, and New Architecture

Bumps core dependencies to their latest versions:
- React Native 0.77.3 → 0.83.9 (New Architecture enabled)
- React 18.3.1 → 19.2.5
- Expo SDK 52 → 55
- TypeScript 5.8.3 → 6.0.3
- Node.js 22.16.0 → 24.15.0
- react-native-reanimated 3.17.3 → 4.3.0
- Various other dependency upgrades

Migrates iOS AppDelegate from Objective-C to Swift, updates Android
build tooling and Gradle wrapper, refreshes all patches and font
assets, and adapts app code, tests, and CI workflows throughout for
API changes introduced by these upgrades.

* remove unecessary dismissKeyboard in notifications

* feedback review

* update intune to use new-arch

* update @mattermost deps

* missing i18n string

* additional fixes

* update fastlane
2026-05-13 17:44:55 +08:00

184 lines
6.5 KiB
TypeScript

// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {Alert} from 'react-native';
import {doAppSubmit, postEphemeralCallResponseForCommandArgs} from '@actions/remote/apps';
import {AppCommandParser} from '@components/autocomplete/slash_suggestion/app_command_parser/app_command_parser';
import {AppCallResponseTypes} from '@constants/apps';
import DatabaseManager from '@database/manager';
import AppsManager from '@managers/apps_manager';
import IntegrationsManager from '@managers/integrations_manager';
import NetworkManager from '@managers/network_manager';
import {getChannelById} from '@queries/servers/channel';
import {getConfig, getCurrentTeamId} from '@queries/servers/system';
import {handleDeepLink, matchDeepLink} from '@utils/deep_link';
import {getFullErrorMessage} from '@utils/errors';
import {logDebug} from '@utils/log';
import {openAppsForm} from '@utils/navigation';
import {tryOpenURL} from '@utils/url';
import type {Client} from '@client/rest';
import type {IntlShape} from 'react-intl';
export const executeCommand = async (serverUrl: string, intl: IntlShape, message: string, channelId: string, rootId?: string): Promise<{data?: CommandResponse; error?: unknown}> => {
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
if (!operator) {
return {error: `${serverUrl} database not found`};
}
const {database} = operator;
let client: Client;
try {
client = NetworkManager.getClient(serverUrl);
} catch (error) {
return {error};
}
const channel = await getChannelById(database, channelId);
const teamId = channel?.teamId || (await getCurrentTeamId(database));
const args: CommandArgs = {
channel_id: channelId,
team_id: teamId,
root_id: rootId,
parent_id: rootId,
};
const appsEnabled = await AppsManager.isAppsEnabled(serverUrl);
if (appsEnabled) {
const parser = new AppCommandParser(serverUrl, intl, channelId, teamId, rootId);
if (parser.isAppCommand(message)) {
return executeAppCommand(serverUrl, intl, parser, message, args);
}
}
let msg = filterEmDashForCommand(message);
let cmdLength = msg.indexOf(' ');
if (cmdLength < 0) {
cmdLength = msg.length;
}
const cmd = msg.substring(0, cmdLength).toLowerCase();
if (cmd === '/code') {
msg = cmd + ' ' + msg.substring(cmdLength, msg.length).trimEnd();
} else {
msg = cmd + ' ' + msg.substring(cmdLength, msg.length).trim();
}
let data;
try {
data = await client.executeCommand(msg, args);
} catch (error) {
logDebug('error on executeCommand', getFullErrorMessage(error));
return {error};
}
if (data?.trigger_id) {
IntegrationsManager.getManager(serverUrl)?.setTriggerId(data.trigger_id);
}
return {data};
};
export const executeAppCommand = async (serverUrl: string, intl: IntlShape, parser: AppCommandParser, msg: string, args: CommandArgs) => {
const {creq, errorMessage} = await parser.composeCommandSubmitCall(msg);
const createErrorMessage = (errMessage: string) => {
return {error: {message: errMessage}};
};
if (!creq) {
return createErrorMessage(errorMessage!);
}
const res = await doAppSubmit(serverUrl, creq, intl);
if ('error' in res) {
const errorResponse = res.error;
return createErrorMessage(errorResponse.text || intl.formatMessage({
id: 'apps.error.unknown',
defaultMessage: 'Unknown error occurred.',
}));
}
const callResp = res.data;
switch (callResp.type) {
case AppCallResponseTypes.OK:
if (callResp.text) {
postEphemeralCallResponseForCommandArgs(serverUrl, callResp, callResp.text, args);
}
return {data: {}};
case AppCallResponseTypes.FORM:
if (callResp.form) {
openAppsForm(callResp.form, creq.context);
}
return {data: {}};
case AppCallResponseTypes.NAVIGATE:
if (callResp.navigate_to_url) {
handleGotoLocation(serverUrl, intl, callResp.navigate_to_url);
}
return {data: {}};
default:
return createErrorMessage(intl.formatMessage({
id: 'apps.error.responses.unknown_type',
defaultMessage: 'App response type not supported. Response type: {type}.',
}, {
type: callResp.type,
}));
}
};
const filterEmDashForCommand = (command: string): string => {
return command.replace(/\u2014/g, '--');
};
export const handleGotoLocation = async (serverUrl: string, intl: IntlShape, location: string) => {
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
if (!operator) {
return {error: `${serverUrl} database not found`};
}
const {database} = operator;
const config = await getConfig(database);
const match = matchDeepLink(location, serverUrl, config?.SiteURL);
if (match) {
handleDeepLink(match, intl, location);
} else {
const {formatMessage} = intl;
const onError = () => Alert.alert(
formatMessage({
id: 'mobile.server_link.error.title',
defaultMessage: 'Link Error',
}),
formatMessage({
id: 'mobile.server_link.error.text',
defaultMessage: 'The link could not be found on this server.',
}),
);
tryOpenURL(location, onError);
}
return {data: true};
};
export const fetchCommands = async (serverUrl: string, teamId: string): Promise<{commands: Command[]} | {error: unknown}> => {
try {
const client = NetworkManager.getClient(serverUrl);
return {commands: await client.getCommandsList(teamId)};
} catch (error) {
logDebug('error on fetchCommands', getFullErrorMessage(error));
return {error};
}
};
export const fetchSuggestions = async (serverUrl: string, term: string, teamId: string, channelId: string, rootId?: string): Promise<{suggestions: AutocompleteSuggestion[]} | {error: unknown}> => {
try {
const client = NetworkManager.getClient(serverUrl);
return {suggestions: await client.getCommandAutocompleteSuggestionsList(term, teamId, channelId, rootId)};
} catch (error) {
logDebug('error on fetchSuggestions', getFullErrorMessage(error));
return {error};
}
};