Unify buttons (#8865)

* Unity buttons

* Fix texts and use defineMessages

* Update snapshots

* Fix disabled style
This commit is contained in:
Daniel Espino García 2025-05-30 15:59:15 +02:00 committed by GitHub
parent 705ed603bc
commit 2ad6fec90f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
31 changed files with 661 additions and 821 deletions

View file

@ -2,14 +2,13 @@
// See LICENSE.txt for license information.
import {BottomSheetScrollView} from '@gorhom/bottom-sheet';
import {Button} from '@rneui/base';
import React, {useCallback, useMemo} from 'react';
import {useIntl} from 'react-intl';
import {ScrollView, Text, View} from 'react-native';
import {useSafeAreaInsets} from 'react-native-safe-area-context';
import {dismissAnnouncement} from '@actions/local/systems';
import FormattedText from '@components/formatted_text';
import Button from '@components/button';
import Markdown from '@components/markdown';
import {Screens} from '@constants';
import {useServerUrl} from '@context/server';
@ -17,7 +16,6 @@ import {useTheme} from '@context/theme';
import {useBottomSheetListsFix} from '@hooks/bottom_sheet_lists_fix';
import {useIsTablet} from '@hooks/device';
import {dismissBottomSheet} from '@screens/navigation';
import {buttonBackgroundStyle, buttonTextStyle} from '@utils/buttonStyles';
import {getMarkdownTextStyles, getMarkdownBlockStyles} from '@utils/markdown';
import {makeStyleSheetFromTheme} from '@utils/theme';
import {typography} from '@utils/typography';
@ -46,6 +44,9 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
color: theme.centerChannelColor,
...typography('Heading', 600, 'SemiBold'),
},
dismissButtonContainer: {
marginTop: 10,
},
};
});
@ -69,20 +70,7 @@ const ExpandedAnnouncementBanner = ({
const dismissBanner = useCallback(() => {
dismissAnnouncement(serverUrl, bannerText);
close();
}, [bannerText]);
const buttonStyles = useMemo(() => {
return {
okay: {
button: buttonBackgroundStyle(theme, 'lg', 'primary'),
text: buttonTextStyle(theme, 'lg', 'primary'),
},
dismiss: {
button: [{marginTop: 10}, buttonBackgroundStyle(theme, 'lg', 'link')],
text: buttonTextStyle(theme, 'lg', 'link'),
},
};
}, [theme]);
}, [bannerText, serverUrl]);
const containerStyle = useMemo(() => {
return [style.container, {marginBottom: insets.bottom + 10}];
@ -118,26 +106,21 @@ const ExpandedAnnouncementBanner = ({
/>
</Scroll>
<Button
buttonStyle={buttonStyles.okay.button}
text={intl.formatMessage({id: 'announcment_banner.okay', defaultMessage: 'Okay'})}
onPress={close}
>
<FormattedText
id='announcment_banner.okay'
defaultMessage={'Okay'}
style={buttonStyles.okay.text}
size='lg'
theme={theme}
/>
</Button>
{allowDismissal && (
<View style={style.dismissButtonContainer}>
<Button
buttonStyle={buttonStyles.dismiss.button}
text={intl.formatMessage({id: 'announcment_banner.dismiss', defaultMessage: 'Dismiss announcement'})}
onPress={dismissBanner}
>
<FormattedText
id='announcment_banner.dismiss'
defaultMessage={'Dismiss announcement'}
style={buttonStyles.dismiss.text}
size='lg'
theme={theme}
emphasis='link'
/>
</Button>
</View>
)}
</View>
);

View file

@ -6,8 +6,8 @@ import React, {useMemo, type ReactNode} from 'react';
import {type StyleProp, StyleSheet, Text, type TextStyle, View, type ViewStyle, type Insets} from 'react-native';
import CompassIcon from '@components/compass_icon';
import Loading from '@components/loading';
import {buttonBackgroundStyle, buttonTextStyle} from '@utils/buttonStyles';
import {changeOpacity} from '@utils/theme';
type Props = {
theme: Theme;
@ -16,16 +16,17 @@ type Props = {
textStyle?: StyleProp<TextStyle>;
size?: ButtonSize;
emphasis?: ButtonEmphasis;
buttonType?: ButtonType;
buttonState?: ButtonState;
testID?: string;
onPress: () => void;
onPress?: () => void;
text: string;
iconComponent?: ReactNode;
disabled?: boolean;
hitSlop?: Insets;
isIconOnTheRight?: boolean;
iconName?: string;
showLoader?: boolean;
isInverted?: boolean;
isDestructive?: boolean;
};
const styles = StyleSheet.create({
@ -50,8 +51,6 @@ const Button = ({
textStyle,
size = 'm',
emphasis,
buttonType,
buttonState,
onPress,
text,
testID,
@ -60,24 +59,44 @@ const Button = ({
iconComponent,
disabled,
hitSlop,
showLoader = false,
isInverted = false,
isDestructive = false,
}: Props) => {
const bgStyle = useMemo(() => [
buttonBackgroundStyle(theme, size, emphasis, buttonType, buttonState),
backgroundStyle,
], [theme, backgroundStyle, size, emphasis, buttonType, buttonState]);
let buttonType: ButtonType = 'default';
if (isDestructive) {
buttonType = 'destructive';
} else if (isInverted) {
buttonType = 'inverted';
}
const txtStyle = useMemo(() => [
const bgStyle = useMemo(() => [
buttonBackgroundStyle(theme, size, emphasis, buttonType),
backgroundStyle,
], [theme, backgroundStyle, size, emphasis, buttonType]);
const bgDisabledStyle = useMemo(() => [
buttonBackgroundStyle(theme, size, emphasis, 'disabled'),
backgroundStyle,
], [theme, backgroundStyle, size, emphasis]);
const txtStyle = useMemo(() => StyleSheet.flatten([
buttonTextStyle(theme, size, emphasis, buttonType),
textStyle,
], [theme, textStyle, size, emphasis, buttonType]);
]), [theme, textStyle, size, emphasis, buttonType]);
let buttonStyle = StyleSheet.flatten(bgStyle);
if (disabled) {
buttonStyle = {
...buttonStyle,
backgroundColor: changeOpacity(buttonStyle.backgroundColor! as string, 0.4),
};
}
const txtDisabledStyle = useMemo(() => StyleSheet.flatten([
buttonTextStyle(theme, size, emphasis, 'disabled'),
textStyle,
]), [theme, textStyle, size, emphasis]);
const txtStyleToUse = disabled ? txtDisabledStyle : txtStyle;
const loadingComponent = (
<Loading
color={txtStyleToUse.color}
/>
);
let icon: ReactNode;
@ -90,7 +109,7 @@ const Button = ({
<CompassIcon
name={iconName!}
size={iconSizePerSize[size]}
color={StyleSheet.flatten(txtStyle).color}
color={txtStyleToUse.color}
testID={`${testID}-icon`}
/>
</View>
@ -99,8 +118,9 @@ const Button = ({
return (
<ElementButton
buttonStyle={buttonStyle}
buttonStyle={bgStyle}
containerStyle={buttonContainerStyle}
disabledStyle={bgDisabledStyle}
onPress={onPress}
testID={testID}
disabled={disabled}
@ -110,9 +130,10 @@ const Button = ({
style={styles.container}
testID={`${testID}-text-container`}
>
{showLoader && loadingComponent}
{!isIconOnTheRight && icon}
<Text
style={[txtStyle]}
style={txtStyleToUse}
numberOfLines={1}
>
{text}

View file

@ -2,14 +2,14 @@
// See LICENSE.txt for license information.
import React from 'react';
import {ActivityIndicator, type StyleProp, View, type ViewStyle, Text, type TextStyle} from 'react-native';
import {ActivityIndicator, type StyleProp, View, type ViewStyle, Text, type TextStyle, type ColorValue} from 'react-native';
import {useTheme} from '@context/theme';
type LoadingProps = {
containerStyle?: StyleProp<ViewStyle>;
size?: number | 'small' | 'large';
color?: string;
color?: ColorValue;
themeColor?: keyof Theme;
footerText?: string;
footerTextStyles?: TextStyle;

View file

@ -73,11 +73,34 @@ exports[`Loading Error should match snapshot 1`] = `
Error description
</Text>
<View
style={
{
"marginTop": 24,
}
}
>
<View
style={
[
{
"overflow": "hidden",
},
{
"borderRadius": 2,
},
undefined,
false,
]
}
testID="RNE_BUTTON_WRAPPER"
>
<View
accessibilityRole="button"
accessibilityState={
{
"busy": undefined,
"busy": false,
"checked": undefined,
"disabled": undefined,
"disabled": false,
"expanded": undefined,
"selected": undefined,
}
@ -102,32 +125,54 @@ exports[`Loading Error should match snapshot 1`] = `
onStartShouldSetResponder={[Function]}
style={
{
"backgroundColor": "#ffffff",
"borderRadius": 4,
"marginTop": 24,
"opacity": 1,
}
}
>
<View
style={
{
"alignItems": "center",
"backgroundColor": "#1c58d9",
"borderColor": "#2089dc",
"borderRadius": 4,
"borderWidth": 0,
"flexDirection": "row",
"justifyContent": "center",
"padding": 8,
"paddingHorizontal": 24,
"paddingVertical": 12,
}
}
>
<Text
<View
style={
[
{
"alignItems": "center",
"flexDirection": "row",
"gap": 7,
}
}
testID="undefined-text-container"
>
<Text
numberOfLines={1}
style={
{
"color": "#ffffff",
"fontFamily": "OpenSans-SemiBold",
"fontSize": 16,
"fontWeight": "600",
"lineHeight": 24,
},
{
"color": "#1c58d9",
},
]
}
}
>
Retry
</Text>
</View>
</View>
</View>
</View>
</View>
</View>
`;

View file

@ -1,14 +1,14 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {useMemo} from 'react';
import React from 'react';
import {useIntl} from 'react-intl';
import {Text, View} from 'react-native';
import Button from '@components/button';
import CompassIcon from '@components/compass_icon';
import Loading from '@components/loading';
import TouchableWithFeedback from '@components/touchable_with_feedback';
import {useTheme} from '@context/theme';
import {buttonBackgroundStyle, buttonTextStyle} from '@utils/buttonStyles';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
import {typography} from '@utils/typography';
@ -49,14 +49,15 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
textAlign: 'center',
marginTop: 4,
},
buttonContainer: {
marginTop: 24,
},
}));
const LoadingError = ({loading, message, onRetry, title}: Props) => {
const theme = useTheme();
const intl = useIntl();
const styles = getStyleSheet(theme);
const buttonStyle = useMemo(() => {
return [{marginTop: 24}, buttonBackgroundStyle(theme, 'lg', 'primary', 'inverted')];
}, [theme]);
if (loading) {
return (
@ -81,13 +82,14 @@ const LoadingError = ({loading, message, onRetry, title}: Props) => {
<Text style={[typography('Body', 200), styles.body]}>
{message}
</Text>
<TouchableWithFeedback
style={buttonStyle}
<View style={styles.buttonContainer}>
<Button
text={intl.formatMessage({id: 'loading_error.retry', defaultMessage: 'Retry'})}
onPress={onRetry}
type={'opacity'}
>
<Text style={buttonTextStyle(theme, 'lg', 'primary', 'inverted')}>{'Retry'}</Text>
</TouchableWithFeedback>
size='lg'
theme={theme}
/>
</View>
</View>
);
};

View file

@ -186,22 +186,13 @@ exports[`Section notice match snapshot 1`] = `
<Text
numberOfLines={1}
style={
[
[
[
{
"color": "#ffffff",
"fontFamily": "OpenSans-SemiBold",
"fontSize": 14,
"fontWeight": "600",
"lineHeight": 20,
},
{
"color": "#ffffff",
},
],
undefined,
],
]
}
}
>
primary button
@ -297,22 +288,13 @@ exports[`Section notice match snapshot 1`] = `
<Text
numberOfLines={1}
style={
[
[
[
{
"color": "#1c58d9",
"fontFamily": "OpenSans-SemiBold",
"fontSize": 14,
"fontWeight": "600",
"lineHeight": 20,
},
{
"color": "#1c58d9",
},
],
undefined,
],
]
}
}
>
secondary button
@ -408,22 +390,13 @@ exports[`Section notice match snapshot 1`] = `
<Text
numberOfLines={1}
style={
[
[
[
{
"color": "#1c58d9",
"fontFamily": "OpenSans-SemiBold",
"fontSize": 14,
"fontWeight": "600",
"lineHeight": 20,
},
{
"color": "#1c58d9",
},
],
undefined,
],
]
}
}
>
link button

View file

@ -270,10 +270,10 @@ export default function SelectedUsers({
iconName={buttonIcon}
text={buttonText}
theme={theme}
buttonType={isDisabled ? 'disabled' : 'default'}
emphasis={'primary'}
size={'lg'}
testID={`${testID}.start.button`}
disabled={isDisabled}
/>
</Animated.View>
</Animated.View>

View file

@ -1,13 +1,13 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {Button} from '@rneui/base';
import React, {useCallback, useEffect, useMemo, useReducer, useRef, useState} from 'react';
import {useIntl} from 'react-intl';
import {Keyboard, ScrollView, Text, View} from 'react-native';
import {Keyboard, ScrollView, View} from 'react-native';
import {SafeAreaView} from 'react-native-safe-area-context';
import {handleGotoLocation} from '@actions/remote/command';
import Button from '@components/button';
import CompassIcon from '@components/compass_icon';
import Markdown from '@components/markdown';
import {Screens} from '@constants';
@ -18,7 +18,6 @@ import useDidUpdate from '@hooks/did_update';
import useNavButtonPressed from '@hooks/navigation_button_pressed';
import SecurityManager from '@managers/security_manager';
import {filterEmptyOptions} from '@utils/apps';
import {buttonBackgroundStyle, buttonTextStyle} from '@utils/buttonStyles';
import {checkDialogElementForError, checkIfErrorsMatchElements} from '@utils/integrations';
import {getMarkdownBlockStyles, getMarkdownTextStyles} from '@utils/markdown';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
@ -383,9 +382,6 @@ function AppsFormComponent({
useNavButtonPressed(CLOSE_BUTTON_ID, componentId, close, [close]);
useNavButtonPressed(SUBMIT_BUTTON_ID, componentId, handleSubmit, [handleSubmit]);
const submitButtonStyle = useMemo(() => buttonBackgroundStyle(theme, 'lg', 'primary', 'default'), [theme]);
const submitButtonTextStyle = useMemo(() => buttonTextStyle(theme, 'lg', 'primary', 'default'), [theme]);
return (
<SafeAreaView
testID='interactive_dialog.screen'
@ -444,10 +440,10 @@ function AppsFormComponent({
>
<Button
onPress={() => handleSubmit(o.value)}
buttonStyle={submitButtonStyle}
>
<Text style={submitButtonTextStyle}>{o.label}</Text>
</Button>
theme={theme}
size='lg'
text={o.label || ''}
/>
</View>
))}
</View>

View file

@ -2,18 +2,16 @@
// See LICENSE.txt for license information.
import React from 'react';
import {type GestureResponderEvent, Platform, Text, useWindowDimensions, View} from 'react-native';
import {Platform, useWindowDimensions, View} from 'react-native';
import CompassIcon from '@components/compass_icon';
import TouchableWithFeedback from '@components/touchable_with_feedback';
import Button from '@components/button';
import {useTheme} from '@context/theme';
import {useIsTablet} from '@hooks/device';
import {buttonBackgroundStyle, buttonTextStyle} from '@utils/buttonStyles';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
type Props = {
disabled?: boolean;
onPress?: (e: GestureResponderEvent) => void;
onPress?: () => void;
icon?: string;
testID?: string;
text?: string;
@ -21,22 +19,12 @@ type Props = {
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
return {
button: {
display: 'flex',
flexDirection: 'row',
},
buttonContainer: {
paddingHorizontal: 20,
},
container: {
backgroundColor: theme.centerChannelBg,
},
iconContainer: {
width: 24,
height: 24,
top: -1,
marginRight: 4,
},
separator: {
height: 1,
right: 20,
@ -56,41 +44,23 @@ function BottomSheetButton({disabled = false, onPress, icon, testID, text}: Prop
const styles = getStyleSheet(theme);
const separatorWidth = Math.max(dimensions.width, 450);
const buttonType = disabled ? 'disabled' : 'default';
const styleButtonText = buttonTextStyle(theme, 'lg', 'primary', buttonType);
const styleButtonBackground = buttonBackgroundStyle(theme, 'lg', 'primary', buttonType);
const iconColor = disabled ? changeOpacity(theme.centerChannelColor, 0.32) : theme.buttonColor;
return (
<View style={styles.container}>
<View style={[styles.separator, {width: separatorWidth}]}/>
<View style={styles.buttonContainer}>
<TouchableWithFeedback
<Button
onPress={onPress}
type='opacity'
style={[styles.button, styleButtonBackground]}
text={text || ''}
iconName={icon}
testID={testID}
>
{icon && (
<View style={styles.iconContainer}>
<CompassIcon
size={24}
name={icon}
color={iconColor}
theme={theme}
size='lg'
disabled={disabled}
/>
</View>
)}
{text && (
<Text
style={styleButtonText}
>{text}</Text>
)}
</TouchableWithFeedback>
<View style={{paddingBottom: Platform.select({ios: (isTablet ? 20 : 0), android: 20})}}/>
</View>
</View>
);
}

View file

@ -2,7 +2,7 @@
// See LICENSE.txt for license information.
import React from 'react';
import {type GestureResponderEvent, Text, useWindowDimensions, View} from 'react-native';
import {Text, useWindowDimensions, View} from 'react-native';
import {useTheme} from '@context/theme';
import {useIsTablet} from '@hooks/device';
@ -15,7 +15,7 @@ type Props = {
buttonText?: string;
children: React.ReactNode;
disableButton?: boolean;
onPress?: (e: GestureResponderEvent) => void;
onPress?: () => void;
showButton: boolean;
showTitle: boolean;
testID?: string;

View file

@ -303,7 +303,6 @@ const ChannelBookmarkAddOrEdit = ({
{canDeleteBookmarks &&
<View style={styles.deleteContainer}>
<Button
buttonType='destructive'
size='m'
text='Delete bookmark'
iconName='trash-can-outline'
@ -311,6 +310,7 @@ const ChannelBookmarkAddOrEdit = ({
onPress={onDelete}
theme={theme}
disabled={isSaving}
isDestructive={true}
/>
</View>
}

View file

@ -85,7 +85,6 @@ const MutedBanner = ({channelId}: Props) => {
/>
<View style={styles.button}>
<Button
buttonType='default'
onPress={onPress}
text={formatMessage({
id: 'channel_notification_preferences.unmute_content',

View file

@ -7,14 +7,13 @@ import {Text, View} from 'react-native';
import {convertGroupMessageToPrivateChannel, switchToChannelById} from '@actions/remote/channel';
import Button from '@components/button';
import Loading from '@components/loading';
import {ServerErrors} from '@constants';
import {useServerUrl} from '@context/server';
import {useTheme} from '@context/theme';
import {isErrorWithMessage, isServerError} from '@utils/errors';
import {logError} from '@utils/log';
import {preventDoubleTap} from '@utils/tap';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
import {makeStyleSheetFromTheme} from '@utils/theme';
import {displayUsername} from '@utils/user';
import {ChannelNameInput} from '../channel_name_input';
@ -135,13 +134,6 @@ export const ConvertGMToChannelForm = ({
memberNames,
});
const buttonIcon = conversionInProgress ? (
<Loading
containerStyle={styles.loadingContainerStyle}
color={changeOpacity(theme.centerChannelColor, 0.32)}
/>
) : null;
return (
<View style={styles.container}>
<MessageBox
@ -164,9 +156,10 @@ export const ConvertGMToChannelForm = ({
onPress={handleOnPress}
text={confirmButtonText}
theme={theme}
buttonType={submitButtonEnabled ? 'destructive' : 'disabled'}
size='lg'
iconComponent={buttonIcon}
disabled={!submitButtonEnabled}
isDestructive={true}
showLoader={conversionInProgress}
/>
{
errorMessage &&

View file

@ -1,17 +1,14 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {Button} from '@rneui/base';
import React, {type MutableRefObject, useCallback, useEffect, useRef} from 'react';
import {useIntl} from 'react-intl';
import {defineMessages, useIntl} from 'react-intl';
import {Keyboard, Platform, useWindowDimensions, View} from 'react-native';
import Button from '@components/button';
import FloatingTextInput, {type FloatingTextInputRef} from '@components/floating_text_input_label';
import FormattedText from '@components/formatted_text';
import Loading from '@components/loading';
import {useIsTablet} from '@hooks/device';
import {t} from '@i18n';
import {buttonBackgroundStyle, buttonTextStyle} from '@utils/buttonStyles';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
import {typography} from '@utils/typography';
import {removeProtocol, stripTrailingSlashes} from '@utils/url';
@ -49,24 +46,23 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
marginTop: 8,
...typography('Body', 75, 'Regular'),
},
connectButton: {
backgroundColor: changeOpacity(theme.centerChannelColor, 0.08),
buttonContainer: {
width: '100%',
marginTop: 32,
marginLeft: 20,
marginRight: 20,
},
loadingContainerStyle: {
marginRight: 10,
padding: 0,
top: -2,
},
loading: {
height: 20,
width: 20,
},
}));
const messages = defineMessages({
save: {
id: 'edit_server.save',
defaultMessage: 'Save',
},
saving: {
id: 'edit_server.saving',
defaultMessage: 'Saving',
},
});
const EditServerForm = ({
buttonDisabled,
connecting,
@ -91,7 +87,7 @@ const EditServerForm = ({
keyboardAwareRef.current?.scrollToPosition(0, 0);
}
}
}, []);
}, [keyboardAwareRef]);
const onUpdate = useCallback(() => {
Keyboard.dismiss();
@ -114,7 +110,7 @@ const EditServerForm = ({
keyboardAwareRef.current?.scrollToPosition(0, offsetY);
});
}
}, [dimensions, isTablet]);
}, [dimensions, isTablet, keyboardAwareRef]);
useEffect(() => {
if (Platform.OS === 'ios' && isTablet) {
@ -126,25 +122,6 @@ const EditServerForm = ({
}
}, [onFocus]);
const buttonType = buttonDisabled ? 'disabled' : 'default';
const styleButtonText = buttonTextStyle(theme, 'lg', 'primary', buttonType);
const styleButtonBackground = buttonBackgroundStyle(theme, 'lg', 'primary', buttonType);
let buttonID = t('edit_server.save');
let buttonText = 'Save';
let buttonIcon;
if (connecting) {
buttonID = t('edit_server.saving');
buttonText = 'Saving';
buttonIcon = (
<Loading
containerStyle={styles.loadingContainerStyle}
color={theme.buttonColor}
/>
);
}
const saveButtonTestId = buttonDisabled ? 'edit_server_form.save.button.disabled' : 'edit_server_form.save.button';
return (
@ -180,21 +157,17 @@ const EditServerForm = ({
values={{url: removeProtocol(stripTrailingSlashes(serverUrl))}}
/>
}
<View style={styles.buttonContainer}>
<Button
containerStyle={styles.connectButton}
buttonStyle={styleButtonBackground}
disabledStyle={styleButtonBackground}
disabled={buttonDisabled}
disabled={buttonDisabled || connecting}
onPress={onUpdate}
testID={saveButtonTestId}
>
{buttonIcon}
<FormattedText
defaultMessage={buttonText}
id={buttonID}
style={styleButtonText}
size='lg'
theme={theme}
text={formatMessage(connecting ? messages.saving : messages.save)}
showLoader={connecting}
/>
</Button>
</View>
</View>
);
};

View file

@ -1,7 +1,6 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {Button} from '@rneui/base';
import React, {useCallback, useEffect, useRef, useState} from 'react';
import {useIntl} from 'react-intl';
import {Keyboard, Platform, Text, useWindowDimensions, View} from 'react-native';
@ -11,6 +10,7 @@ import Animated, {useAnimatedStyle, useSharedValue, withTiming} from 'react-nati
import {SafeAreaView} from 'react-native-safe-area-context';
import {sendPasswordResetEmail} from '@actions/remote/session';
import Button from '@components/button';
import FloatingTextInput from '@components/floating_text_input_label';
import FormattedText from '@components/formatted_text';
import {Screens} from '@constants';
@ -18,7 +18,6 @@ import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
import {useAvoidKeyboard} from '@hooks/device';
import SecurityManager from '@managers/security_manager';
import Background from '@screens/background';
import {buttonBackgroundStyle, buttonTextStyle} from '@utils/buttonStyles';
import {isEmail} from '@utils/helpers';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
import {typography} from '@utils/typography';
@ -65,7 +64,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
justifyContent: 'center',
paddingHorizontal: 24,
},
returnButton: {
returnButtonContainer: {
marginTop: 32,
},
subheader: {
@ -158,17 +157,15 @@ const ForgotPassword = ({componentId, serverUrl, theme}: Props) => {
<Text style={styles.successText}>
{email}
</Text>
<View style={styles.returnButtonContainer}>
<Button
testID='password_send.return'
onPress={onReturn}
buttonStyle={[styles.returnButton, buttonBackgroundStyle(theme, 'lg', 'primary', 'default')]}
>
<FormattedText
id='password_send.return'
defaultMessage='Return to Log In'
style={buttonTextStyle(theme, 'lg', 'primary', 'default')}
size='lg'
theme={theme}
text={formatMessage({id: 'password_send.return', defaultMessage: 'Return to Log In'})}
/>
</Button>
</View>
</View>
);
}
@ -221,19 +218,16 @@ const ForgotPassword = ({componentId, serverUrl, theme}: Props) => {
theme={theme}
value={email}
/>
<View style={styles.returnButtonContainer}>
<Button
testID='forgot.password.button'
buttonStyle={[styles.returnButton, buttonBackgroundStyle(theme, 'lg', 'primary', 'default'), error ? styles.error : undefined]}
disabledStyle={[styles.returnButton, buttonBackgroundStyle(theme, 'lg', 'primary', 'disabled'), error ? styles.error : undefined]}
disabled={!email}
onPress={submitResetPassword}
>
<FormattedText
id='password_send.reset'
defaultMessage='Reset my password'
style={buttonTextStyle(theme, 'lg', 'primary', email ? 'default' : 'disabled')}
size='lg'
text={formatMessage({id: 'password_send.reset', defaultMessage: 'Reset my password'})}
theme={theme}
/>
</Button>
</View>
</View>
</View>
</KeyboardAwareScrollView>

View file

@ -256,11 +256,34 @@ exports[`components/categories_list should render channels error 1`] = `
There was a problem loading content for this team.
</Text>
<View
style={
{
"marginTop": 24,
}
}
>
<View
style={
[
{
"overflow": "hidden",
},
{
"borderRadius": 2,
},
undefined,
false,
]
}
testID="RNE_BUTTON_WRAPPER"
>
<View
accessibilityRole="button"
accessibilityState={
{
"busy": undefined,
"busy": false,
"checked": undefined,
"disabled": undefined,
"disabled": false,
"expanded": undefined,
"selected": undefined,
}
@ -285,34 +308,56 @@ exports[`components/categories_list should render channels error 1`] = `
onStartShouldSetResponder={[Function]}
style={
{
"backgroundColor": "#ffffff",
"borderRadius": 4,
"marginTop": 24,
"opacity": 1,
}
}
>
<View
style={
{
"alignItems": "center",
"backgroundColor": "#1c58d9",
"borderColor": "#2089dc",
"borderRadius": 4,
"borderWidth": 0,
"flexDirection": "row",
"justifyContent": "center",
"padding": 8,
"paddingHorizontal": 24,
"paddingVertical": 12,
}
}
>
<Text
<View
style={
[
{
"alignItems": "center",
"flexDirection": "row",
"gap": 7,
}
}
testID="undefined-text-container"
>
<Text
numberOfLines={1}
style={
{
"color": "#ffffff",
"fontFamily": "OpenSans-SemiBold",
"fontSize": 16,
"fontWeight": "600",
"lineHeight": 24,
},
{
"color": "#1c58d9",
},
]
}
}
>
Retry
</Text>
</View>
</View>
</View>
</View>
</View>
</View>
</View>
`;
@ -643,11 +688,34 @@ exports[`components/categories_list should render team error 1`] = `
There was a problem loading content for this server.
</Text>
<View
style={
{
"marginTop": 24,
}
}
>
<View
style={
[
{
"overflow": "hidden",
},
{
"borderRadius": 2,
},
undefined,
false,
]
}
testID="RNE_BUTTON_WRAPPER"
>
<View
accessibilityRole="button"
accessibilityState={
{
"busy": undefined,
"busy": false,
"checked": undefined,
"disabled": undefined,
"disabled": false,
"expanded": undefined,
"selected": undefined,
}
@ -672,33 +740,55 @@ exports[`components/categories_list should render team error 1`] = `
onStartShouldSetResponder={[Function]}
style={
{
"backgroundColor": "#ffffff",
"borderRadius": 4,
"marginTop": 24,
"opacity": 1,
}
}
>
<View
style={
{
"alignItems": "center",
"backgroundColor": "#1c58d9",
"borderColor": "#2089dc",
"borderRadius": 4,
"borderWidth": 0,
"flexDirection": "row",
"justifyContent": "center",
"padding": 8,
"paddingHorizontal": 24,
"paddingVertical": 12,
}
}
>
<Text
<View
style={
[
{
"alignItems": "center",
"flexDirection": "row",
"gap": 7,
}
}
testID="undefined-text-container"
>
<Text
numberOfLines={1}
style={
{
"color": "#ffffff",
"fontFamily": "OpenSans-SemiBold",
"fontSize": 16,
"fontWeight": "600",
"lineHeight": 24,
},
{
"color": "#1c58d9",
},
]
}
}
>
Retry
</Text>
</View>
</View>
</View>
</View>
</View>
</View>
</View>
`;

View file

@ -1,14 +1,14 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {useMemo} from 'react';
import React from 'react';
import {useIntl} from 'react-intl';
import {View} from 'react-native';
import {showUnreadChannelsOnly} from '@actions/local/channel';
import Button from '@components/button';
import FormattedText from '@components/formatted_text';
import TouchableWithFeedback from '@components/touchable_with_feedback';
import {useServerUrl} from '@context/server';
import {useTheme} from '@context/theme';
import {buttonBackgroundStyle, buttonTextStyle} from '@utils/buttonStyles';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
import {typography} from '@utils/typography';
@ -19,7 +19,7 @@ type Props = {
}
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
button: {
buttonContainer: {
marginTop: 24,
},
container: {
@ -45,13 +45,11 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
}));
function EmptyUnreads({onlyUnreads}: Props) {
const intl = useIntl();
const theme = useTheme();
const serverUrl = useServerUrl();
const styles = getStyleSheet(theme);
const buttonStyle = useMemo(() => [buttonBackgroundStyle(theme, 'lg', 'tertiary', 'inverted'), styles.button],
[theme]);
const onPress = () => {
showUnreadChannelsOnly(serverUrl, !onlyUnreads);
};
@ -71,17 +69,16 @@ function EmptyUnreads({onlyUnreads}: Props) {
style={styles.paragraph}
testID='unreads.empty.paragraph'
/>
<TouchableWithFeedback
style={buttonStyle}
<View style={styles.buttonContainer}>
<Button
text={intl.formatMessage({id: 'unreads.empty.show_all', defaultMessage: 'Show all'})}
theme={theme}
size='lg'
onPress={onPress}
type={'opacity'}
>
<FormattedText
id='unreads.empty.show_all'
defaultMessage='Show all'
style={buttonTextStyle(theme, 'lg', 'tertiary', 'inverted')}
emphasis='tertiary'
isInverted={true}
/>
</TouchableWithFeedback>
</View>
</View>
);
}

View file

@ -1,19 +1,15 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {Button} from '@rneui/base';
import React, {useCallback, useMemo} from 'react';
import {useIntl} from 'react-intl';
import {defineMessages, useIntl} from 'react-intl';
import {View, Text, ScrollView} from 'react-native';
import CompassIcon from '@components/compass_icon';
import FormattedText from '@components/formatted_text';
import Button from '@components/button';
import AlertSvg from '@components/illustrations/alert';
import ErrorSvg from '@components/illustrations/error';
import SuccessSvg from '@components/illustrations/success';
import {useTheme} from '@context/theme';
import {t} from '@i18n';
import {buttonBackgroundStyle, buttonTextStyle} from '@utils/buttonStyles';
import {makeStyleSheetFromTheme, changeOpacity} from '@utils/theme';
import {typography} from '@utils/typography';
@ -77,17 +73,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
flexGrow: 1,
flexDirection: 'row',
justifyContent: 'center',
},
summaryButtonTextContainer: {
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
height: 24,
},
summaryButtonIcon: {
marginRight: 7,
color: theme.buttonColor,
gap: 8,
},
summaryButtonContainerStyle: {flexGrow: 1},
};
@ -109,6 +95,21 @@ type SummaryProps = {
onBack: () => void;
}
const messages = defineMessages({
done: {
id: 'invite.summary.done',
defaultMessage: 'Done',
},
tryAgain: {
id: 'invite.summary.try_again',
defaultMessage: 'Try again',
},
back: {
id: 'invite.summary.back',
defaultMessage: 'Go back',
},
});
export default function Summary({
result,
selectedIds,
@ -118,7 +119,7 @@ export default function Summary({
onRetry,
onBack,
}: SummaryProps) {
const {formatMessage, locale} = useIntl();
const {formatMessage} = useIntl();
const theme = useTheme();
const styles = getStyleSheet(theme);
@ -183,64 +184,41 @@ export default function Summary({
let onPress;
let iconName = '';
let text;
let styleButtonText = buttonTextStyle(theme, 'lg', 'primary');
let styleButtonBackground = buttonBackgroundStyle(theme, 'lg', 'primary');
const styleButtonIcon = [];
styleButtonIcon.push(styles.summaryButtonIcon);
let emphasis: ButtonEmphasis = 'primary';
switch (type) {
case SummaryButtonType.BACK:
onPress = onBack;
iconName = 'chevron-left';
text = {
id: t('invite.summary.back'),
defaultMessage: 'Go back',
};
styleButtonText = buttonTextStyle(theme, 'lg', 'tertiary');
styleButtonBackground = [buttonBackgroundStyle(theme, 'lg', 'tertiary'), {marginRight: 8}];
styleButtonIcon.push({color: theme.buttonBg});
text = messages.back;
emphasis = 'tertiary';
break;
case SummaryButtonType.RETRY:
onPress = onRetry;
iconName = 'refresh';
text = {
id: t('invite.summary.try_again'),
defaultMessage: 'Try again',
};
text = messages.tryAgain;
break;
case SummaryButtonType.DONE:
default:
onPress = onClose;
text = {
id: t('invite.summary.done'),
defaultMessage: 'Done',
};
text = messages.done;
break;
}
return (
<View style={styles.summaryButtonContainerStyle}>
<Button
containerStyle={styles.summaryButtonContainerStyle}
buttonStyle={styleButtonBackground}
onPress={onPress}
testID={`invite.summary_button.${SummaryButtonType.RETRY}`}
>
<View style={styles.summaryButtonTextContainer}>
{iconName && (
<CompassIcon
name={iconName}
size={24}
style={styleButtonIcon}
/>
)}
<FormattedText
{...text}
style={styleButtonText}
text={formatMessage(text)}
iconName={iconName}
emphasis={emphasis}
size='lg'
theme={theme}
/>
</View>
</Button>
);
}, [theme, locale, onClose, onRetry, onBack]);
}, [styles.summaryButtonContainerStyle, formatMessage, theme, onBack, onRetry, onClose]);
return (
<View

View file

@ -2,21 +2,19 @@
// See LICENSE.txt for license information.
import {useManagedConfig} from '@mattermost/react-native-emm';
import {Button} from '@rneui/base';
import {Button as RNEButton} from '@rneui/base';
import React, {useCallback, useEffect, useMemo, useRef, useState, type RefObject} from 'react';
import {useIntl} from 'react-intl';
import {defineMessages, useIntl} from 'react-intl';
import {Keyboard, TextInput, TouchableOpacity, View} from 'react-native';
import {login} from '@actions/remote/session';
import Button from '@components/button';
import CompassIcon from '@components/compass_icon';
import FloatingTextInput from '@components/floating_text_input_label';
import FormattedText from '@components/formatted_text';
import Loading from '@components/loading';
import {FORGOT_PASSWORD, MFA} from '@constants/screens';
import {useAvoidKeyboard} from '@hooks/device';
import {t} from '@i18n';
import {goToScreen, loginAnimationOptions, resetToHome} from '@screens/navigation';
import {buttonBackgroundStyle, buttonTextStyle} from '@utils/buttonStyles';
import {getFullErrorMessage, getServerError, isErrorWithMessage, isServerError} from '@utils/errors';
import {preventDoubleTap} from '@utils/tap';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
@ -67,12 +65,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
fontSize: 14,
fontFamily: 'OpenSans-SemiBold',
},
loadingContainerStyle: {
marginRight: 10,
padding: 0,
top: -2,
},
loginButton: {
loginButtonContainer: {
marginTop: 25,
},
endAdornment: {
@ -80,6 +73,17 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
},
}));
const messages = defineMessages({
signIn: {
id: 'login.signIn',
defaultMessage: 'Log In',
},
signingIn: {
id: 'login.signingIn',
defaultMessage: 'Logging In',
},
});
const LoginForm = ({config, extra, keyboardAwareRef, serverDisplayName, launchError, launchType, license, serverUrl, theme}: LoginProps) => {
const styles = getStyleSheet(theme);
const loginRef = useRef<TextInput>(null);
@ -256,44 +260,20 @@ const LoginForm = ({config, extra, keyboardAwareRef, serverDisplayName, launchEr
}, [loginId, password]);
const renderProceedButton = useMemo(() => {
const buttonType = buttonDisabled ? 'disabled' : 'default';
const styleButtonText = buttonTextStyle(theme, 'lg', 'primary', buttonType);
const styleButtonBackground = buttonBackgroundStyle(theme, 'lg', 'primary', buttonType);
let buttonID = t('login.signIn');
let buttonText = 'Log In';
let buttonIcon;
if (isLoading) {
buttonID = t('login.signingIn');
buttonText = 'Logging In';
buttonIcon = (
<Loading
containerStyle={styles.loadingContainerStyle}
color={theme.buttonColor}
/>
);
}
const signinButtonTestId = buttonDisabled ? 'login_form.signin.button.disabled' : 'login_form.signin.button';
return (
<View style={styles.loginButtonContainer}>
<Button
disabled={buttonDisabled}
onPress={onLogin}
buttonStyle={[styles.loginButton, styleButtonBackground]}
disabledStyle={[styles.loginButton, styleButtonBackground]}
testID={signinButtonTestId}
>
{buttonIcon}
<FormattedText
id={buttonID}
defaultMessage={buttonText}
style={styleButtonText}
size='lg'
testID={buttonDisabled ? 'login_form.signin.button.disabled' : 'login_form.signin.button'}
text={intl.formatMessage(isLoading ? messages.signingIn : messages.signIn)}
showLoader={isLoading}
theme={theme}
/>
</Button>
</View>
);
}, [buttonDisabled, loginId, password, isLoading, theme]);
}, [styles.loginButtonContainer, buttonDisabled, onLogin, intl, isLoading, theme]);
const endAdornment = (
<TouchableOpacity
@ -356,7 +336,7 @@ const LoginForm = ({config, extra, keyboardAwareRef, serverDisplayName, launchEr
/>
{(emailEnabled || usernameEnabled) && config.PasswordEnableForgotLink !== 'false' && (
<Button
<RNEButton
onPress={onPressForgotPassword}
buttonStyle={[styles.forgotPasswordBtn, error ? styles.forgotPasswordError : undefined]}
testID='login_form.forgot_password.button'
@ -366,7 +346,7 @@ const LoginForm = ({config, extra, keyboardAwareRef, serverDisplayName, launchEr
defaultMessage='Forgot your password?'
style={styles.forgotPasswordTxt}
/>
</Button>
</RNEButton>
)}
{renderProceedButton}
</View>

View file

@ -1,15 +1,12 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {Button} from '@rneui/base';
import React from 'react';
import {useIntl} from 'react-intl';
import {Image, type ImageSourcePropType, Text, View} from 'react-native';
import {Image, type ImageSourcePropType, StyleSheet, View} from 'react-native';
import CompassIcon from '@components/compass_icon';
import Button from '@components/button';
import {Sso} from '@constants';
import {buttonBackgroundStyle} from '@utils/buttonStyles';
import {makeStyleSheetFromTheme, changeOpacity} from '@utils/theme';
type SsoInfo = {
text: string;
@ -26,8 +23,6 @@ type Props = {
const SsoOptions = ({goToSso, ssoOnly, ssoOptions, theme}: Props) => {
const {formatMessage} = useIntl();
const styles = getStyleSheet(theme);
const styleButtonBackground = buttonBackgroundStyle(theme, 'lg', 'primary');
const getSsoButtonOptions = ((ssoType: string): SsoInfo => {
const sso: SsoInfo = {} as SsoInfo;
@ -63,12 +58,8 @@ const SsoOptions = ({goToSso, ssoOnly, ssoOptions, theme}: Props) => {
(ssoType: string) => ssoOptions[ssoType].enabled,
);
let styleViewContainer;
let styleButtonContainer;
if (enabledSSOs.length === 2 && !ssoOnly) {
styleViewContainer = styles.containerAsRow;
styleButtonContainer = styles.buttonContainer;
}
const styleViewContainer = enabledSSOs.length === 2 && !ssoOnly ? styles.containerAsRow : undefined;
const styleButtonWrapper = enabledSSOs.length === 2 && !ssoOnly ? styles.buttonWrapper : undefined;
const componentArray = [];
for (const ssoType of enabledSSOs) {
@ -78,38 +69,24 @@ const SsoOptions = ({goToSso, ssoOnly, ssoOptions, theme}: Props) => {
};
componentArray.push(
<View style={styleButtonWrapper}>
<Button
key={ssoType}
onPress={handlePress}
buttonStyle={[styleButtonBackground, styles.button]}
containerStyle={styleButtonContainer}
>
{imageSrc && (
size='lg'
theme={theme}
iconName={compassIcon}
emphasis='secondary'
iconComponent={imageSrc ? (
<Image
key={'image' + ssoType}
source={imageSrc}
style={styles.logoStyle}
/>
)}
{compassIcon &&
<CompassIcon
name={compassIcon}
size={16}
color={theme.centerChannelColor}
) : undefined}
text={text}
/>
}
<View
style={styles.buttonTextContainer}
>
<Text
key={ssoType}
style={styles.buttonText}
testID={text}
>
{text}
</Text>
</View>
</Button>,
</View>,
);
}
@ -120,41 +97,23 @@ const SsoOptions = ({goToSso, ssoOnly, ssoOptions, theme}: Props) => {
);
};
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
const styles = StyleSheet.create({
container: {
marginVertical: 24,
gap: 8,
},
containerAsRow: {
flexDirection: 'row',
alignItems: 'center',
},
buttonContainer: {
width: '48%',
marginRight: 8,
},
button: {
marginVertical: 4,
backgroundColor: 'transparent',
borderWidth: 1,
borderColor: changeOpacity(theme.centerChannelColor, 0.16),
},
buttonTextContainer: {
color: theme.centerChannelColor,
flexDirection: 'row',
marginLeft: 9,
},
buttonText: {
color: theme.centerChannelColor,
fontFamily: 'OpenSans-SemiBold',
fontSize: 16,
lineHeight: 18,
top: 2,
buttonWrapper: {
flex: 1,
},
logoStyle: {
height: 18,
marginRight: 5,
width: 18,
},
}));
});
export default SsoOptions;

View file

@ -1,7 +1,6 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {Button} from '@rneui/base';
import React, {useCallback, useEffect, useRef, useState} from 'react';
import {useIntl} from 'react-intl';
import {Keyboard, Platform, useWindowDimensions, View} from 'react-native';
@ -11,18 +10,16 @@ import Animated, {useAnimatedStyle, useSharedValue, withTiming} from 'react-nati
import {SafeAreaView} from 'react-native-safe-area-context';
import {login} from '@actions/remote/session';
import Button from '@components/button';
import FloatingTextInput from '@components/floating_text_input_label';
import FormattedText from '@components/formatted_text';
import Loading from '@components/loading';
import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
import {useAvoidKeyboard} from '@hooks/device';
import {t} from '@i18n';
import {usePreventDoubleTap} from '@hooks/utils';
import SecurityManager from '@managers/security_manager';
import Background from '@screens/background';
import {popTopScreen} from '@screens/navigation';
import {buttonBackgroundStyle, buttonTextStyle} from '@utils/buttonStyles';
import {getErrorMessage} from '@utils/errors';
import {preventDoubleTap} from '@utils/tap';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
import {typography} from '@utils/typography';
@ -52,9 +49,6 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
justifyContent: 'center',
marginTop: Platform.select({android: 56}),
},
error: {
marginTop: 64,
},
flex: {
flex: 1,
},
@ -72,16 +66,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
paddingHorizontal: 24,
height: '100%',
},
loading: {
height: 20,
width: 20,
},
loadingContainerStyle: {
marginRight: 10,
padding: 0,
top: -2,
},
proceedButton: {
proceedButtonContainer: {
marginTop: 32,
},
shield: {
@ -114,12 +99,12 @@ const MFA = ({componentId, config, goToHome, license, loginId, password, serverD
setError('');
}, []);
const submit = useCallback(preventDoubleTap(async () => {
const submit = usePreventDoubleTap(useCallback(async () => {
Keyboard.dismiss();
if (!token) {
setError(
formatMessage({
id: t('login_mfa.tokenReq'),
id: 'login_mfa.tokenReq',
defaultMessage: 'Please enter an MFA token',
}),
);
@ -133,7 +118,7 @@ const MFA = ({componentId, config, goToHome, license, loginId, password, serverD
return;
}
goToHome(result.error);
}), [token]);
}, [config, formatMessage, goToHome, intl, license, loginId, password, serverDisplayName, serverUrl, token]));
const transform = useAnimatedStyle(() => {
const duration = Platform.OS === 'android' ? 250 : 350;
@ -224,25 +209,18 @@ const MFA = ({componentId, config, goToHome, license, loginId, password, serverD
theme={theme}
value={token}
/>
<View style={styles.proceedButtonContainer}>
<Button
testID='login_mfa.submit'
buttonStyle={[styles.proceedButton, buttonBackgroundStyle(theme, 'lg', 'primary', 'default'), error ? styles.error : undefined]}
disabledStyle={[styles.proceedButton, buttonBackgroundStyle(theme, 'lg', 'primary', 'disabled'), error ? styles.error : undefined]}
size='lg'
disabled={!token}
onPress={submit}
>
{isLoading &&
<Loading
containerStyle={styles.loadingContainerStyle}
color={theme.buttonColor}
theme={theme}
showLoader={isLoading}
text={formatMessage({id: 'mobile.components.select_server_view.proceed', defaultMessage: 'Proceed'})}
isDestructive={Boolean(error)}
/>
}
<FormattedText
id='mobile.components.select_server_view.proceed'
defaultMessage='Proceed'
style={buttonTextStyle(theme, 'lg', 'primary', token ? 'default' : 'disabled')}
/>
</Button>
</View>
</View>
</View>
</KeyboardAwareScrollView>

View file

@ -2,14 +2,13 @@
// See LICENSE.txt for license information.
import React from 'react';
import {Pressable, useWindowDimensions, View} from 'react-native';
import {Pressable, StyleSheet, useWindowDimensions, View} from 'react-native';
import Animated, {Extrapolate, interpolate, useAnimatedStyle} from 'react-native-reanimated';
import CompassIcon from '@components/compass_icon';
import FormattedText from '@components/formatted_text';
import {ONBOARDING_CONTENT_MAX_WIDTH} from '@screens/onboarding/slide';
import {buttonBackgroundStyle, buttonTextStyle} from '@utils/buttonStyles';
import {makeStyleSheetFromTheme} from '@utils/theme';
type Props = {
theme: Theme;
@ -19,24 +18,20 @@ type Props = {
scrollX: Animated.SharedValue<number>;
};
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
const styles = StyleSheet.create({
button: {
marginTop: 5,
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
rowIcon: {
color: theme.buttonColor,
fontSize: 12,
marginLeft: 5,
marginTop: 4.5,
},
nextButtonText: {
flexDirection: 'row',
position: 'absolute',
justifyContent: 'center',
alignItems: 'center',
width: 120,
gap: 5,
},
footerButtonsContainer: {
flexDirection: 'column',
@ -46,7 +41,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
width: '100%',
alignItems: 'center',
},
}));
});
const AnimatedButton = Animated.createAnimatedComponent(Pressable);
const BUTTON_SIZE = 120;
@ -60,7 +55,6 @@ const FooterButtons = ({
}: Props) => {
const {width} = useWindowDimensions();
const buttonWidth = Math.min(width * 0.8, ONBOARDING_CONTENT_MAX_WIDTH);
const styles = getStyleSheet(theme);
// keep in mind penultimate and ultimate slides to run buttons text/opacity/size animations
const penultimateSlide = lastSlideIndex - 1;
@ -114,6 +108,8 @@ const FooterButtons = ({
return {opacity: interpolatedScale};
});
const textStyles = StyleSheet.flatten(buttonTextStyle(theme, 'lg', 'primary', 'default'));
const nextButtonText = (
<Animated.View style={[styles.nextButtonText, opacityNextTextStyle]}>
<FormattedText
@ -123,7 +119,8 @@ const FooterButtons = ({
/>
<CompassIcon
name='arrow-forward-ios'
style={styles.rowIcon}
size={12}
color={textStyles.color}
/>
</Animated.View>
);

View file

@ -2,6 +2,7 @@
// See LICENSE.txt for license information.
import React, {useCallback, useEffect, useMemo, useState} from 'react';
import {useIntl} from 'react-intl';
import {Alert, Text, TouchableOpacity, View} from 'react-native';
import Animated from 'react-native-reanimated';
import {type Edge, SafeAreaView, useSafeAreaInsets} from 'react-native-safe-area-context';
@ -10,6 +11,7 @@ import {getPosts} from '@actions/local/post';
import {fetchChannelById, joinChannel, switchToChannelById} from '@actions/remote/channel';
import {fetchPostById, fetchPostsAround, fetchPostThread} from '@actions/remote/post';
import {addCurrentUserToTeam, fetchTeamByName, removeCurrentUserFromTeam} from '@actions/remote/team';
import Button from '@components/button';
import CompassIcon from '@components/compass_icon';
import FormattedText from '@components/formatted_text';
import Loading from '@components/loading';
@ -21,12 +23,11 @@ import {useTheme} from '@context/theme';
import DatabaseManager from '@database/manager';
import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
import {useIsTablet} from '@hooks/device';
import {usePreventDoubleTap} from '@hooks/utils';
import SecurityManager from '@managers/security_manager';
import {getChannelById, getMyChannel} from '@queries/servers/channel';
import {dismissModal} from '@screens/navigation';
import {buttonBackgroundStyle, buttonTextStyle} from '@utils/buttonStyles';
import {closePermalink} from '@utils/permalink';
import {preventDoubleTap} from '@utils/tap';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
import {secureGetFromRecord} from '@utils/types';
import {typography} from '@utils/typography';
@ -105,11 +106,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
alignItems: 'center',
},
footer: {
alignItems: 'center',
justifyContent: 'center',
flexDirection: 'row',
padding: 20,
width: '100%',
borderBottomLeftRadius: 12,
borderBottomRightRadius: 12,
borderTopWidth: 1,
@ -139,6 +136,7 @@ function Permalink({
isTeamMember,
currentTeamId,
}: Props) {
const intl = useIntl();
const [posts, setPosts] = useState<PostModel[]>([]);
const [loading, setLoading] = useState(true);
const theme = useTheme();
@ -273,17 +271,17 @@ function Permalink({
}
dismissModal({componentId: Screens.PERMALINK});
closePermalink();
}, [error]);
}, [error?.joinedTeam, error?.teamId, serverUrl]);
useAndroidHardwareBackHandler(Screens.PERMALINK, handleClose);
const handlePress = useCallback(preventDoubleTap(() => {
const handlePress = usePreventDoubleTap(useCallback(() => {
if (channel) {
switchToChannelById(serverUrl, channel.id, channel.teamId);
}
}), [channel?.id, channel?.teamId]);
}, [channel, serverUrl]));
const handleJoin = useCallback(preventDoubleTap(async () => {
const handleJoin = usePreventDoubleTap(useCallback(async () => {
setLoading(true);
setError(undefined);
if (error?.teamId && error.channelId) {
@ -296,7 +294,7 @@ function Permalink({
}
setChannelId(error.channelId);
}
}), [error, serverUrl]);
}, [error, serverUrl]));
let content;
if (loading) {
@ -334,18 +332,13 @@ function Permalink({
/>
</View>
<View style={style.footer}>
<TouchableOpacity
style={[buttonBackgroundStyle(theme, 'lg', 'primary'), {width: '100%'}]}
<Button
size='lg'
text={intl.formatMessage({id: 'mobile.search.jump', defaultMessage: 'Jump to recent messages'})}
theme={theme}
onPress={handlePress}
testID='permalink.jump_to_recent_messages.button'
>
<FormattedText
testID='permalink.search.jump'
id='mobile.search.jump'
defaultMessage='Jump to recent messages'
style={buttonTextStyle(theme, 'lg', 'primary')}
/>
</TouchableOpacity>
</View>
</ExtraKeyboardProvider>
);

View file

@ -2,16 +2,15 @@
// See LICENSE.txt for license information.
import React from 'react';
import {useIntl} from 'react-intl';
import {Text, TouchableOpacity, View} from 'react-native';
import {Text, View} from 'react-native';
import FormattedText from '@components/formatted_text';
import Button from '@components/button';
import JoinPrivateChannel from '@components/illustrations/join_private_channel';
import JoinPublicChannel from '@components/illustrations/join_public_channel';
import MessageNotViewable from '@components/illustrations/message_not_viewable';
import Markdown from '@components/markdown';
import {Screens} from '@constants';
import {useTheme} from '@context/theme';
import {buttonBackgroundStyle, buttonTextStyle} from '@utils/buttonStyles';
import {getMarkdownBlockStyles, getMarkdownTextStyles} from '@utils/markdown';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
import {typography} from '@utils/typography';
@ -49,6 +48,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
borderTopWidth: 1,
borderTopColor: changeOpacity(theme.centerChannelColor, 0.16),
padding: 20,
gap: 8,
},
};
@ -63,9 +63,6 @@ function PermalinkError({
const style = getStyleSheet(theme);
const intl = useIntl();
const buttonStylePrimary = buttonBackgroundStyle(theme, 'lg', 'primary');
const buttonTextStylePrimary = buttonTextStyle(theme, 'lg', 'primary');
if (error.notExist || error.unreachable) {
const title = intl.formatMessage({id: 'permalink.error.access.title', defaultMessage: 'Message not viewable'});
const text = intl.formatMessage({id: 'permalink.error.access.text', defaultMessage: 'The message you are trying to view is in a channel you dont have access to or has been deleted.'});
@ -77,25 +74,17 @@ function PermalinkError({
<Text style={style.errorText}>{text}</Text>
</View>
<View style={style.errorButtonContainer}>
<TouchableOpacity
style={buttonStylePrimary}
<Button
size='lg'
text={intl.formatMessage({id: 'permalink.error.okay', defaultMessage: 'Okay'})}
theme={theme}
onPress={handleClose}
>
<FormattedText
testID='permalink.error.okay'
id='permalink.error.okay'
defaultMessage='Okay'
style={buttonTextStylePrimary}
/>
</TouchableOpacity>
</View>
</>
);
}
const buttonStyleTertiary = buttonBackgroundStyle(theme, 'lg', 'tertiary');
const buttonTextStyleTertiary = buttonTextStyle(theme, 'lg', 'tertiary');
const isPrivate = error.privateChannel || error.privateTeam;
let image;
let title;
@ -143,23 +132,19 @@ function PermalinkError({
/>
</View>
<View style={style.errorButtonContainer}>
<TouchableOpacity
style={buttonStylePrimary}
<Button
size='lg'
text={button}
theme={theme}
onPress={handleJoin}
>
<Text style={buttonTextStylePrimary}>{button}</Text>
</TouchableOpacity>
<TouchableOpacity
style={[buttonStyleTertiary, {marginTop: 8}]}
onPress={handleClose}
>
<FormattedText
testID='permalink.error.cancel'
id='permalink.error.cancel'
defaultMessage='Cancel'
style={buttonTextStyleTertiary}
/>
</TouchableOpacity>
<Button
size='lg'
emphasis='tertiary'
onPress={handleClose}
text={intl.formatMessage({id: 'permalink.error.cancel', defaultMessage: 'Cancel'})}
theme={theme}
/>
</View>
</>
);

View file

@ -3,11 +3,10 @@
import React from 'react';
import {useIntl} from 'react-intl';
import {StyleSheet, Text, View} from 'react-native';
import {Text, View} from 'react-native';
import Empty from '@components/illustrations/no_team';
import {useTheme} from '@context/theme';
import {buttonBackgroundStyle, buttonTextStyle} from '@utils/buttonStyles';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
import {typography} from '@utils/typography';
@ -40,21 +39,6 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
marginTop: 12,
...typography('Body', 200, 'Regular'),
},
buttonStyle: {
...StyleSheet.flatten(buttonBackgroundStyle(theme, 'lg', 'primary', 'default')),
flexDirection: 'row',
marginTop: 24,
},
buttonText: {
...StyleSheet.flatten(buttonTextStyle(theme, 'lg', 'primary', 'default')),
marginLeft: 8,
},
plusIcon: {
color: theme.sidebarText,
fontSize: 24,
lineHeight: 22,
},
}));
const NoTeams = () => {
@ -73,7 +57,7 @@ const NoTeams = () => {
<Text style={styles.description}>
{intl.formatMessage({id: 'select_team.no_team.description', defaultMessage: 'To join a team, ask a team admin for an invite, or create your own team. You may also want to check your email inbox for an invitation.'})}
</Text>
{/* {canCreateTeams && // TODO https://mattermost.atlassian.net/browse/MM-43622
{/* {canCreateTeams && // TODO https://mattermost.atlassian.net/browse/MM-43622 (Use Button component instead of touchable)
<TouchableWithFeedback
style={styles.buttonStyle}
type={'opacity'}

View file

@ -1,17 +1,14 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {Button} from '@rneui/base';
import React, {type RefObject, useCallback, useRef} from 'react';
import {useIntl} from 'react-intl';
import {defineMessages, useIntl} from 'react-intl';
import {Keyboard, View} from 'react-native';
import Button from '@components/button';
import FloatingTextInput, {type FloatingTextInputRef} from '@components/floating_text_input_label';
import FormattedText from '@components/formatted_text';
import Loading from '@components/loading';
import {useAvoidKeyboard} from '@hooks/device';
import {t} from '@i18n';
import {buttonBackgroundStyle, buttonTextStyle} from '@utils/buttonStyles';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
import {typography} from '@utils/typography';
@ -52,22 +49,25 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
marginTop: 8,
...typography('Body', 75, 'Regular'),
},
connectButton: {
connectButtonContainer: {
width: '100%',
marginTop: 32,
marginLeft: 20,
marginRight: 20,
},
connectingIndicator: {
marginRight: 10,
},
loadingContainerStyle: {
marginRight: 10,
padding: 0,
top: -2,
},
}));
const messages = defineMessages({
connect: {
id: 'mobile.components.select_server_view.connect',
defaultMessage: 'Connect',
},
connecting: {
id: 'mobile.components.select_server_view.connecting',
defaultMessage: 'Connecting',
},
});
const ServerForm = ({
autoFocus = false,
buttonDisabled,
@ -99,25 +99,6 @@ const ServerForm = ({
displayNameRef.current?.focus();
}, []);
const buttonType = buttonDisabled ? 'disabled' : 'default';
const styleButtonText = buttonTextStyle(theme, 'lg', 'primary', buttonType);
const styleButtonBackground = buttonBackgroundStyle(theme, 'lg', 'primary', buttonType);
let buttonID = t('mobile.components.select_server_view.connect');
let buttonText = 'Connect';
let buttonIcon;
if (connecting) {
buttonID = t('mobile.components.select_server_view.connecting');
buttonText = 'Connecting';
buttonIcon = (
<Loading
containerStyle={styles.loadingContainerStyle}
color={theme.buttonColor}
/>
);
}
const connectButtonTestId = buttonDisabled ? 'server_form.connect.button.disabled' : 'server_form.connect.button';
return (
@ -175,21 +156,17 @@ const ServerForm = ({
testID={'server_form.display_help'}
/>
}
<View style={styles.connectButtonContainer}>
<Button
containerStyle={styles.connectButton}
disabled={buttonDisabled}
onPress={onConnect}
testID={connectButtonTestId}
buttonStyle={styleButtonBackground}
disabledStyle={styleButtonBackground}
>
{buttonIcon}
<FormattedText
defaultMessage={buttonText}
id={buttonID}
style={styleButtonText}
size='lg'
theme={theme}
text={formatMessage(connecting ? messages.connecting : messages.connect)}
showLoader={connecting}
/>
</Button>
</View>
</View>
);
};

View file

@ -187,22 +187,13 @@ exports[`SendTestNotificationNotice should match snapshot 1`] = `
<Text
numberOfLines={1}
style={
[
[
[
{
"color": "#ffffff",
"fontFamily": "OpenSans-SemiBold",
"fontSize": 14,
"fontWeight": "600",
"lineHeight": 20,
},
{
"color": "#ffffff",
},
],
undefined,
],
]
}
}
>
Send a test notification
@ -298,22 +289,13 @@ exports[`SendTestNotificationNotice should match snapshot 1`] = `
<Text
numberOfLines={1}
style={
[
[
[
{
"color": "#1c58d9",
"fontFamily": "OpenSans-SemiBold",
"fontSize": 14,
"fontWeight": "600",
"lineHeight": 20,
},
{
"color": "#1c58d9",
},
],
undefined,
],
]
}
}
>
Troubleshooting docs

View file

@ -1,12 +1,12 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {Button} from '@rneui/base';
import React from 'react';
import {useIntl} from 'react-intl';
import {Text, View} from 'react-native';
import Button from '@components/button';
import FormattedText from '@components/formatted_text';
import {buttonBackgroundStyle, buttonTextStyle} from '@utils/buttonStyles';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
import {typography} from '@utils/typography';
@ -18,7 +18,7 @@ interface AuthErrorProps {
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
return {
button: {
buttonContainer: {
marginTop: 25,
},
errorText: {
@ -44,6 +44,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
});
const AuthError = ({error, retry, theme}: AuthErrorProps) => {
const intl = useIntl();
const style = getStyleSheet(theme);
return (
@ -57,17 +58,15 @@ const AuthError = ({error, retry, theme}: AuthErrorProps) => {
<Text style={style.errorText}>
{`${error}.`}
</Text>
<View style={style.buttonContainer}>
<Button
buttonStyle={[style.button, buttonBackgroundStyle(theme, 'lg', 'primary', 'default')]}
testID='mobile.oauth.try_again'
onPress={retry}
>
<FormattedText
id='mobile.oauth.try_again'
defaultMessage='Try again'
style={buttonTextStyle(theme, 'lg', 'primary', 'default')}
size='lg'
text={intl.formatMessage({id: 'mobile.oauth.try_again', defaultMessage: 'Try again'})}
theme={theme}
/>
</Button>
</View>
</View>
);
};

View file

@ -1,19 +1,17 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {useCallback, useMemo} from 'react';
import React, {useCallback} from 'react';
import {useIntl} from 'react-intl';
import {DeviceEventEmitter, StyleSheet, TouchableOpacity, View} from 'react-native';
import {DeviceEventEmitter, StyleSheet, View} from 'react-native';
import {createDirectChannel, switchToChannelById} from '@actions/remote/channel';
import CompassIcon from '@components/compass_icon';
import FormattedText from '@components/formatted_text';
import Button from '@components/button';
import OptionBox, {OPTIONS_HEIGHT} from '@components/option_box';
import {Events, Screens} from '@constants';
import {useServerUrl} from '@context/server';
import {useTheme} from '@context/theme';
import {dismissBottomSheet} from '@screens/navigation';
import {buttonBackgroundStyle, buttonTextStyle} from '@utils/buttonStyles';
import type {AvailableScreens} from '@typings/screens/navigation';
@ -45,9 +43,7 @@ const styles = StyleSheet.create({
width: '100%',
},
singleContainer: {
flexDirection: 'row',
marginBottom: 20,
width: '100%',
},
});
@ -56,10 +52,6 @@ const UserProfileOptions = ({location, type, userId, username}: Props) => {
const theme = useTheme();
const serverUrl = useServerUrl();
const buttonStyle = useMemo(() => {
return buttonBackgroundStyle(theme, 'lg', 'tertiary', 'default');
}, [theme]);
const mentionUser = useCallback(async () => {
await dismissBottomSheet(Screens.USER_PROFILE);
DeviceEventEmitter.emit(Events.SEND_TO_POST_DRAFT, {location, text: `@${username}`});
@ -80,7 +72,7 @@ const UserProfileOptions = ({location, type, userId, username}: Props) => {
iconName='send'
onPress={openChannel}
testID='user_profile_options.send_message.option'
text={intl.formatMessage({id: 'channel_info.send_mesasge', defaultMessage: 'Send message'})}
text={intl.formatMessage({id: 'channel_info.send_a_mesasge', defaultMessage: 'Send message'})}
/>
<View style={styles.divider}/>
<OptionBox
@ -95,22 +87,15 @@ const UserProfileOptions = ({location, type, userId, username}: Props) => {
return (
<View style={styles.singleContainer}>
<TouchableOpacity
style={[buttonStyle, styles.singleButton]}
<Button
onPress={openChannel}
testID='user_profile_options.send_message.option'
>
<CompassIcon
color={theme.buttonBg}
name='send'
style={styles.icon}
size='lg'
emphasis='tertiary'
theme={theme}
text={intl.formatMessage({id: 'channel_info.send_mesasge', defaultMessage: 'Send message'})}
iconName='send'
/>
<FormattedText
id='channel_info.send_a_mesasge'
defaultMessage='Send a message'
style={[buttonTextStyle(theme, 'lg', 'tertiary', 'default'), {marginLeft: 8}]}
/>
</TouchableOpacity>
</View>
);
};

View file

@ -158,11 +158,17 @@ const UserProfile = ({
'90%',
];
}, [
headerText, showUserProfileOptions, showCustomStatus,
showNickname, showPosition, showLocalTime,
manageMode, bottom, showOptions,
canChangeMemberRoles, canManageAndRemoveMembers,
enableCustomAttributes,
headerText,
showUserProfileOptions,
showCustomStatus,
showNickname,
showPosition,
showLocalTime,
manageMode,
bottom,
showOptions,
canChangeMemberRoles,
canManageAndRemoveMembers,
]);
useEffect(() => {

View file

@ -464,6 +464,7 @@
"load_channels_error.title": "Couldn't load {teamDisplayName}",
"load_teams_error.message": "There was a problem loading content for this server.",
"load_teams_error.title": "Couldn't load {serverName}",
"loading_error.retry": "Retry",
"login_mfa.enterToken": "To complete the sign in process, please enter the code from your mobile device's authenticator app.",
"login_mfa.token": "Enter MFA Token",
"login_mfa.tokenReq": "Please enter an MFA token",