Merge pull request #6085 from mattermost/gekidou-fixes

[Gekidou] UI fixes
This commit is contained in:
Elias Nahum 2022-03-24 16:48:17 -03:00 committed by GitHub
commit 941aa9d0d0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
31 changed files with 108 additions and 1706 deletions

View file

@ -17,7 +17,7 @@ exports[`components/channel_list should render channels error 1`] = `
"maxWidth": "100%",
"paddingLeft": 18,
"paddingRight": 20,
"paddingVertical": 10,
"paddingTop": 10,
}
}
>
@ -187,7 +187,7 @@ exports[`components/channel_list should render team error 1`] = `
"maxWidth": "100%",
"paddingLeft": 18,
"paddingRight": 20,
"paddingVertical": 10,
"paddingTop": 10,
}
}
>

View file

@ -65,7 +65,7 @@ type Props = {
isActive: boolean;
isOwnDirectMessage: boolean;
isMuted: boolean;
myChannel: MyChannelModel;
myChannel?: MyChannelModel;
collapsed: boolean;
}
@ -76,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 && (isActive || myChannel.isUnread || myChannel.mentionsCount > 0);
const bright = !isMuted && (isActive || (myChannel && (myChannel.isUnread || myChannel.mentionsCount > 0)));
const sharedValue = useSharedValue(collapsed && !bright);
@ -92,7 +92,11 @@ const ChannelListItem = ({channel, isActive, isOwnDirectMessage, isMuted, myChan
};
});
const switchChannels = () => switchToChannelById(serverUrl, myChannel.id);
const switchChannels = () => {
if (myChannel) {
switchToChannelById(serverUrl, myChannel.id);
}
};
const membersCount = useMemo(() => {
if (channel.type === General.GM_CHANNEL) {
return channel.displayName?.split(',').length;
@ -112,7 +116,7 @@ const ChannelListItem = ({channel, isActive, isOwnDirectMessage, isMuted, myChan
displayName = formatMessage({id: 'channel_header.directchannel.you', defaultMessage: '{displayName} (you)'}, {displayName});
}
if (channel.deleteAt > 0 && !isActive) {
if ((channel.deleteAt > 0 && !isActive) || !myChannel) {
return null;
}

View file

@ -14,8 +14,6 @@ import {getUserIdFromChannelName} from '@utils/user';
import ChannelListItem from './channel_list_item';
import type {WithDatabaseArgs} from '@typings/database/database';
import type ChannelModel from '@typings/database/models/servers/channel';
import type MyChannelSettingsModel from '@typings/database/models/servers/my_channel_settings';
const enhance = withObservables(['channelId'], ({channelId, database}: {channelId: string} & WithDatabaseArgs) => {
const myChannel = observeMyChannel(database, channelId);
@ -37,16 +35,16 @@ const enhance = withObservables(['channelId'], ({channelId, database}: {channelI
return {
isOwnDirectMessage,
isMuted: settings.pipe(
switchMap((s: MyChannelSettingsModel) => of$(s.notifyProps?.mark_unread === 'mention')),
switchMap((s) => of$(s?.notifyProps?.mark_unread === 'mention')),
),
myChannel,
channel: channel.pipe(
switchMap((c: ChannelModel) => of$({
deleteAt: c.deleteAt,
displayName: c.displayName,
name: c.name,
shared: c.shared,
type: c.type,
switchMap((c) => of$({
deleteAt: c?.deleteAt || 0,
displayName: c?.displayName || '',
name: c?.name || '',
shared: c?.shared || false,
type: c?.type || '',
})),
),
};

View file

@ -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, useEffect, useRef} from 'react';
import {useIntl} from 'react-intl';
import {FlatList, StyleSheet} from 'react-native';
@ -9,12 +9,13 @@ 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[];
currentChannelId: string;
currentUserId: string;
currentTeamId: string;
}
const styles = StyleSheet.create({
@ -25,8 +26,9 @@ const styles = StyleSheet.create({
const extractKey = (item: CategoryModel) => item.id;
const Categories = ({categories, currentChannelId, currentUserId}: Props) => {
const Categories = ({categories, currentChannelId, currentUserId, currentTeamId}: Props) => {
const intl = useIntl();
const listRef = useRef<FlatList>(null);
const renderCategory = useCallback((data: {item: CategoryModel}) => {
return (
@ -42,6 +44,10 @@ const Categories = ({categories, currentChannelId, currentUserId}: Props) => {
);
}, [categories, currentChannelId, intl.locale]);
useEffect(() => {
listRef.current?.scrollToOffset({animated: false, offset: 0});
}, [currentTeamId]);
// Sort Categories
categories.sort((a, b) => a.sortOrder - b.sortOrder);
@ -52,6 +58,7 @@ const Categories = ({categories, currentChannelId, currentUserId}: Props) => {
return (
<FlatList
data={categories}
ref={listRef}
renderItem={renderCategory}
style={styles.flex}
showsHorizontalScrollIndicator={false}

View file

@ -21,7 +21,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
backgroundColor: theme.sidebarBg,
paddingLeft: 18,
paddingRight: 20,
paddingVertical: 10,
paddingTop: 10,
},
}));

View file

@ -235,6 +235,7 @@ const PostList = ({
<ThreadOverview
rootId={rootId!}
testID={`${testID}.thread_overview`}
style={styles.scale}
/>
);
}
@ -312,11 +313,11 @@ const PostList = ({
);
}, [currentTimezone, highlightPinnedOrSaved, isTimezoneEnabled, orderedPosts, shouldRenderReplyButton, theme]);
const scrollToIndex = useCallback((index: number, animated = true) => {
const scrollToIndex = useCallback((index: number, animated = true, applyOffset = true) => {
listRef.current?.scrollToIndex({
animated,
index,
viewOffset: 0,
viewOffset: applyOffset ? Platform.select({ios: -45, default: 0}) : 0,
viewPosition: 1, // 0 is at bottom
});
}, []);

View file

@ -2,7 +2,7 @@
// See LICENSE.txt for license information.
import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react';
import {ActivityIndicator, DeviceEventEmitter, View, ViewToken} from 'react-native';
import {ActivityIndicator, DeviceEventEmitter, Platform, View, ViewToken} from 'react-native';
import Animated, {interpolate, useAnimatedStyle, useSharedValue, withSpring} from 'react-native-reanimated';
import {resetMessageCount} from '@actions/local/channel';
@ -23,14 +23,14 @@ type Props = {
posts: Array<string | PostModel>;
registerScrollEndIndexListener: (fn: (endIndex: number) => void) => () => void;
registerViewableItemsListener: (fn: (viewableItems: ViewToken[]) => void) => () => void;
scrollToIndex: (index: number, animated?: boolean) => void;
scrollToIndex: (index: number, animated?: boolean, applyOffset?: boolean) => void;
unreadCount: number;
theme: Theme;
testID: string;
}
const HIDDEN_TOP = -60;
const SHOWN_TOP = 0;
const SHOWN_TOP = Platform.select({ios: 40, default: 0});
const MIN_INPUT = 0;
const MAX_INPUT = 1;
@ -103,6 +103,7 @@ const MoreMessages = ({
const serverUrl = useServerUrl();
const pressed = useRef(false);
const resetting = useRef(false);
const initialScroll = useRef(false);
const [loading, setLoading] = useState(false);
const [remaining, setRemaining] = useState(0);
const underlayColor = useMemo(() => `hsl(${hexToHue(theme.buttonBg)}, 50%, 38%)`, [theme]);
@ -149,13 +150,14 @@ const MoreMessages = ({
const lastViewableIndex = viewableItems.filter((v) => v.isViewable)[viewableItems.length - 1]?.index || 0;
const nextViewableIndex = lastViewableIndex + 1;
if (viewableItems[0].index === 0 && nextViewableIndex > newMessageLineIndex) {
if (viewableItems[0].index === 0 && nextViewableIndex > newMessageLineIndex && !initialScroll.current) {
// Auto scroll if the first post is viewable and
// * the new message line is viewable OR
// * the new message line will be the first next viewable item
scrollToIndex(newMessageLineIndex, true);
scrollToIndex(newMessageLineIndex, true, false);
resetCount();
top.value = 0;
initialScroll.current = true;
return;
}
@ -212,6 +214,7 @@ const MoreMessages = ({
useEffect(() => {
resetting.current = false;
initialScroll.current = false;
}, [channelId]);
return (

View file

@ -91,6 +91,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
flexDirection: 'column',
},
rightColumnPadding: {paddingBottom: 3},
touchableContainer: {marginHorizontal: -20, paddingHorizontal: 20},
};
});
@ -274,6 +275,7 @@ const Post = ({
onPress={handlePress}
onLongPress={showPostOptions}
underlayColor={changeOpacity(theme.centerChannelColor, 0.1)}
style={styles.touchableContainer}
>
<>
<PreHeader

View file

@ -16,6 +16,7 @@ exports[`ThreadOverview should match snapshot when post is not saved and 0 repli
Object {
"borderBottomWidth": 0,
},
undefined,
]
}
testID="thread-overview"
@ -120,6 +121,7 @@ exports[`ThreadOverview should match snapshot when post is saved and has replies
"paddingHorizontal": 20,
"paddingVertical": 10,
},
undefined,
]
}
testID="thread-overview"

View file

@ -3,7 +3,7 @@
import React, {useCallback, useMemo} from 'react';
import {useIntl} from 'react-intl';
import {Keyboard, Platform, View} from 'react-native';
import {Keyboard, Platform, StyleProp, View, ViewStyle} from 'react-native';
import {TouchableOpacity} from 'react-native-gesture-handler';
import {deleteSavedPost, savePostPreference} from '@actions/remote/preference';
@ -25,6 +25,7 @@ type Props = {
repliesCount: number;
rootPost?: PostModel;
testID: string;
style?: StyleProp<ViewStyle>;
};
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
@ -55,7 +56,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
};
});
const ThreadOverview = ({isSaved, repliesCount, rootPost, testID}: Props) => {
const ThreadOverview = ({isSaved, repliesCount, rootPost, style, testID}: Props) => {
const theme = useTheme();
const styles = getStyleSheet(theme);
@ -85,14 +86,15 @@ const ThreadOverview = ({isSaved, repliesCount, rootPost, testID}: Props) => {
}), [rootPost]);
const containerStyle = useMemo(() => {
const style = [styles.container];
const container = [styles.container];
if (repliesCount === 0) {
style.push({
container.push({
borderBottomWidth: 0,
});
}
return style;
}, [repliesCount]);
container.push(style);
return container;
}, [repliesCount, style]);
return (
<View

View file

@ -87,7 +87,6 @@ const PostHandler = (superclass: any) => class extends superclass {
const emojis: CustomEmoji[] = [];
const files: FileInfo[] = [];
const metadatas: Metadata[] = [];
const postsReactions: ReactionsPerPost[] = [];
const pendingPostsToDelete: Post[] = [];
const postsInThread: Record<string, Post[]> = {};
@ -193,12 +192,6 @@ const PostHandler = (superclass: any) => class extends superclass {
batch.push(...postFiles);
}
if (metadatas.length) {
// calls handler for postMetadata ( embeds and images )
const postMetadata = await this.handlePostMetadata({metadatas, prepareRecordsOnly: true});
batch.push(...postMetadata);
}
if (emojis.length) {
const postEmojis = await this.handleCustomEmojis({emojis, prepareRecordsOnly: true});
batch.push(...postEmojis);

View file

@ -49,7 +49,7 @@ export const transformChannelRecord = ({action, database, value}: TransformerArg
const rawMembers = raw.display_name.split(',').length;
const recordMembers = record?.displayName.split(',').length || rawMembers;
if (recordMembers < rawMembers) {
if (recordMembers < rawMembers && record.displayName) {
displayName = record.displayName;
} else {
displayName = raw.display_name;

View file

@ -1,152 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {useEffect, useMemo} from 'react';
import {Text, View} from 'react-native';
import {fetchProfilesInChannel} from '@actions/remote/user';
import FormattedText from '@components/formatted_text';
import {BotTag} from '@components/tag';
import {General} from '@constants';
import {useServerUrl} from '@context/server';
import {makeStyleSheetFromTheme} from '@utils/theme';
import {typography} from '@utils/typography';
import IntroOptions from '../options';
import Group from './group';
import Member from './member';
import type ChannelModel from '@typings/database/models/servers/channel';
import type ChannelMembershipModel from '@typings/database/models/servers/channel_membership';
type Props = {
channel: ChannelModel;
currentUserId: string;
isBot: boolean;
members?: ChannelMembershipModel[];
theme: Theme;
}
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
botContainer: {
alignSelf: 'flex-end',
bottom: 7.5,
height: 20,
marginBottom: 0,
marginLeft: 4,
paddingVertical: 0,
},
botText: {
fontSize: 14,
lineHeight: 20,
},
container: {
alignItems: 'center',
},
message: {
color: theme.centerChannelColor,
marginTop: 16,
textAlign: 'center',
...typography('Body', 200, 'Regular'),
},
profilesContainer: {
justifyContent: 'center',
alignItems: 'center',
},
title: {
color: theme.centerChannelColor,
marginTop: 16,
textAlign: 'center',
...typography('Heading', 700, 'SemiBold'),
},
titleGroup: {
...typography('Heading', 600, 'SemiBold'),
},
}));
const DirectChannel = ({channel, currentUserId, isBot, members, theme}: Props) => {
const serverUrl = useServerUrl();
const styles = getStyleSheet(theme);
useEffect(() => {
const channelMembers = members?.filter((m) => m.userId !== currentUserId);
if (!channelMembers?.length) {
fetchProfilesInChannel(serverUrl, channel.id, currentUserId, false);
}
}, []);
const message = useMemo(() => {
if (channel.type === General.DM_CHANNEL) {
return (
<FormattedText
defaultMessage={'This is the start of your conversation with {teammate}. Messages and files shared here are not shown to anyone else.'}
id='intro.direct_message'
style={styles.message}
values={{teammate: channel.displayName}}
/>
);
}
return (
<FormattedText
defaultMessage={'This is the start of your conversation with this group. Messages and files shared here are not shown to anyone else outside of the group.'}
id='intro.group_message'
style={styles.message}
/>
);
}, [channel.displayName, theme]);
const profiles = useMemo(() => {
const channelMembers = members?.filter((m) => m.userId !== currentUserId);
if (!channelMembers?.length) {
return null;
}
if (channel.type === General.DM_CHANNEL) {
return (
<Member
containerStyle={{height: 96}}
member={channelMembers[0]}
size={96}
theme={theme}
/>
);
}
return (
<Group
theme={theme}
userIds={channelMembers.map((cm) => cm.userId)}
/>
);
}, [members, theme]);
return (
<View style={styles.container}>
<View style={styles.profilesContainer}>
{profiles}
</View>
<View style={{flexDirection: 'row'}}>
<Text style={[styles.title, channel.type === General.GM_CHANNEL ? styles.titleGroup : undefined]}>
{channel.displayName}
</Text>
{isBot &&
<BotTag
style={styles.botContainer}
textStyle={styles.botText}
/>
}
</View>
{message}
<IntroOptions
channelId={channel.id}
header={true}
favorite={true}
people={false}
theme={theme}
/>
</View>
);
};
export default DirectChannel;

View file

@ -1,78 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {chunk} from 'lodash';
import React from 'react';
import {View} from 'react-native';
import FastImage from 'react-native-fast-image';
import {useServerUrl} from '@context/server';
import NetworkManager from '@init/network_manager';
import {makeStyleSheetFromTheme} from '@utils/theme';
import type {Client} from '@client/rest';
import type UserModel from '@typings/database/models/servers/user';
type Props = {
theme: Theme;
users: UserModel[];
}
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
container: {
alignItems: 'center',
flexDirection: 'row',
marginBottom: 12,
},
profile: {
borderColor: theme.centerChannelBg,
borderRadius: 36,
borderWidth: 2,
height: 72,
width: 72,
},
}));
const Group = ({theme, users}: Props) => {
const serverUrl = useServerUrl();
const styles = getStyleSheet(theme);
let client: Client | undefined;
try {
client = NetworkManager.getClient(serverUrl);
} catch {
return null;
}
const rows = chunk(users, 5);
const groups = rows.map((c, k) => {
const group = c.map((u, i) => {
const pictureUrl = client!.getProfilePictureUrl(u.id, u.lastPictureUpdate);
return (
<FastImage
key={pictureUrl + i.toString()}
style={[styles.profile, {transform: [{translateX: -(i * 24)}]}]}
source={{uri: `${serverUrl}${pictureUrl}`}}
/>
);
});
return (
<View
key={'group_avatar' + k.toString()}
style={[styles.container, {left: (c.length - 1) * 12}]}
>
{group}
</View>
);
});
return (
<>
{groups}
</>
);
};
export default Group;

View file

@ -1,17 +0,0 @@
// 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 {queryUsersById} from '@queries/servers/user';
import Group from './group';
import type {WithDatabaseArgs} from '@typings/database/database';
const enhanced = withObservables([], ({userIds, database}: {userIds: string[]} & WithDatabaseArgs) => ({
users: queryUsersById(database, userIds).observeWithColumns(['last_picture_update']),
}));
export default withDatabase(enhanced(Group));

View file

@ -1,45 +0,0 @@
// 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 {General} from '@constants';
import {observeCurrentUserId} from '@queries/servers/system';
import {observeUser} from '@queries/servers/user';
import {getUserIdFromChannelName} from '@utils/user';
import DirectChannel from './direct_channel';
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';
const observeIsBot = (user: UserModel | undefined) => of$(Boolean(user?.isBot));
const enhanced = withObservables([], ({channel, database}: {channel: ChannelModel} & WithDatabaseArgs) => {
const currentUserId = observeCurrentUserId(database);
const members = channel.members.observe();
let isBot = of$(false);
if (channel.type === General.DM_CHANNEL) {
isBot = currentUserId.pipe(
switchMap((userId) => {
const otherUserId = getUserIdFromChannelName(userId, channel.name);
return observeUser(database, otherUserId).pipe(
switchMap(observeIsBot),
);
}),
);
}
return {
currentUserId,
isBot,
members,
};
});
export default withDatabase(enhanced(DirectChannel));

View file

@ -1,15 +0,0 @@
// 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 Member from './member';
import type ChannelMembershipModel from '@typings/database/models/servers/channel_membership';
const enhanced = withObservables([], ({member}: {member: ChannelMembershipModel}) => ({
user: member.memberUser.observe(),
}));
export default withDatabase(enhanced(Member));

View file

@ -1,74 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {useCallback} from 'react';
import {useIntl} from 'react-intl';
import {StyleProp, StyleSheet, ViewStyle} from 'react-native';
import CompassIcon from '@components/compass_icon';
import ProfilePicture from '@components/profile_picture';
import TouchableWithFeedback from '@components/touchable_with_feedback';
import {Screens} from '@constants';
import {showModal} from '@screens/navigation';
import type UserModel from '@typings/database/models/servers/user';
type Props = {
containerStyle?: StyleProp<ViewStyle>;
size?: number;
showStatus?: boolean;
theme: Theme;
user: UserModel;
}
const styles = StyleSheet.create({
profile: {
height: 67,
marginBottom: 12,
marginRight: 12,
},
});
const Member = ({containerStyle, size = 72, showStatus = true, theme, user}: Props) => {
const intl = useIntl();
const onPress = useCallback(() => {
const screen = Screens.USER_PROFILE;
const title = intl.formatMessage({id: 'mobile.routes.user_profile', defaultMessage: 'Profile'});
const passProps = {
userId: user.id,
};
const closeButton = CompassIcon.getImageSourceSync('close', 24, theme.sidebarHeaderTextColor);
const options = {
topBar: {
leftButtons: [{
id: 'close-user-profile',
icon: closeButton,
testID: 'close.settings.button',
}],
},
};
showModal(screen, title, passProps, options);
}, [theme]);
return (
<TouchableWithFeedback
onPress={onPress}
style={[styles.profile, containerStyle]}
type='opacity'
>
<ProfilePicture
author={user}
size={size}
iconSize={48}
showStatus={showStatus}
statusSize={24}
testID='channel_intro.profile_picture'
/>
</TouchableWithFeedback>
);
};
export default Member;

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1,43 +0,0 @@
// 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 {combineLatest} from 'rxjs';
import {switchMap} from 'rxjs/operators';
import {observeChannel, observeMyChannel} from '@queries/servers/channel';
import {queryRolesByNames} from '@queries/servers/role';
import {observeCurrentUser} from '@queries/servers/user';
import Intro from './intro';
import type {WithDatabaseArgs} from '@typings/database/database';
const enhanced = withObservables(['channelId'], ({channelId, database}: {channelId: string} & WithDatabaseArgs) => {
const channel = observeChannel(database, channelId);
const myChannel = observeMyChannel(database, channelId);
const me = observeCurrentUser(database);
const roles = combineLatest([me, myChannel]).pipe(
switchMap(([user, member]) => {
const userRoles = user?.roles.split(' ');
const memberRoles = member?.roles.split(' ');
const combinedRoles = [];
if (userRoles) {
combinedRoles.push(...userRoles);
}
if (memberRoles) {
combinedRoles.push(...memberRoles);
}
return queryRolesByNames(database, combinedRoles).observe();
}),
);
return {
channel,
roles,
};
});
export default withDatabase(enhanced(Intro));

View file

@ -1,86 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {useMemo} from 'react';
import {ActivityIndicator, Platform, StyleSheet, View} from 'react-native';
import {General} from '@constants';
import {useTheme} from '@context/theme';
import DirectChannel from './direct_channel';
import PublicOrPrivateChannel from './public_or_private_channel';
import TownSquare from './townsquare';
import type ChannelModel from '@typings/database/models/servers/channel';
import type RoleModel from '@typings/database/models/servers/role';
type Props = {
channel: ChannelModel;
loading?: boolean;
roles: RoleModel[];
}
const styles = StyleSheet.create({
container: {
marginVertical: 12,
overflow: 'hidden',
...Platform.select({
android: {
scaleY: -1,
},
}),
},
});
const Intro = ({channel, loading = false, roles}: Props) => {
const theme = useTheme();
const element = useMemo(() => {
if (channel.type === General.OPEN_CHANNEL && channel.name === General.DEFAULT_CHANNEL) {
return (
<TownSquare
channelId={channel.id}
displayName={channel.displayName}
roles={roles}
theme={theme}
/>
);
}
switch (channel.type) {
case General.OPEN_CHANNEL:
case General.PRIVATE_CHANNEL:
return (
<PublicOrPrivateChannel
channel={channel}
roles={roles}
theme={theme}
/>
);
default:
return (
<DirectChannel
channel={channel}
theme={theme}
/>
);
}
}, [channel, roles, theme]);
if (loading) {
return (
<ActivityIndicator
size='small'
color={theme.centerChannelColor}
style={styles.container}
/>
);
}
return (
<View style={styles.container}>
{element}
</View>
);
};
export default Intro;

View file

@ -1,38 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {useCallback} from 'react';
import {useIntl} from 'react-intl';
import {saveFavoriteChannel} from '@actions/remote/preference';
import {useServerUrl} from '@context/server';
import OptionItem from '../item';
type Props = {
channelId: string;
isFavorite: boolean;
theme: Theme;
}
const IntroFavorite = ({channelId, isFavorite, theme}: Props) => {
const {formatMessage} = useIntl();
const serverUrl = useServerUrl();
const toggleFavorite = useCallback(() => {
saveFavoriteChannel(serverUrl, channelId, !isFavorite);
}, [channelId, isFavorite]);
return (
<OptionItem
applyMargin={true}
color={isFavorite ? theme.buttonBg : undefined}
iconName={isFavorite ? 'star' : 'star-outline'}
label={formatMessage({id: 'intro.favorite', defaultMessage: 'Favorite'})}
onPress={toggleFavorite}
theme={theme}
/>
);
};
export default IntroFavorite;

View file

@ -1,24 +0,0 @@
// 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 {Preferences} from '@constants';
import {queryPreferencesByCategoryAndName} from '@queries/servers/preference';
import FavoriteItem from './favorite';
import type {WithDatabaseArgs} from '@typings/database/database';
const enhanced = withObservables([], ({channelId, database}: {channelId: string} & WithDatabaseArgs) => ({
isFavorite: queryPreferencesByCategoryAndName(database, Preferences.CATEGORY_FAVORITE_CHANNEL, channelId).observeWithColumns(['value']).pipe(
switchMap((prefs) => {
return prefs.length ? of$(prefs[0].value === 'true') : of$(false);
}),
),
}));
export default withDatabase(enhanced(FavoriteItem));

View file

@ -1,86 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {useCallback} from 'react';
import {useIntl} from 'react-intl';
import {StyleSheet, View} from 'react-native';
import {Screens} from '@constants';
import {showModal} from '@screens/navigation';
import IntroFavorite from './favorite';
import OptionItem from './item';
type Props = {
channelId: string;
header?: boolean;
favorite?: boolean;
people?: boolean;
theme: Theme;
}
const styles = StyleSheet.create({
container: {
justifyContent: 'center',
flexDirection: 'row',
marginBottom: 8,
marginTop: 28,
width: '100%',
},
});
const IntroOptions = ({channelId, header, favorite, people, theme}: Props) => {
const {formatMessage} = useIntl();
const onAddPeople = useCallback(() => {
const title = formatMessage({id: 'intro.add_people', defaultMessage: 'Add People'});
showModal(Screens.CHANNEL_ADD_PEOPLE, title, {channelId});
}, []);
const onSetHeader = useCallback(() => {
const title = formatMessage({id: 'screens.channel_edit', defaultMessage: 'Edit Channel'});
showModal(Screens.CHANNEL_EDIT, title, {channelId});
}, []);
const onDetails = useCallback(() => {
const title = formatMessage({id: 'screens.channel_details', defaultMessage: 'Channel Details'});
showModal(Screens.CHANNEL_DETAILS, title, {channelId});
}, []);
return (
<View style={styles.container}>
{people &&
<OptionItem
applyMargin={true}
iconName='account-plus-outline'
label={formatMessage({id: 'intro.add_people', defaultMessage: 'Add People'})}
onPress={onAddPeople}
theme={theme}
/>
}
{header &&
<OptionItem
applyMargin={true}
iconName='pencil-outline'
label={formatMessage({id: 'intro.set_header', defaultMessage: 'Set Header'})}
onPress={onSetHeader}
theme={theme}
/>
}
{favorite &&
<IntroFavorite
channelId={channelId}
theme={theme}
/>
}
<OptionItem
iconName='information-outline'
label={formatMessage({id: 'intro.channel_details', defaultMessage: 'Details'})}
onPress={onDetails}
theme={theme}
/>
</View>
);
};
export default IntroOptions;

View file

@ -1,88 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {useCallback} from 'react';
import {Pressable, PressableStateCallbackType, Text} from 'react-native';
import CompassIcon from '@components/compass_icon';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
import {typography} from '@utils/typography';
type Props = {
applyMargin?: boolean;
color?: string;
iconName: string;
label: string;
onPress: () => void;
theme: Theme;
}
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
container: {
alignItems: 'center',
backgroundColor: changeOpacity(theme.centerChannelColor, 0.04),
borderRadius: 4,
height: 70,
justifyContent: 'center',
paddingHorizontal: 16,
paddingVertical: 12,
width: 112,
},
containerPressed: {
backgroundColor: changeOpacity(theme.buttonBg, 0.08),
},
label: {
marginTop: 6,
...typography('Body', 50, 'SemiBold'),
},
margin: {
marginRight: 8,
},
}));
const IntroItem = ({applyMargin, color, iconName, label, onPress, theme}: Props) => {
const styles = getStyleSheet(theme);
const pressedStyle = useCallback(({pressed}: PressableStateCallbackType) => {
const style = [styles.container];
if (pressed) {
style.push(styles.containerPressed);
}
if (applyMargin) {
style.push(styles.margin);
}
return style;
}, [applyMargin, theme]);
const renderPressableChildren = ({pressed}: PressableStateCallbackType) => {
let pressedColor = color || changeOpacity(theme.centerChannelColor, 0.56);
if (pressed) {
pressedColor = theme.linkColor;
}
return (
<>
<CompassIcon
name={iconName}
color={pressedColor}
size={24}
/>
<Text style={[styles.label, {color: pressedColor}]}>
{label}
</Text>
</>
);
};
return (
<Pressable
onPress={onPress}
style={pressedStyle}
>
{renderPressableChildren}
</Pressable>
);
};
export default IntroItem;

View file

@ -1,46 +0,0 @@
// 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 {combineLatest, of as of$} from 'rxjs';
import {map} from 'rxjs/operators';
import {Preferences} from '@constants';
import {getTeammateNameDisplaySetting} from '@helpers/api/preference';
import {queryPreferencesByCategoryAndName} from '@queries/servers/preference';
import {observeConfig, observeLicense} from '@queries/servers/system';
import {observeCurrentUser} from '@queries/servers/user';
import {displayUsername} from '@utils/user';
import PublicOrPrivateChannel from './public_or_private_channel';
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';
const enhanced = withObservables([], ({channel, database}: {channel: ChannelModel} & WithDatabaseArgs) => {
let creator;
if (channel.creatorId) {
const config = observeConfig(database);
const license = observeLicense(database);
const preferences = queryPreferencesByCategoryAndName(database, Preferences.CATEGORY_DISPLAY_SETTINGS).observe();
const me = observeCurrentUser(database);
const profile = channel.creator.observe();
const teammateNameDisplay = combineLatest([preferences, config, license]).pipe(
map(([prefs, cfg, lcs]) => getTeammateNameDisplaySetting(prefs, cfg, lcs)),
);
creator = combineLatest([profile, teammateNameDisplay, me]).pipe(
map(([user, displaySetting, currentUser]) => (user ? displayUsername(user as UserModel, currentUser?.locale, displaySetting, true) : '')),
);
} else {
creator = of$(undefined);
}
return {
creator,
};
});
export default withDatabase(enhanced(PublicOrPrivateChannel));

View file

@ -1,145 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {useEffect, useMemo} from 'react';
import {useIntl} from 'react-intl';
import {Text, View} from 'react-native';
import {fetchChannelCreator} from '@actions/remote/channel';
import CompassIcon from '@components/compass_icon';
import {General, Permissions} from '@constants';
import {useServerUrl} from '@context/server';
import {t} from '@i18n';
import {hasPermission} from '@utils/role';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
import {typography} from '@utils/typography';
import PrivateChannel from '../illustration/private';
import PublicChannel from '../illustration/public';
import IntroOptions from '../options';
import type ChannelModel from '@typings/database/models/servers/channel';
import type RoleModel from '@typings/database/models/servers/role';
type Props = {
channel: ChannelModel;
creator?: string;
roles: RoleModel[];
theme: Theme;
}
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
container: {
alignItems: 'center',
},
created: {
color: changeOpacity(theme.centerChannelColor, 0.64),
...typography('Body', 50, 'Regular'),
},
icon: {
marginRight: 5,
},
message: {
color: theme.centerChannelColor,
marginTop: 16,
textAlign: 'center',
...typography('Body', 200, 'Regular'),
},
title: {
color: theme.centerChannelColor,
marginTop: 16,
marginBottom: 8,
...typography('Heading', 700, 'SemiBold'),
},
}));
const PublicOrPrivateChannel = ({channel, creator, roles, theme}: Props) => {
const intl = useIntl();
const serverUrl = useServerUrl();
const styles = getStyleSheet(theme);
const illustration = useMemo(() => {
if (channel.type === General.OPEN_CHANNEL) {
return <PublicChannel theme={theme}/>;
}
return <PrivateChannel theme={theme}/>;
}, [channel.type, theme]);
useEffect(() => {
if (!creator && channel.creatorId) {
fetchChannelCreator(serverUrl, channel.id);
}
}, []);
const canManagePeople = useMemo(() => {
const permission = channel.type === General.OPEN_CHANNEL ? Permissions.MANAGE_PUBLIC_CHANNEL_MEMBERS : Permissions.MANAGE_PRIVATE_CHANNEL_MEMBERS;
return hasPermission(roles, permission, false);
}, [channel.type, roles]);
const canSetHeader = useMemo(() => {
const permission = channel.type === General.OPEN_CHANNEL ? Permissions.MANAGE_PUBLIC_CHANNEL_PROPERTIES : Permissions.MANAGE_PRIVATE_CHANNEL_PROPERTIES;
return hasPermission(roles, permission, false);
}, [channel.type, roles]);
const createdBy = useMemo(() => {
const id = channel.type === General.OPEN_CHANNEL ? t('intro.public_channel') : t('intro.private_channel');
const defaultMessage = channel.type === General.OPEN_CHANNEL ? 'Public Channel' : 'Private Channel';
const channelType = `${intl.formatMessage({id, defaultMessage})} `;
const date = intl.formatDate(channel.createAt, {
year: 'numeric',
month: 'long',
day: 'numeric',
});
const by = intl.formatMessage({id: 'intro.created_by', defaultMessage: 'created by {creator} on {date}.'}, {
creator,
date,
});
return `${channelType} ${by}`;
}, [channel.type, creator, theme]);
const message = useMemo(() => {
const id = channel.type === General.OPEN_CHANNEL ? t('intro.welcome.public') : t('intro.welcome.private');
const msg = channel.type === General.OPEN_CHANNEL ? 'Add some more team members to the channel or start a conversation below.' : 'Only invited members can see messages posted in this private channel.';
const mainMessage = intl.formatMessage({
id: 'intro.welcome',
defaultMessage: 'Welcome to {displayName} channel.',
}, {displayName: channel.displayName});
const suffix = intl.formatMessage({id, defaultMessage: msg});
return `${mainMessage} ${suffix}`;
}, [channel.displayName, channel.type, theme]);
return (
<View style={styles.container}>
{illustration}
<Text style={styles.title}>
{channel.displayName}
</Text>
<View style={{flexDirection: 'row'}}>
<CompassIcon
name={channel.type === General.OPEN_CHANNEL ? 'globe' : 'lock'}
size={14.4}
color={changeOpacity(theme.centerChannelColor, 0.64)}
style={styles.icon}
/>
<Text style={styles.created}>
{createdBy}
</Text>
</View>
<Text style={styles.message}>
{message}
</Text>
<IntroOptions
channelId={channel.id}
header={canSetHeader}
people={canManagePeople}
theme={theme}
/>
</View>
);
};
export default PublicOrPrivateChannel;

View file

@ -1,66 +0,0 @@
// 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 FormattedText from '@components/formatted_text';
import {Permissions} from '@constants';
import {hasPermission} from '@utils/role';
import {makeStyleSheetFromTheme} from '@utils/theme';
import {typography} from '@utils/typography';
import PublicChannel from '../illustration/public';
import IntroOptions from '../options';
import type RoleModel from '@typings/database/models/servers/role';
type Props = {
channelId: string;
displayName: string;
roles: RoleModel[];
theme: Theme;
}
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
container: {
alignItems: 'center',
},
message: {
color: theme.centerChannelColor,
marginTop: 16,
textAlign: 'center',
...typography('Body', 200, 'Regular'),
width: '100%',
},
title: {
color: theme.centerChannelColor,
marginTop: 16,
...typography('Heading', 700, 'SemiBold'),
},
}));
const TownSquare = ({channelId, displayName, roles, theme}: Props) => {
const styles = getStyleSheet(theme);
return (
<View style={styles.container}>
<PublicChannel theme={theme}/>
<Text style={styles.title}>
{displayName}
</Text>
<FormattedText
defaultMessage='Welcome to {name}. Everyone automatically becomes a member of this channel when they join the team.'
id='intro.townsquare'
style={styles.message}
values={{name: displayName}}
/>
<IntroOptions
channelId={channelId}
header={hasPermission(roles, Permissions.MANAGE_PUBLIC_CHANNEL_PROPERTIES, false)}
theme={theme}
/>
</View>
);
};
export default TownSquare;

View file

@ -2,7 +2,7 @@
// See LICENSE.txt for license information.
import React, {useMemo} from 'react';
import {StyleSheet} from 'react-native';
import {StyleSheet, View} from 'react-native';
import {Edge, SafeAreaView} from 'react-native-safe-area-context';
import PostList from '@components/post_list';
@ -25,8 +25,9 @@ type Props = {
const edges: Edge[] = ['bottom'];
const styles = StyleSheet.create({
container: {marginTop: 20},
container: {marginTop: 10},
flex: {flex: 1},
footer: {height: 20},
});
const ThreadPostList = ({
@ -54,6 +55,7 @@ const ThreadPostList = ({
shouldShowJoinLeaveMessages={false}
showMoreMessages={false}
showNewMessageLine={false}
footer={<View style={styles.footer}/>}
testID='thread.post_list'
/>
);

83
package-lock.json generated
View file

@ -5,7 +5,6 @@
"requires": true,
"packages": {
"": {
"name": "mattermost-mobile",
"version": "2.0.0",
"hasInstallScript": true,
"license": "Apache 2.0",
@ -4262,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"
},
@ -4304,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"
@ -4606,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"
},
@ -4883,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"
@ -5037,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"
@ -8644,7 +8644,6 @@
"node_modules/commonmark-react-renderer": {
"version": "4.3.5",
"resolved": "git+ssh://git@github.com/mattermost/commonmark-react-renderer.git#4e52e1725c0ef5b1e2ecfe9883220ec36c2eb67d",
"license": "MIT",
"dependencies": {
"lodash.assign": "^4.2.0",
"lodash.isplainobject": "^4.0.6",
@ -8833,7 +8832,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",
@ -13444,7 +13443,7 @@
"integrity": "sha512-qvUtwJ3j6qwsF3jLxkZ72qCgjMysPzDfeV240JHiGZsANBYd+EEuu35v7dfrJ9Up0Ak07D7GGSkGhCHTqg/5wA==",
"dev": true,
"dependencies": {
"node-fetch": "^2.6.7",
"node-fetch": "^2.6.1",
"whatwg-fetch": "^3.4.1"
}
},
@ -16560,7 +16559,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"
},
@ -17599,9 +17598,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",
@ -17667,11 +17665,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"
@ -17680,6 +17680,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",
@ -17693,6 +17694,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"
@ -17701,6 +17703,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",
@ -17718,6 +17721,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"
@ -18204,11 +18208,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": {
@ -19129,9 +19142,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"
@ -20233,7 +20246,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"
},
@ -21666,6 +21679,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",
@ -21694,7 +21708,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",
@ -23503,7 +23518,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,
@ -27223,7 +27238,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"
}
@ -27658,7 +27673,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"
}
@ -27696,7 +27711,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"
@ -28055,7 +28070,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"
@ -34584,7 +34599,7 @@
"integrity": "sha512-qvUtwJ3j6qwsF3jLxkZ72qCgjMysPzDfeV240JHiGZsANBYd+EEuu35v7dfrJ9Up0Ak07D7GGSkGhCHTqg/5wA==",
"dev": true,
"requires": {
"node-fetch": "^2.6.7",
"node-fetch": "^2.6.1",
"whatwg-fetch": "^3.4.1"
}
},
@ -37326,7 +37341,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"
}
@ -37887,9 +37902,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",
@ -37943,16 +37957,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",
@ -37963,11 +37980,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",
@ -37982,6 +38001,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
}
}
@ -38361,6 +38381,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"
},
@ -39076,9 +39097,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"
@ -39551,7 +39572,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"
}