forked from Ivasoft/mattermost-mobile
Merge pull request #6061 from larkox/CommandAutocomplete
[Gekidou] Add command autocomplete
This commit is contained in:
@@ -4,6 +4,7 @@
|
||||
import {IntlShape} from 'react-intl';
|
||||
|
||||
import {sendEphemeralPost} from '@actions/local/post';
|
||||
import ClientError from '@client/rest/error';
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import {Screens} from '@constants';
|
||||
import {AppCallResponseTypes, AppCallTypes} from '@constants/apps';
|
||||
@@ -20,7 +21,7 @@ export async function doAppCall<Res=unknown>(serverUrl: string, call: AppCallReq
|
||||
try {
|
||||
client = NetworkManager.getClient(serverUrl);
|
||||
} catch (error) {
|
||||
return {error};
|
||||
return {error: makeCallErrorResponse((error as ClientError).message)};
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
@@ -914,3 +914,19 @@ export const searchChannels = async (serverUrl: string, term: string) => {
|
||||
return {error};
|
||||
}
|
||||
};
|
||||
|
||||
export const fetchChannelById = async (serverUrl: string, id: string) => {
|
||||
let client: Client;
|
||||
try {
|
||||
client = NetworkManager.getClient(serverUrl);
|
||||
} catch (error) {
|
||||
return {error};
|
||||
}
|
||||
|
||||
try {
|
||||
const channel = await client.getChannel(id);
|
||||
return {channel};
|
||||
} catch (error) {
|
||||
return {error};
|
||||
}
|
||||
};
|
||||
|
||||
@@ -200,3 +200,32 @@ export const handleGotoLocation = async (serverUrl: string, intl: IntlShape, loc
|
||||
}
|
||||
return {data: true};
|
||||
};
|
||||
|
||||
export const fetchCommands = async (serverUrl: string, teamId: string) => {
|
||||
let client: Client;
|
||||
try {
|
||||
client = NetworkManager.getClient(serverUrl);
|
||||
} catch (error) {
|
||||
return {error: error as ClientErrorProps};
|
||||
}
|
||||
try {
|
||||
return {commands: await client.getCommandsList(teamId)};
|
||||
} catch (error) {
|
||||
return {error: error as ClientErrorProps};
|
||||
}
|
||||
};
|
||||
|
||||
export const fetchSuggestions = async (serverUrl: string, term: string, teamId: string, channelId: string, rootId?: string) => {
|
||||
let client: Client;
|
||||
try {
|
||||
client = NetworkManager.getClient(serverUrl);
|
||||
} catch (error) {
|
||||
return {error: error as ClientErrorProps};
|
||||
}
|
||||
|
||||
try {
|
||||
return {suggestions: await client.getCommandAutocompleteSuggestionsList(term, teamId, channelId, rootId)};
|
||||
} catch (error) {
|
||||
return {error: error as ClientErrorProps};
|
||||
}
|
||||
};
|
||||
|
||||
@@ -7,7 +7,7 @@ import {PER_PAGE_DEFAULT} from './constants';
|
||||
|
||||
export interface ClientIntegrationsMix {
|
||||
getCommandsList: (teamId: string) => Promise<Command[]>;
|
||||
getCommandAutocompleteSuggestionsList: (userInput: string, teamId: string, commandArgs?: CommandArgs) => Promise<Command[]>;
|
||||
getCommandAutocompleteSuggestionsList: (userInput: string, teamId: string, channelId: string, rootId?: string) => Promise<AutocompleteSuggestion[]>;
|
||||
getAutocompleteCommandsList: (teamId: string, page?: number, perPage?: number) => Promise<Command[]>;
|
||||
executeCommand: (command: string, commandArgs?: CommandArgs) => Promise<CommandResponse>;
|
||||
addCommand: (command: Command) => Promise<Command>;
|
||||
@@ -22,9 +22,9 @@ const ClientIntegrations = (superclass: any) => class extends superclass {
|
||||
);
|
||||
};
|
||||
|
||||
getCommandAutocompleteSuggestionsList = async (userInput: string, teamId: string, commandArgs: {}) => {
|
||||
getCommandAutocompleteSuggestionsList = async (userInput: string, teamId: string, channelId: string, rootId?: string) => {
|
||||
return this.doFetch(
|
||||
`${this.getTeamRoute(teamId)}/commands/autocomplete_suggestions${buildQueryString({...commandArgs, user_input: userInput})}`,
|
||||
`${this.getTeamRoute(teamId)}/commands/autocomplete_suggestions${buildQueryString({user_input: userInput, team_id: teamId, channel_id: channelId, root_id: rootId})}`,
|
||||
{method: 'get'},
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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 && (
|
||||
<AppSlashSuggestion
|
||||
maxListHeight={maxListHeight}
|
||||
updateValue={updateValue}
|
||||
onResultCountChange={setShowingAppCommand}
|
||||
onShowingChange={setShowingAppCommand}
|
||||
value={value || ''}
|
||||
nestedScrollEnabled={nestedScrollEnabled}
|
||||
channelId={channelId}
|
||||
rootId={rootId}
|
||||
/>
|
||||
)} */}
|
||||
)}
|
||||
{(!appsTakeOver || !isAppsEnabled) && (<>
|
||||
<AtMention
|
||||
cursorPosition={cursorPosition}
|
||||
@@ -192,14 +197,18 @@ const Autocomplete = ({
|
||||
hasFilesAttached={hasFilesAttached}
|
||||
/>
|
||||
}
|
||||
{/* <SlashSuggestion
|
||||
{showCommands &&
|
||||
<SlashSuggestion
|
||||
maxListHeight={maxListHeight}
|
||||
updateValue={updateValue}
|
||||
onResultCountChange={setShowingCommand}
|
||||
onShowingChange={setShowingCommand}
|
||||
value={value || ''}
|
||||
nestedScrollEnabled={nestedScrollEnabled}
|
||||
channelId={channelId}
|
||||
rootId={rootId}
|
||||
/>
|
||||
{(isSearch && enableDateSuggestion) &&
|
||||
}
|
||||
{/* {(isSearch && enableDateSuggestion) &&
|
||||
<DateSuggestion
|
||||
cursorPosition={cursorPosition}
|
||||
updateValue={updateValue}
|
||||
|
||||
@@ -12,6 +12,8 @@ import {General} from '@constants';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {makeStyleSheetFromTheme, changeOpacity} from '@utils/theme';
|
||||
|
||||
import type ChannelModel from '@typings/database/models/servers/channel';
|
||||
|
||||
const getStyleFromTheme = makeStyleSheetFromTheme((theme: Theme) => {
|
||||
return {
|
||||
icon: {
|
||||
@@ -37,7 +39,7 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme: Theme) => {
|
||||
});
|
||||
|
||||
type Props = {
|
||||
channel: Channel;
|
||||
channel: Channel | ChannelModel;
|
||||
displayName?: string;
|
||||
isBot: boolean;
|
||||
isGuest: boolean;
|
||||
@@ -74,6 +76,8 @@ const ChannelMentionItem = ({
|
||||
|
||||
let component;
|
||||
|
||||
const isArchived = ('delete_at' in channel ? channel.delete_at : channel.deleteAt) > 0;
|
||||
|
||||
if (channel.type === General.DM_CHANNEL || channel.type === General.GM_CHANNEL) {
|
||||
if (!displayName) {
|
||||
return null;
|
||||
@@ -112,7 +116,7 @@ const ChannelMentionItem = ({
|
||||
shared={channel.shared}
|
||||
type={channel.type}
|
||||
isInfo={true}
|
||||
isArchived={channel.delete_at > 0}
|
||||
isArchived={isArchived}
|
||||
size={18}
|
||||
style={style.icon}
|
||||
/>
|
||||
|
||||
@@ -12,21 +12,24 @@ import {observeUser} from '@queries/servers/user';
|
||||
import ChannelMentionItem from './channel_mention_item';
|
||||
|
||||
import type {WithDatabaseArgs} from '@typings/database/database';
|
||||
import type ChannelModel from '@typings/database/models/servers/channel';
|
||||
import type UserModel from '@typings/database/models/servers/user';
|
||||
|
||||
type OwnProps = {
|
||||
channel: Channel;
|
||||
channel: Channel | ChannelModel;
|
||||
}
|
||||
|
||||
const enhanced = withObservables([], ({database, channel}: WithDatabaseArgs & OwnProps) => {
|
||||
let user = of$<UserModel | undefined>(undefined);
|
||||
if (channel.type === General.DM_CHANNEL) {
|
||||
user = observeUser(database, channel.teammate_id!);
|
||||
const teammateId = 'teammate_id' in channel ? channel.teammate_id : '';
|
||||
const channelDisplayName = 'display_name' in channel ? channel.display_name : channel.displayName;
|
||||
if (channel.type === General.DM_CHANNEL && teammateId) {
|
||||
user = observeUser(database, teammateId!);
|
||||
}
|
||||
|
||||
const isBot = user.pipe(switchMap((u) => of$(u ? u.isBot : false)));
|
||||
const isGuest = user.pipe(switchMap((u) => of$(u ? u.isGuest : false)));
|
||||
const displayName = user.pipe(switchMap((u) => of$(u ? u.username : channel.display_name)));
|
||||
const displayName = user.pipe(switchMap((u) => of$(u ? u.username : channelDisplayName)));
|
||||
|
||||
return {
|
||||
isBot,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,105 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {searchChannels} from '@actions/remote/channel';
|
||||
import {searchUsers} from '@actions/remote/user';
|
||||
import {COMMAND_SUGGESTION_CHANNEL, COMMAND_SUGGESTION_USER} from '@constants/apps';
|
||||
|
||||
export async function inTextMentionSuggestions(serverUrl: string, pretext: string, channelID: string, teamID: string, delimiter = ''): Promise<AutocompleteSuggestion[] | null> {
|
||||
const separatedWords = pretext.split(' ');
|
||||
const incompleteLessLastWord = separatedWords.slice(0, -1).join(' ');
|
||||
const lastWord = separatedWords[separatedWords.length - 1];
|
||||
if (lastWord.startsWith('@')) {
|
||||
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;
|
||||
if (delimiter) {
|
||||
complete = delimiter + complete;
|
||||
}
|
||||
u.Complete = complete;
|
||||
});
|
||||
return users;
|
||||
}
|
||||
|
||||
if (lastWord.startsWith('~') && !lastWord.startsWith('~~')) {
|
||||
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;
|
||||
if (delimiter) {
|
||||
complete = delimiter + complete;
|
||||
}
|
||||
c.Complete = complete;
|
||||
});
|
||||
return channels;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export async function getUserSuggestions(usersAutocomplete?: {users: UserProfile[]; out_of_channel?: UserProfile[]}): Promise<AutocompleteSuggestion[]> {
|
||||
const notFoundSuggestions = [{
|
||||
Complete: '',
|
||||
Suggestion: '',
|
||||
Description: 'No user found',
|
||||
Hint: '',
|
||||
IconData: '',
|
||||
}];
|
||||
if (!usersAutocomplete) {
|
||||
return notFoundSuggestions;
|
||||
}
|
||||
|
||||
if (!usersAutocomplete.users.length && !usersAutocomplete.out_of_channel?.length) {
|
||||
return notFoundSuggestions;
|
||||
}
|
||||
|
||||
const items: AutocompleteSuggestion[] = [];
|
||||
usersAutocomplete.users.forEach((u) => {
|
||||
items.push(getUserSuggestion(u));
|
||||
});
|
||||
usersAutocomplete.out_of_channel?.forEach((u) => {
|
||||
items.push(getUserSuggestion(u));
|
||||
});
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
export async function getChannelSuggestions(channels?: Channel[]): Promise<AutocompleteSuggestion[]> {
|
||||
const notFoundSuggestion = [{
|
||||
Complete: '',
|
||||
Suggestion: '',
|
||||
Description: 'No channel found',
|
||||
Hint: '',
|
||||
IconData: '',
|
||||
}];
|
||||
if (!channels?.length) {
|
||||
return notFoundSuggestion;
|
||||
}
|
||||
|
||||
const items = channels.map((c) => {
|
||||
return {
|
||||
Complete: '~' + c.name,
|
||||
Suggestion: '',
|
||||
Description: '',
|
||||
Hint: '',
|
||||
IconData: '',
|
||||
type: COMMAND_SUGGESTION_CHANNEL,
|
||||
item: c,
|
||||
};
|
||||
});
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
function getUserSuggestion(u: UserProfile) {
|
||||
return {
|
||||
Complete: '@' + u.username,
|
||||
Suggestion: '',
|
||||
Description: '',
|
||||
Hint: '',
|
||||
IconData: '',
|
||||
type: COMMAND_SUGGESTION_USER,
|
||||
item: u,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,204 @@
|
||||
// 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} from 'react-native';
|
||||
|
||||
import AtMentionItem from '@components/autocomplete/at_mention_item';
|
||||
import ChannelMentionItem from '@components/autocomplete/channel_mention_item';
|
||||
import {COMMAND_SUGGESTION_CHANNEL, COMMAND_SUGGESTION_USER} from '@constants/apps';
|
||||
import {useServerUrl} from '@context/server';
|
||||
import {useTheme} from '@context/theme';
|
||||
import analytics from '@init/analytics';
|
||||
import ChannelModel from '@typings/database/models/servers/channel';
|
||||
import UserModel from '@typings/database/models/servers/user';
|
||||
import {makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
||||
import {AppCommandParser, ExtendedAutocompleteSuggestion} from '../app_command_parser/app_command_parser';
|
||||
import SlashSuggestionItem from '../slash_suggestion_item';
|
||||
|
||||
export type Props = {
|
||||
currentTeamId: string;
|
||||
isSearch?: boolean;
|
||||
maxListHeight?: number;
|
||||
updateValue: (text: string) => void;
|
||||
onShowingChange: (c: boolean) => void;
|
||||
value: string;
|
||||
nestedScrollEnabled?: boolean;
|
||||
rootId?: string;
|
||||
channelId: string;
|
||||
isAppsEnabled: boolean;
|
||||
};
|
||||
|
||||
const keyExtractor = (item: ExtendedAutocompleteSuggestion): string => item.Suggestion + item.type + item.item;
|
||||
|
||||
const getStyleFromTheme = makeStyleSheetFromTheme((theme: Theme) => {
|
||||
return {
|
||||
listView: {
|
||||
flex: 1,
|
||||
backgroundColor: theme.centerChannelBg,
|
||||
paddingTop: 8,
|
||||
borderRadius: 4,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const emptySuggestonList: AutocompleteSuggestion[] = [];
|
||||
|
||||
const AppSlashSuggestion = ({
|
||||
channelId,
|
||||
currentTeamId,
|
||||
rootId,
|
||||
value = '',
|
||||
isAppsEnabled,
|
||||
maxListHeight,
|
||||
nestedScrollEnabled,
|
||||
updateValue,
|
||||
onShowingChange,
|
||||
}: Props) => {
|
||||
const intl = useIntl();
|
||||
const theme = useTheme();
|
||||
const serverUrl = useServerUrl();
|
||||
const appCommandParser = useRef<AppCommandParser>(new AppCommandParser(serverUrl, intl, channelId, currentTeamId, rootId, theme));
|
||||
const [dataSource, setDataSource] = useState<AutocompleteSuggestion[]>(emptySuggestonList);
|
||||
const active = isAppsEnabled && Boolean(dataSource.length);
|
||||
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);
|
||||
if (!mounted.current) {
|
||||
return;
|
||||
}
|
||||
updateSuggestions(suggestions);
|
||||
}), []);
|
||||
|
||||
const updateSuggestions = (matches: ExtendedAutocompleteSuggestion[]) => {
|
||||
setDataSource(matches);
|
||||
onShowingChange(Boolean(matches.length));
|
||||
};
|
||||
|
||||
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} `));
|
||||
});
|
||||
}
|
||||
}, [serverUrl, updateValue]);
|
||||
|
||||
const completeIgnoringSuggestion = useCallback((base: string): (toIgnore: string) => void => {
|
||||
return () => {
|
||||
completeSuggestion(base);
|
||||
};
|
||||
}, [completeSuggestion]);
|
||||
|
||||
const renderItem = useCallback(({item}: {item: ExtendedAutocompleteSuggestion}) => {
|
||||
switch (item.type) {
|
||||
case COMMAND_SUGGESTION_USER:
|
||||
if (!item.item) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<AtMentionItem
|
||||
user={item.item as UserProfile | UserModel}
|
||||
onPress={completeIgnoringSuggestion(item.Complete)}
|
||||
testID={`autocomplete.at_mention.item.${item.item}`}
|
||||
/>
|
||||
);
|
||||
case COMMAND_SUGGESTION_CHANNEL:
|
||||
if (!item.item) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<ChannelMentionItem
|
||||
channel={item.item as Channel | ChannelModel}
|
||||
onPress={completeIgnoringSuggestion(item.Complete)}
|
||||
testID={`autocomplete.channel_mention.item.${item.item}`}
|
||||
/>
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<SlashSuggestionItem
|
||||
description={item.Description}
|
||||
hint={item.Hint}
|
||||
onPress={completeSuggestion}
|
||||
suggestion={item.Suggestion}
|
||||
complete={item.Complete}
|
||||
icon={item.IconData}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}, [completeSuggestion, completeIgnoringSuggestion]);
|
||||
|
||||
const isAppCommand = (pretext: string, channelID: string, teamID = '', rootID?: string) => {
|
||||
appCommandParser.current.setChannelContext(channelID, teamID, rootID);
|
||||
return appCommandParser.current.isAppCommand(pretext);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (value[0] !== '/') {
|
||||
fetchAndShowAppCommandSuggestions.cancel();
|
||||
updateSuggestions(emptySuggestonList);
|
||||
return;
|
||||
}
|
||||
|
||||
if (value.indexOf(' ') === -1) {
|
||||
// Let slash command suggestions handle base commands.
|
||||
fetchAndShowAppCommandSuggestions.cancel();
|
||||
updateSuggestions(emptySuggestonList);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isAppCommand(value, channelId, currentTeamId, rootId)) {
|
||||
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.
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<FlatList
|
||||
testID='app_slash_suggestion.list'
|
||||
keyboardShouldPersistTaps='always'
|
||||
style={listStyle}
|
||||
data={dataSource}
|
||||
keyExtractor={keyExtractor}
|
||||
removeClippedSubviews={true}
|
||||
renderItem={renderItem}
|
||||
nestedScrollEnabled={nestedScrollEnabled}
|
||||
/>
|
||||
);
|
||||
};
|
||||
export default AppSlashSuggestion;
|
||||
@@ -0,0 +1,18 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {withDatabase} from '@nozbe/watermelondb/DatabaseProvider';
|
||||
import withObservables from '@nozbe/with-observables';
|
||||
|
||||
import {observeConfigBooleanValue, observeCurrentTeamId} from '@queries/servers/system';
|
||||
|
||||
import AppSlashSuggestion from './app_slash_suggestion';
|
||||
|
||||
import type {WithDatabaseArgs} from '@typings/database/database';
|
||||
|
||||
const enhanced = withObservables([], ({database}: WithDatabaseArgs) => ({
|
||||
currentTeamId: observeCurrentTeamId(database),
|
||||
isAppsEnabled: observeConfigBooleanValue(database, 'FeatureFlagAppsEnabled'),
|
||||
}));
|
||||
|
||||
export default withDatabase(enhanced(AppSlashSuggestion));
|
||||
18
app/components/autocomplete/slash_suggestion/index.ts
Normal file
18
app/components/autocomplete/slash_suggestion/index.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {withDatabase} from '@nozbe/watermelondb/DatabaseProvider';
|
||||
import withObservables from '@nozbe/with-observables';
|
||||
|
||||
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: observeCurrentTeamId(database),
|
||||
isAppsEnabled: observeConfigBooleanValue(database, 'FeatureFlagAppsEnabled'),
|
||||
}));
|
||||
|
||||
export default withDatabase(enhanced(SlashSuggestion));
|
||||
@@ -0,0 +1,247 @@
|
||||
// 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,
|
||||
} from 'react-native';
|
||||
|
||||
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';
|
||||
import {makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
||||
import {AppCommandParser} from './app_command_parser/app_command_parser';
|
||||
import SlashSuggestionItem from './slash_suggestion_item';
|
||||
|
||||
// TODO: Remove when all below commands have been implemented
|
||||
const COMMANDS_TO_IMPLEMENT_LATER = ['collapse', 'expand', 'join', 'open', 'leave', 'logout', 'msg', 'grpmsg'];
|
||||
const NON_MOBILE_COMMANDS = ['rename', 'invite_people', 'shortcuts', 'search', 'help', 'settings', 'remove'];
|
||||
|
||||
const COMMANDS_TO_HIDE_ON_MOBILE = [...COMMANDS_TO_IMPLEMENT_LATER, ...NON_MOBILE_COMMANDS];
|
||||
|
||||
const commandFilter = (v: Command) => !COMMANDS_TO_HIDE_ON_MOBILE.includes(v.trigger);
|
||||
|
||||
const getStyleFromTheme = makeStyleSheetFromTheme((theme: Theme) => {
|
||||
return {
|
||||
listView: {
|
||||
flex: 1,
|
||||
backgroundColor: theme.centerChannelBg,
|
||||
paddingTop: 8,
|
||||
borderRadius: 4,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
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;
|
||||
maxListHeight?: number;
|
||||
updateValue: (text: string) => void;
|
||||
onShowingChange: (c: boolean) => void;
|
||||
value: string;
|
||||
nestedScrollEnabled?: boolean;
|
||||
rootId?: string;
|
||||
channelId: string;
|
||||
isAppsEnabled: boolean;
|
||||
};
|
||||
|
||||
const emptyCommandList: Command[] = [];
|
||||
const emptySuggestionList: AutocompleteSuggestion[] = [];
|
||||
|
||||
const SlashSuggestion = ({
|
||||
channelId,
|
||||
currentTeamId,
|
||||
rootId,
|
||||
onShowingChange,
|
||||
isAppsEnabled,
|
||||
maxListHeight,
|
||||
nestedScrollEnabled,
|
||||
updateValue,
|
||||
value = '',
|
||||
}: Props) => {
|
||||
const intl = useIntl();
|
||||
const theme = useTheme();
|
||||
const style = getStyleFromTheme(theme);
|
||||
const serverUrl = useServerUrl();
|
||||
const appCommandParser = useRef<AppCommandParser>(new AppCommandParser(serverUrl, intl, channelId, currentTeamId, rootId, theme));
|
||||
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 listStyle = useMemo(() => [style.listView, {maxHeight: maxListHeight}], [maxListHeight, style]);
|
||||
|
||||
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 (res.error) {
|
||||
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
|
||||
testID='slash_suggestion.list'
|
||||
keyboardShouldPersistTaps='always'
|
||||
style={listStyle}
|
||||
data={dataSource}
|
||||
keyExtractor={keyExtractor}
|
||||
removeClippedSubviews={true}
|
||||
renderItem={renderItem}
|
||||
nestedScrollEnabled={nestedScrollEnabled}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default SlashSuggestion;
|
||||
@@ -0,0 +1,182 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import base64 from 'base-64';
|
||||
import React, {useCallback, useMemo} from 'react';
|
||||
import {Image, Text, View} from 'react-native';
|
||||
import FastImage from 'react-native-fast-image';
|
||||
import {useSafeAreaInsets} from 'react-native-safe-area-context';
|
||||
import {SvgXml} from 'react-native-svg';
|
||||
|
||||
import CompassIcon from '@app/components/compass_icon';
|
||||
import TouchableWithFeedback from '@components/touchable_with_feedback';
|
||||
import {COMMAND_SUGGESTION_ERROR} from '@constants/apps';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
||||
const slashIcon = require('@assets/images/autocomplete/slash_command.png');
|
||||
|
||||
const getStyleFromTheme = makeStyleSheetFromTheme((theme: Theme) => {
|
||||
return {
|
||||
icon: {
|
||||
fontSize: 24,
|
||||
backgroundColor: changeOpacity(theme.centerChannelColor, 0.08),
|
||||
width: 35,
|
||||
height: 35,
|
||||
marginRight: 12,
|
||||
borderRadius: 4,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
marginTop: 8,
|
||||
},
|
||||
uriIcon: {
|
||||
width: 16,
|
||||
height: 16,
|
||||
},
|
||||
iconColor: {
|
||||
tintColor: theme.centerChannelColor,
|
||||
},
|
||||
container: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
paddingBottom: 8,
|
||||
paddingHorizontal: 16,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
suggestionContainer: {
|
||||
flex: 1,
|
||||
},
|
||||
suggestionDescription: {
|
||||
fontSize: 12,
|
||||
color: changeOpacity(theme.centerChannelColor, 0.56),
|
||||
},
|
||||
suggestionName: {
|
||||
fontSize: 15,
|
||||
color: theme.centerChannelColor,
|
||||
marginBottom: 4,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
type Props = {
|
||||
complete: string;
|
||||
description: string;
|
||||
hint: string;
|
||||
onPress: (complete: string) => void;
|
||||
suggestion: string;
|
||||
icon: string;
|
||||
}
|
||||
|
||||
const SlashSuggestionItem = ({
|
||||
complete = '',
|
||||
description,
|
||||
hint,
|
||||
onPress,
|
||||
suggestion,
|
||||
icon,
|
||||
}: Props) => {
|
||||
const insets = useSafeAreaInsets();
|
||||
const theme = useTheme();
|
||||
const style = getStyleFromTheme(theme);
|
||||
|
||||
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) {
|
||||
suggestionText = suggestionText.substring(1);
|
||||
}
|
||||
|
||||
if (hint) {
|
||||
if (suggestionText?.length) {
|
||||
suggestionText += ` ${hint}`;
|
||||
} else {
|
||||
suggestionText = hint;
|
||||
}
|
||||
}
|
||||
|
||||
let image = (
|
||||
<Image
|
||||
style={style.iconColor}
|
||||
width={10}
|
||||
height={16}
|
||||
source={slashIcon}
|
||||
/>
|
||||
);
|
||||
if (icon === COMMAND_SUGGESTION_ERROR) {
|
||||
image = (
|
||||
<CompassIcon
|
||||
name='alert-circle-outline'
|
||||
size={24}
|
||||
/>
|
||||
);
|
||||
} else if (icon.startsWith('http')) {
|
||||
image = (
|
||||
<FastImage
|
||||
source={iconAsSource}
|
||||
style={style.uriIcon}
|
||||
/>
|
||||
);
|
||||
} else if (icon.startsWith('data:')) {
|
||||
if (icon.startsWith('data:image/svg+xml')) {
|
||||
let xml = '';
|
||||
try {
|
||||
xml = base64.decode(icon.substring('data:image/svg+xml;base64,'.length));
|
||||
image = (
|
||||
<SvgXml
|
||||
xml={xml}
|
||||
width={32}
|
||||
height={32}
|
||||
/>
|
||||
);
|
||||
} catch (error) {
|
||||
// Do nothing
|
||||
}
|
||||
} else {
|
||||
image = (
|
||||
<Image
|
||||
source={iconAsSource}
|
||||
style={style.uriIcon}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<TouchableWithFeedback
|
||||
onPress={completeSuggestion}
|
||||
style={touchableStyle}
|
||||
underlayColor={changeOpacity(theme.buttonBg, 0.08)}
|
||||
type={'native'}
|
||||
>
|
||||
<View style={style.container}>
|
||||
<View style={style.icon}>
|
||||
{image}
|
||||
</View>
|
||||
<View style={style.suggestionContainer}>
|
||||
<Text style={style.suggestionName}>{`${suggestionText}`}</Text>
|
||||
{Boolean(description) &&
|
||||
<Text
|
||||
ellipsizeMode='tail'
|
||||
numberOfLines={1}
|
||||
style={style.suggestionDescription}
|
||||
>
|
||||
{description}
|
||||
</Text>
|
||||
}
|
||||
</View>
|
||||
</View>
|
||||
</TouchableWithFeedback>
|
||||
);
|
||||
};
|
||||
|
||||
export default SlashSuggestionItem;
|
||||
@@ -44,6 +44,10 @@ export const AppFieldTypes: { [name: string]: AppFieldType } = {
|
||||
MARKDOWN: 'markdown',
|
||||
};
|
||||
|
||||
export const COMMAND_SUGGESTION_ERROR = 'error';
|
||||
export const COMMAND_SUGGESTION_CHANNEL = 'channel';
|
||||
export const COMMAND_SUGGESTION_USER = 'user';
|
||||
|
||||
export default {
|
||||
AppBindingLocations,
|
||||
AppBindingPresentations,
|
||||
@@ -51,4 +55,7 @@ export default {
|
||||
AppCallTypes,
|
||||
AppExpandLevels,
|
||||
AppFieldTypes,
|
||||
COMMAND_SUGGESTION_ERROR,
|
||||
COMMAND_SUGGESTION_CHANNEL,
|
||||
COMMAND_SUGGESTION_USER,
|
||||
};
|
||||
|
||||
86
app/init/integrations_manager.ts
Normal file
86
app/init/integrations_manager.ts
Normal file
@@ -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();
|
||||
@@ -170,3 +170,5 @@ export const makeCallErrorResponse = (errMessage: string) => {
|
||||
error: errMessage,
|
||||
};
|
||||
};
|
||||
|
||||
export const filterEmptyOptions = (option: AppSelectOption): boolean => Boolean(option.value && !option.value.match(/^[ \t]+$/));
|
||||
|
||||
@@ -62,6 +62,9 @@ export function buildQueryString(parameters: Dictionary<any>): string {
|
||||
let query = '?';
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
const key = keys[i];
|
||||
if (parameters[key] == null) {
|
||||
continue;
|
||||
}
|
||||
query += key + '=' + encodeURIComponent(parameters[key]);
|
||||
|
||||
if (i < keys.length - 1) {
|
||||
|
||||
24
package-lock.json
generated
24
package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
10
types/api/apps.d.ts
vendored
10
types/api/apps.d.ts
vendored
@@ -178,11 +178,11 @@ type AppField = {
|
||||
};
|
||||
|
||||
type AutocompleteSuggestion = {
|
||||
suggestion: string;
|
||||
complete?: string;
|
||||
description?: string;
|
||||
hint?: string;
|
||||
iconData?: string;
|
||||
Suggestion: string;
|
||||
Complete: string;
|
||||
Description: string;
|
||||
Hint: string;
|
||||
IconData: string;
|
||||
};
|
||||
|
||||
type AutocompleteSuggestionWithComplete = AutocompleteSuggestion & {
|
||||
|
||||
1
types/api/integrations.d.ts
vendored
1
types/api/integrations.d.ts
vendored
@@ -19,6 +19,7 @@ type Command = {
|
||||
'display_name': string;
|
||||
'description': string;
|
||||
'url': string;
|
||||
'autocomplete_icon_data'?: string;
|
||||
};
|
||||
|
||||
type CommandArgs = {
|
||||
|
||||
Reference in New Issue
Block a user