MM-40364 [Gekidou] - Image Picker for Edit Profile (#5901)
* feature edit profile screen * minor refactoring * Apply suggestions from code review Co-authored-by: Elias Nahum <nahumhbl@gmail.com> * ts fixes * revert floatingTextInput label This reverts commit a778e1f76191aea7c1a18d60a23ffbd6d3dec0eb. * code clean up * Apply suggestions from code review Co-authored-by: Elias Nahum <nahumhbl@gmail.com> * code fix * code fix * Adding preventDoubleTap * rename id to fieldKey * Update edit_profile.tsx * wip * navigating through fields; partly done * navigating through fields - partly done * navigating through fields; partly done * completed field navigation * added theme into dependency array * code clean up * revert conditions for disabling fields * Added colorFilters prop to Loading component * Completed loading feature on Edit Profile screen * code clean up * Add profile_error * renamed valid_mime_types to valid_image_mime_types * added props isDisabled to email field * refactored next field logic * fix * fix * code clean up * code clean up * Updated ESLINT hook rules to warning instead of disabled * code fix * code fix * new line within your_profile component * added memo for Field component * added canSave to dependency array * update loading component - color filter * Update app/screens/edit_profile/edit_profile.tsx Co-authored-by: Elias Nahum <nahumhbl@gmail.com> * dependency fix * fix to fetch my latest status * fix to remove unnecessary user id for local action updateLocalUser * prevents bouncing for iOS * code revert * Adding textInputStyle and animatedTextStyle to FloatingTextInput component * correction after dev session * adding changes as per new ux * Update edit_profile.tsx * corrections after ux review * ux review * ux review * code clean up * Adding userProfileFields into useMemo * Add enableSaveButton to dependency of submitUser * Added react-native-image-picker * fix picker util * Added action for setDefaultProfileImage * account outline on remove picture * Update edit_profile.tsx * fix image picker * style fix * fix image picker * removed unused types * mmjstool issue with integrity checksum * perform camera permission check for Android * fix to pull latest status * updated ChangeProfilePicture to EditProfilePciture * removed integrity key for mmjstool in package-lock.json * corrections from pr review * bumping react-native-image-picker to v4.7.1 * pod install * update to hooks dependency * fix profile picture component * added event emitter from edit_profile_picture * made hitslop a constant * code clean up * uploadProfilePicture as a remote action * else if profileImage removed * removed check on isBot * update renderProfilePicture dependencies * extractFileInfo with try catch * updated snappoints * Revert "updated snappoints" This reverts commit 6d16d480a168755fc80e5bc80569ad3ba561f73b. * profile picture size * refactored renderProfilePicture into its own component * change to if else * platform select for hasPermissions * unneeded comment removed * else if on prefix in edit profile picture * track has update user info now * moved image_picker under edit_screen and increased actionSheets item height * added preventDoubleTap for imagePicker * multiple uploads * switch the conditions * added alert box as requested by Marina * Revert "added alert box as requested by Marina" This reverts commit 20735c17a87b40995e05eb4318c138c1adcc6c8c. * Apply suggestions from code review Co-authored-by: Elias Nahum <nahumhbl@gmail.com> * removed userInfos constant * added useMemo for certain components on profile_picture * converting account-outline into a constant * added panelItem component * adding return instead of making the function return * eslint fix * update i18n desc Co-authored-by: Elias Nahum <nahumhbl@gmail.com> * hasPictureUrl transferred to file utils * removing excess mediaType prop * add USER_PROFILE_PICTURE_SIZE into constant/profile * relocate hasPictureUrl method * relocate hasPictureUrl * rename ImagePicker to ProfileImagePicker * removing isDestructive property from panelTypes. * update sectionLimit for attachFileFromPhotoGallery * Change animation for showModalOverCurrentContext to a quick alpha on iOS * re-create PickerUtil if intl changes * Combine styles in edit_profile_picture component * Split profile image component into smaller components * useCallback for showFileAttachmentOptions * split comment into multiple lines * edit_profile group refs * remove unnecessary casting * add new line to file.d.ts * remove extra space for utils/index.d.ts * allowMultiSelection for attachFilesFromFiles, default is false * Split edit profile screen into smaller components Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
This commit is contained in:
parent
be63e3c780
commit
dff4f91441
28 changed files with 1485 additions and 386 deletions
|
|
@ -475,3 +475,54 @@ export const unsetCustomStatus = async (serverUrl: string) => {
|
|||
|
||||
return {data: true};
|
||||
};
|
||||
|
||||
export const setDefaultProfileImage = async (serverUrl: string, userId: string) => {
|
||||
let client: Client;
|
||||
|
||||
try {
|
||||
client = NetworkManager.getClient(serverUrl);
|
||||
} catch (error) {
|
||||
return {error};
|
||||
}
|
||||
|
||||
try {
|
||||
await client.setDefaultProfileImage(userId);
|
||||
updateLocalUser(serverUrl, {last_picture_update: Date.now()});
|
||||
} catch (error) {
|
||||
return {error};
|
||||
}
|
||||
|
||||
return {data: true};
|
||||
};
|
||||
|
||||
export const uploadUserProfileImage = async (serverUrl: string, localPath: string) => {
|
||||
const database = DatabaseManager.serverDatabases[serverUrl]?.database;
|
||||
if (!database) {
|
||||
return {error: `${serverUrl} database not found`};
|
||||
}
|
||||
|
||||
let client: Client;
|
||||
try {
|
||||
client = NetworkManager.getClient(serverUrl);
|
||||
} catch (error) {
|
||||
return {error};
|
||||
}
|
||||
|
||||
try {
|
||||
const currentUser = await queryCurrentUser(database);
|
||||
if (currentUser) {
|
||||
const endpoint = `${client.getUserRoute(currentUser.id)}/image`;
|
||||
|
||||
await client.apiClient.upload(endpoint, localPath, {
|
||||
skipBytes: 0,
|
||||
method: 'POST',
|
||||
multipart: {
|
||||
fileKey: 'image',
|
||||
},
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
return {error: e};
|
||||
}
|
||||
return {error: undefined};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -7,8 +7,10 @@ import {StyleProp, Text, TextStyle, ViewStyle} from 'react-native';
|
|||
import FormattedText from '@components/formatted_text';
|
||||
import {makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
||||
import type {ErrorText as ErrorType} from '@typings/utils/file';
|
||||
|
||||
type ErrorProps = {
|
||||
error: ErrorText;
|
||||
error: ErrorType;
|
||||
testID?: string;
|
||||
textStyle?: StyleProp<ViewStyle> | StyleProp<TextStyle>;
|
||||
theme: Theme;
|
||||
|
|
|
|||
|
|
@ -62,7 +62,7 @@ const FileInfo = ({file, onPress, theme}: FileInfoProps) => {
|
|||
ellipsizeMode='tail'
|
||||
style={style.fileInfo}
|
||||
>
|
||||
{`${getFormattedFileSize(file)}`}
|
||||
{`${getFormattedFileSize(file.size)}`}
|
||||
</Text>
|
||||
</View>
|
||||
</>
|
||||
|
|
|
|||
80
app/components/profile_picture/image.tsx
Normal file
80
app/components/profile_picture/image.tsx
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useMemo} from 'react';
|
||||
import FastImage, {Source} from 'react-native-fast-image';
|
||||
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import {ACCOUNT_OUTLINE_IMAGE} from '@constants/profile';
|
||||
import {useServerUrl} from '@context/server';
|
||||
import {useTheme} from '@context/theme';
|
||||
import NetworkManager from '@init/network_manager';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
||||
import type {Client} from '@client/rest';
|
||||
import type UserModel from '@typings/database/models/servers/user';
|
||||
|
||||
type Props = {
|
||||
author?: UserModel;
|
||||
iconSize?: number;
|
||||
size: number;
|
||||
source?: Source | string;
|
||||
};
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
||||
return {
|
||||
icon: {
|
||||
color: changeOpacity(theme.centerChannelColor, 0.48),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const Image = ({author, iconSize, size, source}: Props) => {
|
||||
const theme = useTheme();
|
||||
const serverUrl = useServerUrl();
|
||||
const style = getStyleSheet(theme);
|
||||
const fIStyle = useMemo(() => ({
|
||||
borderRadius: size / 2,
|
||||
height: size,
|
||||
width: size,
|
||||
}), [size]);
|
||||
|
||||
if (typeof source === 'string') {
|
||||
return (
|
||||
<CompassIcon
|
||||
name={source}
|
||||
size={iconSize || size}
|
||||
style={style.icon}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
let client: Client | undefined;
|
||||
|
||||
try {
|
||||
client = NetworkManager.getClient(serverUrl);
|
||||
} catch {
|
||||
// handle below that the client is not set
|
||||
}
|
||||
|
||||
if (author && client) {
|
||||
const pictureUrl = client.getProfilePictureUrl(author.id, author.lastPictureUpdate);
|
||||
const imgSource = source ?? {uri: `${serverUrl}${pictureUrl}`};
|
||||
return (
|
||||
<FastImage
|
||||
key={pictureUrl}
|
||||
style={fIStyle}
|
||||
source={imgSource as Source}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<CompassIcon
|
||||
name={ACCOUNT_OUTLINE_IMAGE}
|
||||
size={iconSize || size}
|
||||
style={style.icon}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default Image;
|
||||
|
|
@ -1,20 +1,19 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useEffect} from 'react';
|
||||
import {Platform, StyleProp, View, ViewProps, ViewStyle} from 'react-native';
|
||||
import FastImage from 'react-native-fast-image';
|
||||
import React, {useEffect, useMemo} from 'react';
|
||||
import {Platform, StyleProp, View, ViewProps} from 'react-native';
|
||||
|
||||
import {fetchStatusInBatch} from '@actions/remote/user';
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import UserStatus from '@components/user_status';
|
||||
import {useServerUrl} from '@context/server';
|
||||
import {useTheme} from '@context/theme';
|
||||
import NetworkManager from '@init/network_manager';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
||||
import type {Client} from '@client/rest';
|
||||
import Image from './image';
|
||||
import Status from './status';
|
||||
|
||||
import type UserModel from '@typings/database/models/servers/user';
|
||||
import type {Source} from 'react-native-fast-image';
|
||||
|
||||
const STATUS_BUFFER = Platform.select({
|
||||
ios: 3,
|
||||
|
|
@ -29,6 +28,7 @@ type ProfilePictureProps = {
|
|||
statusSize?: number;
|
||||
statusStyle?: StyleProp<ViewProps>;
|
||||
testID?: string;
|
||||
source?: Source | string;
|
||||
};
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
||||
|
|
@ -66,18 +66,13 @@ const ProfilePicture = ({
|
|||
statusSize = 14,
|
||||
statusStyle,
|
||||
testID,
|
||||
source,
|
||||
}: ProfilePictureProps) => {
|
||||
const theme = useTheme();
|
||||
const serverUrl = useServerUrl();
|
||||
const style = getStyleSheet(theme);
|
||||
const buffer = showStatus ? (STATUS_BUFFER || 0) : 0;
|
||||
let client: Client | undefined;
|
||||
|
||||
try {
|
||||
client = NetworkManager.getClient(serverUrl);
|
||||
} catch {
|
||||
// handle below that the client is not set
|
||||
}
|
||||
const style = getStyleSheet(theme);
|
||||
const buffer = showStatus ? STATUS_BUFFER || 0 : 0;
|
||||
|
||||
useEffect(() => {
|
||||
if (author && !author.status && showStatus) {
|
||||
|
|
@ -85,55 +80,43 @@ const ProfilePicture = ({
|
|||
}
|
||||
}, []);
|
||||
|
||||
let statusIcon;
|
||||
let containerStyle: StyleProp<ViewStyle> = {
|
||||
width: size + buffer,
|
||||
height: size + buffer,
|
||||
};
|
||||
const containerStyle = useMemo(() => {
|
||||
if (author) {
|
||||
return {
|
||||
width: size + (buffer - 1),
|
||||
height: size + (buffer - 1),
|
||||
backgroundColor: changeOpacity(theme.centerChannelColor, 0.08),
|
||||
borderRadius: (size + buffer) / 2,
|
||||
};
|
||||
}
|
||||
|
||||
if (author?.status && !author.isBot && showStatus) {
|
||||
statusIcon = (
|
||||
<View style={[style.statusWrapper, statusStyle, {borderRadius: statusSize / 2}]}>
|
||||
<UserStatus
|
||||
size={statusSize}
|
||||
status={author.status}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
let image;
|
||||
if (author && client) {
|
||||
const pictureUrl = client.getProfilePictureUrl(author.id, author.lastPictureUpdate);
|
||||
image = (
|
||||
<FastImage
|
||||
key={pictureUrl}
|
||||
style={{width: size, height: size, borderRadius: (size / 2)}}
|
||||
source={{uri: `${serverUrl}${pictureUrl}`}}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
containerStyle = {
|
||||
width: size + (buffer - 1),
|
||||
height: size + (buffer - 1),
|
||||
backgroundColor: changeOpacity(theme.centerChannelColor, 0.08),
|
||||
return {
|
||||
...style.container,
|
||||
width: size + buffer,
|
||||
height: size + buffer,
|
||||
borderRadius: (size + buffer) / 2,
|
||||
};
|
||||
image = (
|
||||
<CompassIcon
|
||||
name='account-outline'
|
||||
size={iconSize || size}
|
||||
style={style.icon}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}, [author, size]);
|
||||
|
||||
return (
|
||||
<View
|
||||
style={[style.container, containerStyle]}
|
||||
style={containerStyle}
|
||||
testID={`${testID}.${author?.id}`}
|
||||
>
|
||||
{image}
|
||||
{statusIcon}
|
||||
<Image
|
||||
author={author}
|
||||
iconSize={iconSize}
|
||||
size={size}
|
||||
source={source}
|
||||
/>
|
||||
{showStatus &&
|
||||
<Status
|
||||
author={author}
|
||||
statusSize={statusSize}
|
||||
statusStyle={statusStyle}
|
||||
theme={theme}
|
||||
/>
|
||||
}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
57
app/components/profile_picture/status.tsx
Normal file
57
app/components/profile_picture/status.tsx
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useMemo} from 'react';
|
||||
import {StyleProp, View, ViewProps} from 'react-native';
|
||||
|
||||
import UserStatus from '@components/user_status';
|
||||
import {makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
||||
import type UserModel from '@typings/database/models/servers/user';
|
||||
|
||||
type Props = {
|
||||
author?: UserModel;
|
||||
statusSize: number;
|
||||
statusStyle?: StyleProp<ViewProps>;
|
||||
theme: Theme;
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
||||
return {
|
||||
statusWrapper: {
|
||||
position: 'absolute',
|
||||
bottom: 0,
|
||||
right: 0,
|
||||
overflow: 'hidden',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
backgroundColor: theme.centerChannelBg,
|
||||
borderWidth: 1,
|
||||
borderColor: theme.centerChannelBg,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const Status = ({author, statusSize, statusStyle, theme}: Props) => {
|
||||
const styles = getStyleSheet(theme);
|
||||
const containerStyle = useMemo(() => ([
|
||||
styles.statusWrapper,
|
||||
statusStyle,
|
||||
{borderRadius: statusSize / 2},
|
||||
]), []);
|
||||
if (author?.status && !author.isBot) {
|
||||
return (
|
||||
<View
|
||||
style={containerStyle}
|
||||
>
|
||||
<UserStatus
|
||||
size={statusSize}
|
||||
status={author.status}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
export default Status;
|
||||
|
|
@ -16,6 +16,7 @@ import Network from './network';
|
|||
import Permissions from './permissions';
|
||||
import Post from './post';
|
||||
import Preferences from './preferences';
|
||||
import Profile from './profile';
|
||||
import Screens from './screens';
|
||||
import Sso from './sso';
|
||||
import SupportedServer from './supported_server';
|
||||
|
|
@ -38,6 +39,7 @@ export {
|
|||
Permissions,
|
||||
Post,
|
||||
Preferences,
|
||||
Profile,
|
||||
Screens,
|
||||
SupportedServer,
|
||||
Sso,
|
||||
|
|
|
|||
10
app/constants/profile.ts
Normal file
10
app/constants/profile.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
export const ACCOUNT_OUTLINE_IMAGE = 'account-outline';
|
||||
export const USER_PROFILE_PICTURE_SIZE = 153;
|
||||
|
||||
export default {
|
||||
ACCOUNT_OUTLINE_IMAGE,
|
||||
USER_PROFILE_PICTURE_SIZE,
|
||||
};
|
||||
49
app/screens/edit_profile/components/disabled_fields.tsx
Normal file
49
app/screens/edit_profile/components/disabled_fields.tsx
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useMemo} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {Text, View} from 'react-native';
|
||||
|
||||
import {useTheme} from '@app/context/theme';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
import {typography} from '@utils/typography';
|
||||
|
||||
type Props = {
|
||||
isTablet?: boolean;
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
||||
return {
|
||||
text: {
|
||||
...typography('Body', 75),
|
||||
color: changeOpacity(theme.centerChannelColor, 0.5),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const DisabledFields = ({isTablet}: Props) => {
|
||||
const {formatMessage} = useIntl();
|
||||
const theme = useTheme();
|
||||
const styles = getStyleSheet(theme);
|
||||
|
||||
const containerStyle = useMemo(() => ({
|
||||
paddingHorizontal: isTablet ? 42 : 20,
|
||||
marginBottom: 16,
|
||||
}), [isTablet]);
|
||||
|
||||
return (
|
||||
<View
|
||||
style={containerStyle}
|
||||
>
|
||||
<Text style={styles.text}>
|
||||
{formatMessage({
|
||||
id: 'user.settings.general.field_handled_externally',
|
||||
defaultMessage: 'Some fields below are handled through your login provider. If you want to change them, you’ll need to do so through your login provider.',
|
||||
})}
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default DisabledFields;
|
||||
129
app/screens/edit_profile/components/edit_profile_picture.tsx
Normal file
129
app/screens/edit_profile/components/edit_profile_picture.tsx
Normal file
|
|
@ -0,0 +1,129 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback, useMemo, useState} from 'react';
|
||||
import {DeviceEventEmitter, Platform, View} from 'react-native';
|
||||
|
||||
import {Client} from '@client/rest';
|
||||
import ProfileImage from '@components/profile_picture';
|
||||
import {Navigation} from '@constants';
|
||||
import {ACCOUNT_OUTLINE_IMAGE} from '@constants/profile';
|
||||
import {useServerUrl} from '@context/server';
|
||||
import {useTheme} from '@context/theme';
|
||||
import useDidUpdate from '@hooks/did_update';
|
||||
import NetworkManager from '@init/network_manager';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
||||
import ProfileImagePicker from './profile_image_picker';
|
||||
|
||||
import type UserModel from '@typings/database/models/servers/user';
|
||||
|
||||
type ChangeProfilePictureProps = {
|
||||
user: UserModel;
|
||||
onUpdateProfilePicture: (info: { localPath?: string; isRemoved?: boolean }) => void;
|
||||
};
|
||||
|
||||
const SIZE = 128;
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
||||
return {
|
||||
container: {
|
||||
alignItems: 'center',
|
||||
backgroundColor: changeOpacity(theme.centerChannelColor, 0.08),
|
||||
borderRadius: SIZE / 2,
|
||||
height: SIZE,
|
||||
justifyContent: 'center',
|
||||
width: SIZE,
|
||||
},
|
||||
camera: {
|
||||
position: 'absolute',
|
||||
overflow: 'hidden',
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const EditProfilePicture = ({user, onUpdateProfilePicture}: ChangeProfilePictureProps) => {
|
||||
const serverUrl = useServerUrl();
|
||||
const theme = useTheme();
|
||||
|
||||
let client: Client | undefined;
|
||||
|
||||
try {
|
||||
client = NetworkManager.getClient(serverUrl);
|
||||
} catch {
|
||||
// does nothing
|
||||
}
|
||||
|
||||
const [pictureUrl, setPictureUrl] = useState<string|undefined>(client?.getProfilePictureUrl(user.id, user.lastPictureUpdate));
|
||||
|
||||
const styles = getStyleSheet(theme);
|
||||
|
||||
useDidUpdate(() => {
|
||||
const url = user.id && client ? client.getProfilePictureUrl(user.id, user.lastPictureUpdate) : undefined;
|
||||
if (url !== pictureUrl) {
|
||||
setPictureUrl(url);
|
||||
}
|
||||
}, [user.id, user.lastPictureUpdate]);
|
||||
|
||||
const handleProfileImage = useCallback((images?: FileInfo[]) => {
|
||||
let isRemoved = true;
|
||||
let localPath;
|
||||
let pUrl = ACCOUNT_OUTLINE_IMAGE;
|
||||
|
||||
const newImage = images?.[0]?.localPath;
|
||||
if (newImage) {
|
||||
isRemoved = false;
|
||||
localPath = newImage;
|
||||
pUrl = newImage;
|
||||
}
|
||||
|
||||
setPictureUrl(pUrl);
|
||||
onUpdateProfilePicture({isRemoved, localPath});
|
||||
DeviceEventEmitter.emit(Navigation.NAVIGATION_CLOSE_MODAL);
|
||||
}, [onUpdateProfilePicture]);
|
||||
|
||||
const pictureSource = useMemo(() => {
|
||||
if (pictureUrl === ACCOUNT_OUTLINE_IMAGE) {
|
||||
return pictureUrl;
|
||||
} else if (pictureUrl) {
|
||||
let prefix = '';
|
||||
if (pictureUrl.includes('/api/')) {
|
||||
prefix = serverUrl;
|
||||
} else if (Platform.OS === 'android' && !pictureUrl.startsWith('content://') && !pictureUrl.startsWith('http://') && !pictureUrl.startsWith('https://')) {
|
||||
prefix = 'file://';
|
||||
}
|
||||
|
||||
return {
|
||||
uri: `${prefix}${pictureUrl}`,
|
||||
};
|
||||
}
|
||||
return undefined;
|
||||
}, [pictureUrl]);
|
||||
|
||||
return (
|
||||
<View
|
||||
style={styles.container}
|
||||
testID={`${EditProfilePicture}.${user.id}`}
|
||||
>
|
||||
<ProfileImage
|
||||
size={SIZE}
|
||||
source={pictureSource}
|
||||
author={user}
|
||||
showStatus={false}
|
||||
/>
|
||||
<View
|
||||
style={styles.camera}
|
||||
>
|
||||
<ProfileImagePicker
|
||||
onRemoveProfileImage={handleProfileImage}
|
||||
uploadFiles={handleProfileImage}
|
||||
user={user}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default EditProfilePicture;
|
||||
240
app/screens/edit_profile/components/form.tsx
Normal file
240
app/screens/edit_profile/components/form.tsx
Normal file
|
|
@ -0,0 +1,240 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback, useMemo, useRef} from 'react';
|
||||
import {MessageDescriptor, useIntl} from 'react-intl';
|
||||
import {Keyboard, StyleSheet, View} from 'react-native';
|
||||
|
||||
import {FloatingTextInputRef} from '@app/components/floating_text_input_label';
|
||||
import {t} from '@app/i18n';
|
||||
import {useTheme} from '@context/theme';
|
||||
|
||||
import DisabledFields from './disabled_fields';
|
||||
import EmailField from './email_field';
|
||||
import Field from './field';
|
||||
|
||||
import type UserModel from '@typings/database/models/servers/user';
|
||||
import type {FieldConfig, FieldSequence, UserInfo} from '@typings/screens/edit_profile';
|
||||
|
||||
type Props = {
|
||||
canSave: boolean;
|
||||
currentUser: UserModel;
|
||||
isTablet?: boolean;
|
||||
lockedFirstName: boolean;
|
||||
lockedLastName: boolean;
|
||||
lockedNickname: boolean;
|
||||
lockedPosition: boolean;
|
||||
onUpdateField: (fieldKey: string, name: string) => void;
|
||||
userInfo: UserInfo;
|
||||
submitUser: () => void;
|
||||
}
|
||||
|
||||
const includesSsoService = (sso: string) => ['gitlab', 'google', 'office365'].includes(sso);
|
||||
const isSAMLOrLDAP = (protocol: string) => ['ldap', 'saml'].includes(protocol);
|
||||
|
||||
const FIELDS: { [id: string]: MessageDescriptor } = {
|
||||
firstName: {
|
||||
id: t('user.settings.general.firstName'),
|
||||
defaultMessage: 'First Name',
|
||||
},
|
||||
lastName: {
|
||||
id: t('user.settings.general.lastName'),
|
||||
defaultMessage: 'Last Name',
|
||||
},
|
||||
username: {
|
||||
id: t('user.settings.general.username'),
|
||||
defaultMessage: 'Username',
|
||||
},
|
||||
nickname: {
|
||||
id: t('user.settings.general.nickname'),
|
||||
defaultMessage: 'Nickname',
|
||||
},
|
||||
position: {
|
||||
id: t('user.settings.general.position'),
|
||||
defaultMessage: 'Position',
|
||||
},
|
||||
email: {
|
||||
id: t('user.settings.general.email'),
|
||||
defaultMessage: 'Email',
|
||||
},
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
footer: {
|
||||
height: 40,
|
||||
width: '100%',
|
||||
},
|
||||
separator: {
|
||||
height: 15,
|
||||
},
|
||||
});
|
||||
|
||||
const ProfileForm = ({
|
||||
canSave, currentUser, isTablet,
|
||||
lockedFirstName, lockedLastName, lockedNickname, lockedPosition,
|
||||
onUpdateField, userInfo, submitUser,
|
||||
}: Props) => {
|
||||
const theme = useTheme();
|
||||
const {formatMessage} = useIntl();
|
||||
const firstNameRef = useRef<FloatingTextInputRef>(null);
|
||||
const lastNameRef = useRef<FloatingTextInputRef>(null);
|
||||
const usernameRef = useRef<FloatingTextInputRef>(null);
|
||||
const emailRef = useRef<FloatingTextInputRef>(null);
|
||||
const nicknameRef = useRef<FloatingTextInputRef>(null);
|
||||
const positionRef = useRef<FloatingTextInputRef>(null);
|
||||
|
||||
const userProfileFields: FieldSequence = useMemo(() => {
|
||||
const service = currentUser.authService;
|
||||
return {
|
||||
firstName: {
|
||||
ref: firstNameRef,
|
||||
isDisabled: (isSAMLOrLDAP(service) && lockedFirstName) || includesSsoService(service),
|
||||
},
|
||||
lastName: {
|
||||
ref: lastNameRef,
|
||||
isDisabled: (isSAMLOrLDAP(service) && lockedLastName) || includesSsoService(service),
|
||||
},
|
||||
username: {
|
||||
ref: usernameRef,
|
||||
isDisabled: service !== '',
|
||||
},
|
||||
email: {
|
||||
ref: emailRef,
|
||||
isDisabled: true,
|
||||
},
|
||||
nickname: {
|
||||
ref: nicknameRef,
|
||||
isDisabled: isSAMLOrLDAP(service) && lockedNickname,
|
||||
},
|
||||
position: {
|
||||
ref: positionRef,
|
||||
isDisabled: isSAMLOrLDAP(service) && lockedPosition,
|
||||
},
|
||||
};
|
||||
}, [lockedFirstName, lockedLastName, lockedNickname, lockedPosition, currentUser.authService]);
|
||||
|
||||
const onFocusNextField = useCallback(((fieldKey: string) => {
|
||||
const findNextField = () => {
|
||||
const fields = Object.keys(userProfileFields);
|
||||
const curIndex = fields.indexOf(fieldKey);
|
||||
const searchIndex = curIndex + 1;
|
||||
|
||||
if (curIndex === -1 || searchIndex > fields.length) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const remainingFields = fields.slice(searchIndex);
|
||||
|
||||
const nextFieldIndex = remainingFields.findIndex((f: string) => {
|
||||
const field = userProfileFields[f];
|
||||
return !field.isDisabled;
|
||||
});
|
||||
|
||||
if (nextFieldIndex === -1) {
|
||||
return {isLastEnabledField: true, nextField: undefined};
|
||||
}
|
||||
|
||||
const fieldName = remainingFields[nextFieldIndex];
|
||||
|
||||
return {isLastEnabledField: false, nextField: userProfileFields[fieldName]};
|
||||
};
|
||||
|
||||
const next = findNextField();
|
||||
if (next?.isLastEnabledField && canSave) {
|
||||
// performs form submission
|
||||
Keyboard.dismiss();
|
||||
submitUser();
|
||||
} else if (next?.nextField) {
|
||||
next?.nextField?.ref?.current?.focus();
|
||||
} else {
|
||||
Keyboard.dismiss();
|
||||
}
|
||||
}), [canSave, userProfileFields]);
|
||||
|
||||
const hasDisabledFields = Object.values(userProfileFields).filter((field) => field.isDisabled).length > 0;
|
||||
|
||||
const fieldConfig: FieldConfig = {
|
||||
blurOnSubmit: false,
|
||||
enablesReturnKeyAutomatically: true,
|
||||
onFocusNextField,
|
||||
onTextChange: onUpdateField,
|
||||
returnKeyType: 'next',
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{hasDisabledFields && <DisabledFields isTablet={isTablet}/>}
|
||||
<Field
|
||||
fieldKey='firstName'
|
||||
fieldRef={firstNameRef}
|
||||
isDisabled={userProfileFields.firstName.isDisabled}
|
||||
label={formatMessage(FIELDS.firstName)}
|
||||
testID='edit_profile.text_setting.firstName'
|
||||
value={userInfo.firstName}
|
||||
{...fieldConfig}
|
||||
/>
|
||||
<View style={styles.separator}/>
|
||||
<Field
|
||||
fieldKey='lastName'
|
||||
fieldRef={lastNameRef}
|
||||
isDisabled={userProfileFields.lastName.isDisabled}
|
||||
label={formatMessage(FIELDS.lastName)}
|
||||
testID='edit_profile.text_setting.lastName'
|
||||
value={userInfo.lastName}
|
||||
{...fieldConfig}
|
||||
/>
|
||||
<View style={styles.separator}/>
|
||||
<Field
|
||||
fieldKey='username'
|
||||
fieldRef={usernameRef}
|
||||
isDisabled={userProfileFields.username.isDisabled}
|
||||
label={formatMessage(FIELDS.username)}
|
||||
maxLength={22}
|
||||
testID='edit_profile.text_setting.username'
|
||||
value={userInfo.username}
|
||||
{...fieldConfig}
|
||||
/>
|
||||
<View style={styles.separator}/>
|
||||
{userInfo.email && (
|
||||
<EmailField
|
||||
authService={currentUser.authService}
|
||||
isDisabled={userProfileFields.email.isDisabled}
|
||||
email={userInfo.email}
|
||||
label={formatMessage(FIELDS.email)}
|
||||
fieldRef={emailRef}
|
||||
onChange={onUpdateField}
|
||||
onFocusNextField={onFocusNextField}
|
||||
theme={theme}
|
||||
isTablet={Boolean(isTablet)}
|
||||
/>
|
||||
)}
|
||||
<View style={styles.separator}/>
|
||||
<Field
|
||||
fieldKey='nickname'
|
||||
fieldRef={nicknameRef}
|
||||
isDisabled={userProfileFields.nickname.isDisabled}
|
||||
label={formatMessage(FIELDS.nickname)}
|
||||
maxLength={22}
|
||||
testID='edit_profile.text_setting.nickname'
|
||||
value={userInfo.nickname}
|
||||
{...fieldConfig}
|
||||
/>
|
||||
<View style={styles.separator}/>
|
||||
<Field
|
||||
fieldKey='position'
|
||||
fieldRef={positionRef}
|
||||
isDisabled={userProfileFields.position.isDisabled}
|
||||
isOptional={true}
|
||||
label={formatMessage(FIELDS.position)}
|
||||
maxLength={128}
|
||||
{...fieldConfig}
|
||||
returnKeyType='done'
|
||||
testID='edit_profile.text_setting.position'
|
||||
value={userInfo.position}
|
||||
/>
|
||||
<View style={styles.footer}/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProfileForm;
|
||||
74
app/screens/edit_profile/components/panel_item.tsx
Normal file
74
app/screens/edit_profile/components/panel_item.tsx
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useMemo} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import DocumentPicker from 'react-native-document-picker';
|
||||
|
||||
import SlideUpPanelItem from '@components/slide_up_panel_item';
|
||||
|
||||
import type {MessageDescriptor} from '@formatjs/intl/src/types';
|
||||
import type {UploadExtractedFile} from '@typings/utils/file';
|
||||
import type PickerUtil from '@utils/file/file_picker';
|
||||
import type {Source} from 'react-native-fast-image';
|
||||
|
||||
type PanelType = {
|
||||
icon: string | Source;
|
||||
onPress: () => Promise<void> | void;
|
||||
testID: string;
|
||||
text: MessageDescriptor;
|
||||
isDestructive?: boolean;
|
||||
}
|
||||
|
||||
type PanelItemProps = {
|
||||
onRemoveProfileImage?: UploadExtractedFile;
|
||||
pickerAction: 'takePhoto' | 'browsePhotoLibrary' | 'browseFiles' | 'removeProfilePicture';
|
||||
pictureUtils?: PickerUtil;
|
||||
};
|
||||
|
||||
const PanelItem = ({pickerAction, pictureUtils, onRemoveProfileImage}: PanelItemProps) => {
|
||||
const intl = useIntl();
|
||||
|
||||
const panelTypes = useMemo(() => ({
|
||||
takePhoto: {
|
||||
icon: 'camera-outline',
|
||||
onPress: () => pictureUtils?.attachFileFromCamera(),
|
||||
testID: 'attachment.takePhoto',
|
||||
text: {id: 'mobile.file_upload.camera_photo', defaultMessage: 'Take Photo'},
|
||||
},
|
||||
browsePhotoLibrary: {
|
||||
icon: 'file-generic-outline',
|
||||
onPress: () => pictureUtils?.attachFileFromPhotoGallery(),
|
||||
testID: 'attachment.browsePhotoLibrary',
|
||||
text: {id: 'mobile.file_upload.library', defaultMessage: 'Photo Library'},
|
||||
},
|
||||
browseFiles: {
|
||||
icon: 'file-multiple-outline',
|
||||
onPress: () => pictureUtils?.attachFileFromFiles(DocumentPicker.types.images),
|
||||
testID: 'attachment.browseFiles',
|
||||
text: {id: 'mobile.file_upload.browse', defaultMessage: 'Browse Files'},
|
||||
},
|
||||
removeProfilePicture: {
|
||||
icon: 'trash-can-outline',
|
||||
onPress: () => {
|
||||
return onRemoveProfileImage && onRemoveProfileImage();
|
||||
},
|
||||
testID: 'attachment.removeImage',
|
||||
text: {id: 'mobile.edit_profile.remove_profile_photo', defaultMessage: 'Remove Photo'},
|
||||
},
|
||||
}), [pictureUtils, onRemoveProfileImage]);
|
||||
|
||||
const item: PanelType = panelTypes[pickerAction];
|
||||
|
||||
return (
|
||||
<SlideUpPanelItem
|
||||
icon={item.icon}
|
||||
onPress={item.onPress}
|
||||
testID={item.testID}
|
||||
text={intl.formatMessage(item.text)}
|
||||
destructive={pickerAction === 'removeProfilePicture'}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default PanelItem;
|
||||
133
app/screens/edit_profile/components/profile_image_picker.tsx
Normal file
133
app/screens/edit_profile/components/profile_image_picker.tsx
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback, useMemo} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {TouchableOpacity} from 'react-native';
|
||||
|
||||
import {Client} from '@client/rest';
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import {useServerUrl} from '@context/server';
|
||||
import {useTheme} from '@context/theme';
|
||||
import NetworkManager from '@init/network_manager';
|
||||
import PanelItem from '@screens/edit_profile/components/panel_item';
|
||||
import {bottomSheet} from '@screens/navigation';
|
||||
import PickerUtil from '@utils/file/file_picker';
|
||||
import {preventDoubleTap} from '@utils/tap';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
||||
import type UserModel from '@typings/database/models/servers/user';
|
||||
import type {UploadExtractedFile} from '@typings/utils/file';
|
||||
|
||||
const hitSlop = {top: 100, bottom: 20, right: 20, left: 100};
|
||||
const ACTION_HEIGHT = 55;
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
||||
return {
|
||||
touchable: {
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
borderWidth: 1,
|
||||
borderColor: theme.centerChannelBg,
|
||||
borderRadius: 18,
|
||||
height: 36,
|
||||
width: 36,
|
||||
position: 'absolute',
|
||||
bottom: 0,
|
||||
right: 0,
|
||||
backgroundColor: theme.centerChannelBg,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
type ImagePickerProps = {
|
||||
onRemoveProfileImage: UploadExtractedFile;
|
||||
uploadFiles: UploadExtractedFile;
|
||||
user: UserModel;
|
||||
};
|
||||
|
||||
const hasPictureUrl = (user: UserModel, serverUrl: string) => {
|
||||
const {id, lastPictureUpdate} = user;
|
||||
|
||||
let client: Client | undefined;
|
||||
let profileImageUrl: string | undefined;
|
||||
|
||||
try {
|
||||
client = NetworkManager.getClient(serverUrl);
|
||||
profileImageUrl = client.getProfilePictureUrl(id, lastPictureUpdate);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if image url includes query string for timestamp. If so,
|
||||
// it means the image has been updated from the default, i.e. '.../image?_=1544159746868'
|
||||
return Boolean(profileImageUrl?.includes('image?_'));
|
||||
};
|
||||
|
||||
const ProfileImagePicker = ({
|
||||
onRemoveProfileImage,
|
||||
uploadFiles,
|
||||
user,
|
||||
}: ImagePickerProps) => {
|
||||
const theme = useTheme();
|
||||
const intl = useIntl();
|
||||
const serverUrl = useServerUrl();
|
||||
const pictureUtils = useMemo(() => new PickerUtil(intl, uploadFiles), [uploadFiles, intl]);
|
||||
const canRemovePicture = hasPictureUrl(user, serverUrl);
|
||||
const styles = getStyleSheet(theme);
|
||||
|
||||
const showFileAttachmentOptions = useCallback(preventDoubleTap(() => {
|
||||
const renderContent = () => {
|
||||
return (
|
||||
<>
|
||||
<PanelItem
|
||||
pickerAction='takePhoto'
|
||||
pictureUtils={pictureUtils}
|
||||
/>
|
||||
<PanelItem
|
||||
pickerAction='browsePhotoLibrary'
|
||||
pictureUtils={pictureUtils}
|
||||
/>
|
||||
<PanelItem
|
||||
pickerAction='browseFiles'
|
||||
pictureUtils={pictureUtils}
|
||||
|
||||
/>
|
||||
{canRemovePicture && (
|
||||
<PanelItem
|
||||
pickerAction='removeProfilePicture'
|
||||
onRemoveProfileImage={onRemoveProfileImage}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const snapPoints = canRemovePicture ? 5 : 4;
|
||||
|
||||
return bottomSheet({
|
||||
closeButtonId: 'close-edit-profile',
|
||||
renderContent,
|
||||
snapPoints: [(snapPoints * ACTION_HEIGHT), 10],
|
||||
title: '',
|
||||
theme,
|
||||
});
|
||||
}), [canRemovePicture, onRemoveProfileImage, pictureUtils, theme]);
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
onPress={showFileAttachmentOptions}
|
||||
hitSlop={hitSlop}
|
||||
style={styles.touchable}
|
||||
>
|
||||
<CompassIcon
|
||||
name='camera-outline'
|
||||
size={24}
|
||||
color={changeOpacity(theme.centerChannelColor, 0.6)}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
|
||||
);
|
||||
};
|
||||
|
||||
export default ProfileImagePicker;
|
||||
35
app/screens/edit_profile/components/updating.tsx
Normal file
35
app/screens/edit_profile/components/updating.tsx
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {StyleSheet, View} from 'react-native';
|
||||
|
||||
import Loading from '@components/loading';
|
||||
import {useTheme} from '@context/theme';
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
spinner: {
|
||||
position: 'absolute',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
zIndex: 1000,
|
||||
},
|
||||
});
|
||||
|
||||
const Updating = () => {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<View
|
||||
style={styles.spinner}
|
||||
>
|
||||
<Loading
|
||||
color={theme.buttonBg}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default Updating;
|
||||
39
app/screens/edit_profile/components/user_profile_picture.tsx
Normal file
39
app/screens/edit_profile/components/user_profile_picture.tsx
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import ProfilePicture from '@components/profile_picture';
|
||||
import {USER_PROFILE_PICTURE_SIZE} from '@constants/profile';
|
||||
|
||||
import EditProfilePicture from './edit_profile_picture';
|
||||
|
||||
import type UserModel from '@typings/database/models/servers/user';
|
||||
import type {NewProfileImage} from '@typings/screens/edit_profile';
|
||||
|
||||
type Props = {
|
||||
currentUser: UserModel;
|
||||
lockedPicture: boolean;
|
||||
onUpdateProfilePicture: (newProfileImage: NewProfileImage) => void;
|
||||
}
|
||||
|
||||
const UserProfilePicture = ({currentUser, lockedPicture, onUpdateProfilePicture}: Props) => {
|
||||
if (lockedPicture) {
|
||||
return (
|
||||
<ProfilePicture
|
||||
author={currentUser}
|
||||
size={USER_PROFILE_PICTURE_SIZE}
|
||||
showStatus={false}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<EditProfilePicture
|
||||
onUpdateProfilePicture={onUpdateProfilePicture}
|
||||
user={currentUser}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default UserProfilePicture;
|
||||
|
|
@ -3,126 +3,56 @@
|
|||
|
||||
import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {BackHandler, DeviceEventEmitter, Keyboard, Platform, Text, View} from 'react-native';
|
||||
import {BackHandler, DeviceEventEmitter, Keyboard, Platform, StyleSheet, View} from 'react-native';
|
||||
import {KeyboardAwareScrollView} from 'react-native-keyboard-aware-scroll-view';
|
||||
import {Navigation} from 'react-native-navigation';
|
||||
import {Edge, SafeAreaView} from 'react-native-safe-area-context';
|
||||
|
||||
import {updateMe} from '@actions/remote/user';
|
||||
import {updateLocalUser} from '@actions/local/user';
|
||||
import {setDefaultProfileImage, updateMe, uploadUserProfileImage} from '@actions/remote/user';
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import {FloatingTextInputRef} from '@components/floating_text_input_label';
|
||||
import Loading from '@components/loading';
|
||||
import ProfilePicture from '@components/profile_picture';
|
||||
import TabletTitle from '@components/tablet_title';
|
||||
import {Events} from '@constants';
|
||||
import {useServerUrl} from '@context/server';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {t} from '@i18n';
|
||||
import {dismissModal, popTopScreen, setButtons} from '@screens/navigation';
|
||||
import {EditProfileProps, FieldConfig, FieldSequence, UserInfo} from '@typings/screens/edit_profile';
|
||||
import {preventDoubleTap} from '@utils/tap';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
import {typography} from '@utils/typography';
|
||||
|
||||
import EmailField from './components/email_field';
|
||||
import Field from './components/field';
|
||||
import ProfileForm from './components/form';
|
||||
import ProfileError from './components/profile_error';
|
||||
import Updating from './components/updating';
|
||||
import UserProfilePicture from './components/user_profile_picture';
|
||||
|
||||
import type {MessageDescriptor} from '@formatjs/intl/src/types';
|
||||
import type {EditProfileProps, NewProfileImage, UserInfo} from '@typings/screens/edit_profile';
|
||||
import type {ErrorText} from '@typings/utils/file';
|
||||
|
||||
const edges: Edge[] = ['bottom', 'left', 'right'];
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
return {
|
||||
flex: {
|
||||
flex: 1,
|
||||
},
|
||||
top: {
|
||||
marginVertical: 32,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
separator: {
|
||||
height: 15,
|
||||
},
|
||||
footer: {
|
||||
height: 40,
|
||||
width: '100%',
|
||||
},
|
||||
spinner: {
|
||||
position: 'absolute',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
zIndex: 1000,
|
||||
},
|
||||
description: {
|
||||
alignSelf: 'center',
|
||||
marginBottom: 24,
|
||||
},
|
||||
text: {
|
||||
...typography('Body', 75),
|
||||
color: changeOpacity(theme.centerChannelColor, 0.5),
|
||||
},
|
||||
};
|
||||
const styles = StyleSheet.create({
|
||||
flex: {
|
||||
flex: 1,
|
||||
},
|
||||
top: {
|
||||
marginVertical: 32,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
});
|
||||
|
||||
const FIELDS: { [id: string]: MessageDescriptor } = {
|
||||
firstName: {
|
||||
id: t('user.settings.general.firstName'),
|
||||
defaultMessage: 'First Name',
|
||||
},
|
||||
lastName: {
|
||||
id: t('user.settings.general.lastName'),
|
||||
defaultMessage: 'Last Name',
|
||||
},
|
||||
username: {
|
||||
id: t('user.settings.general.username'),
|
||||
defaultMessage: 'Username',
|
||||
},
|
||||
nickname: {
|
||||
id: t('user.settings.general.nickname'),
|
||||
defaultMessage: 'Nickname',
|
||||
},
|
||||
position: {
|
||||
id: t('user.settings.general.position'),
|
||||
defaultMessage: 'Position',
|
||||
},
|
||||
email: {
|
||||
id: t('user.settings.general.email'),
|
||||
defaultMessage: 'Email',
|
||||
},
|
||||
};
|
||||
|
||||
const CLOSE_BUTTON_ID = 'close-edit-profile';
|
||||
const UPDATE_BUTTON_ID = 'update-profile';
|
||||
|
||||
const includesSsoService = (sso: string) => ['gitlab', 'google', 'office365'].includes(sso);
|
||||
const isSAMLOrLDAP = (protocol: string) => ['ldap', 'saml'].includes(protocol);
|
||||
|
||||
const EditProfile = ({
|
||||
componentId,
|
||||
currentUser,
|
||||
isModal,
|
||||
isTablet,
|
||||
lockedFirstName,
|
||||
lockedLastName,
|
||||
lockedNickname,
|
||||
lockedPosition,
|
||||
componentId, currentUser, isModal, isTablet,
|
||||
lockedFirstName, lockedLastName, lockedNickname, lockedPosition, lockedPicture,
|
||||
}: EditProfileProps) => {
|
||||
const intl = useIntl();
|
||||
const serverUrl = useServerUrl();
|
||||
const theme = useTheme();
|
||||
const keyboardAwareRef = useRef<KeyboardAwareScrollView>();
|
||||
const firstNameRef = useRef<FloatingTextInputRef>(null);
|
||||
const lastNameRef = useRef<FloatingTextInputRef>(null);
|
||||
const usernameRef = useRef<FloatingTextInputRef>(null);
|
||||
const emailRef = useRef<FloatingTextInputRef>(null);
|
||||
const nicknameRef = useRef<FloatingTextInputRef>(null);
|
||||
const positionRef = useRef<FloatingTextInputRef>(null);
|
||||
|
||||
const styles = getStyleSheet(theme);
|
||||
const changedProfilePicture = useRef<NewProfileImage | undefined>(undefined);
|
||||
const scrollViewRef = useRef<KeyboardAwareScrollView>();
|
||||
const hasUpdateUserInfo = useRef<boolean>(false);
|
||||
const [userInfo, setUserInfo] = useState<UserInfo>({
|
||||
email: currentUser.email,
|
||||
firstName: currentUser.firstName,
|
||||
|
|
@ -135,8 +65,6 @@ const EditProfile = ({
|
|||
const [error, setError] = useState<ErrorText | undefined>();
|
||||
const [updating, setUpdating] = useState(false);
|
||||
|
||||
const scrollViewRef = useRef<KeyboardAwareScrollView>();
|
||||
|
||||
const buttonText = intl.formatMessage({id: 'mobile.account.settings.save', defaultMessage: 'Save'});
|
||||
const rightButton = useMemo(() => {
|
||||
return isTablet ? null : {
|
||||
|
|
@ -149,15 +77,13 @@ const EditProfile = ({
|
|||
};
|
||||
}, [isTablet, theme.sidebarHeaderTextColor]);
|
||||
|
||||
const service = currentUser.authService;
|
||||
|
||||
const leftButton = useMemo(() => {
|
||||
return isTablet ? null : {
|
||||
id: CLOSE_BUTTON_ID,
|
||||
icon: CompassIcon.getImageSourceSync('close', 24, theme.centerChannelColor),
|
||||
testID: CLOSE_BUTTON_ID,
|
||||
};
|
||||
}, [isTablet, theme.sidebarHeaderTextColor]);
|
||||
}, [isTablet, theme.centerChannelColor]);
|
||||
|
||||
useEffect(() => {
|
||||
const unsubscribe = Navigation.events().registerComponentListener({
|
||||
|
|
@ -219,12 +145,29 @@ const EditProfile = ({
|
|||
setCanSave(value);
|
||||
}, [componentId, rightButton]);
|
||||
|
||||
const onUpdateProfilePicture = useCallback((newProfileImage: NewProfileImage) => {
|
||||
changedProfilePicture.current = newProfileImage;
|
||||
enableSaveButton(true);
|
||||
}, [enableSaveButton]);
|
||||
|
||||
const onUpdateField = useCallback((fieldKey: string, name: string) => {
|
||||
const update = {...userInfo};
|
||||
update[fieldKey] = name;
|
||||
setUserInfo(update);
|
||||
|
||||
// @ts-expect-error access object property by string key
|
||||
const currentValue = currentUser[fieldKey];
|
||||
const didChange = currentValue !== name;
|
||||
hasUpdateUserInfo.current = currentValue !== name;
|
||||
enableSaveButton(didChange);
|
||||
}, [userInfo, currentUser, enableSaveButton]);
|
||||
|
||||
const submitUser = useCallback(preventDoubleTap(async () => {
|
||||
enableSaveButton(false);
|
||||
setError(undefined);
|
||||
setUpdating(true);
|
||||
try {
|
||||
const partialUser: Partial<UserProfile> = {
|
||||
const newUserInfo: Partial<UserProfile> = {
|
||||
email: userInfo.email,
|
||||
first_name: userInfo.firstName,
|
||||
last_name: userInfo.lastName,
|
||||
|
|
@ -232,15 +175,30 @@ const EditProfile = ({
|
|||
position: userInfo.position,
|
||||
username: userInfo.username,
|
||||
};
|
||||
const localPath = changedProfilePicture.current?.localPath;
|
||||
const profileImageRemoved = changedProfilePicture.current?.isRemoved;
|
||||
if (localPath) {
|
||||
const now = Date.now();
|
||||
const {error: uploadError} = await uploadUserProfileImage(serverUrl, localPath);
|
||||
if (uploadError) {
|
||||
resetScreen(uploadError as Error);
|
||||
return;
|
||||
}
|
||||
updateLocalUser(serverUrl, {last_picture_update: now});
|
||||
} else if (profileImageRemoved) {
|
||||
await setDefaultProfileImage(serverUrl, currentUser.id);
|
||||
}
|
||||
|
||||
const {error: reqError} = await updateMe(serverUrl, partialUser);
|
||||
|
||||
if (reqError) {
|
||||
resetScreen(reqError as Error);
|
||||
return;
|
||||
if (hasUpdateUserInfo.current) {
|
||||
const {error: reqError} = await updateMe(serverUrl, newUserInfo);
|
||||
if (reqError) {
|
||||
resetScreen(reqError as Error);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
close();
|
||||
return;
|
||||
} catch (e) {
|
||||
resetScreen(e as Error);
|
||||
}
|
||||
|
|
@ -254,94 +212,6 @@ const EditProfile = ({
|
|||
scrollViewRef.current?.scrollToPosition(0, 0, true);
|
||||
}, [enableSaveButton]);
|
||||
|
||||
const updateField = useCallback((fieldKey: string, name: string) => {
|
||||
const update = {...userInfo};
|
||||
update[fieldKey] = name;
|
||||
setUserInfo(update);
|
||||
|
||||
// @ts-expect-error access object property by string key
|
||||
const currentValue = currentUser[fieldKey];
|
||||
const didChange = currentValue !== name;
|
||||
enableSaveButton(didChange);
|
||||
}, [userInfo, currentUser]);
|
||||
|
||||
const userProfileFields: FieldSequence = useMemo(() => {
|
||||
return {
|
||||
firstName: {
|
||||
ref: firstNameRef,
|
||||
isDisabled: (isSAMLOrLDAP(service) && lockedFirstName) || includesSsoService(service),
|
||||
},
|
||||
lastName: {
|
||||
ref: lastNameRef,
|
||||
isDisabled: (isSAMLOrLDAP(service) && lockedLastName) || includesSsoService(service),
|
||||
},
|
||||
username: {
|
||||
ref: usernameRef,
|
||||
isDisabled: service !== '',
|
||||
},
|
||||
email: {
|
||||
ref: emailRef,
|
||||
isDisabled: true,
|
||||
},
|
||||
nickname: {
|
||||
ref: nicknameRef,
|
||||
isDisabled: isSAMLOrLDAP(service) && lockedNickname,
|
||||
},
|
||||
position: {
|
||||
ref: positionRef,
|
||||
isDisabled: isSAMLOrLDAP(service) && lockedPosition,
|
||||
},
|
||||
};
|
||||
}, [lockedFirstName, lockedLastName, lockedNickname, lockedPosition, currentUser.authService]);
|
||||
|
||||
const hasDisabledFields = Object.values(userProfileFields).filter((field) => field.isDisabled).length > 0;
|
||||
|
||||
const onFocusNextField = useCallback(((fieldKey: string) => {
|
||||
const findNextField = () => {
|
||||
const fields = Object.keys(userProfileFields);
|
||||
const curIndex = fields.indexOf(fieldKey);
|
||||
const searchIndex = curIndex + 1;
|
||||
|
||||
if (curIndex === -1 || searchIndex > fields.length) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const remainingFields = fields.slice(searchIndex);
|
||||
|
||||
const nextFieldIndex = remainingFields.findIndex((f: string) => {
|
||||
const field = userProfileFields[f];
|
||||
return !field.isDisabled;
|
||||
});
|
||||
|
||||
if (nextFieldIndex === -1) {
|
||||
return {isLastEnabledField: true, nextField: undefined};
|
||||
}
|
||||
|
||||
const fieldName = remainingFields[nextFieldIndex];
|
||||
|
||||
return {isLastEnabledField: false, nextField: userProfileFields[fieldName]};
|
||||
};
|
||||
|
||||
const next = findNextField();
|
||||
if (next?.isLastEnabledField && canSave) {
|
||||
// performs form submission
|
||||
Keyboard.dismiss();
|
||||
submitUser();
|
||||
} else if (next?.nextField) {
|
||||
next?.nextField?.ref?.current?.focus();
|
||||
} else {
|
||||
Keyboard.dismiss();
|
||||
}
|
||||
}), [canSave, userProfileFields]);
|
||||
|
||||
const fieldConfig: FieldConfig = {
|
||||
blurOnSubmit: false,
|
||||
enablesReturnKeyAutomatically: true,
|
||||
onFocusNextField,
|
||||
onTextChange: updateField,
|
||||
returnKeyType: 'next',
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{isTablet &&
|
||||
|
|
@ -374,108 +244,27 @@ const EditProfile = ({
|
|||
testID='edit_profile.scroll_view'
|
||||
style={styles.flex}
|
||||
>
|
||||
{updating && (
|
||||
<View
|
||||
style={styles.spinner}
|
||||
>
|
||||
<Loading
|
||||
color={theme.buttonBg}
|
||||
/>
|
||||
</View>
|
||||
|
||||
)}
|
||||
{updating && <Updating/>}
|
||||
{Boolean(error) && <ProfileError error={error!}/>}
|
||||
<View style={styles.top}>
|
||||
<ProfilePicture
|
||||
author={currentUser}
|
||||
size={153}
|
||||
showStatus={false}
|
||||
<UserProfilePicture
|
||||
currentUser={currentUser}
|
||||
lockedPicture={lockedPicture}
|
||||
onUpdateProfilePicture={onUpdateProfilePicture}
|
||||
/>
|
||||
</View>
|
||||
{hasDisabledFields && (
|
||||
<View
|
||||
style={{
|
||||
paddingHorizontal: isTablet ? 42 : 20,
|
||||
marginBottom: 16,
|
||||
}}
|
||||
>
|
||||
<Text style={styles.text}>
|
||||
{intl.formatMessage({
|
||||
id: 'user.settings.general.field_handled_externally',
|
||||
defaultMessage: 'Some fields below are handled through your login provider. If you want to change them, you’ll need to do so through your login provider.',
|
||||
})}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
<Field
|
||||
fieldKey='firstName'
|
||||
fieldRef={firstNameRef}
|
||||
isDisabled={userProfileFields.firstName.isDisabled}
|
||||
label={intl.formatMessage(FIELDS.firstName)}
|
||||
testID='edit_profile.text_setting.firstName'
|
||||
value={userInfo.firstName}
|
||||
{...fieldConfig}
|
||||
<ProfileForm
|
||||
canSave={canSave}
|
||||
currentUser={currentUser}
|
||||
isTablet={isTablet}
|
||||
lockedFirstName={lockedFirstName}
|
||||
lockedLastName={lockedLastName}
|
||||
lockedNickname={lockedNickname}
|
||||
lockedPosition={lockedPosition}
|
||||
onUpdateField={onUpdateField}
|
||||
userInfo={userInfo}
|
||||
submitUser={submitUser}
|
||||
/>
|
||||
<View style={styles.separator}/>
|
||||
<Field
|
||||
fieldKey='lastName'
|
||||
fieldRef={lastNameRef}
|
||||
isDisabled={userProfileFields.lastName.isDisabled}
|
||||
label={intl.formatMessage(FIELDS.lastName)}
|
||||
testID='edit_profile.text_setting.lastName'
|
||||
value={userInfo.lastName}
|
||||
{...fieldConfig}
|
||||
/>
|
||||
<View style={styles.separator}/>
|
||||
<Field
|
||||
fieldKey='username'
|
||||
fieldRef={usernameRef}
|
||||
isDisabled={userProfileFields.username.isDisabled}
|
||||
label={intl.formatMessage(FIELDS.username)}
|
||||
maxLength={22}
|
||||
testID='edit_profile.text_setting.username'
|
||||
value={userInfo.username}
|
||||
{...fieldConfig}
|
||||
/>
|
||||
<View style={styles.separator}/>
|
||||
{userInfo.email && (
|
||||
<EmailField
|
||||
authService={currentUser.authService}
|
||||
isDisabled={userProfileFields.email.isDisabled}
|
||||
email={userInfo.email}
|
||||
label={intl.formatMessage(FIELDS.email)}
|
||||
fieldRef={emailRef}
|
||||
onChange={updateField}
|
||||
onFocusNextField={onFocusNextField}
|
||||
theme={theme}
|
||||
isTablet={Boolean(isTablet)}
|
||||
/>
|
||||
)}
|
||||
<View style={styles.separator}/>
|
||||
<Field
|
||||
fieldKey='nickname'
|
||||
fieldRef={nicknameRef}
|
||||
isDisabled={userProfileFields.nickname.isDisabled}
|
||||
label={intl.formatMessage(FIELDS.nickname)}
|
||||
maxLength={22}
|
||||
testID='edit_profile.text_setting.nickname'
|
||||
value={userInfo.nickname}
|
||||
{...fieldConfig}
|
||||
/>
|
||||
<View style={styles.separator}/>
|
||||
<Field
|
||||
fieldKey='position'
|
||||
fieldRef={positionRef}
|
||||
isDisabled={userProfileFields.position.isDisabled}
|
||||
isOptional={true}
|
||||
label={intl.formatMessage(FIELDS.position)}
|
||||
maxLength={128}
|
||||
{...fieldConfig}
|
||||
returnKeyType='done'
|
||||
testID='edit_profile.text_setting.position'
|
||||
value={userInfo.position}
|
||||
/>
|
||||
<View style={styles.footer}/>
|
||||
</KeyboardAwareScrollView>
|
||||
</SafeAreaView>
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import {DeviceEventEmitter, TextStyle} from 'react-native';
|
|||
import DrawerItem from '@components/drawer_item';
|
||||
import FormattedText from '@components/formatted_text';
|
||||
import {Events, Screens} from '@constants';
|
||||
import {ACCOUNT_OUTLINE_IMAGE} from '@constants/profile';
|
||||
import {showModal} from '@screens/navigation';
|
||||
import {preventDoubleTap} from '@utils/tap';
|
||||
|
||||
|
|
@ -40,7 +41,7 @@ const YourProfile = ({isTablet, style, theme}: Props) => {
|
|||
style={style}
|
||||
/>
|
||||
}
|
||||
iconName='account-outline'
|
||||
iconName={ACCOUNT_OUTLINE_IMAGE}
|
||||
onPress={openProfile}
|
||||
separator={false}
|
||||
theme={theme}
|
||||
|
|
|
|||
|
|
@ -4,15 +4,17 @@
|
|||
import {withDatabase} from '@nozbe/watermelondb/DatabaseProvider';
|
||||
import withObservables from '@nozbe/with-observables';
|
||||
import {useRoute} from '@react-navigation/native';
|
||||
import React, {useCallback, useState} from 'react';
|
||||
import React, {useCallback, useEffect, useState} from 'react';
|
||||
import {ScrollView, View} from 'react-native';
|
||||
import Animated, {useAnimatedStyle, withTiming} from 'react-native-reanimated';
|
||||
import {Edge, SafeAreaView, useSafeAreaInsets} from 'react-native-safe-area-context';
|
||||
import {of as of$} from 'rxjs';
|
||||
import {switchMap} from 'rxjs/operators';
|
||||
|
||||
import {fetchStatusInBatch} from '@actions/remote/user';
|
||||
import {View as ViewConstants} from '@constants';
|
||||
import {MM_TABLES, SYSTEM_IDENTIFIERS} from '@constants/database';
|
||||
import {useServerUrl} from '@context/server';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {useIsTablet} from '@hooks/device';
|
||||
import {isCustomStatusExpirySupported, isMinimumServerVersion} from '@utils/helpers';
|
||||
|
|
@ -71,6 +73,14 @@ const AccountScreen = ({currentUser, enableCustomUserStatuses, customStatusExpir
|
|||
const route = useRoute();
|
||||
const insets = useSafeAreaInsets();
|
||||
const isTablet = useIsTablet();
|
||||
const serverUrl = useServerUrl();
|
||||
|
||||
useEffect(() => {
|
||||
if (currentUser) {
|
||||
fetchStatusInBatch(serverUrl, currentUser.id);
|
||||
}
|
||||
}, []);
|
||||
|
||||
let tabletSidebarStyle;
|
||||
if (isTablet) {
|
||||
const {TABLET_SIDEBAR_WIDTH} = ViewConstants;
|
||||
|
|
|
|||
|
|
@ -417,11 +417,10 @@ export function showModalOverCurrentContext(name: string, passProps = {}, option
|
|||
default:
|
||||
animations = {
|
||||
showModal: {
|
||||
enter: {
|
||||
enabled: false,
|
||||
},
|
||||
exit: {
|
||||
enabled: false,
|
||||
alpha: {
|
||||
from: 0,
|
||||
to: 1,
|
||||
duration: 250,
|
||||
},
|
||||
},
|
||||
dismissModal: {
|
||||
|
|
|
|||
317
app/utils/file/file_picker/index.ts
Normal file
317
app/utils/file/file_picker/index.ts
Normal file
|
|
@ -0,0 +1,317 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {IntlShape} from 'react-intl';
|
||||
import {Alert, DeviceEventEmitter, NativeModules, Platform, StatusBar} from 'react-native';
|
||||
import AndroidOpenSettings from 'react-native-android-open-settings';
|
||||
import DeviceInfo from 'react-native-device-info';
|
||||
import DocumentPicker, {DocumentPickerResponse} from 'react-native-document-picker';
|
||||
import {Asset, CameraOptions, ImageLibraryOptions, ImagePickerResponse, launchCamera, launchImageLibrary} from 'react-native-image-picker';
|
||||
import Permissions, {AndroidPermission, IOSPermission} from 'react-native-permissions';
|
||||
|
||||
import {Navigation} from '@constants';
|
||||
import {extractFileInfo, lookupMimeType} from '@utils/file';
|
||||
|
||||
import type {ExtractedFileInfo} from '@typings/utils/file';
|
||||
|
||||
const ShareExtension = NativeModules.MattermostShare;
|
||||
|
||||
type PermissionSource = 'camera' | 'storage' | 'denied_android' | 'denied_ios' | 'photo';
|
||||
|
||||
export default class FilePickerUtil {
|
||||
private readonly uploadFiles: (files: ExtractedFileInfo[]) => void;
|
||||
private readonly intl: IntlShape;
|
||||
|
||||
constructor(
|
||||
intl: IntlShape,
|
||||
uploadFiles: (files: ExtractedFileInfo[]) => void) {
|
||||
this.intl = intl;
|
||||
this.uploadFiles = uploadFiles;
|
||||
}
|
||||
|
||||
private getPermissionMessages = (source: PermissionSource) => {
|
||||
const {formatMessage} = this.intl;
|
||||
const applicationName = DeviceInfo.getApplicationName();
|
||||
|
||||
const permissions: Record<string, { title: string; text: string }> = {
|
||||
camera: {
|
||||
title: formatMessage(
|
||||
{
|
||||
id: 'mobile.camera_photo_permission_denied_title',
|
||||
defaultMessage:
|
||||
'{applicationName} would like to access your camera',
|
||||
},
|
||||
{applicationName},
|
||||
),
|
||||
text: formatMessage({
|
||||
id: 'mobile.camera_photo_permission_denied_description',
|
||||
defaultMessage:
|
||||
'Take photos and upload them to your server or save them to your device. Open Settings to grant {applicationName} Read and Write access to your camera.',
|
||||
}),
|
||||
},
|
||||
storage: {
|
||||
title: formatMessage(
|
||||
{
|
||||
id: 'mobile.storage_permission_denied_title',
|
||||
defaultMessage:
|
||||
'{applicationName} would like to access your files',
|
||||
},
|
||||
{applicationName},
|
||||
),
|
||||
text: formatMessage({
|
||||
id: 'mobile.storage_permission_denied_description',
|
||||
defaultMessage:
|
||||
'Upload files to your server. Open Settings to grant {applicationName} Read and Write access to files on this device.',
|
||||
}),
|
||||
},
|
||||
denied_ios: {
|
||||
title: formatMessage(
|
||||
{
|
||||
id: 'mobile.ios.photos_permission_denied_title',
|
||||
defaultMessage:
|
||||
'{applicationName} would like to access your photos',
|
||||
},
|
||||
{applicationName},
|
||||
),
|
||||
text: formatMessage({
|
||||
id: 'mobile.ios.photos_permission_denied_description',
|
||||
defaultMessage:
|
||||
'Upload photos and videos to your server or save them to your device. Open Settings to grant {applicationName} Read and Write access to your photo and video library.',
|
||||
}),
|
||||
},
|
||||
denied_android: {
|
||||
title: formatMessage(
|
||||
{
|
||||
id: 'mobile.android.photos_permission_denied_title',
|
||||
defaultMessage:
|
||||
'{applicationName} would like to access your photos',
|
||||
},
|
||||
{applicationName},
|
||||
),
|
||||
text: formatMessage({
|
||||
id: 'mobile.android.photos_permission_denied_description',
|
||||
defaultMessage:
|
||||
'Upload photos to your server or save them to your device. Open Settings to grant {applicationName} Read and Write access to your photo library.',
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
return permissions[source];
|
||||
};
|
||||
|
||||
private prepareFileUpload = async (files: Array<Asset | DocumentPickerResponse>) => {
|
||||
const out = await extractFileInfo(files);
|
||||
|
||||
if (out.length > 0) {
|
||||
DeviceEventEmitter.emit(Navigation.NAVIGATION_CLOSE_MODAL);
|
||||
this.uploadFiles(out);
|
||||
}
|
||||
};
|
||||
|
||||
private getPermissionDeniedMessage = (source?: PermissionSource) => {
|
||||
const sources = ['camera', 'storage', 'photo'];
|
||||
const deniedSource: PermissionSource = Platform.select({android: 'denied_android', ios: 'denied_ios'})!;
|
||||
const msgForSource = source && sources.includes(source) ? source : deniedSource;
|
||||
|
||||
return this.getPermissionMessages(msgForSource);
|
||||
};
|
||||
|
||||
private getFilesFromResponse = async (response: ImagePickerResponse): Promise<Asset[]> => {
|
||||
if (!response?.assets?.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const files: Asset[] = [];
|
||||
|
||||
await Promise.all((response.assets.map(async (file) => {
|
||||
if (Platform.OS === 'ios') {
|
||||
files.push(file);
|
||||
} else {
|
||||
// For android we need to retrieve the realPath in case the file being imported is from the cloud
|
||||
const uri = (await ShareExtension.getFilePath(file.uri)).filePath;
|
||||
const type = file.type || lookupMimeType(uri);
|
||||
let fileName = file.fileName;
|
||||
if (type.includes('video/')) {
|
||||
fileName = uri.split('\\').pop().split('/').pop();
|
||||
}
|
||||
|
||||
if (uri) {
|
||||
files.push({...file, fileName, uri, type});
|
||||
}
|
||||
}
|
||||
})));
|
||||
|
||||
return files;
|
||||
};
|
||||
|
||||
private hasPhotoPermission = async (source: PermissionSource) => {
|
||||
let permissionRequest;
|
||||
|
||||
const targetSource = Platform.select({
|
||||
ios: source === 'camera' ? Permissions.PERMISSIONS.IOS.CAMERA : Permissions.PERMISSIONS.IOS.PHOTO_LIBRARY,
|
||||
android: Permissions.PERMISSIONS.ANDROID.CAMERA,
|
||||
}) as AndroidPermission | IOSPermission;
|
||||
|
||||
const hasPhotoLibraryPermission = await Permissions.check(targetSource);
|
||||
|
||||
switch (hasPhotoLibraryPermission) {
|
||||
case Permissions.RESULTS.DENIED:
|
||||
permissionRequest = await Permissions.request(targetSource);
|
||||
if (permissionRequest !== Permissions.RESULTS.GRANTED) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
case Permissions.RESULTS.BLOCKED: {
|
||||
const grantOption = {
|
||||
text: this.intl.formatMessage({
|
||||
id: 'mobile.permission_denied_retry',
|
||||
defaultMessage: 'Settings',
|
||||
}),
|
||||
onPress: () => Permissions.openSettings(),
|
||||
};
|
||||
|
||||
const {title, text} = this.getPermissionDeniedMessage(source);
|
||||
|
||||
Alert.alert(title, text, [
|
||||
grantOption,
|
||||
{
|
||||
text: this.intl.formatMessage({
|
||||
id: 'mobile.permission_denied_dismiss',
|
||||
defaultMessage: "Don't Allow",
|
||||
}),
|
||||
},
|
||||
]);
|
||||
return false;
|
||||
}
|
||||
default: return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
private hasStoragePermission = async () => {
|
||||
if (Platform.OS === 'ios') {
|
||||
return true;
|
||||
}
|
||||
|
||||
const storagePermission = Permissions.PERMISSIONS.ANDROID.READ_EXTERNAL_STORAGE;
|
||||
let permissionRequest;
|
||||
const hasPermissionToStorage = await Permissions.check(storagePermission);
|
||||
switch (hasPermissionToStorage) {
|
||||
case Permissions.RESULTS.DENIED:
|
||||
permissionRequest = await Permissions.request(storagePermission);
|
||||
if (permissionRequest !== Permissions.RESULTS.GRANTED) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
case Permissions.RESULTS.BLOCKED: {
|
||||
const {title, text} = this.getPermissionDeniedMessage();
|
||||
|
||||
Alert.alert(title, text, [
|
||||
{
|
||||
text: this.intl.formatMessage({
|
||||
id: 'mobile.permission_denied_dismiss',
|
||||
defaultMessage: "Don't Allow",
|
||||
}),
|
||||
},
|
||||
{
|
||||
text: this.intl.formatMessage({
|
||||
id: 'mobile.permission_denied_retry',
|
||||
defaultMessage: 'Settings',
|
||||
}),
|
||||
onPress: () => AndroidOpenSettings.appDetailsSettings(),
|
||||
},
|
||||
]);
|
||||
return false;
|
||||
}
|
||||
default: return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
private buildUri = async (doc: DocumentPickerResponse) => {
|
||||
let uri: string = doc.uri;
|
||||
|
||||
if (Platform.OS === 'android') {
|
||||
// For android we need to retrieve the realPath in case the file being imported is from the cloud
|
||||
const newUri = await ShareExtension.getFilePath(doc.uri);
|
||||
uri = newUri?.filePath;
|
||||
if (uri === undefined) {
|
||||
return {doc: undefined};
|
||||
}
|
||||
}
|
||||
|
||||
// Decode file uri to get the actual path
|
||||
doc.uri = decodeURIComponent(uri);
|
||||
return {doc};
|
||||
};
|
||||
|
||||
attachFileFromCamera = async (customOptions?: CameraOptions) => {
|
||||
let options = customOptions;
|
||||
if (!options) {
|
||||
options = {
|
||||
quality: 0.8,
|
||||
videoQuality: 'high',
|
||||
mediaType: 'photo',
|
||||
saveToPhotos: true,
|
||||
};
|
||||
}
|
||||
|
||||
const hasCameraPermission = await this.hasPhotoPermission('camera');
|
||||
if (hasCameraPermission) {
|
||||
launchCamera(options, async (response: ImagePickerResponse) => {
|
||||
StatusBar.setHidden(false);
|
||||
|
||||
if (response.errorCode || response.didCancel) {
|
||||
return;
|
||||
}
|
||||
|
||||
const files = await this.getFilesFromResponse(response);
|
||||
await this.prepareFileUpload(files);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
attachFileFromFiles = async (browseFileType?: string, allowMultiSelection = false) => {
|
||||
const hasPermission = await this.hasStoragePermission();
|
||||
const fileType = browseFileType ?? Platform.select({ios: 'public.item', default: '*/*'});
|
||||
|
||||
if (hasPermission) {
|
||||
try {
|
||||
const docResponse = (await DocumentPicker.pick({allowMultiSelection, type: [fileType]}));
|
||||
const proDocs = docResponse.map(async (d: DocumentPickerResponse) => {
|
||||
const {doc} = await this.buildUri(d);
|
||||
return doc;
|
||||
});
|
||||
|
||||
const docs = await Promise.all(proDocs) as unknown as DocumentPickerResponse[];
|
||||
|
||||
await this.prepareFileUpload(docs);
|
||||
} catch (error) {
|
||||
// Do nothing
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
attachFileFromPhotoGallery = async (selectionLimit = 1) => {
|
||||
const options: ImageLibraryOptions = {
|
||||
quality: 1,
|
||||
mediaType: 'mixed',
|
||||
includeBase64: false,
|
||||
selectionLimit,
|
||||
};
|
||||
|
||||
const hasPermission = await this.hasPhotoPermission('photo');
|
||||
if (hasPermission) {
|
||||
launchImageLibrary(options, async (response: ImagePickerResponse) => {
|
||||
StatusBar.setHidden(false);
|
||||
if (response.errorMessage || response.didCancel) {
|
||||
return;
|
||||
}
|
||||
|
||||
const files = await this.getFilesFromResponse(response);
|
||||
await this.prepareFileUpload(files);
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
@ -1,17 +1,23 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {PastedFile} from '@mattermost/react-native-paste-input';
|
||||
import Model from '@nozbe/watermelondb/Model';
|
||||
import * as FileSystem from 'expo-file-system';
|
||||
import mimeDB from 'mime-db';
|
||||
import {IntlShape} from 'react-intl';
|
||||
import {Platform} from 'react-native';
|
||||
import {DocumentPickerResponse} from 'react-native-document-picker';
|
||||
import {Asset} from 'react-native-image-picker';
|
||||
|
||||
import {Files} from '@constants';
|
||||
import {generateId} from '@utils/general';
|
||||
import {deleteEntititesFile, getIOSAppGroupDetails} from '@utils/mattermost_managed';
|
||||
import {hashCode} from '@utils/security';
|
||||
import {removeProtocol} from '@utils/url';
|
||||
|
||||
import type FileModel from '@typings/database/models/servers/file';
|
||||
import type {ExtractedFileInfo} from '@typings/utils/file';
|
||||
|
||||
const EXTRACT_TYPE_REGEXP = /^\s*([^;\s]*)(?:;|\s|$)/;
|
||||
const CONTENT_DISPOSITION_REGEXP = /inline;filename=".*\.([a-z]+)";/i;
|
||||
|
|
@ -288,8 +294,7 @@ export const isVideo = (file?: FileInfo | FileModel) => {
|
|||
return SUPPORTED_VIDEO_FORMAT!.includes(mime);
|
||||
};
|
||||
|
||||
export function getFormattedFileSize(file: FileInfo): string {
|
||||
const bytes = file.size;
|
||||
export function getFormattedFileSize(bytes: number): string {
|
||||
const fileSizes = [
|
||||
['TB', 1024 * 1024 * 1024 * 1024],
|
||||
['GB', 1024 * 1024 * 1024],
|
||||
|
|
@ -365,3 +370,74 @@ export function getLocalFilePathFromFile(dir: string, serverUrl: string, file: F
|
|||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export async function extractFileInfo(files: Array<Asset | DocumentPickerResponse | PastedFile>) {
|
||||
const out: ExtractedFileInfo[] = [];
|
||||
|
||||
await Promise.all(files.map(async (file) => {
|
||||
if (!file) {
|
||||
return;
|
||||
}
|
||||
|
||||
const outFile = {
|
||||
progress: 0,
|
||||
localPath: file.uri,
|
||||
clientId: generateId(),
|
||||
loading: true,
|
||||
} as unknown as ExtractedFileInfo;
|
||||
|
||||
if ('fileSize' in file) {
|
||||
outFile.size = file.fileSize || 0;
|
||||
outFile.name = file.fileName || '';
|
||||
} else {
|
||||
const path = Platform.select({
|
||||
ios: (file.uri || '').replace('file://', ''),
|
||||
default: file.uri || '',
|
||||
});
|
||||
let fileInfo;
|
||||
try {
|
||||
fileInfo = await FileSystem.getInfoAsync(path);
|
||||
const uri = fileInfo.uri;
|
||||
outFile.size = fileInfo.size || 0;
|
||||
outFile.name = uri.substring(uri.lastIndexOf('/') + 1);
|
||||
} catch (e) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (file.type) {
|
||||
outFile.mime_type = file.type;
|
||||
} else {
|
||||
outFile.mime_type = lookupMimeType(outFile.name);
|
||||
}
|
||||
|
||||
out.push(outFile);
|
||||
}));
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
export function fileSizeWarning(intl: IntlShape, maxFileSize: number) {
|
||||
return intl.formatMessage({
|
||||
id: 'file_upload.fileAbove',
|
||||
defaultMessage: 'Files must be less than {max}',
|
||||
}, {
|
||||
max: getFormattedFileSize(maxFileSize),
|
||||
});
|
||||
}
|
||||
|
||||
export function fileMaxWarning(intl: IntlShape, maxFileCount: number) {
|
||||
return intl.formatMessage({
|
||||
id: 'mobile.file_upload.max_warning',
|
||||
defaultMessage: 'Uploads limited to {count} files maximum.',
|
||||
}, {
|
||||
count: maxFileCount,
|
||||
});
|
||||
}
|
||||
|
||||
export function uploadDisabledWarning(intl: IntlShape) {
|
||||
return intl.formatMessage({
|
||||
id: 'mobile.file_upload.disabled2',
|
||||
defaultMessage: 'File uploads from mobile are disabled.',
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -270,6 +270,8 @@ PODS:
|
|||
- React-Core
|
||||
- react-native-hw-keyboard-event (0.0.4):
|
||||
- React
|
||||
- react-native-image-picker (4.7.1):
|
||||
- React-Core
|
||||
- react-native-netinfo (7.1.7):
|
||||
- React-Core
|
||||
- react-native-network-client (0.1.0):
|
||||
|
|
@ -501,6 +503,7 @@ DEPENDENCIES:
|
|||
- react-native-document-picker (from `../node_modules/react-native-document-picker`)
|
||||
- "react-native-emm (from `../node_modules/@mattermost/react-native-emm`)"
|
||||
- react-native-hw-keyboard-event (from `../node_modules/react-native-hw-keyboard-event`)
|
||||
- react-native-image-picker (from `../node_modules/react-native-image-picker`)
|
||||
- "react-native-netinfo (from `../node_modules/@react-native-community/netinfo`)"
|
||||
- "react-native-network-client (from `../node_modules/@mattermost/react-native-network-client`)"
|
||||
- react-native-notifications (from `../node_modules/react-native-notifications`)
|
||||
|
|
@ -634,6 +637,8 @@ EXTERNAL SOURCES:
|
|||
:path: "../node_modules/@mattermost/react-native-emm"
|
||||
react-native-hw-keyboard-event:
|
||||
:path: "../node_modules/react-native-hw-keyboard-event"
|
||||
react-native-image-picker:
|
||||
:path: "../node_modules/react-native-image-picker"
|
||||
react-native-netinfo:
|
||||
:path: "../node_modules/@react-native-community/netinfo"
|
||||
react-native-network-client:
|
||||
|
|
@ -773,6 +778,7 @@ SPEC CHECKSUMS:
|
|||
react-native-document-picker: 429972f7ece4463aa5bcdd789622b3a674a3c5d1
|
||||
react-native-emm: a326f295d2bd3444178cf36a9e2d9307e0dc0dcc
|
||||
react-native-hw-keyboard-event: b517cefb8d5c659a38049c582de85ff43337dc53
|
||||
react-native-image-picker: 5fe0a96bef4935bbdfb02f59b910bf40d5526109
|
||||
react-native-netinfo: 27f287f2d191693f3b9d01a4273137fcf91c3b5d
|
||||
react-native-network-client: 30ab97e7e6c8d6f2d2b10cc1ebad0cbf9c894c6e
|
||||
react-native-notifications: 805108822ceff3440644d5701944f0cda35f5b4b
|
||||
|
|
|
|||
28
package-lock.json
generated
28
package-lock.json
generated
|
|
@ -61,6 +61,7 @@
|
|||
"react-native-gesture-handler": "2.2.0",
|
||||
"react-native-haptic-feedback": "1.13.0",
|
||||
"react-native-hw-keyboard-event": "0.0.4",
|
||||
"react-native-image-picker": "4.7.1",
|
||||
"react-native-keyboard-aware-scroll-view": "0.9.5",
|
||||
"react-native-keyboard-tracking-view": "5.7.0",
|
||||
"react-native-keychain": "8.0.0",
|
||||
|
|
@ -17742,13 +17743,11 @@
|
|||
"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"
|
||||
|
|
@ -17757,7 +17756,6 @@
|
|||
"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",
|
||||
|
|
@ -17771,7 +17769,6 @@
|
|||
"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"
|
||||
|
|
@ -17780,7 +17777,6 @@
|
|||
"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",
|
||||
|
|
@ -17798,7 +17794,6 @@
|
|||
"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"
|
||||
|
|
@ -19848,6 +19843,15 @@
|
|||
"react-native": ">=0.50"
|
||||
}
|
||||
},
|
||||
"node_modules/react-native-image-picker": {
|
||||
"version": "4.7.1",
|
||||
"resolved": "https://registry.npmjs.org/react-native-image-picker/-/react-native-image-picker-4.7.1.tgz",
|
||||
"integrity": "sha512-8jOHvSX2UMX+76Ixjt2fCOuTLfBx55WGlmn4HK7ZTWfZWXGilpEDDXGnchwBAJkw4Xv953/8tsvSNFEoyvNjFg==",
|
||||
"peerDependencies": {
|
||||
"react": "*",
|
||||
"react-native": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/react-native-iphone-x-helper": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/react-native-iphone-x-helper/-/react-native-iphone-x-helper-1.3.1.tgz",
|
||||
|
|
@ -37896,19 +37900,16 @@
|
|||
"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",
|
||||
|
|
@ -37919,13 +37920,11 @@
|
|||
"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",
|
||||
|
|
@ -37940,7 +37939,6 @@
|
|||
"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
|
||||
}
|
||||
}
|
||||
|
|
@ -39631,6 +39629,12 @@
|
|||
"integrity": "sha512-G8qp0nm17PHigLb/axgdF9xg51BKCG2p1AGeq//J/luLp5zNczIcQJh+nm02R1MeEUE3e53wqO4LMe0MV3raZg==",
|
||||
"requires": {}
|
||||
},
|
||||
"react-native-image-picker": {
|
||||
"version": "4.7.1",
|
||||
"resolved": "https://registry.npmjs.org/react-native-image-picker/-/react-native-image-picker-4.7.1.tgz",
|
||||
"integrity": "sha512-8jOHvSX2UMX+76Ixjt2fCOuTLfBx55WGlmn4HK7ZTWfZWXGilpEDDXGnchwBAJkw4Xv953/8tsvSNFEoyvNjFg==",
|
||||
"requires": {}
|
||||
},
|
||||
"react-native-iphone-x-helper": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/react-native-iphone-x-helper/-/react-native-iphone-x-helper-1.3.1.tgz",
|
||||
|
|
|
|||
|
|
@ -59,6 +59,7 @@
|
|||
"react-native-gesture-handler": "2.2.0",
|
||||
"react-native-haptic-feedback": "1.13.0",
|
||||
"react-native-hw-keyboard-event": "0.0.4",
|
||||
"react-native-image-picker": "4.7.1",
|
||||
"react-native-keyboard-aware-scroll-view": "0.9.5",
|
||||
"react-native-keyboard-tracking-view": "5.7.0",
|
||||
"react-native-keychain": "8.0.0",
|
||||
|
|
|
|||
30
types/api/files.d.ts
vendored
30
types/api/files.d.ts
vendored
|
|
@ -3,23 +3,23 @@
|
|||
|
||||
type FileInfo = {
|
||||
id?: string;
|
||||
user_id: string;
|
||||
post_id: string;
|
||||
create_at: number;
|
||||
update_at: number;
|
||||
delete_at: number;
|
||||
name: string;
|
||||
extension: string;
|
||||
mini_preview?: string;
|
||||
size: number;
|
||||
mime_type: string;
|
||||
width: number;
|
||||
height: number;
|
||||
has_preview_image: boolean;
|
||||
clientId?: string;
|
||||
localPath?: string;
|
||||
uri?: string;
|
||||
create_at: number;
|
||||
delete_at: number;
|
||||
extension: string;
|
||||
has_preview_image: boolean;
|
||||
height: number;
|
||||
loading?: boolean;
|
||||
localPath?: string;
|
||||
mime_type: string;
|
||||
mini_preview?: string;
|
||||
name: string;
|
||||
post_id: string;
|
||||
size: number;
|
||||
update_at: number;
|
||||
uri?: string;
|
||||
user_id: string;
|
||||
width: number;
|
||||
};
|
||||
|
||||
type FilesState = {
|
||||
|
|
|
|||
6
types/screens/edit_profile.d.ts
vendored
6
types/screens/edit_profile.d.ts
vendored
|
|
@ -5,7 +5,8 @@ import {RefObject} from 'react';
|
|||
|
||||
import {FloatingTextInputRef} from '@components/floating_text_input_label';
|
||||
import {FieldProps} from '@screens/edit_profile/components/field';
|
||||
import UserModel from '@typings/database/models/servers/user';
|
||||
|
||||
import type UserModel from '@typings/database/models/servers/user';
|
||||
|
||||
interface UserInfo extends Record<string, string | undefined | null| boolean> {
|
||||
email: string;
|
||||
|
|
@ -28,9 +29,12 @@ type EditProfileProps = {
|
|||
lockedPicture: boolean;
|
||||
};
|
||||
|
||||
type NewProfileImage = { localPath?: string; isRemoved?: boolean };
|
||||
|
||||
type FieldSequence = Record<string, {
|
||||
ref: RefObject<FloatingTextInputRef>;
|
||||
isDisabled: boolean;
|
||||
}>
|
||||
|
||||
type FieldConfig = Pick<FieldProps, 'blurOnSubmit' | 'enablesReturnKeyAutomatically' | 'onFocusNextField' | 'onTextChange' | 'returnKeyType'>
|
||||
|
||||
|
|
|
|||
10
types/utils/file.d.ts
vendored
Normal file
10
types/utils/file.d.ts
vendored
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import * as FileSystem from 'expo-file-system';
|
||||
|
||||
export type ErrorText = Partial<ClientErrorProps> | string;
|
||||
|
||||
export type ExtractedFileInfo = Partial<FileSystem.FileInfo> & { name: string; mime_type: string}
|
||||
|
||||
export type UploadExtractedFile = (files?: ExtractedFileInfo[]) => void;
|
||||
2
types/utils/index.d.ts
vendored
2
types/utils/index.d.ts
vendored
|
|
@ -4,5 +4,3 @@
|
|||
type Dictionary<T> = {
|
||||
[key: string]: T;
|
||||
};
|
||||
|
||||
type ErrorText = Partial<ClientErrorProps> | string;
|
||||
|
|
|
|||
Loading…
Reference in a new issue