forked from Ivasoft/mattermost-mobile
MM-39712 - Edit Profile Screen without image picker (#5849)
* 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 a778e1f761.
* 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
* Revert fetching status on userProfile
* EditProfile enable extraScrollHeight on iOS only
Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
This commit is contained in:
@@ -34,8 +34,7 @@ export const autoUpdateTimezone = async (serverUrl: string, {deviceTimezone, use
|
||||
|
||||
if (currentTimezone.useAutomaticTimezone && newTimezoneExists) {
|
||||
const timezone = {useAutomaticTimezone: 'true', automaticTimezone: deviceTimezone, manualTimezone: currentTimezone.manualTimezone};
|
||||
const updatedUser = {...currentUser, timezone} as UserModel;
|
||||
await updateMe(serverUrl, updatedUser);
|
||||
await updateMe(serverUrl, {timezone});
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
@@ -5,7 +5,7 @@ import {SYSTEM_IDENTIFIERS} from '@constants/database';
|
||||
import General from '@constants/general';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {queryRecentCustomStatuses} from '@queries/servers/system';
|
||||
import {queryCurrentUser, queryUserById} from '@queries/servers/user';
|
||||
import {queryCurrentUser} from '@queries/servers/user';
|
||||
|
||||
import {addRecentReaction} from './reactions';
|
||||
|
||||
@@ -105,25 +105,40 @@ export const updateRecentCustomStatuses = async (serverUrl: string, customStatus
|
||||
});
|
||||
};
|
||||
|
||||
export const updateUserPresence = async (serverUrl: string, userStatus: UserStatus) => {
|
||||
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
||||
if (!operator) {
|
||||
export const updateLocalUser = async (
|
||||
serverUrl: string,
|
||||
userDetails: Partial<UserProfile> & { status?: string},
|
||||
) => {
|
||||
const database = DatabaseManager.serverDatabases[serverUrl]?.database;
|
||||
if (!database) {
|
||||
return {error: `${serverUrl} database not found`};
|
||||
}
|
||||
|
||||
const user = await queryUserById(operator.database, userStatus.user_id);
|
||||
if (user) {
|
||||
user.prepareUpdate((record) => {
|
||||
record.status = userStatus.status;
|
||||
});
|
||||
try {
|
||||
await operator.batchRecords([user]);
|
||||
} catch {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('FAILED TO BATCH CHANGES FOR UPDATE USER PRESENCE');
|
||||
try {
|
||||
const user = await queryCurrentUser(database);
|
||||
if (user) {
|
||||
await database.write(async () => {
|
||||
await user.update((userRecord: UserModel) => {
|
||||
userRecord.authService = userDetails.auth_service ?? user.authService;
|
||||
userRecord.email = userDetails.email ?? user.email;
|
||||
userRecord.firstName = userDetails.first_name ?? user.firstName;
|
||||
userRecord.lastName = userDetails.last_name ?? user.lastName;
|
||||
userRecord.lastPictureUpdate = userDetails.last_picture_update ?? user.lastPictureUpdate;
|
||||
userRecord.locale = userDetails.locale ?? user.locale;
|
||||
userRecord.nickname = userDetails.nickname ?? user.nickname;
|
||||
userRecord.notifyProps = userDetails.notify_props ?? user.notifyProps;
|
||||
userRecord.position = userDetails?.position ?? user.position;
|
||||
userRecord.props = userDetails.props ?? user.props;
|
||||
userRecord.roles = userDetails.roles ?? user.roles;
|
||||
userRecord.status = userDetails?.status ?? user.status;
|
||||
userRecord.timezone = userDetails.timezone ?? user.timezone;
|
||||
userRecord.username = userDetails.username ?? user.username;
|
||||
});
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
return {error};
|
||||
}
|
||||
|
||||
return {};
|
||||
};
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
import {Model, Q} from '@nozbe/watermelondb';
|
||||
|
||||
import {updateRecentCustomStatuses, updateUserPresence} from '@actions/local/user';
|
||||
import {updateRecentCustomStatuses, updateLocalUser} from '@actions/local/user';
|
||||
import {fetchRolesIfNeeded} from '@actions/remote/role';
|
||||
import {Database, General} from '@constants';
|
||||
import DatabaseManager from '@database/manager';
|
||||
@@ -143,7 +143,7 @@ export const fetchProfilesPerChannels = async (serverUrl: string, channelIds: st
|
||||
}
|
||||
};
|
||||
|
||||
export const updateMe = async (serverUrl: string, user: UserModel) => {
|
||||
export const updateMe = async (serverUrl: string, user: Partial<UserProfile>) => {
|
||||
const database = DatabaseManager.serverDatabases[serverUrl]?.database;
|
||||
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
||||
if (!database) {
|
||||
@@ -159,7 +159,7 @@ export const updateMe = async (serverUrl: string, user: UserModel) => {
|
||||
|
||||
let data: UserProfile;
|
||||
try {
|
||||
data = await client.patchMe(user._raw);
|
||||
data = await client.patchMe(user);
|
||||
} catch (e) {
|
||||
forceLogoutIfNecessary(serverUrl, e as ClientError);
|
||||
return {error: e};
|
||||
@@ -396,6 +396,7 @@ export const updateUsersNoLongerVisible = async (serverUrl: string): Promise<{er
|
||||
|
||||
return {};
|
||||
};
|
||||
|
||||
export const setStatus = async (serverUrl: string, status: UserStatus) => {
|
||||
let client: Client;
|
||||
try {
|
||||
@@ -406,13 +407,13 @@ export const setStatus = async (serverUrl: string, status: UserStatus) => {
|
||||
|
||||
try {
|
||||
const data = await client.updateStatus(status);
|
||||
updateUserPresence(serverUrl, status);
|
||||
await updateLocalUser(serverUrl, {status: status.status});
|
||||
|
||||
return {
|
||||
data,
|
||||
};
|
||||
} catch (error: any) {
|
||||
forceLogoutIfNecessary(serverUrl, error);
|
||||
} catch (error) {
|
||||
forceLogoutIfNecessary(serverUrl, error as ClientErrorProps);
|
||||
return {error};
|
||||
}
|
||||
};
|
||||
|
||||
@@ -8,7 +8,7 @@ import FormattedText from '@components/formatted_text';
|
||||
import {makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
||||
type ErrorProps = {
|
||||
error: Partial<ClientErrorProps> | string;
|
||||
error: ErrorText;
|
||||
testID?: string;
|
||||
textStyle?: StyleProp<ViewStyle> | StyleProp<TextStyle>;
|
||||
theme: Theme;
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
import {debounce} from 'lodash';
|
||||
import React, {useState, useEffect, useRef, useImperativeHandle, forwardRef} from 'react';
|
||||
import {View, TextInput, TouchableWithoutFeedback, Text, Platform, TextStyle, NativeSyntheticEvent, TextInputFocusEventData, TextInputProps, GestureResponderEvent, TargetedEvent} from 'react-native';
|
||||
import {GestureResponderEvent, NativeSyntheticEvent, Platform, TargetedEvent, Text, TextInput, TextInputFocusEventData, TextInputProps, TextStyle, TouchableWithoutFeedback, View, ViewStyle} from 'react-native';
|
||||
import Animated, {useCode, interpolateNode, EasingNode, Value, set, Clock} from 'react-native-reanimated';
|
||||
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
@@ -93,7 +93,9 @@ export type FloatingTextInputRef = {
|
||||
}
|
||||
|
||||
type FloatingTextInputProps = TextInputProps & {
|
||||
containerStyle?: TextStyle;
|
||||
containerStyle?: ViewStyle;
|
||||
textInputStyle?: TextStyle;
|
||||
labelTextStyle?: TextStyle;
|
||||
editable?: boolean;
|
||||
error?: string;
|
||||
errorIcon?: string;
|
||||
@@ -120,6 +122,8 @@ const FloatingTextInput = forwardRef<FloatingTextInputRef, FloatingTextInputProp
|
||||
showErrorIcon = true,
|
||||
theme,
|
||||
value = '',
|
||||
textInputStyle,
|
||||
labelTextStyle,
|
||||
...props
|
||||
}: FloatingTextInputProps, ref) => {
|
||||
const [focusedLabel, setIsFocusLabel] = useState<boolean | undefined>();
|
||||
@@ -218,8 +222,8 @@ const FloatingTextInput = forwardRef<FloatingTextInputRef, FloatingTextInputProp
|
||||
borderWidth: focusedLabel ? BORDER_FOCUSED_WIDTH : BORDER_DEFAULT_WIDTH,
|
||||
height: DEFAULT_INPUT_HEIGHT + ((focusedLabel ? BORDER_FOCUSED_WIDTH : BORDER_DEFAULT_WIDTH) * 2),
|
||||
};
|
||||
const combinedTextInputStyle = [styles.textInput, textInputBorder, textInputColorStyles];
|
||||
const textAnimatedTextStyle = [styles.label, focusStyle, labelColorStyles];
|
||||
const combinedTextInputStyle = [styles.textInput, textInputBorder, textInputColorStyles, textInputStyle];
|
||||
const textAnimatedTextStyle = [styles.label, focusStyle, labelColorStyles, labelTextStyle];
|
||||
|
||||
if (error && !focused) {
|
||||
textAnimatedTextStyle.push({color: theme.errorTextColor});
|
||||
|
||||
@@ -58,7 +58,15 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
||||
};
|
||||
});
|
||||
|
||||
const ProfilePicture = ({author, iconSize, showStatus = true, size = 64, statusSize = 14, statusStyle, testID}: ProfilePictureProps) => {
|
||||
const ProfilePicture = ({
|
||||
author,
|
||||
iconSize,
|
||||
showStatus = true,
|
||||
size = 64,
|
||||
statusSize = 14,
|
||||
statusStyle,
|
||||
testID,
|
||||
}: ProfilePictureProps) => {
|
||||
const theme = useTheme();
|
||||
const serverUrl = useServerUrl();
|
||||
const style = getStyleSheet(theme);
|
||||
|
||||
@@ -10,6 +10,7 @@ import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
||||
type Props = {
|
||||
action?: string;
|
||||
enabled?: boolean;
|
||||
onPress: () => void;
|
||||
title: string;
|
||||
testID: string;
|
||||
@@ -19,10 +20,12 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
|
||||
actionContainer: {
|
||||
alignItems: 'flex-end',
|
||||
justifyContent: 'center',
|
||||
marginRight: 20,
|
||||
right: 20,
|
||||
bottom: 7,
|
||||
position: 'absolute',
|
||||
},
|
||||
action: {
|
||||
color: theme.buttonBg,
|
||||
color: changeOpacity(theme.centerChannelColor, 0.7),
|
||||
fontFamily: 'OpenSans-Semibold',
|
||||
fontSize: 16,
|
||||
lineHeight: 24,
|
||||
@@ -34,6 +37,11 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
|
||||
flexDirection: 'row',
|
||||
height: 34,
|
||||
width: '100%',
|
||||
alignItems: 'center',
|
||||
paddingBottom: 5,
|
||||
},
|
||||
enabled: {
|
||||
color: theme.buttonBg,
|
||||
},
|
||||
titleContainer: {
|
||||
alignItems: 'center',
|
||||
@@ -48,9 +56,10 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
|
||||
},
|
||||
}));
|
||||
|
||||
const TabletTitle = ({action, onPress, testID, title}: Props) => {
|
||||
const TabletTitle = ({action, enabled = true, onPress, testID, title}: Props) => {
|
||||
const theme = useTheme();
|
||||
const styles = getStyleSheet(theme);
|
||||
const textStyle = [styles.action, enabled && styles.enabled];
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -61,12 +70,13 @@ const TabletTitle = ({action, onPress, testID, title}: Props) => {
|
||||
{Boolean(action) &&
|
||||
<View style={styles.actionContainer}>
|
||||
<TouchableWithFeedback
|
||||
disabled={!enabled}
|
||||
onPress={onPress}
|
||||
type={Platform.select({android: 'native', ios: 'opacity'})}
|
||||
testID={testID}
|
||||
underlayColor={changeOpacity(theme.centerChannelColor, 0.1)}
|
||||
>
|
||||
<Text style={styles.action}>{action}</Text>
|
||||
<Text style={textStyle}>{action}</Text>
|
||||
</TouchableWithFeedback>
|
||||
</View>
|
||||
}
|
||||
|
||||
@@ -1,6 +1,33 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
export const VALID_IMAGE_MIME_TYPES = [
|
||||
'image/jpeg',
|
||||
'image/jpeg',
|
||||
'image/jpg',
|
||||
'image/jp_',
|
||||
'application/jpg',
|
||||
'application/x-jpg',
|
||||
'image/pjpeg',
|
||||
'image/pipeg',
|
||||
'image/vnd.swiftview-jpeg',
|
||||
'image/x-xbitmap',
|
||||
'image/png',
|
||||
'application/png',
|
||||
'application/x-png',
|
||||
'image/bmp',
|
||||
'image/x-bmp',
|
||||
'image/x-bitmap',
|
||||
'image/x-xbitmap',
|
||||
'image/x-win-bitmap',
|
||||
'image/x-windows-bmp',
|
||||
'image/ms-bmp',
|
||||
'image/x-ms-bmp',
|
||||
'application/bmp',
|
||||
'application/x-bmp',
|
||||
'application/x-win-bitmap',
|
||||
] as const;
|
||||
|
||||
const Files: Record<string, string[]> = {
|
||||
AUDIO_TYPES: ['mp3', 'wav', 'wma', 'm4a', 'flac', 'aac', 'ogg'],
|
||||
CODE_TYPES: ['as', 'applescript', 'osascript', 'scpt', 'bash', 'sh', 'zsh', 'clj', 'boot', 'cl2', 'cljc', 'cljs', 'cljs.hl', 'cljscm', 'cljx', 'hic', 'coffee', '_coffee', 'cake', 'cjsx', 'cson', 'iced', 'cpp', 'c', 'cc', 'h', 'c++', 'h++', 'hpp', 'cs', 'csharp', 'css', 'd', 'di', 'dart', 'delphi', 'dpr', 'dfm', 'pas', 'pascal', 'freepascal', 'lazarus', 'lpr', 'lfm', 'diff', 'django', 'jinja', 'dockerfile', 'docker', 'erl', 'f90', 'f95', 'fsharp', 'fs', 'gcode', 'nc', 'go', 'groovy', 'handlebars', 'hbs', 'html.hbs', 'html.handlebars', 'hs', 'hx', 'java', 'jsp', 'js', 'jsx', 'json', 'jl', 'kt', 'ktm', 'kts', 'less', 'lisp', 'lua', 'mk', 'mak', 'md', 'mkdown', 'mkd', 'matlab', 'm', 'mm', 'objc', 'obj-c', 'ml', 'perl', 'pl', 'php', 'php3', 'php4', 'php5', 'php6', 'ps', 'ps1', 'pp', 'py', 'gyp', 'r', 'ruby', 'rb', 'gemspec', 'podspec', 'thor', 'irb', 'rs', 'scala', 'scm', 'sld', 'scss', 'st', 'sql', 'swift', 'tex', 'vbnet', 'vb', 'bas', 'vbs', 'v', 'veo', 'xml', 'html', 'xhtml', 'rss', 'atom', 'xsl', 'plist', 'yaml'],
|
||||
|
||||
@@ -11,6 +11,7 @@ export const CHANNEL_DETAILS = 'ChannelDetails';
|
||||
export const CHANNEL_EDIT = 'ChannelEdit';
|
||||
export const CUSTOM_STATUS_CLEAR_AFTER = 'CustomStatusClearAfter';
|
||||
export const CUSTOM_STATUS = 'CustomStatus';
|
||||
export const EDIT_PROFILE = 'EditProfile';
|
||||
export const FORGOT_PASSWORD = 'ForgotPassword';
|
||||
export const HOME = 'Home';
|
||||
export const INTEGRATION_SELECTOR = 'IntegrationSelector';
|
||||
@@ -38,6 +39,7 @@ export default {
|
||||
CHANNEL_DETAILS,
|
||||
CUSTOM_STATUS_CLEAR_AFTER,
|
||||
CUSTOM_STATUS,
|
||||
EDIT_PROFILE,
|
||||
FORGOT_PASSWORD,
|
||||
HOME,
|
||||
INTEGRATION_SELECTOR,
|
||||
|
||||
@@ -9,7 +9,7 @@ import en from '@assets/i18n/en.json';
|
||||
import availableLanguages from './languages';
|
||||
|
||||
const deviceLocale = getLocales()[0].languageTag;
|
||||
export const DEFAULT_LOCALE = deviceLocale;
|
||||
export const DEFAULT_LOCALE = getLocaleFromLanguage(deviceLocale);
|
||||
|
||||
function loadTranslation(locale?: string) {
|
||||
try {
|
||||
|
||||
102
app/screens/edit_profile/components/email_field.tsx
Normal file
102
app/screens/edit_profile/components/email_field.tsx
Normal file
@@ -0,0 +1,102 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {RefObject} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {Text, View} from 'react-native';
|
||||
|
||||
import {FloatingTextInputRef} from '@components/floating_text_input_label';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
import {typography} from '@utils/typography';
|
||||
|
||||
import Field from './field';
|
||||
|
||||
const services: Record<string, string> = {
|
||||
gitlab: 'GitLab',
|
||||
google: 'Google Apps',
|
||||
office365: 'Office 365',
|
||||
ldap: 'AD/LDAP',
|
||||
saml: 'SAML',
|
||||
};
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
return {
|
||||
container: {
|
||||
marginTop: 2,
|
||||
},
|
||||
text: {
|
||||
...typography('Body', 75),
|
||||
color: changeOpacity(theme.centerChannelColor, 0.5),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
type EmailSettingsProps = {
|
||||
authService: string;
|
||||
email: string;
|
||||
fieldRef: RefObject<FloatingTextInputRef>;
|
||||
onChange: (fieldKey: string, value: string) => void;
|
||||
onFocusNextField: (fieldKey: string) => void;
|
||||
isDisabled: boolean;
|
||||
label: string;
|
||||
theme: Theme;
|
||||
isTablet: boolean;
|
||||
}
|
||||
|
||||
const EmailField = ({
|
||||
authService,
|
||||
email,
|
||||
fieldRef,
|
||||
onChange,
|
||||
onFocusNextField,
|
||||
isDisabled,
|
||||
label,
|
||||
theme,
|
||||
isTablet,
|
||||
}: EmailSettingsProps) => {
|
||||
const intl = useIntl();
|
||||
const service = services[authService];
|
||||
const style = getStyleSheet(theme);
|
||||
|
||||
let fieldDescription: string;
|
||||
|
||||
if (service) {
|
||||
fieldDescription = intl.formatMessage({
|
||||
id: 'user.edit_profile.email.auth_service',
|
||||
defaultMessage: 'Login occurs through {service}. Email cannot be updated. Email address used for notifications is {email}.'}, {email, service});
|
||||
} else {
|
||||
fieldDescription = intl.formatMessage({
|
||||
id: 'user.edit_profile.email.web_client',
|
||||
defaultMessage: 'Email must be updated using a web client or desktop application.'}, {email, service});
|
||||
}
|
||||
|
||||
const descContainer = [style.container, {paddingHorizontal: isTablet ? 42 : 20}];
|
||||
|
||||
return (
|
||||
<>
|
||||
<Field
|
||||
blurOnSubmit={false}
|
||||
enablesReturnKeyAutomatically={true}
|
||||
fieldKey='email'
|
||||
fieldRef={fieldRef}
|
||||
isDisabled={isDisabled}
|
||||
keyboardType='email-address'
|
||||
label={label}
|
||||
onFocusNextField={onFocusNextField}
|
||||
onTextChange={onChange}
|
||||
returnKeyType='next'
|
||||
testID='edit_profile.text_setting.email'
|
||||
value={email}
|
||||
/>
|
||||
<View
|
||||
style={descContainer}
|
||||
>
|
||||
<Text style={style.text}>{fieldDescription}</Text>
|
||||
</View>
|
||||
|
||||
</>
|
||||
|
||||
);
|
||||
};
|
||||
|
||||
export default EmailField;
|
||||
103
app/screens/edit_profile/components/field.tsx
Normal file
103
app/screens/edit_profile/components/field.tsx
Normal file
@@ -0,0 +1,103 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {memo, RefObject, useCallback} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {Platform, StyleSheet, TextInputProps, View} from 'react-native';
|
||||
|
||||
import FloatingTextInput, {FloatingTextInputRef} from '@components/floating_text_input_label';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {useIsTablet} from '@hooks/device';
|
||||
import {changeOpacity, getKeyboardAppearanceFromTheme, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
||||
export type FieldProps = TextInputProps & {
|
||||
isDisabled?: boolean;
|
||||
fieldKey: string;
|
||||
label: string;
|
||||
maxLength?: number;
|
||||
onTextChange: (fieldKey: string, value: string) => void;
|
||||
isOptional?: boolean;
|
||||
testID: string;
|
||||
value: string;
|
||||
fieldRef: RefObject<FloatingTextInputRef>;
|
||||
onFocusNextField: (fieldKey: string) => void;
|
||||
};
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
return StyleSheet.create({
|
||||
viewContainer: {
|
||||
marginVertical: 8,
|
||||
alignItems: 'center',
|
||||
width: '100%',
|
||||
},
|
||||
disabledStyle: {
|
||||
backgroundColor: changeOpacity(theme.centerChannelColor, 0.04),
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
const Field = ({
|
||||
autoCapitalize = 'none',
|
||||
autoCorrect = false,
|
||||
fieldKey,
|
||||
isDisabled = false,
|
||||
isOptional = false,
|
||||
keyboardType = 'default',
|
||||
label,
|
||||
maxLength,
|
||||
onTextChange,
|
||||
testID,
|
||||
value,
|
||||
fieldRef,
|
||||
onFocusNextField,
|
||||
...props
|
||||
}: FieldProps) => {
|
||||
const theme = useTheme();
|
||||
const intl = useIntl();
|
||||
const isTablet = useIsTablet();
|
||||
|
||||
const onChangeText = useCallback((text: string) => onTextChange(fieldKey, text), [fieldKey, onTextChange]);
|
||||
|
||||
const onSubmitEditing = useCallback(() => {
|
||||
onFocusNextField(fieldKey);
|
||||
}, [fieldKey, onFocusNextField]);
|
||||
|
||||
const style = getStyleSheet(theme);
|
||||
|
||||
const keyboard = (Platform.OS === 'android' && keyboardType === 'url') ? 'default' : keyboardType;
|
||||
|
||||
const optionalText = intl.formatMessage({id: 'channel_modal.optional', defaultMessage: '(optional)'});
|
||||
|
||||
const formattedLabel = isOptional ? `${label} ${optionalText}` : label;
|
||||
|
||||
const textInputStyle = isDisabled ? style.disabledStyle : undefined;
|
||||
const subContainer = [style.viewContainer, {paddingHorizontal: isTablet ? 42 : 20}];
|
||||
|
||||
return (
|
||||
<View
|
||||
testID={testID}
|
||||
style={subContainer}
|
||||
>
|
||||
<FloatingTextInput
|
||||
autoCapitalize={autoCapitalize}
|
||||
autoCorrect={autoCorrect}
|
||||
disableFullscreenUI={true}
|
||||
editable={!isDisabled}
|
||||
keyboardAppearance={getKeyboardAppearanceFromTheme(theme)}
|
||||
keyboardType={keyboard}
|
||||
label={formattedLabel}
|
||||
maxLength={maxLength}
|
||||
onChangeText={onChangeText}
|
||||
testID={`${testID}.input`}
|
||||
theme={theme}
|
||||
value={value}
|
||||
ref={fieldRef}
|
||||
onSubmitEditing={onSubmitEditing}
|
||||
textInputStyle={textInputStyle}
|
||||
{...props}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(Field);
|
||||
60
app/screens/edit_profile/components/profile_error.tsx
Normal file
60
app/screens/edit_profile/components/profile_error.tsx
Normal file
@@ -0,0 +1,60 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {View} from 'react-native';
|
||||
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import ErrorText from '@components/error_text';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
import {typography} from '@utils/typography';
|
||||
|
||||
type DisplayErrorProps = {
|
||||
error: Partial<ClientErrorProps> | string;
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
return {
|
||||
errorContainer: {
|
||||
backgroundColor: changeOpacity(theme.errorTextColor, 0.08),
|
||||
width: '100%',
|
||||
maxHeight: 48,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
flexDirection: 'row',
|
||||
},
|
||||
text: {
|
||||
...typography('Heading', 100),
|
||||
color: theme.centerChannelColor,
|
||||
},
|
||||
icon: {
|
||||
color: changeOpacity(theme.dndIndicator, 0.64),
|
||||
...typography('Heading', 300),
|
||||
marginRight: 9,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const ProfileError = ({error}: DisplayErrorProps) => {
|
||||
const theme = useTheme();
|
||||
const style = getStyleSheet(theme);
|
||||
|
||||
return (
|
||||
<View style={style.errorContainer}>
|
||||
<CompassIcon
|
||||
style={style.icon}
|
||||
size={18}
|
||||
name='alert-outline'
|
||||
/>
|
||||
<ErrorText
|
||||
theme={theme}
|
||||
testID='edit_profile.error.text'
|
||||
error={error}
|
||||
textStyle={style.text}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProfileError;
|
||||
485
app/screens/edit_profile/edit_profile.tsx
Normal file
485
app/screens/edit_profile/edit_profile.tsx
Normal file
@@ -0,0 +1,485 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
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 {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 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 ProfileError from './components/profile_error';
|
||||
|
||||
import type {MessageDescriptor} from '@formatjs/intl/src/types';
|
||||
|
||||
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 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,
|
||||
}: 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 [userInfo, setUserInfo] = useState<UserInfo>({
|
||||
email: currentUser.email,
|
||||
firstName: currentUser.firstName,
|
||||
lastName: currentUser.lastName,
|
||||
nickname: currentUser.nickname,
|
||||
position: currentUser.position,
|
||||
username: currentUser.username,
|
||||
});
|
||||
const [canSave, setCanSave] = useState(false);
|
||||
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 : {
|
||||
id: 'update-profile',
|
||||
enabled: false,
|
||||
showAsAction: 'always' as const,
|
||||
testID: 'edit_profile.save.button',
|
||||
color: theme.sidebarHeaderTextColor,
|
||||
text: buttonText,
|
||||
};
|
||||
}, [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]);
|
||||
|
||||
useEffect(() => {
|
||||
const unsubscribe = Navigation.events().registerComponentListener({
|
||||
navigationButtonPressed: ({buttonId}: { buttonId: string }) => {
|
||||
switch (buttonId) {
|
||||
case UPDATE_BUTTON_ID:
|
||||
submitUser();
|
||||
break;
|
||||
case CLOSE_BUTTON_ID:
|
||||
close();
|
||||
break;
|
||||
}
|
||||
},
|
||||
}, componentId);
|
||||
|
||||
return () => {
|
||||
unsubscribe.remove();
|
||||
};
|
||||
}, [userInfo]);
|
||||
|
||||
useEffect(() => {
|
||||
const backHandler = BackHandler.addEventListener('hardwareBackPress', close);
|
||||
return () => {
|
||||
backHandler.remove();
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isTablet) {
|
||||
setButtons(componentId, {
|
||||
rightButtons: [rightButton!],
|
||||
leftButtons: [leftButton!],
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
|
||||
const close = useCallback(() => {
|
||||
if (isModal) {
|
||||
dismissModal({componentId});
|
||||
} else if (isTablet) {
|
||||
DeviceEventEmitter.emit(Events.ACCOUNT_SELECT_TABLET_VIEW, '');
|
||||
} else {
|
||||
popTopScreen(componentId);
|
||||
}
|
||||
|
||||
return true;
|
||||
}, []);
|
||||
|
||||
const enableSaveButton = useCallback((value: boolean) => {
|
||||
if (!isTablet) {
|
||||
const buttons = {
|
||||
rightButtons: [{
|
||||
...rightButton!,
|
||||
enabled: value,
|
||||
}],
|
||||
};
|
||||
setButtons(componentId, buttons);
|
||||
}
|
||||
setCanSave(value);
|
||||
}, [componentId, rightButton]);
|
||||
|
||||
const submitUser = useCallback(preventDoubleTap(async () => {
|
||||
enableSaveButton(false);
|
||||
setError(undefined);
|
||||
setUpdating(true);
|
||||
try {
|
||||
const partialUser: Partial<UserProfile> = {
|
||||
email: userInfo.email,
|
||||
first_name: userInfo.firstName,
|
||||
last_name: userInfo.lastName,
|
||||
nickname: userInfo.nickname,
|
||||
position: userInfo.position,
|
||||
username: userInfo.username,
|
||||
};
|
||||
|
||||
const {error: reqError} = await updateMe(serverUrl, partialUser);
|
||||
|
||||
if (reqError) {
|
||||
resetScreen(reqError as Error);
|
||||
return;
|
||||
}
|
||||
|
||||
close();
|
||||
} catch (e) {
|
||||
resetScreen(e as Error);
|
||||
}
|
||||
}), [userInfo, enableSaveButton]);
|
||||
|
||||
const resetScreen = useCallback((resetError: Error) => {
|
||||
setError(resetError?.message);
|
||||
Keyboard.dismiss();
|
||||
setUpdating(false);
|
||||
enableSaveButton(true);
|
||||
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 &&
|
||||
<TabletTitle
|
||||
action={buttonText}
|
||||
enabled={canSave}
|
||||
onPress={submitUser}
|
||||
testID='custom_status.done.button'
|
||||
title={intl.formatMessage({id: 'mobile.screen.your_profile', defaultMessage: 'Your Profile'})}
|
||||
/>
|
||||
}
|
||||
<SafeAreaView
|
||||
edges={edges}
|
||||
style={styles.flex}
|
||||
testID='edit_profile.screen'
|
||||
>
|
||||
<KeyboardAwareScrollView
|
||||
bounces={false}
|
||||
enableAutomaticScroll={true}
|
||||
enableOnAndroid={true}
|
||||
enableResetScrollToCoords={true}
|
||||
extraScrollHeight={Platform.select({ios: 45})}
|
||||
keyboardOpeningTime={0}
|
||||
keyboardDismissMode='on-drag'
|
||||
keyboardShouldPersistTaps='handled'
|
||||
|
||||
// @ts-expect-error legacy ref
|
||||
ref={keyboardAwareRef}
|
||||
scrollToOverflowEnabled={true}
|
||||
testID='edit_profile.scroll_view'
|
||||
style={styles.flex}
|
||||
>
|
||||
{updating && (
|
||||
<View
|
||||
style={styles.spinner}
|
||||
>
|
||||
<Loading
|
||||
color={theme.buttonBg}
|
||||
/>
|
||||
</View>
|
||||
|
||||
)}
|
||||
{Boolean(error) && <ProfileError error={error!}/>}
|
||||
<View style={styles.top}>
|
||||
<ProfilePicture
|
||||
author={currentUser}
|
||||
size={153}
|
||||
showStatus={false}
|
||||
/>
|
||||
</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}
|
||||
/>
|
||||
<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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default EditProfile;
|
||||
55
app/screens/edit_profile/index.ts
Normal file
55
app/screens/edit_profile/index.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
// 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 {MM_TABLES, SYSTEM_IDENTIFIERS} from '@constants/database';
|
||||
import {WithDatabaseArgs} from '@typings/database/database';
|
||||
import SystemModel from '@typings/database/models/servers/system';
|
||||
import UserModel from '@typings/database/models/servers/user';
|
||||
|
||||
import EditProfile from './edit_profile';
|
||||
|
||||
const {SERVER: {SYSTEM, USER}} = MM_TABLES;
|
||||
|
||||
const enhanced = withObservables([], ({database}: WithDatabaseArgs) => {
|
||||
const config = database.get<SystemModel>(MM_TABLES.SERVER.SYSTEM).findAndObserve(SYSTEM_IDENTIFIERS.CONFIG);
|
||||
|
||||
return {
|
||||
currentUser: database.get<SystemModel>(SYSTEM).findAndObserve(SYSTEM_IDENTIFIERS.CURRENT_USER_ID).pipe(
|
||||
switchMap(
|
||||
(id) => database.get<UserModel>(USER).findAndObserve(id.value),
|
||||
),
|
||||
),
|
||||
lockedFirstName: config.pipe(
|
||||
switchMap(
|
||||
({value}: {value: ClientConfig}) => of$(value.LdapFirstNameAttributeSet === 'true' || value.SamlFirstNameAttributeSet === 'true'),
|
||||
),
|
||||
),
|
||||
lockedLastName: config.pipe(
|
||||
switchMap(
|
||||
({value}: {value: ClientConfig}) => of$(value.LdapLastNameAttributeSet === 'true' || value.SamlLastNameAttributeSet === 'true'),
|
||||
),
|
||||
),
|
||||
lockedNickname: config.pipe(
|
||||
switchMap(
|
||||
({value}: {value: ClientConfig}) => of$(value.LdapNicknameAttributeSet === 'true' || value.SamlNicknameAttributeSet === 'true'),
|
||||
),
|
||||
),
|
||||
lockedPosition: config.pipe(
|
||||
switchMap(
|
||||
({value}: {value: ClientConfig}) => of$(value.LdapPositionAttributeSet === 'true' || value.SamlPositionAttributeSet === 'true'),
|
||||
),
|
||||
),
|
||||
lockedPicture: config.pipe(
|
||||
switchMap(
|
||||
({value}: {value: ClientConfig}) => of$(value.LdapPictureAttributeSet === 'true'),
|
||||
),
|
||||
),
|
||||
};
|
||||
});
|
||||
|
||||
export default withDatabase(enhanced(EditProfile));
|
||||
@@ -2,10 +2,13 @@
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback} from 'react';
|
||||
import {TextStyle} from 'react-native';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {DeviceEventEmitter, TextStyle} from 'react-native';
|
||||
|
||||
import DrawerItem from '@components/drawer_item';
|
||||
import FormattedText from '@components/formatted_text';
|
||||
import {Events, Screens} from '@constants';
|
||||
import {showModal} from '@screens/navigation';
|
||||
import {preventDoubleTap} from '@utils/tap';
|
||||
|
||||
type Props = {
|
||||
@@ -15,9 +18,17 @@ type Props = {
|
||||
}
|
||||
|
||||
const YourProfile = ({isTablet, style, theme}: Props) => {
|
||||
const intl = useIntl();
|
||||
const openProfile = useCallback(preventDoubleTap(() => {
|
||||
// TODO: Open Profile screen in either a screen or in line for tablets
|
||||
}), [isTablet]);
|
||||
if (isTablet) {
|
||||
DeviceEventEmitter.emit(Events.ACCOUNT_SELECT_TABLET_VIEW, Screens.EDIT_PROFILE);
|
||||
} else {
|
||||
showModal(
|
||||
Screens.EDIT_PROFILE,
|
||||
intl.formatMessage({id: 'mobile.screen.your_profile', defaultMessage: 'Your Profile'}),
|
||||
);
|
||||
}
|
||||
}), [isTablet, theme]);
|
||||
|
||||
return (
|
||||
<DrawerItem
|
||||
|
||||
@@ -6,6 +6,7 @@ import {DeviceEventEmitter} from 'react-native';
|
||||
|
||||
import {Events, Screens} from '@constants';
|
||||
import CustomStatus from '@screens/custom_status';
|
||||
import EditProfile from '@screens/edit_profile';
|
||||
|
||||
type SelectedView = {
|
||||
id: string;
|
||||
@@ -14,6 +15,7 @@ type SelectedView = {
|
||||
|
||||
const TabletView: Record<string, React.ReactNode> = {
|
||||
[Screens.CUSTOM_STATUS]: CustomStatus,
|
||||
[Screens.EDIT_PROFILE]: EditProfile,
|
||||
};
|
||||
|
||||
const AccountTabletView = () => {
|
||||
|
||||
@@ -34,7 +34,7 @@ type AccountScreenProps = {
|
||||
};
|
||||
|
||||
const {SERVER: {SYSTEM, USER}} = MM_TABLES;
|
||||
const edges: Edge[] = ['bottom', 'left', 'right'];
|
||||
const edges: Edge[] = ['left', 'right'];
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
||||
return {
|
||||
|
||||
@@ -105,9 +105,9 @@ Navigation.setLazyComponentRegistrator((screenName) => {
|
||||
// case 'EditPost':
|
||||
// screen = require('@screens/edit_post').default;
|
||||
// break;
|
||||
// case 'EditProfile':
|
||||
// screen = require('@screens/edit_profile').default;
|
||||
// break;
|
||||
case Screens.EDIT_PROFILE:
|
||||
screen = withServerDatabase((require('@screens/edit_profile').default));
|
||||
break;
|
||||
// case 'ErrorTeamsList':
|
||||
// screen = require('@screens/error_teams_list').default;
|
||||
// break;
|
||||
|
||||
@@ -12,6 +12,7 @@ import CompassIcon from '@components/compass_icon';
|
||||
import {Device, Preferences, Screens} from '@constants';
|
||||
import NavigationConstants from '@constants/navigation';
|
||||
import EphemeralStore from '@store/ephemeral_store';
|
||||
import {NavButtons} from '@typings/screens/navigation';
|
||||
import {changeOpacity, setNavigatorStyles} from '@utils/theme';
|
||||
|
||||
import type {LaunchProps} from '@typings/launch';
|
||||
@@ -464,12 +465,12 @@ export function showSearchModal(initialValue = '') {
|
||||
showModal(name, title, passProps, options);
|
||||
}
|
||||
|
||||
export async function dismissModal(options = {}) {
|
||||
export async function dismissModal(options?: Options & { componentId: string}) {
|
||||
if (!EphemeralStore.hasModalsOpened()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const componentId = EphemeralStore.getNavigationTopModalId();
|
||||
const componentId = options?.componentId || EphemeralStore.getNavigationTopModalId();
|
||||
if (componentId) {
|
||||
try {
|
||||
await Navigation.dismissModal(componentId, options);
|
||||
@@ -481,7 +482,7 @@ export async function dismissModal(options = {}) {
|
||||
}
|
||||
}
|
||||
|
||||
export async function dismissAllModals(options = {}) {
|
||||
export async function dismissAllModals(options: Options = {}) {
|
||||
if (!EphemeralStore.hasModalsOpened()) {
|
||||
return;
|
||||
}
|
||||
@@ -495,7 +496,7 @@ export async function dismissAllModals(options = {}) {
|
||||
}
|
||||
}
|
||||
|
||||
export function setButtons(componentId: string, buttons = {leftButtons: [], rightButtons: []}) {
|
||||
export function setButtons(componentId: string, buttons: NavButtons = {leftButtons: [], rightButtons: []}) {
|
||||
const options = {
|
||||
topBar: {
|
||||
...buttons,
|
||||
|
||||
@@ -150,6 +150,7 @@
|
||||
"mobile.about.powered_by": "{site} is powered by Mattermost",
|
||||
"mobile.about.serverVersion": "Server Version: {version} (Build {number})",
|
||||
"mobile.about.serverVersionNoBuild": "Server Version: {version}",
|
||||
"mobile.account.settings.save": "Save",
|
||||
"mobile.action_menu.select": "Select an option",
|
||||
"mobile.add_team.create_team": "Create a New Team",
|
||||
"mobile.add_team.join_team": "Join Another Team",
|
||||
@@ -239,6 +240,7 @@
|
||||
"mobile.routes.custom_status": "Set a Status",
|
||||
"mobile.routes.table": "Table",
|
||||
"mobile.routes.user_profile": "Profile",
|
||||
"mobile.screen.your_profile": "Your Profile",
|
||||
"mobile.server_identifier.exists": "You are already connected to this server.",
|
||||
"mobile.server_link.unreachable_channel.error": "This link belongs to a deleted channel or to a channel to which you do not have access.",
|
||||
"mobile.server_link.unreachable_team.error": "This link belongs to a deleted team or to a team to which you do not have access.",
|
||||
@@ -320,5 +322,8 @@
|
||||
"status_dropdown.set_online": "Online",
|
||||
"status_dropdown.set_ooo": "Out Of Office",
|
||||
"team_list.no_other_teams.description": "To join another team, ask a Team Admin for an invitation, or create your own team.",
|
||||
"team_list.no_other_teams.title": "No additional teams to join"
|
||||
"team_list.no_other_teams.title": "No additional teams to join",
|
||||
"user.edit_profile.email.auth_service": "Login occurs through {service}. Email cannot be updated. Email address used for notifications is {email}.",
|
||||
"user.edit_profile.email.web_client": "Email must be updated using a web client or desktop application.",
|
||||
"user.settings.general.field_handled_externally": "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."
|
||||
}
|
||||
|
||||
1
types/api/config.d.ts
vendored
1
types/api/config.d.ts
vendored
@@ -129,6 +129,7 @@ interface ClientConfig {
|
||||
LdapLoginButtonTextColor: string;
|
||||
LdapLoginFieldName: string;
|
||||
LdapNicknameAttributeSet: string;
|
||||
LdapPictureAttributeSet: string;
|
||||
LdapPositionAttributeSet: string;
|
||||
LockTeammateNameDisplay: string;
|
||||
MaxFileSize: string;
|
||||
|
||||
36
types/screens/edit_profile.d.ts
vendored
Normal file
36
types/screens/edit_profile.d.ts
vendored
Normal file
@@ -0,0 +1,36 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
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';
|
||||
|
||||
interface UserInfo extends Record<string, string | undefined | null| boolean> {
|
||||
email: string;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
nickname: string;
|
||||
position: string;
|
||||
username: string;
|
||||
}
|
||||
|
||||
type EditProfileProps = {
|
||||
componentId: string;
|
||||
currentUser: UserModel;
|
||||
isModal?: boolean;
|
||||
isTablet?: boolean;
|
||||
lockedFirstName: boolean;
|
||||
lockedLastName: boolean;
|
||||
lockedNickname: boolean;
|
||||
lockedPosition: boolean;
|
||||
lockedPicture: boolean;
|
||||
};
|
||||
|
||||
type FieldSequence = Record<string, {
|
||||
ref: RefObject<FloatingTextInputRef>;
|
||||
isDisabled: boolean;
|
||||
}>
|
||||
|
||||
type FieldConfig = Pick<FieldProps, 'blurOnSubmit' | 'enablesReturnKeyAutomatically' | 'onFocusNextField' | 'onTextChange' | 'returnKeyType'>
|
||||
9
types/screens/navigation.d.ts
vendored
Normal file
9
types/screens/navigation.d.ts
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {OptionsTopBarButton} from 'react-native-navigation/lib/src/interfaces/Options';
|
||||
|
||||
export type NavButtons = {
|
||||
leftButtons?: OptionsTopBarButton[];
|
||||
rightButtons?: OptionsTopBarButton[];
|
||||
}
|
||||
4
types/utils/index.d.ts
vendored
4
types/utils/index.d.ts
vendored
@@ -1,10 +1,8 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {ImageStyle, TextStyle, ViewStyle} from 'react-native';
|
||||
|
||||
type Dictionary<T> = {
|
||||
[key: string]: T;
|
||||
};
|
||||
|
||||
type Styles = ViewStyle | TextStyle | ImageStyle
|
||||
type ErrorText = Partial<ClientErrorProps> | string;
|
||||
|
||||
Reference in New Issue
Block a user