Merge branch 'gekidou' into gekidou-fixes
This commit is contained in:
commit
b6f373df5c
64 changed files with 3659 additions and 341 deletions
|
|
@ -4,6 +4,7 @@
|
|||
import {SYSTEM_IDENTIFIERS} from '@constants/database';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {getRecentReactions} from '@queries/servers/system';
|
||||
import {getEmojiFirstAlias} from '@utils/emoji/helpers';
|
||||
|
||||
const MAXIMUM_RECENT_EMOJI = 27;
|
||||
|
||||
|
|
@ -22,16 +23,17 @@ export const addRecentReaction = async (serverUrl: string, emojiNames: string[],
|
|||
|
||||
try {
|
||||
const recentEmojis = new Set(recent);
|
||||
for (const name of emojiNames) {
|
||||
if (recentEmojis.has(name)) {
|
||||
recentEmojis.delete(name);
|
||||
const aliases = emojiNames.map((e) => getEmojiFirstAlias(e));
|
||||
for (const alias of aliases) {
|
||||
if (recentEmojis.has(alias)) {
|
||||
recentEmojis.delete(alias);
|
||||
}
|
||||
}
|
||||
|
||||
recent = Array.from(recentEmojis);
|
||||
|
||||
for (const name of emojiNames) {
|
||||
recent.unshift(name);
|
||||
for (const alias of aliases) {
|
||||
recent.unshift(alias);
|
||||
}
|
||||
return operator.handleSystem({
|
||||
systems: [{
|
||||
|
|
|
|||
|
|
@ -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,8 +7,9 @@ import {addRecentReaction} from '@actions/local/reactions';
|
|||
import DatabaseManager from '@database/manager';
|
||||
import NetworkManager from '@init/network_manager';
|
||||
import {getRecentPostsInChannel, getRecentPostsInThread} from '@queries/servers/post';
|
||||
import {queryReaction} from '@queries/servers/reactions';
|
||||
import {queryReaction} from '@queries/servers/reaction';
|
||||
import {getCurrentChannelId, getCurrentUserId} from '@queries/servers/system';
|
||||
import {getEmojiFirstAlias} from '@utils/emoji/helpers';
|
||||
|
||||
import {forceLogoutIfNecessary} from './session';
|
||||
|
||||
|
|
@ -30,29 +31,41 @@ export const addReaction = async (serverUrl: string, postId: string, emojiName:
|
|||
|
||||
try {
|
||||
const currentUserId = await getCurrentUserId(operator.database);
|
||||
const reaction = await client.addReaction(currentUserId, postId, emojiName);
|
||||
const models: Model[] = [];
|
||||
const emojiAlias = getEmojiFirstAlias(emojiName);
|
||||
const reacted = await queryReaction(operator.database, emojiAlias, postId, currentUserId).fetchCount() > 0;
|
||||
if (!reacted) {
|
||||
const reaction = await client.addReaction(currentUserId, postId, emojiAlias);
|
||||
const models: Model[] = [];
|
||||
|
||||
const reactions = await operator.handleReactions({
|
||||
postsReactions: [{
|
||||
const reactions = await operator.handleReactions({
|
||||
postsReactions: [{
|
||||
post_id: postId,
|
||||
reactions: [reaction],
|
||||
}],
|
||||
prepareRecordsOnly: true,
|
||||
skipSync: true, // this prevents the handler from deleting previous reactions
|
||||
});
|
||||
models.push(...reactions);
|
||||
|
||||
const recent = await addRecentReaction(serverUrl, [emojiName], true);
|
||||
if (Array.isArray(recent)) {
|
||||
models.push(...recent);
|
||||
}
|
||||
|
||||
if (models.length) {
|
||||
await operator.batchRecords(models);
|
||||
}
|
||||
|
||||
return {reaction};
|
||||
}
|
||||
return {
|
||||
reaction: {
|
||||
user_id: currentUserId,
|
||||
post_id: postId,
|
||||
reactions: [reaction],
|
||||
}],
|
||||
prepareRecordsOnly: true,
|
||||
skipSync: true, // this prevents the handler from deleting previous reactions
|
||||
});
|
||||
models.push(...reactions);
|
||||
|
||||
const recent = await addRecentReaction(serverUrl, [emojiName], true);
|
||||
if (Array.isArray(recent)) {
|
||||
models.push(...recent);
|
||||
}
|
||||
|
||||
if (models.length) {
|
||||
await operator.batchRecords(models);
|
||||
}
|
||||
|
||||
return {reaction};
|
||||
emoji_name: emojiAlias,
|
||||
create_at: 0,
|
||||
} as Reaction,
|
||||
};
|
||||
} catch (error) {
|
||||
forceLogoutIfNecessary(serverUrl, error as ClientErrorProps);
|
||||
return {error};
|
||||
|
|
@ -74,10 +87,11 @@ export const removeReaction = async (serverUrl: string, postId: string, emojiNam
|
|||
|
||||
try {
|
||||
const currentUserId = await getCurrentUserId(database);
|
||||
await client.removeReaction(currentUserId, postId, emojiName);
|
||||
const emojiAlias = getEmojiFirstAlias(emojiName);
|
||||
await client.removeReaction(currentUserId, postId, emojiAlias);
|
||||
|
||||
// should return one or no reaction
|
||||
const reaction = await queryReaction(database, emojiName, postId, currentUserId).fetch();
|
||||
const reaction = await queryReaction(database, emojiAlias, postId, currentUserId).fetch();
|
||||
|
||||
if (reaction.length) {
|
||||
await database.write(async () => {
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
// See LICENSE.txt for license information.
|
||||
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {queryReaction} from '@queries/servers/reactions';
|
||||
import {queryReaction} from '@queries/servers/reaction';
|
||||
|
||||
export async function handleAddCustomEmoji(serverUrl: string, msg: WebSocketMessage): Promise<void> {
|
||||
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
||||
|
|
|
|||
|
|
@ -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'},
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,175 +0,0 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback} from 'react';
|
||||
import {Text, View} from 'react-native';
|
||||
import {useSafeAreaInsets} from 'react-native-safe-area-context';
|
||||
|
||||
import ChannelIcon from '@components/channel_icon';
|
||||
import CustomStatusEmoji from '@components/custom_status/custom_status_emoji';
|
||||
import FormattedText from '@components/formatted_text';
|
||||
import ProfilePicture from '@components/profile_picture';
|
||||
import {BotTag, GuestTag} from '@components/tag';
|
||||
import TouchableWithFeedback from '@components/touchable_with_feedback';
|
||||
import {General} from '@constants';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {makeStyleSheetFromTheme, changeOpacity} from '@utils/theme';
|
||||
import {getUserCustomStatus, isGuest, isShared} from '@utils/user';
|
||||
|
||||
type AtMentionItemProps = {
|
||||
user: UserProfile;
|
||||
currentUserId: string;
|
||||
onPress: (username: string) => void;
|
||||
showFullName: boolean;
|
||||
testID?: string;
|
||||
isCustomStatusEnabled: boolean;
|
||||
}
|
||||
|
||||
const getName = (user: UserProfile, showFullName: boolean, isCurrentUser: boolean) => {
|
||||
let name = '';
|
||||
const hasNickname = user.nickname.length > 0;
|
||||
|
||||
if (showFullName) {
|
||||
name += `${user.first_name} ${user.last_name} `;
|
||||
}
|
||||
|
||||
if (hasNickname && !isCurrentUser) {
|
||||
name += name.length > 0 ? `(${user.nickname})` : user.nickname;
|
||||
}
|
||||
|
||||
return name.trim();
|
||||
};
|
||||
|
||||
const getStyleFromTheme = makeStyleSheetFromTheme((theme: Theme) => {
|
||||
return {
|
||||
row: {
|
||||
height: 40,
|
||||
paddingVertical: 8,
|
||||
paddingTop: 4,
|
||||
paddingHorizontal: 16,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
},
|
||||
rowPicture: {
|
||||
marginRight: 10,
|
||||
marginLeft: 2,
|
||||
width: 24,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
rowInfo: {
|
||||
flexDirection: 'row',
|
||||
overflow: 'hidden',
|
||||
},
|
||||
rowFullname: {
|
||||
fontSize: 15,
|
||||
color: theme.centerChannelColor,
|
||||
paddingLeft: 4,
|
||||
flexShrink: 1,
|
||||
},
|
||||
rowUsername: {
|
||||
color: changeOpacity(theme.centerChannelColor, 0.56),
|
||||
fontSize: 15,
|
||||
flexShrink: 5,
|
||||
},
|
||||
icon: {
|
||||
marginLeft: 4,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const AtMentionItem = ({
|
||||
user,
|
||||
currentUserId,
|
||||
onPress,
|
||||
showFullName,
|
||||
testID,
|
||||
isCustomStatusEnabled,
|
||||
}: AtMentionItemProps) => {
|
||||
const insets = useSafeAreaInsets();
|
||||
const theme = useTheme();
|
||||
const style = getStyleFromTheme(theme);
|
||||
|
||||
const guest = isGuest(user.roles);
|
||||
const shared = isShared(user);
|
||||
|
||||
const completeMention = useCallback(() => {
|
||||
onPress(user.username);
|
||||
}, [user.username]);
|
||||
|
||||
const isCurrentUser = currentUserId === user.id;
|
||||
const name = getName(user, showFullName, isCurrentUser);
|
||||
const customStatus = getUserCustomStatus(user);
|
||||
|
||||
return (
|
||||
<TouchableWithFeedback
|
||||
testID={testID}
|
||||
key={user.id}
|
||||
onPress={completeMention}
|
||||
underlayColor={changeOpacity(theme.buttonBg, 0.08)}
|
||||
style={{marginLeft: insets.left, marginRight: insets.right}}
|
||||
type={'native'}
|
||||
>
|
||||
<View style={style.row}>
|
||||
<View style={style.rowPicture}>
|
||||
<ProfilePicture
|
||||
author={user}
|
||||
size={24}
|
||||
showStatus={false}
|
||||
testID='at_mention_item.profile_picture'
|
||||
/>
|
||||
</View>
|
||||
<View
|
||||
style={[style.rowInfo, {maxWidth: shared ? '75%' : '80%'}]}
|
||||
>
|
||||
{Boolean(user.is_bot) && (<BotTag/>)}
|
||||
{guest && (<GuestTag/>)}
|
||||
{Boolean(name.length) && (
|
||||
<Text
|
||||
style={style.rowFullname}
|
||||
numberOfLines={1}
|
||||
testID='at_mention_item.name'
|
||||
>
|
||||
{name}
|
||||
</Text>
|
||||
)}
|
||||
{isCurrentUser && (
|
||||
<FormattedText
|
||||
id='suggestion.mention.you'
|
||||
defaultMessage=' (you)'
|
||||
style={style.rowUsername}
|
||||
/>
|
||||
)}
|
||||
<Text
|
||||
style={style.rowUsername}
|
||||
numberOfLines={1}
|
||||
testID='at_mention_item.username'
|
||||
>
|
||||
{` @${user.username}`}
|
||||
</Text>
|
||||
</View>
|
||||
{isCustomStatusEnabled && !user.is_bot && customStatus && (
|
||||
<CustomStatusEmoji
|
||||
customStatus={customStatus}
|
||||
style={style.icon}
|
||||
/>
|
||||
)}
|
||||
{shared && (
|
||||
<ChannelIcon
|
||||
name={name}
|
||||
isActive={false}
|
||||
isArchived={false}
|
||||
isInfo={true}
|
||||
isUnread={true}
|
||||
size={18}
|
||||
shared={true}
|
||||
type={General.DM_CHANNEL}
|
||||
style={style.icon}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
</TouchableWithFeedback>
|
||||
);
|
||||
};
|
||||
|
||||
export default AtMentionItem;
|
||||
49
app/components/autocomplete/at_mention_item/index.tsx
Normal file
49
app/components/autocomplete/at_mention_item/index.tsx
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback} from 'react';
|
||||
import {useSafeAreaInsets} from 'react-native-safe-area-context';
|
||||
|
||||
import TouchableWithFeedback from '@components/touchable_with_feedback';
|
||||
import UserItem from '@components/user_item';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {changeOpacity} from '@utils/theme';
|
||||
|
||||
import type UserModel from '@typings/database/models/servers/user';
|
||||
|
||||
type AtMentionItemProps = {
|
||||
user: UserProfile | UserModel;
|
||||
onPress?: (username: string) => void;
|
||||
testID?: string;
|
||||
}
|
||||
|
||||
const AtMentionItem = ({
|
||||
user,
|
||||
onPress,
|
||||
testID,
|
||||
}: AtMentionItemProps) => {
|
||||
const insets = useSafeAreaInsets();
|
||||
const theme = useTheme();
|
||||
|
||||
const completeMention = useCallback(() => {
|
||||
onPress?.(user.username);
|
||||
}, [user.username]);
|
||||
|
||||
return (
|
||||
<TouchableWithFeedback
|
||||
testID={testID}
|
||||
key={user.id}
|
||||
onPress={completeMention}
|
||||
underlayColor={changeOpacity(theme.buttonBg, 0.08)}
|
||||
style={{marginLeft: insets.left, marginRight: insets.right}}
|
||||
type={'native'}
|
||||
>
|
||||
<UserItem
|
||||
user={user}
|
||||
testID='at_mention'
|
||||
/>
|
||||
</TouchableWithFeedback>
|
||||
);
|
||||
};
|
||||
|
||||
export default AtMentionItem;
|
||||
|
|
@ -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;
|
||||
|
|
@ -13,6 +13,7 @@ Object {
|
|||
Object {
|
||||
"value": Object {
|
||||
"height": 40,
|
||||
"marginVertical": 2,
|
||||
"opacity": 1,
|
||||
},
|
||||
}
|
||||
|
|
@ -21,6 +22,7 @@ Object {
|
|||
style={
|
||||
Object {
|
||||
"height": 40,
|
||||
"marginVertical": 2,
|
||||
"opacity": 1,
|
||||
}
|
||||
}
|
||||
|
|
@ -46,10 +48,10 @@ Object {
|
|||
<View
|
||||
style={
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"flexDirection": "row",
|
||||
"marginBottom": 8,
|
||||
"height": 40,
|
||||
"paddingLeft": 2,
|
||||
"paddingVertical": 4,
|
||||
}
|
||||
}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ exports[`components/channel_list/categories/body/channel/item should match snaps
|
|||
Object {
|
||||
"value": Object {
|
||||
"height": 40,
|
||||
"marginVertical": 2,
|
||||
"opacity": 1,
|
||||
},
|
||||
}
|
||||
|
|
@ -14,6 +15,7 @@ exports[`components/channel_list/categories/body/channel/item should match snaps
|
|||
style={
|
||||
Object {
|
||||
"height": 40,
|
||||
"marginVertical": 2,
|
||||
"opacity": 1,
|
||||
}
|
||||
}
|
||||
|
|
@ -39,10 +41,10 @@ exports[`components/channel_list/categories/body/channel/item should match snaps
|
|||
<View
|
||||
style={
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"flexDirection": "row",
|
||||
"marginBottom": 8,
|
||||
"height": 40,
|
||||
"paddingLeft": 2,
|
||||
"paddingVertical": 4,
|
||||
}
|
||||
}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import {TouchableOpacity} from 'react-native-gesture-handler';
|
|||
import Animated, {Easing, useAnimatedStyle, useSharedValue, withTiming} from 'react-native-reanimated';
|
||||
|
||||
import {switchToChannelById} from '@actions/remote/channel';
|
||||
import Badge from '@components/badge';
|
||||
import ChannelIcon from '@components/channel_icon';
|
||||
import {General} from '@constants';
|
||||
import {useServerUrl} from '@context/server';
|
||||
|
|
@ -21,9 +22,9 @@ import type MyChannelModel from '@typings/database/models/servers/my_channel';
|
|||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
|
||||
container: {
|
||||
flexDirection: 'row',
|
||||
marginBottom: 8,
|
||||
paddingLeft: 2,
|
||||
paddingVertical: 4,
|
||||
height: 40,
|
||||
alignItems: 'center',
|
||||
},
|
||||
icon: {
|
||||
fontSize: 24,
|
||||
|
|
@ -42,6 +43,16 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
|
|||
muted: {
|
||||
color: changeOpacity(theme.sidebarText, 0.4),
|
||||
},
|
||||
badge: {
|
||||
position: 'relative',
|
||||
borderWidth: 0,
|
||||
left: 0,
|
||||
top: 0,
|
||||
alignSelf: undefined,
|
||||
},
|
||||
mutedBadge: {
|
||||
opacity: 0.4,
|
||||
},
|
||||
}));
|
||||
|
||||
const textStyle = StyleSheet.create({
|
||||
|
|
@ -65,7 +76,7 @@ const ChannelListItem = ({channel, isActive, isOwnDirectMessage, isMuted, myChan
|
|||
const serverUrl = useServerUrl();
|
||||
|
||||
// Make it brighter if it's not muted, and highlighted or has unreads
|
||||
const bright = !isMuted && myChannel && (myChannel.isUnread || myChannel.mentionsCount > 0);
|
||||
const bright = !isMuted && (isActive || (myChannel && (myChannel.isUnread || myChannel.mentionsCount > 0)));
|
||||
|
||||
const sharedValue = useSharedValue(collapsed && !bright);
|
||||
|
||||
|
|
@ -75,6 +86,7 @@ const ChannelListItem = ({channel, isActive, isOwnDirectMessage, isMuted, myChan
|
|||
|
||||
const animatedStyle = useAnimatedStyle(() => {
|
||||
return {
|
||||
marginVertical: withTiming(sharedValue.value ? 0 : 2, {duration: 500}),
|
||||
height: withTiming(sharedValue.value ? 0 : 40, {duration: 500}),
|
||||
opacity: withTiming(sharedValue.value ? 0 : 1, {duration: 500, easing: Easing.inOut(Easing.exp)}),
|
||||
};
|
||||
|
|
@ -89,7 +101,6 @@ const ChannelListItem = ({channel, isActive, isOwnDirectMessage, isMuted, myChan
|
|||
if (channel.type === General.GM_CHANNEL) {
|
||||
return channel.displayName?.split(',').length;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}, [channel.type, channel.displayName]);
|
||||
|
||||
|
|
@ -121,6 +132,7 @@ const ChannelListItem = ({channel, isActive, isOwnDirectMessage, isMuted, myChan
|
|||
shared={channel.shared}
|
||||
size={24}
|
||||
type={channel.type}
|
||||
isMuted={isMuted}
|
||||
/>
|
||||
<Text
|
||||
ellipsizeMode='tail'
|
||||
|
|
@ -129,7 +141,11 @@ const ChannelListItem = ({channel, isActive, isOwnDirectMessage, isMuted, myChan
|
|||
>
|
||||
{displayName}
|
||||
</Text>
|
||||
|
||||
<Badge
|
||||
visible={myChannel.mentionsCount > 0}
|
||||
value={myChannel.mentionsCount}
|
||||
style={[styles.badge, isMuted && styles.mutedBadge]}
|
||||
/>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
</Animated.View>
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import CategoryBody from './body';
|
|||
import LoadCategoriesError from './error';
|
||||
import CategoryHeader from './header';
|
||||
|
||||
import type {CategoryModel} from '@database/models/server';
|
||||
import type CategoryModel from '@typings/database/models/servers/category';
|
||||
|
||||
type Props = {
|
||||
categories: CategoryModel[];
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback} from 'react';
|
||||
import React, {useCallback, useMemo} from 'react';
|
||||
import {TouchableOpacity, View} from 'react-native';
|
||||
import AnimatedNumbers from 'react-native-animated-numbers';
|
||||
|
||||
|
|
@ -14,17 +14,20 @@ type ReactionProps = {
|
|||
emojiName: string;
|
||||
highlight: boolean;
|
||||
onPress: (emojiName: string, highlight: boolean) => void;
|
||||
onLongPress: () => void;
|
||||
onLongPress: (initialEmoji: string) => void;
|
||||
theme: Theme;
|
||||
}
|
||||
|
||||
const MIN_WIDTH = 50;
|
||||
const DIGIT_WIDTH = 5;
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
||||
return {
|
||||
count: {
|
||||
color: changeOpacity(theme.centerChannelColor, 0.56),
|
||||
...typography('Body', 100, 'SemiBold'),
|
||||
},
|
||||
countContainer: {marginRight: 5},
|
||||
countContainer: {marginRight: 8},
|
||||
countHighlight: {
|
||||
color: theme.buttonBg,
|
||||
},
|
||||
|
|
@ -44,13 +47,22 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
|||
justifyContent: 'center',
|
||||
marginBottom: 12,
|
||||
marginRight: 8,
|
||||
minWidth: 50,
|
||||
minWidth: MIN_WIDTH,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const Reaction = ({count, emojiName, highlight, onPress, onLongPress, theme}: ReactionProps) => {
|
||||
const styles = getStyleSheet(theme);
|
||||
const digits = String(count).length;
|
||||
const containerStyle = useMemo(() => {
|
||||
const minWidth = MIN_WIDTH + (digits * DIGIT_WIDTH);
|
||||
return [styles.reaction, (highlight && styles.highlight), {minWidth}];
|
||||
}, [styles.reaction, highlight, digits]);
|
||||
|
||||
const handleLongPress = useCallback(() => {
|
||||
onLongPress(emojiName);
|
||||
}, []);
|
||||
|
||||
const handlePress = useCallback(() => {
|
||||
onPress(emojiName, highlight);
|
||||
|
|
@ -59,9 +71,9 @@ const Reaction = ({count, emojiName, highlight, onPress, onLongPress, theme}: Re
|
|||
return (
|
||||
<TouchableOpacity
|
||||
onPress={handlePress}
|
||||
onLongPress={onLongPress}
|
||||
onLongPress={handleLongPress}
|
||||
delayLongPress={350}
|
||||
style={[styles.reaction, (highlight && styles.highlight)]}
|
||||
style={containerStyle}
|
||||
>
|
||||
<View style={styles.emoji}>
|
||||
<Emoji
|
||||
|
|
|
|||
|
|
@ -3,13 +3,16 @@
|
|||
|
||||
import React, {useCallback, useEffect, useRef, useState} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {TouchableOpacity, View} from 'react-native';
|
||||
import {Keyboard, TouchableOpacity, View} from 'react-native';
|
||||
|
||||
import {addReaction, removeReaction} from '@actions/remote/reactions';
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import {Screens} from '@constants';
|
||||
import {MAX_ALLOWED_REACTIONS} from '@constants/emoji';
|
||||
import {useServerUrl} from '@context/server';
|
||||
import {showModal, showModalOverCurrentContext} from '@screens/navigation';
|
||||
import {useIsTablet} from '@hooks/device';
|
||||
import {bottomSheetModalOptions, showModal, showModalOverCurrentContext} from '@screens/navigation';
|
||||
import {getEmojiFirstAlias} from '@utils/emoji/helpers';
|
||||
import {preventDoubleTap} from '@utils/tap';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
||||
|
|
@ -58,13 +61,14 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
|||
const Reactions = ({currentUserId, canAddReaction, canRemoveReaction, disabled, postId, reactions, theme}: ReactionsProps) => {
|
||||
const intl = useIntl();
|
||||
const serverUrl = useServerUrl();
|
||||
const isTablet = useIsTablet();
|
||||
const pressed = useRef(false);
|
||||
const [sortedReactions, setSortedReactions] = useState(new Set(reactions.map((r) => r.emojiName)));
|
||||
const [sortedReactions, setSortedReactions] = useState(new Set(reactions.map((r) => getEmojiFirstAlias(r.emojiName))));
|
||||
const styles = getStyleSheet(theme);
|
||||
|
||||
useEffect(() => {
|
||||
// This helps keep the reactions in the same position at all times until unmounted
|
||||
const rs = reactions.map((r) => r.emojiName);
|
||||
const rs = reactions.map((r) => getEmojiFirstAlias(r.emojiName));
|
||||
const sorted = new Set([...sortedReactions]);
|
||||
const added = rs.filter((r) => !sorted.has(r));
|
||||
added.forEach(sorted.add, sorted);
|
||||
|
|
@ -78,14 +82,20 @@ const Reactions = ({currentUserId, canAddReaction, canRemoveReaction, disabled,
|
|||
|
||||
const reactionsByName = reactions.reduce((acc, reaction) => {
|
||||
if (reaction) {
|
||||
if (acc.has(reaction.emojiName)) {
|
||||
acc.get(reaction.emojiName)!.push(reaction);
|
||||
const emojiAlias = getEmojiFirstAlias(reaction.emojiName);
|
||||
if (acc.has(emojiAlias)) {
|
||||
const rs = acc.get(emojiAlias);
|
||||
// eslint-disable-next-line max-nested-callbacks
|
||||
const present = rs!.findIndex((r) => r.userId === reaction.userId) > -1;
|
||||
if (!present) {
|
||||
rs!.push(reaction);
|
||||
}
|
||||
} else {
|
||||
acc.set(reaction.emojiName, [reaction]);
|
||||
acc.set(emojiAlias, [reaction]);
|
||||
}
|
||||
|
||||
if (reaction.userId === currentUserId) {
|
||||
highlightedReactions.push(reaction.emojiName);
|
||||
highlightedReactions.push(emojiAlias);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -99,8 +109,7 @@ const Reactions = ({currentUserId, canAddReaction, canRemoveReaction, disabled,
|
|||
addReaction(serverUrl, postId, emoji);
|
||||
};
|
||||
|
||||
const handleAddReaction = preventDoubleTap(() => {
|
||||
const screen = 'AddReaction';
|
||||
const handleAddReaction = useCallback(preventDoubleTap(() => {
|
||||
const title = intl.formatMessage({id: 'mobile.post_info.add_reaction', defaultMessage: 'Add Reaction'});
|
||||
|
||||
const closeButton = CompassIcon.getImageSourceSync('close', 24, theme.sidebarHeaderTextColor);
|
||||
|
|
@ -109,10 +118,10 @@ const Reactions = ({currentUserId, canAddReaction, canRemoveReaction, disabled,
|
|||
onEmojiPress: handleAddReactionToPost,
|
||||
};
|
||||
|
||||
showModal(screen, title, passProps);
|
||||
});
|
||||
showModal(Screens.EMOJI_PICKER, title, passProps);
|
||||
}), [intl, theme]);
|
||||
|
||||
const handleReactionPress = async (emoji: string, remove: boolean) => {
|
||||
const handleReactionPress = useCallback(async (emoji: string, remove: boolean) => {
|
||||
pressed.current = true;
|
||||
if (remove && canRemoveReaction && !disabled) {
|
||||
await removeReaction(serverUrl, postId, emoji);
|
||||
|
|
@ -121,18 +130,26 @@ const Reactions = ({currentUserId, canAddReaction, canRemoveReaction, disabled,
|
|||
}
|
||||
|
||||
pressed.current = false;
|
||||
};
|
||||
}, [canRemoveReaction, canAddReaction, disabled]);
|
||||
|
||||
const showReactionList = () => {
|
||||
const screen = 'ReactionList';
|
||||
const showReactionList = useCallback((initialEmoji: string) => {
|
||||
const screen = Screens.REACTIONS;
|
||||
const passProps = {
|
||||
initialEmoji,
|
||||
postId,
|
||||
};
|
||||
|
||||
Keyboard.dismiss();
|
||||
const title = isTablet ? intl.formatMessage({id: 'post.reactions.title', defaultMessage: 'Reactions'}) : '';
|
||||
|
||||
if (!pressed.current) {
|
||||
showModalOverCurrentContext(screen, passProps);
|
||||
if (isTablet) {
|
||||
showModal(screen, title, passProps, bottomSheetModalOptions(theme, 'close-post-reactions'));
|
||||
} else {
|
||||
showModalOverCurrentContext(screen, passProps);
|
||||
}
|
||||
}
|
||||
};
|
||||
}, [intl, isTablet, postId, theme]);
|
||||
|
||||
let addMoreReactions = null;
|
||||
const {reactionsByName, highlightedReactions} = buildReactionsMap();
|
||||
|
|
@ -175,4 +192,4 @@ const Reactions = ({currentUserId, canAddReaction, canRemoveReaction, disabled,
|
|||
);
|
||||
};
|
||||
|
||||
export default React.memo(Reactions);
|
||||
export default Reactions;
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import {map, switchMap} from 'rxjs/operators';
|
|||
|
||||
import {Preferences} from '@constants';
|
||||
import {getPreferenceAsBool, getTeammateNameDisplaySetting} from '@helpers/api/preference';
|
||||
import {queryPostsInThread} from '@queries/servers/post';
|
||||
import {queryPostRepliesCount} from '@queries/servers/post';
|
||||
import {queryPreferencesByCategoryAndName} from '@queries/servers/preference';
|
||||
import {observeConfig, observeLicense} from '@queries/servers/system';
|
||||
import {isMinimumServerVersion} from '@utils/helpers';
|
||||
|
|
@ -37,7 +37,7 @@ const withHeaderProps = withObservables(
|
|||
const teammateNameDisplay = combineLatest([preferences, config, license]).pipe(
|
||||
map(([prefs, cfg, lcs]) => getTeammateNameDisplaySetting(prefs, cfg, lcs)),
|
||||
);
|
||||
const commentCount = queryPostsInThread(database, post.rootId || post.id).observeCount();
|
||||
const commentCount = queryPostRepliesCount(database, post.rootId || post.id).observeCount();
|
||||
const rootPostAuthor = differentThreadSequence ? post.root.observe().pipe(switchMap((root) => {
|
||||
if (root.length) {
|
||||
return root[0].author.observe();
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import {of as of$} from 'rxjs';
|
|||
import {switchMap} from 'rxjs/operators';
|
||||
|
||||
import {Preferences} from '@constants';
|
||||
import {observePost, queryPostsInThread} from '@queries/servers/post';
|
||||
import {observePost, queryPostRepliesCount} from '@queries/servers/post';
|
||||
import {queryPreferencesByCategoryAndName} from '@queries/servers/preference';
|
||||
|
||||
import ThreadOverview from './thread_overview';
|
||||
|
|
@ -24,7 +24,7 @@ const enhanced = withObservables(
|
|||
pipe(
|
||||
switchMap((pref) => of$(Boolean(pref[0]?.value === 'true'))),
|
||||
),
|
||||
repliesCount: queryPostsInThread(database, rootId).observeCount(),
|
||||
repliesCount: queryPostRepliesCount(database, rootId).observeCount(false),
|
||||
};
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import {switchMap} from 'rxjs/operators';
|
|||
|
||||
import {observeConfig, observeCurrentUserId} from '@queries/servers/system';
|
||||
|
||||
import AtMentionItem from './at_mention_item';
|
||||
import UserItem from './user_item';
|
||||
|
||||
import type {WithDatabaseArgs} from '@typings/database/database';
|
||||
|
||||
|
|
@ -28,4 +28,4 @@ const enhanced = withObservables([], ({database}: WithDatabaseArgs) => {
|
|||
};
|
||||
});
|
||||
|
||||
export default withDatabase(enhanced(AtMentionItem));
|
||||
export default withDatabase(enhanced(UserItem));
|
||||
174
app/components/user_item/user_item.tsx
Normal file
174
app/components/user_item/user_item.tsx
Normal file
|
|
@ -0,0 +1,174 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {IntlShape, useIntl} from 'react-intl';
|
||||
import {StyleProp, Text, View, ViewStyle} from 'react-native';
|
||||
|
||||
import ChannelIcon from '@components/channel_icon';
|
||||
import CustomStatusEmoji from '@components/custom_status/custom_status_emoji';
|
||||
import FormattedText from '@components/formatted_text';
|
||||
import ProfilePicture from '@components/profile_picture';
|
||||
import {BotTag, GuestTag} from '@components/tag';
|
||||
import {General} from '@constants';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {makeStyleSheetFromTheme, changeOpacity} from '@utils/theme';
|
||||
import {getUserCustomStatus, isBot, isGuest, isShared} from '@utils/user';
|
||||
|
||||
import type UserModel from '@typings/database/models/servers/user';
|
||||
|
||||
type AtMentionItemProps = {
|
||||
user?: UserProfile | UserModel;
|
||||
containerStyle?: StyleProp<ViewStyle>;
|
||||
currentUserId: string;
|
||||
showFullName: boolean;
|
||||
testID?: string;
|
||||
isCustomStatusEnabled: boolean;
|
||||
}
|
||||
|
||||
const getName = (user: UserProfile | UserModel | undefined, showFullName: boolean, isCurrentUser: boolean, intl: IntlShape) => {
|
||||
let name = '';
|
||||
if (!user) {
|
||||
return intl.formatMessage({id: 'channel_loader.someone', defaultMessage: 'Someone'});
|
||||
}
|
||||
|
||||
const hasNickname = user.nickname.length > 0;
|
||||
|
||||
if (showFullName) {
|
||||
const first = 'first_name' in user ? user.first_name : user.firstName;
|
||||
const last = 'last_name' in user ? user.last_name : user.lastName;
|
||||
name += `${first} ${last} `;
|
||||
}
|
||||
|
||||
if (hasNickname && !isCurrentUser) {
|
||||
name += name.length > 0 ? `(${user.nickname})` : user.nickname;
|
||||
}
|
||||
|
||||
return name.trim();
|
||||
};
|
||||
|
||||
const getStyleFromTheme = makeStyleSheetFromTheme((theme: Theme) => {
|
||||
return {
|
||||
row: {
|
||||
height: 40,
|
||||
paddingVertical: 8,
|
||||
paddingTop: 4,
|
||||
paddingHorizontal: 16,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
},
|
||||
rowPicture: {
|
||||
marginRight: 10,
|
||||
marginLeft: 2,
|
||||
width: 24,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
rowInfo: {
|
||||
flexDirection: 'row',
|
||||
overflow: 'hidden',
|
||||
},
|
||||
rowFullname: {
|
||||
fontSize: 15,
|
||||
color: theme.centerChannelColor,
|
||||
fontFamily: 'OpenSans',
|
||||
paddingLeft: 4,
|
||||
flexShrink: 1,
|
||||
},
|
||||
rowUsername: {
|
||||
color: changeOpacity(theme.centerChannelColor, 0.56),
|
||||
fontSize: 15,
|
||||
fontFamily: 'OpenSans',
|
||||
flexShrink: 5,
|
||||
},
|
||||
icon: {
|
||||
marginLeft: 4,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const UserItem = ({
|
||||
containerStyle,
|
||||
user,
|
||||
currentUserId,
|
||||
showFullName,
|
||||
testID,
|
||||
isCustomStatusEnabled,
|
||||
}: AtMentionItemProps) => {
|
||||
const theme = useTheme();
|
||||
const style = getStyleFromTheme(theme);
|
||||
const intl = useIntl();
|
||||
|
||||
const bot = user ? isBot(user) : false;
|
||||
const guest = user ? isGuest(user.roles) : false;
|
||||
const shared = user ? isShared(user) : false;
|
||||
|
||||
const isCurrentUser = currentUserId === user?.id;
|
||||
const name = getName(user, showFullName, isCurrentUser, intl);
|
||||
const customStatus = getUserCustomStatus(user);
|
||||
|
||||
return (
|
||||
<View style={[style.row, containerStyle]}>
|
||||
<View style={style.rowPicture}>
|
||||
<ProfilePicture
|
||||
author={user}
|
||||
size={24}
|
||||
showStatus={false}
|
||||
testID={`${testID}.profile_picture`}
|
||||
/>
|
||||
</View>
|
||||
<View
|
||||
style={[style.rowInfo, {maxWidth: shared ? '75%' : '80%'}]}
|
||||
>
|
||||
{bot && <BotTag/>}
|
||||
{guest && <GuestTag/>}
|
||||
{Boolean(name.length) &&
|
||||
<Text
|
||||
style={style.rowFullname}
|
||||
numberOfLines={1}
|
||||
testID={`${testID}.name`}
|
||||
>
|
||||
{name}
|
||||
</Text>
|
||||
}
|
||||
{isCurrentUser &&
|
||||
<FormattedText
|
||||
id='suggestion.mention.you'
|
||||
defaultMessage=' (you)'
|
||||
style={style.rowUsername}
|
||||
/>
|
||||
}
|
||||
{Boolean(user) &&
|
||||
<Text
|
||||
style={style.rowUsername}
|
||||
numberOfLines={1}
|
||||
testID='at_mention_item.username'
|
||||
>
|
||||
{` @${user!.username}`}
|
||||
</Text>
|
||||
}
|
||||
</View>
|
||||
{isCustomStatusEnabled && !bot && customStatus && (
|
||||
<CustomStatusEmoji
|
||||
customStatus={customStatus}
|
||||
style={style.icon}
|
||||
/>
|
||||
)}
|
||||
{shared && (
|
||||
<ChannelIcon
|
||||
name={name}
|
||||
isActive={false}
|
||||
isArchived={false}
|
||||
isInfo={true}
|
||||
isUnread={true}
|
||||
size={18}
|
||||
shared={true}
|
||||
type={General.DM_CHANNEL}
|
||||
style={style.icon}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default UserItem;
|
||||
|
|
@ -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,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
export const ABOUT = 'About';
|
||||
export const ACCOUNT = 'Account';
|
||||
export const EMOJI_PICKER = 'AddReaction';
|
||||
export const EMOJI_PICKER = 'EmojiPicker';
|
||||
export const APP_FORM = 'AppForm';
|
||||
export const BOTTOM_SHEET = 'BottomSheet';
|
||||
export const BROWSE_CHANNELS = 'BrowseChannels';
|
||||
|
|
@ -11,6 +11,7 @@ export const CHANNEL = 'Channel';
|
|||
export const CHANNEL_ADD_PEOPLE = 'ChannelAddPeople';
|
||||
export const CHANNEL_DETAILS = 'ChannelDetails';
|
||||
export const CHANNEL_EDIT = 'ChannelEdit';
|
||||
export const CREATE_DIRECT_MESSAGE = 'CreateDirectMessage';
|
||||
export const CUSTOM_STATUS_CLEAR_AFTER = 'CustomStatusClearAfter';
|
||||
export const CUSTOM_STATUS = 'CustomStatus';
|
||||
export const EDIT_POST = 'EditPost';
|
||||
|
|
@ -24,16 +25,16 @@ export const IN_APP_NOTIFICATION = 'InAppNotification';
|
|||
export const LOGIN = 'Login';
|
||||
export const MENTIONS = 'Mentions';
|
||||
export const MFA = 'MFA';
|
||||
export const CREATE_DIRECT_MESSAGE = 'CreateDirectMessage';
|
||||
export const PERMALINK = 'Permalink';
|
||||
export const POST_OPTIONS = 'PostOptions';
|
||||
export const REACTIONS = 'Reactions';
|
||||
export const SAVED_POSTS = 'SavedPosts';
|
||||
export const SEARCH = 'Search';
|
||||
export const SERVER = 'Server';
|
||||
export const SETTINGS_SIDEBAR = 'SettingsSidebar';
|
||||
export const SSO = 'SSO';
|
||||
export const THREAD = 'Thread';
|
||||
export const USER_PROFILE = 'UserProfile';
|
||||
export const POST_OPTIONS = 'PostOptions';
|
||||
export const SAVED_POSTS = 'SavedPosts';
|
||||
|
||||
export default {
|
||||
ABOUT,
|
||||
|
|
@ -46,6 +47,7 @@ export default {
|
|||
CHANNEL_ADD_PEOPLE,
|
||||
CHANNEL_EDIT,
|
||||
CHANNEL_DETAILS,
|
||||
CREATE_DIRECT_MESSAGE,
|
||||
CUSTOM_STATUS_CLEAR_AFTER,
|
||||
CUSTOM_STATUS,
|
||||
EDIT_POST,
|
||||
|
|
@ -59,14 +61,21 @@ export default {
|
|||
LOGIN,
|
||||
MENTIONS,
|
||||
MFA,
|
||||
CREATE_DIRECT_MESSAGE,
|
||||
PERMALINK,
|
||||
POST_OPTIONS,
|
||||
REACTIONS,
|
||||
SAVED_POSTS,
|
||||
SEARCH,
|
||||
SERVER,
|
||||
SETTINGS_SIDEBAR,
|
||||
SSO,
|
||||
THREAD,
|
||||
USER_PROFILE,
|
||||
POST_OPTIONS,
|
||||
SAVED_POSTS,
|
||||
};
|
||||
|
||||
export const MODAL_SCREENS_WITHOUT_BACK = [
|
||||
CREATE_DIRECT_MESSAGE,
|
||||
EMOJI_PICKER,
|
||||
EDIT_POST,
|
||||
PERMALINK,
|
||||
];
|
||||
|
|
|
|||
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();
|
||||
|
|
@ -75,6 +75,14 @@ export const queryPostsInThread = (database: Database, rootId: string, sorted =
|
|||
return database.get<PostsInThreadModel>(POSTS_IN_THREAD).query(...clauses);
|
||||
};
|
||||
|
||||
export const queryPostRepliesCount = (database: Database, rootId: string, excludeDeleted = true) => {
|
||||
const clauses: Q.Clause[] = [Q.where('root_id', rootId)];
|
||||
if (excludeDeleted) {
|
||||
clauses.push(Q.where('delete_at', Q.eq(0)));
|
||||
}
|
||||
return database.get(POST).query(...clauses);
|
||||
};
|
||||
|
||||
export const getRecentPostsInThread = async (database: Database, rootId: string) => {
|
||||
const chunks = await queryPostsInThread(database, rootId, true, true).fetch();
|
||||
if (chunks.length) {
|
||||
|
|
|
|||
|
|
@ -145,6 +145,8 @@ const EditServerForm = ({
|
|||
);
|
||||
}
|
||||
|
||||
const saveButtonTestId = buttonDisabled ? 'edit_server_form.save.button.disabled' : 'edit_server_form.save.button';
|
||||
|
||||
return (
|
||||
<View style={styles.formContainer}>
|
||||
<View style={[styles.fullWidth, displayNameError?.length ? styles.error : undefined]}>
|
||||
|
|
@ -164,7 +166,7 @@ const EditServerForm = ({
|
|||
ref={displayNameRef}
|
||||
returnKeyType='done'
|
||||
spellCheck={false}
|
||||
testID='select_server.server_display_name.input'
|
||||
testID='edit_server_form.server_display_name.input'
|
||||
theme={theme}
|
||||
value={displayName}
|
||||
/>
|
||||
|
|
@ -174,7 +176,7 @@ const EditServerForm = ({
|
|||
defaultMessage={'Server: {url}'}
|
||||
id={'edit_server.display_help'}
|
||||
style={styles.chooseText}
|
||||
testID={'edit_server.display_help'}
|
||||
testID={'edit_server_form.display_help'}
|
||||
values={{url: removeProtocol(stripTrailingSlashes(serverUrl))}}
|
||||
/>
|
||||
}
|
||||
|
|
@ -182,7 +184,7 @@ const EditServerForm = ({
|
|||
containerStyle={[styles.connectButton, styleButtonBackground]}
|
||||
disabled={buttonDisabled}
|
||||
onPress={onUpdate}
|
||||
testID='select_server.connect.button'
|
||||
testID={saveButtonTestId}
|
||||
>
|
||||
{buttonIcon}
|
||||
<FormattedText
|
||||
|
|
|
|||
|
|
@ -40,13 +40,13 @@ const EditServerHeader = ({theme}: Props) => {
|
|||
defaultMessage='Edit server name'
|
||||
id='edit_server.title'
|
||||
style={styles.title}
|
||||
testID='edit_server.title'
|
||||
testID='edit_server_header.title'
|
||||
/>
|
||||
<FormattedText
|
||||
defaultMessage='Specify a display name for this server'
|
||||
id='edit_server.description'
|
||||
style={styles.description}
|
||||
testID='edit_server.description'
|
||||
testID='edit_server_header.description'
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -99,7 +99,7 @@ const EditServer = ({closeButtonId, componentId, server, theme}: ServerProps) =>
|
|||
<SafeAreaView
|
||||
key={'server_content'}
|
||||
style={styles.flex}
|
||||
testID='select_server.screen'
|
||||
testID='edit_server.screen'
|
||||
>
|
||||
<KeyboardAwareScrollView
|
||||
bounces={false}
|
||||
|
|
|
|||
|
|
@ -80,6 +80,9 @@ Navigation.setLazyComponentRegistrator((screenName) => {
|
|||
require('@screens/custom_status_clear_after').default,
|
||||
);
|
||||
break;
|
||||
case Screens.CREATE_DIRECT_MESSAGE:
|
||||
screen = withServerDatabase(require('@screens/create_direct_message').default);
|
||||
break;
|
||||
case Screens.EDIT_POST:
|
||||
screen = withServerDatabase(require('@screens/edit_post').default);
|
||||
break;
|
||||
|
|
@ -127,6 +130,9 @@ Navigation.setLazyComponentRegistrator((screenName) => {
|
|||
require('@screens/post_options').default,
|
||||
);
|
||||
break;
|
||||
case Screens.REACTIONS:
|
||||
screen = withServerDatabase(require('@screens/reactions').default);
|
||||
break;
|
||||
case Screens.SAVED_POSTS:
|
||||
screen = withServerDatabase((require('@screens/home/saved_posts').default));
|
||||
break;
|
||||
|
|
|
|||
44
app/screens/reactions/emoji_aliases/index.tsx
Normal file
44
app/screens/reactions/emoji_aliases/index.tsx
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {Text, View} from 'react-native';
|
||||
|
||||
import {useTheme} from '@context/theme';
|
||||
import {getEmojiByName} from '@utils/emoji/helpers';
|
||||
import {makeStyleSheetFromTheme} from '@utils/theme';
|
||||
import {typography} from '@utils/typography';
|
||||
|
||||
type Props = {
|
||||
emoji: string;
|
||||
};
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
|
||||
container: {
|
||||
marginBottom: 16,
|
||||
},
|
||||
title: {
|
||||
color: theme.centerChannelColor,
|
||||
...typography('Body', 75, 'SemiBold'),
|
||||
},
|
||||
}));
|
||||
|
||||
const EmojiAliases = ({emoji}: Props) => {
|
||||
const theme = useTheme();
|
||||
const style = getStyleSheet(theme);
|
||||
const aliases = getEmojiByName(emoji, [])?.short_names?.map((n: string) => `:${n}:`).join(' ') || `:${emoji}:`;
|
||||
|
||||
return (
|
||||
<View style={style.container}>
|
||||
<Text
|
||||
ellipsizeMode='tail'
|
||||
numberOfLines={1}
|
||||
style={style.title}
|
||||
>
|
||||
{aliases}
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default EmojiAliases;
|
||||
91
app/screens/reactions/emoji_bar/index.tsx
Normal file
91
app/screens/reactions/emoji_bar/index.tsx
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback, useEffect, useRef} from 'react';
|
||||
import {StyleSheet} from 'react-native';
|
||||
import {FlatList} from 'react-native-gesture-handler';
|
||||
|
||||
import Item from './item';
|
||||
|
||||
import type ReactionModel from '@typings/database/models/servers/reaction';
|
||||
|
||||
type Props = {
|
||||
emojiSelected: string;
|
||||
reactionsByName: Map<string, ReactionModel[]>;
|
||||
setIndex: (idx: number) => void;
|
||||
sortedReactions: string[];
|
||||
}
|
||||
|
||||
type ScrollIndexFailed = {
|
||||
index: number;
|
||||
highestMeasuredFrameIndex: number;
|
||||
averageItemLength: number;
|
||||
};
|
||||
|
||||
const style = StyleSheet.create({
|
||||
container: {
|
||||
maxHeight: 44,
|
||||
},
|
||||
});
|
||||
|
||||
const EmojiBar = ({emojiSelected, reactionsByName, setIndex, sortedReactions}: Props) => {
|
||||
const listRef = useRef<FlatList<string>>(null);
|
||||
|
||||
const scrollToIndex = (index: number, animated = false) => {
|
||||
listRef.current?.scrollToIndex({
|
||||
animated,
|
||||
index,
|
||||
viewOffset: 0,
|
||||
viewPosition: 1, // 0 is at bottom
|
||||
});
|
||||
};
|
||||
|
||||
const onPress = useCallback((emoji: string) => {
|
||||
const index = sortedReactions.indexOf(emoji);
|
||||
setIndex(index);
|
||||
}, [sortedReactions]);
|
||||
|
||||
const onScrollToIndexFailed = useCallback((info: ScrollIndexFailed) => {
|
||||
const index = Math.min(info.highestMeasuredFrameIndex, info.index);
|
||||
|
||||
scrollToIndex(index);
|
||||
}, []);
|
||||
|
||||
const renderItem = useCallback(({item}) => {
|
||||
return (
|
||||
<Item
|
||||
count={reactionsByName.get(item)?.length || 0}
|
||||
emojiName={item}
|
||||
highlight={item === emojiSelected}
|
||||
onPress={onPress}
|
||||
/>
|
||||
);
|
||||
}, [onPress, emojiSelected, reactionsByName]);
|
||||
|
||||
useEffect(() => {
|
||||
const t = setTimeout(() => {
|
||||
listRef.current?.scrollToItem({
|
||||
item: emojiSelected,
|
||||
animated: false,
|
||||
viewPosition: 1,
|
||||
});
|
||||
}, 100);
|
||||
|
||||
return () => clearTimeout(t);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<FlatList
|
||||
bounces={false}
|
||||
data={sortedReactions}
|
||||
horizontal={true}
|
||||
ref={listRef}
|
||||
renderItem={renderItem}
|
||||
style={style.container}
|
||||
onScrollToIndexFailed={onScrollToIndexFailed}
|
||||
overScrollMode='never'
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default EmojiBar;
|
||||
81
app/screens/reactions/emoji_bar/item.tsx
Normal file
81
app/screens/reactions/emoji_bar/item.tsx
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback} from 'react';
|
||||
import {TouchableOpacity, View} from 'react-native';
|
||||
import AnimatedNumbers from 'react-native-animated-numbers';
|
||||
|
||||
import Emoji from '@components/emoji';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
import {typography} from '@utils/typography';
|
||||
|
||||
type ReactionProps = {
|
||||
count: number;
|
||||
emojiName: string;
|
||||
highlight: boolean;
|
||||
onPress: (emojiName: string) => void;
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
||||
return {
|
||||
count: {
|
||||
color: changeOpacity(theme.centerChannelColor, 0.56),
|
||||
...typography('Body', 100, 'SemiBold'),
|
||||
},
|
||||
countContainer: {marginRight: 5},
|
||||
countHighlight: {
|
||||
color: theme.buttonBg,
|
||||
},
|
||||
customEmojiStyle: {color: '#000'},
|
||||
emoji: {marginHorizontal: 5},
|
||||
highlight: {
|
||||
backgroundColor: changeOpacity(theme.buttonBg, 0.08),
|
||||
},
|
||||
reaction: {
|
||||
alignItems: 'center',
|
||||
borderRadius: 4,
|
||||
backgroundColor: theme.centerChannelBg,
|
||||
flexDirection: 'row',
|
||||
height: 32,
|
||||
justifyContent: 'center',
|
||||
marginRight: 12,
|
||||
minWidth: 50,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const Reaction = ({count, emojiName, highlight, onPress}: ReactionProps) => {
|
||||
const theme = useTheme();
|
||||
const styles = getStyleSheet(theme);
|
||||
|
||||
const handlePress = useCallback(() => {
|
||||
onPress(emojiName);
|
||||
}, [onPress, emojiName]);
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
onPress={handlePress}
|
||||
style={[styles.reaction, (highlight && styles.highlight)]}
|
||||
>
|
||||
<View style={styles.emoji}>
|
||||
<Emoji
|
||||
emojiName={emojiName}
|
||||
size={20}
|
||||
textStyle={styles.customEmojiStyle}
|
||||
testID={`reaction.emoji.${emojiName}`}
|
||||
/>
|
||||
</View>
|
||||
<View style={styles.countContainer}>
|
||||
<AnimatedNumbers
|
||||
includeComma={false}
|
||||
fontStyle={[styles.count, (highlight && styles.countHighlight)]}
|
||||
animateToNumber={count}
|
||||
animationDuration={450}
|
||||
/>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
};
|
||||
|
||||
export default Reaction;
|
||||
30
app/screens/reactions/index.ts
Normal file
30
app/screens/reactions/index.ts
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
// 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 {of as of$} from 'rxjs';
|
||||
import {switchMap} from 'rxjs/operators';
|
||||
|
||||
import {observePost} from '@queries/servers/post';
|
||||
|
||||
import Reactions from './reactions';
|
||||
|
||||
import type {WithDatabaseArgs} from '@typings/database/database';
|
||||
|
||||
type EnhancedProps = WithDatabaseArgs & {
|
||||
postId: string;
|
||||
}
|
||||
|
||||
const enhanced = withObservables([], ({postId, database}: EnhancedProps) => {
|
||||
const post = observePost(database, postId);
|
||||
|
||||
return {
|
||||
reactions: post.pipe(
|
||||
switchMap((p) => (p ? p.reactions.observe() : of$(undefined))),
|
||||
),
|
||||
};
|
||||
});
|
||||
|
||||
export default withDatabase(enhanced(Reactions));
|
||||
|
||||
87
app/screens/reactions/reactions.tsx
Normal file
87
app/screens/reactions/reactions.tsx
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback, useEffect, useMemo, useState} from 'react';
|
||||
|
||||
import {Screens} from '@constants';
|
||||
import BottomSheet from '@screens/bottom_sheet';
|
||||
import {getEmojiFirstAlias} from '@utils/emoji/helpers';
|
||||
|
||||
import EmojiAliases from './emoji_aliases';
|
||||
import EmojiBar from './emoji_bar';
|
||||
import ReactorsList from './reactors_list';
|
||||
|
||||
import type ReactionModel from '@typings/database/models/servers/reaction';
|
||||
|
||||
type Props = {
|
||||
initialEmoji: string;
|
||||
reactions?: ReactionModel[];
|
||||
}
|
||||
|
||||
const Reactions = ({initialEmoji, reactions}: Props) => {
|
||||
const [sortedReactions, setSortedReactions] = useState(Array.from(new Set(reactions?.map((r) => getEmojiFirstAlias(r.emojiName)))));
|
||||
const [index, setIndex] = useState(sortedReactions.indexOf(initialEmoji));
|
||||
const reactionsByName = useMemo(() => {
|
||||
return reactions?.reduce((acc, reaction) => {
|
||||
const emojiAlias = getEmojiFirstAlias(reaction.emojiName);
|
||||
if (acc.has(emojiAlias)) {
|
||||
const rs = acc.get(emojiAlias);
|
||||
// eslint-disable-next-line max-nested-callbacks
|
||||
const present = rs!.findIndex((r) => r.userId === reaction.userId) > -1;
|
||||
if (!present) {
|
||||
rs!.push(reaction);
|
||||
}
|
||||
} else {
|
||||
acc.set(emojiAlias, [reaction]);
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, new Map<string, ReactionModel[]>());
|
||||
}, [reactions]);
|
||||
|
||||
const renderContent = useCallback(() => {
|
||||
const emojiAlias = sortedReactions[index];
|
||||
if (!reactionsByName) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<EmojiBar
|
||||
emojiSelected={emojiAlias}
|
||||
reactionsByName={reactionsByName}
|
||||
setIndex={setIndex}
|
||||
sortedReactions={sortedReactions}
|
||||
/>
|
||||
<EmojiAliases emoji={emojiAlias}/>
|
||||
<ReactorsList
|
||||
key={emojiAlias}
|
||||
reactions={reactionsByName.get(emojiAlias)!}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}, [index, reactions, sortedReactions]);
|
||||
|
||||
useEffect(() => {
|
||||
// This helps keep the reactions in the same position at all times until unmounted
|
||||
const rs = reactions?.map((r) => getEmojiFirstAlias(r.emojiName));
|
||||
const sorted = new Set([...sortedReactions]);
|
||||
const added = rs?.filter((r) => !sorted.has(r));
|
||||
added?.forEach(sorted.add, sorted);
|
||||
const removed = [...sorted].filter((s) => !rs?.includes(s));
|
||||
removed.forEach(sorted.delete, sorted);
|
||||
setSortedReactions(Array.from(sorted));
|
||||
}, [reactions]);
|
||||
|
||||
return (
|
||||
<BottomSheet
|
||||
renderContent={renderContent}
|
||||
closeButtonId='close-post-reactions'
|
||||
componentId={Screens.REACTIONS}
|
||||
initialSnapIndex={1}
|
||||
snapPoints={['90%', '50%', 10]}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default Reactions;
|
||||
70
app/screens/reactions/reactors_list/index.tsx
Normal file
70
app/screens/reactions/reactors_list/index.tsx
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback, useEffect, useRef, useState} from 'react';
|
||||
import {NativeScrollEvent, NativeSyntheticEvent, PanResponder} from 'react-native';
|
||||
import {FlatList} from 'react-native-gesture-handler';
|
||||
|
||||
import {fetchUsersByIds} from '@actions/remote/user';
|
||||
import {useServerUrl} from '@context/server';
|
||||
|
||||
import Reactor from './reactor';
|
||||
|
||||
import type ReactionModel from '@typings/database/models/servers/reaction';
|
||||
|
||||
type Props = {
|
||||
reactions: ReactionModel[];
|
||||
}
|
||||
|
||||
const ReactorsList = ({reactions}: Props) => {
|
||||
const serverUrl = useServerUrl();
|
||||
const [enabled, setEnabled] = useState(false);
|
||||
const [direction, setDirection] = useState<'down' | 'up'>('down');
|
||||
const listRef = useRef<FlatList>(null);
|
||||
const prevOffset = useRef(0);
|
||||
const panResponder = useRef(PanResponder.create({
|
||||
onMoveShouldSetPanResponderCapture: (evt, g) => {
|
||||
const dir = prevOffset.current < g.dy ? 'down' : 'up';
|
||||
prevOffset.current = g.dy;
|
||||
if (!enabled && dir === 'up') {
|
||||
setEnabled(true);
|
||||
}
|
||||
setDirection(dir);
|
||||
return false;
|
||||
},
|
||||
})).current;
|
||||
|
||||
const renderItem = useCallback(({item}) => (
|
||||
<Reactor reaction={item}/>
|
||||
), [reactions]);
|
||||
|
||||
const onScroll = useCallback((e: NativeSyntheticEvent<NativeScrollEvent>) => {
|
||||
if (e.nativeEvent.contentOffset.y <= 0 && enabled && direction === 'down') {
|
||||
setEnabled(false);
|
||||
listRef.current?.scrollToOffset({animated: true, offset: 0});
|
||||
}
|
||||
}, [enabled, direction]);
|
||||
|
||||
useEffect(() => {
|
||||
const userIds = reactions.map((r) => r.userId);
|
||||
|
||||
// Fetch any missing user
|
||||
fetchUsersByIds(serverUrl, userIds);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<FlatList
|
||||
data={reactions}
|
||||
ref={listRef}
|
||||
renderItem={renderItem}
|
||||
onScroll={onScroll}
|
||||
overScrollMode={'always'}
|
||||
scrollEnabled={enabled}
|
||||
scrollEventThrottle={60}
|
||||
{...panResponder.panHandlers}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default ReactorsList;
|
||||
|
||||
18
app/screens/reactions/reactors_list/reactor/index.ts
Normal file
18
app/screens/reactions/reactors_list/reactor/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 {observeUser} from '@app/queries/servers/user';
|
||||
import {WithDatabaseArgs} from '@typings/database/database';
|
||||
|
||||
import Reactor from './reactor';
|
||||
|
||||
import type ReactionModel from '@typings/database/models/servers/reaction';
|
||||
|
||||
const enhance = withObservables(['reaction'], ({database, reaction}: {reaction: ReactionModel} & WithDatabaseArgs) => ({
|
||||
user: observeUser(database, reaction.userId),
|
||||
}));
|
||||
|
||||
export default withDatabase(enhance(Reactor));
|
||||
30
app/screens/reactions/reactors_list/reactor/reactor.tsx
Normal file
30
app/screens/reactions/reactors_list/reactor/reactor.tsx
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {StyleSheet} from 'react-native';
|
||||
|
||||
import UserItem from '@components/user_item';
|
||||
|
||||
import type UserModel from '@typings/database/models/servers/user';
|
||||
|
||||
type Props = {
|
||||
user?: UserModel;
|
||||
}
|
||||
|
||||
const style = StyleSheet.create({
|
||||
container: {
|
||||
marginBottom: 8,
|
||||
},
|
||||
});
|
||||
|
||||
const Reactor = ({user}: Props) => {
|
||||
return (
|
||||
<UserItem
|
||||
containerStyle={style.container}
|
||||
user={user}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default Reactor;
|
||||
|
|
@ -319,7 +319,6 @@ const Server = ({
|
|||
<AnimatedSafeArea
|
||||
key={'server_content'}
|
||||
style={[styles.flex, transform]}
|
||||
testID='select_server.screen'
|
||||
>
|
||||
<KeyboardAwareScrollView
|
||||
bounces={false}
|
||||
|
|
|
|||
|
|
@ -170,3 +170,5 @@ export const makeCallErrorResponse = (errMessage: string) => {
|
|||
error: errMessage,
|
||||
};
|
||||
};
|
||||
|
||||
export const filterEmptyOptions = (option: AppSelectOption): boolean => Boolean(option.value && !option.value.match(/^[ \t]+$/));
|
||||
|
|
|
|||
|
|
@ -193,6 +193,10 @@ export function doesMatchNamedEmoji(emojiName: string) {
|
|||
return false;
|
||||
}
|
||||
|
||||
export const getEmojiFirstAlias = (emoji: string) => {
|
||||
return getEmojiByName(emoji, [])?.short_names?.[0] || emoji;
|
||||
};
|
||||
|
||||
export function getEmojiByName(emojiName: string, customEmojis: CustomEmojiModel[]) {
|
||||
if (EmojiIndicesByAlias.has(emojiName)) {
|
||||
return Emojis[EmojiIndicesByAlias.get(emojiName)!];
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -6,27 +6,12 @@ import {StatusBar, StyleSheet} from 'react-native';
|
|||
import tinyColor from 'tinycolor2';
|
||||
|
||||
import {Preferences} from '@constants';
|
||||
import {MODAL_SCREENS_WITHOUT_BACK} from '@constants/screens';
|
||||
import {appearanceControlledScreens, mergeNavigationOptions} from '@screens/navigation';
|
||||
import EphemeralStore from '@store/ephemeral_store';
|
||||
|
||||
import type {Options} from 'react-native-navigation';
|
||||
|
||||
const MODAL_SCREENS_WITHOUT_BACK = [
|
||||
'AddReaction',
|
||||
'ChannelInfo',
|
||||
'ClientUpgrade',
|
||||
'CreateChannel',
|
||||
'EditPost',
|
||||
'ErrorTeamsList',
|
||||
'MoreChannels',
|
||||
'MoreDirectMessages',
|
||||
'Permalink',
|
||||
'SelectTeam',
|
||||
'Settings',
|
||||
'TermsOfService',
|
||||
'UserProfile',
|
||||
];
|
||||
|
||||
const rgbPattern = /^rgba?\((\d+),(\d+),(\d+)(?:,([\d.]+))?\)$/;
|
||||
|
||||
export function getComponents(inColor: string): {red: number; green: number; blue: number; alpha: number} {
|
||||
|
|
|
|||
|
|
@ -123,13 +123,13 @@ export const getTimezone = (timezone: UserTimezone | null) => {
|
|||
return timezone.manualTimezone;
|
||||
};
|
||||
|
||||
export const getUserCustomStatus = (user: UserModel | UserProfile): UserCustomStatus | undefined => {
|
||||
export const getUserCustomStatus = (user?: UserModel | UserProfile): UserCustomStatus | undefined => {
|
||||
try {
|
||||
if (typeof user.props?.customStatus === 'string') {
|
||||
if (typeof user?.props?.customStatus === 'string') {
|
||||
return JSON.parse(user.props.customStatus) as UserCustomStatus;
|
||||
}
|
||||
|
||||
return user.props?.customStatus;
|
||||
return user?.props?.customStatus;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
|
|
@ -192,8 +192,12 @@ export function confirmOutOfOfficeDisabled(intl: IntlShape, status: string, upda
|
|||
);
|
||||
}
|
||||
|
||||
export function isShared(user: UserProfile): boolean {
|
||||
return Boolean(user.remote_id);
|
||||
export function isBot(user: UserProfile | UserModel): boolean {
|
||||
return 'is_bot' in user ? Boolean(user.is_bot) : Boolean(user.isBot);
|
||||
}
|
||||
|
||||
export function isShared(user: UserProfile | UserModel): boolean {
|
||||
return 'remote_id' in user ? Boolean(user.remote_id) : Boolean(user.props?.remote_id);
|
||||
}
|
||||
|
||||
export function removeUserFromList(userId: string, originalList: UserProfile[]): UserProfile[] {
|
||||
|
|
|
|||
|
|
@ -227,7 +227,7 @@ export const generateRandomUser = ({prefix = 'user', randomIdLength = 6} = {}) =
|
|||
return {
|
||||
email: `${prefix}${randomId}@sample.mattermost.com`,
|
||||
username: `${prefix}${randomId}`,
|
||||
password: 'passwd',
|
||||
password: `P${randomId}!1234`,
|
||||
first_name: `F${randomId}`,
|
||||
last_name: `L${randomId}`,
|
||||
nickname: `N${randomId}`,
|
||||
|
|
|
|||
|
|
@ -10,10 +10,16 @@ class Alert {
|
|||
|
||||
return isAndroid() ? element(by.text(title)) : element(by.label(title)).atIndex(0);
|
||||
};
|
||||
removeServerTitle = (serverDisplayName: string) => {
|
||||
const title = `Are you sure you want to remove ${serverDisplayName}?`;
|
||||
|
||||
return isAndroid() ? element(by.text(title)) : element(by.label(title)).atIndex(0);
|
||||
};
|
||||
|
||||
// alert buttons
|
||||
cancelButton = isAndroid() ? element(by.text('CANCEL')) : element(by.label('Cancel')).atIndex(1);
|
||||
logoutButton = isAndroid() ? element(by.text('LOG OUT')) : element(by.label('Log out')).atIndex(1);
|
||||
removeButton = isAndroid() ? element(by.text('REMOVE')) : element(by.label('Remove')).atIndex(1);
|
||||
}
|
||||
|
||||
const alert = new Alert();
|
||||
|
|
|
|||
38
detox/e2e/support/ui/screen/edit_server.ts
Normal file
38
detox/e2e/support/ui/screen/edit_server.ts
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {timeouts} from '@support/utils';
|
||||
|
||||
class EditServerScreen {
|
||||
testID = {
|
||||
editServerScreen: 'edit_server.screen',
|
||||
closeButton: 'close-server-edit',
|
||||
headerTitle: 'edit_server_header.title',
|
||||
headerDescription: 'edit_server_header.description',
|
||||
serverDisplayNameInput: 'edit_server_form.server_display_name.input',
|
||||
serverDisplayNameInputError: 'edit_server_form.server_display_name.input.error',
|
||||
displayHelp: 'edit_server_form.display_help',
|
||||
saveButton: 'edit_server_form.save.button',
|
||||
saveButtonDisabled: 'edit_server_form.save.button.disabled',
|
||||
};
|
||||
|
||||
editServerScreen = element(by.id(this.testID.editServerScreen));
|
||||
closeButton = element(by.id(this.testID.closeButton));
|
||||
headerTitle = element(by.id(this.testID.headerTitle));
|
||||
headerDescription = element(by.id(this.testID.headerDescription));
|
||||
serverDisplayNameInput = element(by.id(this.testID.serverDisplayNameInput));
|
||||
serverDisplayNameInputError = element(by.id(this.testID.serverDisplayNameInputError));
|
||||
displayHelp = element(by.id(this.testID.displayHelp));
|
||||
saveButton = element(by.id(this.testID.saveButton));
|
||||
saveButtonDisabled = element(by.id(this.testID.saveButtonDisabled));
|
||||
|
||||
toBeVisible = async () => {
|
||||
await waitFor(this.editServerScreen).toExist().withTimeout(timeouts.TEN_SEC);
|
||||
await waitFor(this.serverDisplayNameInput).toBeVisible().withTimeout(timeouts.TEN_SEC);
|
||||
|
||||
return this.editServerScreen;
|
||||
};
|
||||
}
|
||||
|
||||
const editServerScreen = new EditServerScreen();
|
||||
export default editServerScreen;
|
||||
|
|
@ -3,6 +3,7 @@
|
|||
|
||||
import AccountScreen from './account';
|
||||
import ChannelListScreen from './channel_list';
|
||||
import EditServerScreen from './edit_server';
|
||||
import HomeScreen from './home';
|
||||
import LoginScreen from './login';
|
||||
import ServerScreen from './server';
|
||||
|
|
@ -11,6 +12,7 @@ import ServerListScreen from './server_list';
|
|||
export {
|
||||
AccountScreen,
|
||||
ChannelListScreen,
|
||||
EditServerScreen,
|
||||
HomeScreen,
|
||||
LoginScreen,
|
||||
ServerScreen,
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import {timeouts} from '@support/utils';
|
|||
class ServerScreen {
|
||||
testID = {
|
||||
serverScreen: 'server.screen',
|
||||
closeButton: 'close-server',
|
||||
headerTitleAddServer: 'server_header.title.add_server',
|
||||
headerTitleConnectToServer: 'server_header.title.connect_to_server',
|
||||
headerWelcome: 'server_header.welcome',
|
||||
|
|
@ -20,6 +21,7 @@ class ServerScreen {
|
|||
};
|
||||
|
||||
serverScreen = element(by.id(this.testID.serverScreen));
|
||||
closeButton = element(by.id(this.testID.closeButton));
|
||||
headerTitleAddServer = element(by.id(this.testID.headerTitleAddServer));
|
||||
headerTitleConnectToServer = element(by.id(this.testID.headerTitleConnectToServer));
|
||||
headerWelcome = element(by.id(this.testID.headerWelcome));
|
||||
|
|
|
|||
255
detox/e2e/test/server_login/server_list.e2e.ts
Normal file
255
detox/e2e/test/server_login/server_list.e2e.ts
Normal file
|
|
@ -0,0 +1,255 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
// *******************************************************************
|
||||
// - [#] indicates a test step (e.g. # Go to a screen)
|
||||
// - [*] indicates an assertion (e.g. * Check the title)
|
||||
// - Use element testID when selecting an element. Create one if none.
|
||||
// *******************************************************************
|
||||
|
||||
import {
|
||||
User,
|
||||
Setup,
|
||||
} from '@support/server_api';
|
||||
import {
|
||||
serverOneUrl,
|
||||
serverTwoUrl,
|
||||
serverThreeUrl,
|
||||
siteOneUrl,
|
||||
siteTwoUrl,
|
||||
siteThreeUrl,
|
||||
} from '@support/test_config';
|
||||
import {
|
||||
Alert,
|
||||
} from '@support/ui/component';
|
||||
import {
|
||||
ChannelListScreen,
|
||||
EditServerScreen,
|
||||
HomeScreen,
|
||||
LoginScreen,
|
||||
ServerScreen,
|
||||
ServerListScreen,
|
||||
} from '@support/ui/screen';
|
||||
import {expect} from 'detox';
|
||||
|
||||
describe('Server Login - Server List', () => {
|
||||
const serverOneDisplayName = 'Server 1';
|
||||
const serverTwoDisplayName = 'Server 2';
|
||||
const serverThreeDisplayName = 'Server 3';
|
||||
let serverOneUser: any;
|
||||
let serverTwoUser: any;
|
||||
let serverThreeUser: any;
|
||||
|
||||
beforeAll(async () => {
|
||||
// # Log in to the first server
|
||||
({user: serverOneUser} = await Setup.apiInit(siteOneUrl));
|
||||
await expect(ServerScreen.headerTitleConnectToServer).toBeVisible();
|
||||
await ServerScreen.connectToServer(serverOneUrl, serverOneDisplayName);
|
||||
await LoginScreen.login(serverOneUser);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
// # Log out
|
||||
await HomeScreen.logout();
|
||||
});
|
||||
|
||||
it('MM-T4691_1 - should be able to add and log in to new servers', async () => {
|
||||
// * Verify on channel list screen of the first server
|
||||
await ChannelListScreen.toBeVisible();
|
||||
await expect(ChannelListScreen.headerServerDisplayName).toHaveText(serverOneDisplayName);
|
||||
|
||||
// # Open server list screen
|
||||
await ServerListScreen.open();
|
||||
|
||||
// * Verify first server is active
|
||||
await ServerListScreen.toBeVisible();
|
||||
await expect(ServerListScreen.getServerItemActive(serverOneDisplayName)).toBeVisible();
|
||||
|
||||
// # Add a second server and log in to the second server
|
||||
await User.apiAdminLogin(siteTwoUrl);
|
||||
({user: serverTwoUser} = await Setup.apiInit(siteTwoUrl));
|
||||
await ServerListScreen.addServerButton.tap();
|
||||
await expect(ServerScreen.headerTitleAddServer).toBeVisible();
|
||||
await ServerScreen.connectToServer(serverTwoUrl, serverTwoDisplayName);
|
||||
await LoginScreen.login(serverTwoUser);
|
||||
|
||||
// * Verify on channel list screen of the second server
|
||||
await ChannelListScreen.toBeVisible();
|
||||
await expect(ChannelListScreen.headerServerDisplayName).toHaveText(serverTwoDisplayName);
|
||||
|
||||
// # Open server list screen
|
||||
await ServerListScreen.open();
|
||||
|
||||
// * Verify second server is active and first server is inactive
|
||||
await ServerListScreen.toBeVisible();
|
||||
await expect(ServerListScreen.getServerItemActive(serverTwoDisplayName)).toBeVisible();
|
||||
await expect(ServerListScreen.getServerItemInactive(serverOneDisplayName)).toBeVisible();
|
||||
|
||||
// # Add a third server and log in to the third server
|
||||
await User.apiAdminLogin(siteThreeUrl);
|
||||
({user: serverThreeUser} = await Setup.apiInit(siteThreeUrl));
|
||||
await ServerListScreen.addServerButton.tap();
|
||||
await expect(ServerScreen.headerTitleAddServer).toBeVisible();
|
||||
await ServerScreen.connectToServer(serverThreeUrl, serverThreeDisplayName);
|
||||
await LoginScreen.login(serverThreeUser);
|
||||
|
||||
// * Verify on channel list screen of the third server
|
||||
await ChannelListScreen.toBeVisible();
|
||||
await expect(ChannelListScreen.headerServerDisplayName).toHaveText(serverThreeDisplayName);
|
||||
|
||||
// # Open server list screen
|
||||
await ServerListScreen.open();
|
||||
|
||||
// * Verify third server is active, and first and second servers are inactive
|
||||
await ServerListScreen.toBeVisible();
|
||||
await expect(ServerListScreen.getServerItemActive(serverThreeDisplayName)).toBeVisible();
|
||||
await expect(ServerListScreen.getServerItemInactive(serverOneDisplayName)).toBeVisible();
|
||||
await expect(ServerListScreen.getServerItemInactive(serverTwoDisplayName)).toBeVisible();
|
||||
|
||||
// # Go back to first server
|
||||
await ServerListScreen.getServerItemInactive(serverOneDisplayName).tap();
|
||||
});
|
||||
|
||||
it('MM-T4691_2 - should be able to switch to another existing server', async () => {
|
||||
// * Verify on channel list screen of the first server
|
||||
await ChannelListScreen.toBeVisible();
|
||||
await expect(ChannelListScreen.headerServerDisplayName).toHaveText(serverOneDisplayName);
|
||||
|
||||
// # Open server list screen and tap on third server
|
||||
await ServerListScreen.open();
|
||||
await ServerListScreen.getServerItemInactive(serverThreeDisplayName).tap();
|
||||
|
||||
// * Verify on channel list screen of the third server
|
||||
await ChannelListScreen.toBeVisible();
|
||||
await expect(ChannelListScreen.headerServerDisplayName).toHaveText(serverThreeDisplayName);
|
||||
|
||||
// # Open server list screen and go back to first server
|
||||
await ServerListScreen.open();
|
||||
await ServerListScreen.getServerItemInactive(serverOneDisplayName).tap();
|
||||
});
|
||||
|
||||
it('MM-T4691_3 - should be able to edit server display name of active and inactive servers', async () => {
|
||||
// * Verify on channel list screen of the first server
|
||||
await ChannelListScreen.toBeVisible();
|
||||
await expect(ChannelListScreen.headerServerDisplayName).toHaveText(serverOneDisplayName);
|
||||
|
||||
// # Open server list screen, swipe left on first server and tap on edit option
|
||||
await ServerListScreen.open();
|
||||
await ServerListScreen.getServerItemActive(serverOneDisplayName).swipe('left');
|
||||
await ServerListScreen.getServerItemEditOption(serverOneDisplayName).tap();
|
||||
|
||||
// * Verify on edit server screen
|
||||
await EditServerScreen.toBeVisible();
|
||||
|
||||
// # Enter the same first server display name
|
||||
await EditServerScreen.serverDisplayNameInput.replaceText(serverOneDisplayName);
|
||||
|
||||
// * Verify save button is disabled
|
||||
await expect(EditServerScreen.saveButtonDisabled).toBeVisible();
|
||||
|
||||
// # Enter a new first server display name
|
||||
const newServerOneDisplayName = `${serverOneDisplayName} new`;
|
||||
await EditServerScreen.serverDisplayNameInput.replaceText(newServerOneDisplayName);
|
||||
|
||||
// * Verify save button is enabled
|
||||
await expect(EditServerScreen.saveButton).toBeVisible();
|
||||
|
||||
// # Tap on save button
|
||||
await EditServerScreen.saveButton.tap();
|
||||
|
||||
// * Verify the new first server display name
|
||||
await expect(ServerListScreen.getServerItemActive(newServerOneDisplayName)).toBeVisible();
|
||||
|
||||
// # Revert back to original first server display name and go back to first server
|
||||
await ServerListScreen.getServerItemActive(newServerOneDisplayName).swipe('left');
|
||||
await ServerListScreen.getServerItemEditOption(newServerOneDisplayName).tap();
|
||||
await EditServerScreen.serverDisplayNameInput.replaceText(serverOneDisplayName);
|
||||
await EditServerScreen.saveButton.tap();
|
||||
await ServerListScreen.getServerItemActive(serverOneDisplayName).tap();
|
||||
});
|
||||
|
||||
it('MM-T4691_4 - should be able to remove a server from the list', async () => {
|
||||
// * Verify on channel list screen of the first server
|
||||
await ChannelListScreen.toBeVisible();
|
||||
await expect(ChannelListScreen.headerServerDisplayName).toHaveText(serverOneDisplayName);
|
||||
|
||||
// # Open server list screen, swipe left on first server and tap on remove option
|
||||
await ServerListScreen.open();
|
||||
await ServerListScreen.getServerItemActive(serverOneDisplayName).swipe('left');
|
||||
await ServerListScreen.getServerItemRemoveOption(serverOneDisplayName).tap();
|
||||
|
||||
// * Verify remove server alert is displayed
|
||||
await expect(Alert.removeServerTitle(serverOneDisplayName)).toBeVisible();
|
||||
|
||||
// # Tap on remove button and go back to server list screen
|
||||
await Alert.removeButton.tap();
|
||||
await ServerListScreen.open();
|
||||
|
||||
// * Verify first server is removed
|
||||
await expect(ServerListScreen.getServerItemActive(serverOneDisplayName)).not.toExist();
|
||||
await expect(ServerListScreen.getServerItemInactive(serverOneDisplayName)).not.toExist();
|
||||
|
||||
// # Add first server back to the list and log in to the first server
|
||||
await ServerListScreen.addServerButton.tap();
|
||||
await expect(ServerScreen.headerTitleAddServer).toBeVisible();
|
||||
await ServerScreen.connectToServer(serverOneUrl, serverOneDisplayName);
|
||||
await LoginScreen.login(serverOneUser);
|
||||
});
|
||||
|
||||
it('MM-T4691_5 - should be able to log out a server from the list', async () => {
|
||||
// * Verify on channel list screen of the first server
|
||||
await ChannelListScreen.toBeVisible();
|
||||
await expect(ChannelListScreen.headerServerDisplayName).toHaveText(serverOneDisplayName);
|
||||
|
||||
// # Open server list screen, swipe left on first server and tap on logout option
|
||||
await ServerListScreen.open();
|
||||
await ServerListScreen.getServerItemActive(serverOneDisplayName).swipe('left');
|
||||
await ServerListScreen.getServerItemLogoutOption(serverOneDisplayName).tap();
|
||||
|
||||
// * Verify logout server alert is displayed
|
||||
await expect(Alert.logoutTitle(serverOneDisplayName)).toBeVisible();
|
||||
|
||||
// # Tap on logout button and go back to server list screen
|
||||
await Alert.logoutButton.tap();
|
||||
await ServerListScreen.open();
|
||||
|
||||
// * Verify first server is logged out
|
||||
await ServerListScreen.getServerItemInactive(serverOneDisplayName).swipe('left');
|
||||
await expect(ServerListScreen.getServerItemLoginOption(serverOneDisplayName)).toBeVisible();
|
||||
|
||||
// # Log back in to first server
|
||||
await ServerListScreen.getServerItemLoginOption(serverOneDisplayName).tap();
|
||||
await expect(ServerScreen.headerTitleAddServer).toBeVisible();
|
||||
await ServerScreen.connectToServer(serverOneUrl, serverOneDisplayName);
|
||||
await LoginScreen.login(serverOneUser);
|
||||
});
|
||||
|
||||
it('MM-T4691_6 - should not be able to add server for an already existing server', async () => {
|
||||
// * Verify on channel list screen of the first server
|
||||
await ChannelListScreen.toBeVisible();
|
||||
await expect(ChannelListScreen.headerServerDisplayName).toHaveText(serverOneDisplayName);
|
||||
|
||||
// # Open server list screen, attempt to add a server already logged in and with inactive session
|
||||
await ServerListScreen.open();
|
||||
await ServerListScreen.addServerButton.tap();
|
||||
await expect(ServerScreen.headerTitleAddServer).toBeVisible();
|
||||
await ServerScreen.serverUrlInput.replaceText(serverTwoUrl);
|
||||
await ServerScreen.serverDisplayNameInput.replaceText(serverTwoDisplayName);
|
||||
await ServerScreen.connectButton.tap();
|
||||
|
||||
// * Verify same name server error
|
||||
const sameNameServerError = 'You are using this name for another server.';
|
||||
await expect(ServerScreen.serverDisplayNameInputError).toHaveText(sameNameServerError);
|
||||
|
||||
// # Attempt to add a server already logged in and with active session, with the same server display name
|
||||
await ServerScreen.serverUrlInput.replaceText(serverOneUrl);
|
||||
await ServerScreen.serverDisplayNameInput.replaceText(serverOneDisplayName);
|
||||
await ServerScreen.connectButton.tap();
|
||||
|
||||
// * Verify same name server error
|
||||
await expect(ServerScreen.serverDisplayNameInputError).toHaveText(sameNameServerError);
|
||||
|
||||
// # Close server screen to go back to first server
|
||||
await ServerScreen.closeButton.tap();
|
||||
});
|
||||
});
|
||||
|
|
@ -7,28 +7,35 @@
|
|||
// - Use element testID when selecting an element. Create one if none.
|
||||
// *******************************************************************
|
||||
|
||||
import {Setup} from '@support/server_api';
|
||||
import {
|
||||
Setup,
|
||||
User,
|
||||
} from '@support/server_api';
|
||||
import {
|
||||
serverOneUrl,
|
||||
siteOneUrl,
|
||||
serverTwoUrl,
|
||||
siteTwoUrl,
|
||||
} from '@support/test_config';
|
||||
import {
|
||||
ChannelListScreen,
|
||||
HomeScreen,
|
||||
LoginScreen,
|
||||
ServerListScreen,
|
||||
ServerScreen,
|
||||
} from '@support/ui/screen';
|
||||
import {expect} from 'detox';
|
||||
|
||||
describe('Server Login', () => {
|
||||
const serverOneDisplayName = 'Server 1';
|
||||
const serverTwoDisplayName = 'Server 2';
|
||||
|
||||
afterAll(async () => {
|
||||
// # Log out
|
||||
await HomeScreen.logout(serverOneDisplayName);
|
||||
});
|
||||
|
||||
it('MM-T4675 - should be able to connect to a server, log in, and show channel list screen', async () => {
|
||||
it('MM-T4675_1 - should be able to connect to a server, log in, and show channel list screen', async () => {
|
||||
// * Verify on server screen
|
||||
await ServerScreen.toBeVisible();
|
||||
|
||||
|
|
@ -47,4 +54,28 @@ describe('Server Login', () => {
|
|||
await expect(ChannelListScreen.headerTeamDisplayName).toHaveText(team.display_name);
|
||||
await expect(ChannelListScreen.headerServerDisplayName).toHaveText(serverOneDisplayName);
|
||||
});
|
||||
|
||||
it('MM-T4675_2 - should be able to add a new server and log in to the new server', async () => {
|
||||
// # Open server list screen
|
||||
await ServerListScreen.open();
|
||||
|
||||
// * Verify on server list screen
|
||||
await ServerListScreen.toBeVisible();
|
||||
|
||||
// # Add a second server and log in to the second server
|
||||
await User.apiAdminLogin(siteTwoUrl);
|
||||
const {user} = await Setup.apiInit(siteTwoUrl);
|
||||
await ServerListScreen.addServerButton.tap();
|
||||
await expect(ServerScreen.headerTitleAddServer).toBeVisible();
|
||||
await ServerScreen.connectToServer(serverTwoUrl, serverTwoDisplayName);
|
||||
await LoginScreen.login(user);
|
||||
|
||||
// * Verify on channel list screen of the second server
|
||||
await ChannelListScreen.toBeVisible();
|
||||
await expect(ChannelListScreen.headerServerDisplayName).toHaveText(serverTwoDisplayName);
|
||||
|
||||
// # Go back to first server
|
||||
await ServerListScreen.open();
|
||||
await ServerListScreen.getServerItemInactive(serverOneDisplayName).tap();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
109
package-lock.json
generated
109
package-lock.json
generated
|
|
@ -5,7 +5,6 @@
|
|||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "mattermost-mobile",
|
||||
"version": "2.0.0",
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache 2.0",
|
||||
|
|
@ -35,6 +34,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 +109,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",
|
||||
|
|
@ -4260,7 +4261,7 @@
|
|||
"integrity": "sha512-7AeGuCYNGmycyQbCqd/3PWH4eOoX/OiCa0uphp57NVTeAGdJGaAliecxwBDHYQCIvrW7aDBZCYeNTP/WX69mkg==",
|
||||
"dependencies": {
|
||||
"@jest/types": "^26.6.2",
|
||||
"ansi-regex": "^5.0.1",
|
||||
"ansi-regex": "^5.0.0",
|
||||
"ansi-styles": "^4.0.0",
|
||||
"react-is": "^17.0.1"
|
||||
},
|
||||
|
|
@ -4302,7 +4303,7 @@
|
|||
"chalk": "^4.1.2",
|
||||
"lodash": "^4.17.15",
|
||||
"mime": "^2.4.1",
|
||||
"node-fetch": "^2.6.7",
|
||||
"node-fetch": "^2.6.0",
|
||||
"open": "^6.2.0",
|
||||
"semver": "^6.3.0",
|
||||
"shell-quote": "1.6.1"
|
||||
|
|
@ -4604,7 +4605,7 @@
|
|||
"integrity": "sha512-7AeGuCYNGmycyQbCqd/3PWH4eOoX/OiCa0uphp57NVTeAGdJGaAliecxwBDHYQCIvrW7aDBZCYeNTP/WX69mkg==",
|
||||
"dependencies": {
|
||||
"@jest/types": "^26.6.2",
|
||||
"ansi-regex": "^5.0.1",
|
||||
"ansi-regex": "^5.0.0",
|
||||
"ansi-styles": "^4.0.0",
|
||||
"react-is": "^17.0.1"
|
||||
},
|
||||
|
|
@ -4881,6 +4882,7 @@
|
|||
"version": "0.1.11",
|
||||
"resolved": "https://registry.npmjs.org/@react-native-community/masked-view/-/masked-view-0.1.11.tgz",
|
||||
"integrity": "sha512-rQfMIGSR/1r/SyN87+VD8xHHzDYeHaJq6elOSCAD+0iLagXkSI2pfA0LmSXP21uw5i3em7GkkRjfJ8wpqWXZNw==",
|
||||
"deprecated": "Repository was moved to @react-native-masked-view/masked-view",
|
||||
"peerDependencies": {
|
||||
"react": ">=16.0",
|
||||
"react-native": ">=0.57"
|
||||
|
|
@ -5035,7 +5037,7 @@
|
|||
"dependencies": {
|
||||
"https-proxy-agent": "^5.0.0",
|
||||
"mkdirp": "^0.5.5",
|
||||
"node-fetch": "^2.6.7",
|
||||
"node-fetch": "^2.6.0",
|
||||
"npmlog": "^4.1.2",
|
||||
"progress": "^2.0.3",
|
||||
"proxy-from-env": "^1.1.0"
|
||||
|
|
@ -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",
|
||||
|
|
@ -8631,7 +8644,7 @@
|
|||
"node_modules/commonmark-react-renderer": {
|
||||
"version": "4.3.5",
|
||||
"resolved": "git+ssh://git@github.com/mattermost/commonmark-react-renderer.git#4e52e1725c0ef5b1e2ecfe9883220ec36c2eb67d",
|
||||
"license": "MIT",
|
||||
"integrity": "sha512-UwUgplz8kFSMCe9+Dg/BcV75lc7R/V6mvMYJq2p29i5aaIBd0252k9HeSGa2VtEPHfg2/trS9qC7iAxnO7r6ng==",
|
||||
"dependencies": {
|
||||
"lodash.assign": "^4.2.0",
|
||||
"lodash.isplainobject": "^4.0.6",
|
||||
|
|
@ -8820,7 +8833,7 @@
|
|||
"version": "1.2.7",
|
||||
"resolved": "https://registry.npmjs.org/core-js/-/core-js-1.2.7.tgz",
|
||||
"integrity": "sha1-ZSKUwUZR2yj6k70tX/KYOk8IxjY=",
|
||||
"deprecated": "core-js@<3.3 is no longer maintained and not recommended for usage due to the number of issues. Because of the V8 engine whims, feature detection in old core-js versions could cause a slowdown up to 100x even if nothing is polyfilled. Please, upgrade your dependencies to the actual version of core-js."
|
||||
"deprecated": "core-js@<3.4 is no longer maintained and not recommended for usage due to the number of issues. Because of the V8 engine whims, feature detection in old core-js versions could cause a slowdown up to 100x even if nothing is polyfilled. Please, upgrade your dependencies to the actual version of core-js."
|
||||
},
|
||||
"node_modules/core-js-compat": {
|
||||
"version": "3.20.3",
|
||||
|
|
@ -13431,7 +13444,7 @@
|
|||
"integrity": "sha512-qvUtwJ3j6qwsF3jLxkZ72qCgjMysPzDfeV240JHiGZsANBYd+EEuu35v7dfrJ9Up0Ak07D7GGSkGhCHTqg/5wA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"node-fetch": "^2.6.7",
|
||||
"node-fetch": "^2.6.1",
|
||||
"whatwg-fetch": "^3.4.1"
|
||||
}
|
||||
},
|
||||
|
|
@ -16547,7 +16560,7 @@
|
|||
"integrity": "sha512-7AeGuCYNGmycyQbCqd/3PWH4eOoX/OiCa0uphp57NVTeAGdJGaAliecxwBDHYQCIvrW7aDBZCYeNTP/WX69mkg==",
|
||||
"dependencies": {
|
||||
"@jest/types": "^26.6.2",
|
||||
"ansi-regex": "^5.0.1",
|
||||
"ansi-regex": "^5.0.0",
|
||||
"ansi-styles": "^4.0.0",
|
||||
"react-is": "^17.0.1"
|
||||
},
|
||||
|
|
@ -17586,9 +17599,8 @@
|
|||
}
|
||||
},
|
||||
"node_modules/minimist": {
|
||||
"version": "1.2.5",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
|
||||
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw=="
|
||||
"version": "1.2.6",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz"
|
||||
},
|
||||
"node_modules/mississippi": {
|
||||
"version": "3.0.0",
|
||||
|
|
@ -17654,11 +17666,13 @@
|
|||
"node_modules/mmjstool/node_modules/emoji-regex": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/mmjstool/node_modules/is-fullwidth-code-point": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
|
||||
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
|
|
@ -17667,6 +17681,7 @@
|
|||
"node_modules/mmjstool/node_modules/string-width": {
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
||||
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"emoji-regex": "^8.0.0",
|
||||
|
|
@ -17680,6 +17695,7 @@
|
|||
"node_modules/mmjstool/node_modules/y18n": {
|
||||
"version": "5.0.8",
|
||||
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
|
||||
"integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
|
|
@ -17688,6 +17704,7 @@
|
|||
"node_modules/mmjstool/node_modules/yargs": {
|
||||
"version": "17.3.1",
|
||||
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.3.1.tgz",
|
||||
"integrity": "sha512-WUANQeVgjLbNsEmGk20f+nlHgOqzRFpiGWVaBrYGYIGANIIu3lWjoyi0fNlFmJkvfhCZ6BXINe7/W2O2bV4iaA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"cliui": "^7.0.2",
|
||||
|
|
@ -17705,6 +17722,7 @@
|
|||
"node_modules/mmjstool/node_modules/yargs-parser": {
|
||||
"version": "21.0.0",
|
||||
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.0.0.tgz",
|
||||
"integrity": "sha512-z9kApYUOCwoeZ78rfRYYWdiU/iNL6mwwYlkkZfJoyMR1xps+NEBX5X7XmRpxkZHhXJ6+Ey00IwKxBBSW9FIjyA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
|
|
@ -18191,11 +18209,20 @@
|
|||
"node_modules/node-fetch": {
|
||||
"version": "2.6.7",
|
||||
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz",
|
||||
"integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==",
|
||||
"dependencies": {
|
||||
"whatwg-url": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "4.x || >=6.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"encoding": "^0.1.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"encoding": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/node-fetch/node_modules/tr46": {
|
||||
|
|
@ -19116,9 +19143,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/plist": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/plist/-/plist-3.0.4.tgz",
|
||||
"integrity": "sha512-ksrr8y9+nXOxQB2osVNqrgvX/XQPOXaU4BQMKjYq8PvaY1U18mo+fKgBSwzK+luSyinOuPae956lSVcBwxlAMg==",
|
||||
"version": "3.0.5",
|
||||
"resolved": "https://registry.npmjs.org/plist/-/plist-3.0.5.tgz",
|
||||
"integrity": "sha512-83vX4eYdQp3vP9SxuYgEM/G/pJQqLUz/V/xzPrzruLs7fz7jxGQ1msZ/mg1nwZxUSuOp4sb+/bEIbRrbzZRxDA==",
|
||||
"dependencies": {
|
||||
"base64-js": "^1.5.1",
|
||||
"xmlbuilder": "^9.0.7"
|
||||
|
|
@ -20220,7 +20247,7 @@
|
|||
"integrity": "sha512-7AeGuCYNGmycyQbCqd/3PWH4eOoX/OiCa0uphp57NVTeAGdJGaAliecxwBDHYQCIvrW7aDBZCYeNTP/WX69mkg==",
|
||||
"dependencies": {
|
||||
"@jest/types": "^26.6.2",
|
||||
"ansi-regex": "^5.0.1",
|
||||
"ansi-regex": "^5.0.0",
|
||||
"ansi-styles": "^4.0.0",
|
||||
"react-is": "^17.0.1"
|
||||
},
|
||||
|
|
@ -21653,6 +21680,7 @@
|
|||
"version": "0.5.3",
|
||||
"resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.3.tgz",
|
||||
"integrity": "sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw==",
|
||||
"deprecated": "See https://github.com/lydell/source-map-resolve#deprecated",
|
||||
"dependencies": {
|
||||
"atob": "^2.1.2",
|
||||
"decode-uri-component": "^0.2.0",
|
||||
|
|
@ -21681,7 +21709,8 @@
|
|||
"node_modules/source-map-url": {
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.1.tgz",
|
||||
"integrity": "sha512-cPiFOTLUKvJFIg4SKVScy4ilPPW6rFgMgfuZJPNoDuMs3nC1HbMUycBoJw77xFIp6z1UJQJOfx6C9GMH80DiTw=="
|
||||
"integrity": "sha512-cPiFOTLUKvJFIg4SKVScy4ilPPW6rFgMgfuZJPNoDuMs3nC1HbMUycBoJw77xFIp6z1UJQJOfx6C9GMH80DiTw==",
|
||||
"deprecated": "See https://github.com/lydell/source-map-url#deprecated"
|
||||
},
|
||||
"node_modules/split-on-first": {
|
||||
"version": "1.1.0",
|
||||
|
|
@ -23490,7 +23519,7 @@
|
|||
"version": "2.1.8",
|
||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.8.tgz",
|
||||
"integrity": "sha512-ZmZUazfOzf0Nve7duiCKD23PFSCs4JPoYyccjUFF3aQkQadqBhfzhjkwBH2mNOG9cTBwhamM37EIsIkZw3nRgg==",
|
||||
"deprecated": "Chokidar 2 will break on node v14+. Upgrade to chokidar 3 with 15x less dependencies.",
|
||||
"deprecated": "Chokidar 2 does not receive security updates since 2019. Upgrade to chokidar 3 with 15x fewer dependencies",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
|
|
@ -27210,7 +27239,7 @@
|
|||
"integrity": "sha512-7AeGuCYNGmycyQbCqd/3PWH4eOoX/OiCa0uphp57NVTeAGdJGaAliecxwBDHYQCIvrW7aDBZCYeNTP/WX69mkg==",
|
||||
"requires": {
|
||||
"@jest/types": "^26.6.2",
|
||||
"ansi-regex": "^5.0.1",
|
||||
"ansi-regex": "^5.0.0",
|
||||
"ansi-styles": "^4.0.0",
|
||||
"react-is": "^17.0.1"
|
||||
}
|
||||
|
|
@ -27645,7 +27674,7 @@
|
|||
"integrity": "sha512-7AeGuCYNGmycyQbCqd/3PWH4eOoX/OiCa0uphp57NVTeAGdJGaAliecxwBDHYQCIvrW7aDBZCYeNTP/WX69mkg==",
|
||||
"requires": {
|
||||
"@jest/types": "^26.6.2",
|
||||
"ansi-regex": "^5.0.1",
|
||||
"ansi-regex": "^5.0.0",
|
||||
"ansi-styles": "^4.0.0",
|
||||
"react-is": "^17.0.1"
|
||||
}
|
||||
|
|
@ -27683,7 +27712,7 @@
|
|||
"chalk": "^4.1.2",
|
||||
"lodash": "^4.17.15",
|
||||
"mime": "^2.4.1",
|
||||
"node-fetch": "^2.6.7",
|
||||
"node-fetch": "^2.6.0",
|
||||
"open": "^6.2.0",
|
||||
"semver": "^6.3.0",
|
||||
"shell-quote": "1.6.1"
|
||||
|
|
@ -28042,7 +28071,7 @@
|
|||
"requires": {
|
||||
"https-proxy-agent": "^5.0.0",
|
||||
"mkdirp": "^0.5.5",
|
||||
"node-fetch": "^2.6.7",
|
||||
"node-fetch": "^2.6.0",
|
||||
"npmlog": "^4.1.2",
|
||||
"progress": "^2.0.3",
|
||||
"proxy-from-env": "^1.1.0"
|
||||
|
|
@ -28525,6 +28554,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 +30086,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",
|
||||
|
|
@ -30878,6 +30918,7 @@
|
|||
},
|
||||
"commonmark-react-renderer": {
|
||||
"version": "git+ssh://git@github.com/mattermost/commonmark-react-renderer.git#4e52e1725c0ef5b1e2ecfe9883220ec36c2eb67d",
|
||||
"integrity": "sha512-UwUgplz8kFSMCe9+Dg/BcV75lc7R/V6mvMYJq2p29i5aaIBd0252k9HeSGa2VtEPHfg2/trS9qC7iAxnO7r6ng==",
|
||||
"from": "commonmark-react-renderer@github:mattermost/commonmark-react-renderer#4e52e1725c0ef5b1e2ecfe9883220ec36c2eb67d",
|
||||
"requires": {
|
||||
"lodash.assign": "^4.2.0",
|
||||
|
|
@ -34560,7 +34601,7 @@
|
|||
"integrity": "sha512-qvUtwJ3j6qwsF3jLxkZ72qCgjMysPzDfeV240JHiGZsANBYd+EEuu35v7dfrJ9Up0Ak07D7GGSkGhCHTqg/5wA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"node-fetch": "^2.6.7",
|
||||
"node-fetch": "^2.6.1",
|
||||
"whatwg-fetch": "^3.4.1"
|
||||
}
|
||||
},
|
||||
|
|
@ -37302,7 +37343,7 @@
|
|||
"integrity": "sha512-7AeGuCYNGmycyQbCqd/3PWH4eOoX/OiCa0uphp57NVTeAGdJGaAliecxwBDHYQCIvrW7aDBZCYeNTP/WX69mkg==",
|
||||
"requires": {
|
||||
"@jest/types": "^26.6.2",
|
||||
"ansi-regex": "^5.0.1",
|
||||
"ansi-regex": "^5.0.0",
|
||||
"ansi-styles": "^4.0.0",
|
||||
"react-is": "^17.0.1"
|
||||
}
|
||||
|
|
@ -37863,9 +37904,8 @@
|
|||
}
|
||||
},
|
||||
"minimist": {
|
||||
"version": "1.2.5",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
|
||||
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw=="
|
||||
"version": "1.2.6",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz"
|
||||
},
|
||||
"mississippi": {
|
||||
"version": "3.0.0",
|
||||
|
|
@ -37919,16 +37959,19 @@
|
|||
"emoji-regex": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
|
||||
"dev": true
|
||||
},
|
||||
"is-fullwidth-code-point": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
|
||||
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
|
||||
"dev": true
|
||||
},
|
||||
"string-width": {
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
||||
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"emoji-regex": "^8.0.0",
|
||||
|
|
@ -37939,11 +37982,13 @@
|
|||
"y18n": {
|
||||
"version": "5.0.8",
|
||||
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
|
||||
"integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
|
||||
"dev": true
|
||||
},
|
||||
"yargs": {
|
||||
"version": "17.3.1",
|
||||
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.3.1.tgz",
|
||||
"integrity": "sha512-WUANQeVgjLbNsEmGk20f+nlHgOqzRFpiGWVaBrYGYIGANIIu3lWjoyi0fNlFmJkvfhCZ6BXINe7/W2O2bV4iaA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"cliui": "^7.0.2",
|
||||
|
|
@ -37958,6 +38003,7 @@
|
|||
"yargs-parser": {
|
||||
"version": "21.0.0",
|
||||
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.0.0.tgz",
|
||||
"integrity": "sha512-z9kApYUOCwoeZ78rfRYYWdiU/iNL6mwwYlkkZfJoyMR1xps+NEBX5X7XmRpxkZHhXJ6+Ey00IwKxBBSW9FIjyA==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
|
|
@ -38337,6 +38383,7 @@
|
|||
"node-fetch": {
|
||||
"version": "2.6.7",
|
||||
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz",
|
||||
"integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==",
|
||||
"requires": {
|
||||
"whatwg-url": "^5.0.0"
|
||||
},
|
||||
|
|
@ -39052,9 +39099,9 @@
|
|||
}
|
||||
},
|
||||
"plist": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/plist/-/plist-3.0.4.tgz",
|
||||
"integrity": "sha512-ksrr8y9+nXOxQB2osVNqrgvX/XQPOXaU4BQMKjYq8PvaY1U18mo+fKgBSwzK+luSyinOuPae956lSVcBwxlAMg==",
|
||||
"version": "3.0.5",
|
||||
"resolved": "https://registry.npmjs.org/plist/-/plist-3.0.5.tgz",
|
||||
"integrity": "sha512-83vX4eYdQp3vP9SxuYgEM/G/pJQqLUz/V/xzPrzruLs7fz7jxGQ1msZ/mg1nwZxUSuOp4sb+/bEIbRrbzZRxDA==",
|
||||
"requires": {
|
||||
"base64-js": "^1.5.1",
|
||||
"xmlbuilder": "^9.0.7"
|
||||
|
|
@ -39527,7 +39574,7 @@
|
|||
"integrity": "sha512-7AeGuCYNGmycyQbCqd/3PWH4eOoX/OiCa0uphp57NVTeAGdJGaAliecxwBDHYQCIvrW7aDBZCYeNTP/WX69mkg==",
|
||||
"requires": {
|
||||
"@jest/types": "^26.6.2",
|
||||
"ansi-regex": "^5.0.1",
|
||||
"ansi-regex": "^5.0.0",
|
||||
"ansi-styles": "^4.0.0",
|
||||
"react-is": "^17.0.1"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
1
types/api/users.d.ts
vendored
1
types/api/users.d.ts
vendored
|
|
@ -43,7 +43,6 @@ type UserProfile = {
|
|||
last_picture_update: number;
|
||||
remote_id?: string;
|
||||
status?: string;
|
||||
remote_id?: string;
|
||||
};
|
||||
|
||||
type UsersState = {
|
||||
|
|
|
|||
Loading…
Reference in a new issue