Add Report a Problem functionality (#8605)
* Add Report a Problem functionality * update cache pinned SHA version from 4.0.2 to 4.2.0 * Address feedback * Fix tests * Fix some issues and update kotlin coroutines version * Fix delete file for iOS * Bump 1 more version for coroutines * Use rxjava instead of kotlin coroutines to avoid security issue * Move path prefix to avoid test error * Address feedback * Address feedback * Address feedback * Use mailto on iOS * Fix tests related to button changes * Address feedback * Update icon and fix onboarding buttons * Fix test --------- Co-authored-by: Angelos Kyratzakos <angelos.kyratzakos@mattermost.com> Co-authored-by: Mattermost Build <build@mattermost.com>
This commit is contained in:
parent
3cbde1663d
commit
5c2153f83b
62 changed files with 3139 additions and 597 deletions
|
|
@ -22,6 +22,7 @@ import NetworkManager from '@managers/network_manager';
|
||||||
import {getDeviceToken} from '@queries/app/global';
|
import {getDeviceToken} from '@queries/app/global';
|
||||||
import {getCurrentChannelId, getCurrentTeamId, setCurrentTeamAndChannelId} from '@queries/servers/system';
|
import {getCurrentChannelId, getCurrentTeamId, setCurrentTeamAndChannelId} from '@queries/servers/system';
|
||||||
import NavigationStore from '@store/navigation_store';
|
import NavigationStore from '@store/navigation_store';
|
||||||
|
import {logDebug} from '@utils/log';
|
||||||
|
|
||||||
import {entry, setExtraSessionProps, verifyPushProxy, entryInitialChannelId, restDeferredAppEntryActions, handleEntryAfterLoadNavigation, deferredAppEntryActions} from './common';
|
import {entry, setExtraSessionProps, verifyPushProxy, entryInitialChannelId, restDeferredAppEntryActions, handleEntryAfterLoadNavigation, deferredAppEntryActions} from './common';
|
||||||
|
|
||||||
|
|
@ -866,7 +867,6 @@ describe('actions/remote/entry/common', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle error gracefully', async () => {
|
it('should handle error gracefully', async () => {
|
||||||
const consoleDebugSpy = jest.spyOn(console, 'debug').mockImplementation(() => {});
|
|
||||||
(DatabaseManager.getServerDatabaseAndOperator as jest.Mock).mockImplementation(() => {
|
(DatabaseManager.getServerDatabaseAndOperator as jest.Mock).mockImplementation(() => {
|
||||||
throw new Error('Test error');
|
throw new Error('Test error');
|
||||||
});
|
});
|
||||||
|
|
@ -882,8 +882,7 @@ describe('actions/remote/entry/common', () => {
|
||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(consoleDebugSpy).toHaveBeenCalled();
|
expect(logDebug).toHaveBeenCalled();
|
||||||
consoleDebugSpy.mockRestore();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle tablet device scenario', async () => {
|
it('should handle tablet device scenario', async () => {
|
||||||
|
|
|
||||||
85
app/components/button/index.test.tsx
Normal file
85
app/components/button/index.test.tsx
Normal file
|
|
@ -0,0 +1,85 @@
|
||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import {fireEvent, render, within} from '@testing-library/react-native';
|
||||||
|
import React, {type ComponentProps} from 'react';
|
||||||
|
import {View, Text} from 'react-native';
|
||||||
|
|
||||||
|
import {Preferences} from '@constants';
|
||||||
|
|
||||||
|
import Button from './index';
|
||||||
|
|
||||||
|
describe('components/button', () => {
|
||||||
|
const getBaseProps = (): ComponentProps<typeof Button> => ({
|
||||||
|
onPress: jest.fn(),
|
||||||
|
text: 'Test Button',
|
||||||
|
theme: Preferences.THEMES.denim,
|
||||||
|
testID: 'test-button',
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render button with text', () => {
|
||||||
|
const props = getBaseProps();
|
||||||
|
const {getByText} = render(<Button {...props}/>);
|
||||||
|
|
||||||
|
expect(getByText('Test Button')).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle onPress', () => {
|
||||||
|
const props = getBaseProps();
|
||||||
|
const {getByTestId} = render(<Button {...props}/>);
|
||||||
|
|
||||||
|
fireEvent.press(getByTestId('test-button'));
|
||||||
|
expect(props.onPress).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render with icon', () => {
|
||||||
|
const props = getBaseProps();
|
||||||
|
props.iconName = 'close';
|
||||||
|
const {getByTestId} = render(<Button {...props}/>);
|
||||||
|
|
||||||
|
const icon = getByTestId('test-button-icon');
|
||||||
|
expect(icon).toBeTruthy();
|
||||||
|
expect(icon.props.name).toBe('close');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render disabled button', () => {
|
||||||
|
const props = getBaseProps();
|
||||||
|
props.disabled = true;
|
||||||
|
const {getByTestId} = render(<Button {...props}/>);
|
||||||
|
|
||||||
|
fireEvent.press(getByTestId('test-button'));
|
||||||
|
expect(props.onPress).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render icon on the right', () => {
|
||||||
|
const props = getBaseProps();
|
||||||
|
props.isIconOnTheRight = false;
|
||||||
|
props.iconName = 'close';
|
||||||
|
|
||||||
|
const {getByTestId, rerender} = render(<Button {...props}/>);
|
||||||
|
|
||||||
|
const container = getByTestId('test-button-text-container');
|
||||||
|
|
||||||
|
// When icon is on the left, it should be the first child
|
||||||
|
expect(within(container.children[0]).getByTestId('test-button-icon')).toBeVisible();
|
||||||
|
|
||||||
|
props.isIconOnTheRight = true;
|
||||||
|
rerender(<Button {...props}/>);
|
||||||
|
|
||||||
|
// When icon is on the right, it should be the last child
|
||||||
|
expect(within(container.children[1]).getByTestId('test-button-icon')).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render custom icon component', () => {
|
||||||
|
const CustomIcon = () => (
|
||||||
|
<View testID='custom-icon'>
|
||||||
|
<Text>{'Custom Icon'}</Text>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
const props = getBaseProps();
|
||||||
|
props.iconComponent = <CustomIcon/>;
|
||||||
|
const {getByTestId} = render(<Button {...props}/>);
|
||||||
|
|
||||||
|
expect(getByTestId('custom-icon')).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -9,9 +9,7 @@ import CompassIcon from '@components/compass_icon';
|
||||||
import {buttonBackgroundStyle, buttonTextStyle} from '@utils/buttonStyles';
|
import {buttonBackgroundStyle, buttonTextStyle} from '@utils/buttonStyles';
|
||||||
import {changeOpacity} from '@utils/theme';
|
import {changeOpacity} from '@utils/theme';
|
||||||
|
|
||||||
type ConditionalProps = | {iconName: string; iconSize: number} | {iconName?: never; iconSize?: never}
|
type Props = {
|
||||||
|
|
||||||
type Props = ConditionalProps & {
|
|
||||||
theme: Theme;
|
theme: Theme;
|
||||||
backgroundStyle?: StyleProp<ViewStyle>;
|
backgroundStyle?: StyleProp<ViewStyle>;
|
||||||
buttonContainerStyle?: StyleProp<ViewStyle>;
|
buttonContainerStyle?: StyleProp<ViewStyle>;
|
||||||
|
|
@ -26,19 +24,31 @@ type Props = ConditionalProps & {
|
||||||
iconComponent?: ReactNode;
|
iconComponent?: ReactNode;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
hitSlop?: Insets;
|
hitSlop?: Insets;
|
||||||
|
isIconOnTheRight?: boolean;
|
||||||
|
iconName?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
container: {flexDirection: 'row'},
|
container: {
|
||||||
icon: {marginRight: 7},
|
flexDirection: 'row',
|
||||||
|
gap: 7,
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const iconSizePerSize: Record<ButtonSize, number> = {
|
||||||
|
xs: 14,
|
||||||
|
s: 14,
|
||||||
|
m: 18,
|
||||||
|
lg: 22,
|
||||||
|
};
|
||||||
|
|
||||||
const Button = ({
|
const Button = ({
|
||||||
theme,
|
theme,
|
||||||
backgroundStyle,
|
backgroundStyle,
|
||||||
buttonContainerStyle,
|
buttonContainerStyle,
|
||||||
textStyle,
|
textStyle,
|
||||||
size,
|
size = 'm',
|
||||||
emphasis,
|
emphasis,
|
||||||
buttonType,
|
buttonType,
|
||||||
buttonState,
|
buttonState,
|
||||||
|
|
@ -46,7 +56,7 @@ const Button = ({
|
||||||
text,
|
text,
|
||||||
testID,
|
testID,
|
||||||
iconName,
|
iconName,
|
||||||
iconSize,
|
isIconOnTheRight = false,
|
||||||
iconComponent,
|
iconComponent,
|
||||||
disabled,
|
disabled,
|
||||||
hitSlop,
|
hitSlop,
|
||||||
|
|
@ -61,15 +71,6 @@ const Button = ({
|
||||||
textStyle,
|
textStyle,
|
||||||
], [theme, textStyle, size, emphasis, buttonType]);
|
], [theme, textStyle, size, emphasis, buttonType]);
|
||||||
|
|
||||||
const textContainerStyle = useMemo(
|
|
||||||
() =>
|
|
||||||
(iconSize ? [
|
|
||||||
styles.container,
|
|
||||||
{minHeight: iconSize},
|
|
||||||
] : styles.container),
|
|
||||||
[iconSize],
|
|
||||||
);
|
|
||||||
|
|
||||||
let buttonStyle = StyleSheet.flatten(bgStyle);
|
let buttonStyle = StyleSheet.flatten(bgStyle);
|
||||||
if (disabled) {
|
if (disabled) {
|
||||||
buttonStyle = {
|
buttonStyle = {
|
||||||
|
|
@ -83,13 +84,16 @@ const Button = ({
|
||||||
if (iconComponent) {
|
if (iconComponent) {
|
||||||
icon = iconComponent;
|
icon = iconComponent;
|
||||||
} else if (iconName) {
|
} else if (iconName) {
|
||||||
|
// We wrap the icon in a view to avoid it to follow text layout
|
||||||
icon = (
|
icon = (
|
||||||
<CompassIcon
|
<View>
|
||||||
name={iconName!}
|
<CompassIcon
|
||||||
size={iconSize}
|
name={iconName!}
|
||||||
color={StyleSheet.flatten(txtStyle).color}
|
size={iconSizePerSize[size]}
|
||||||
style={styles.icon}
|
color={StyleSheet.flatten(txtStyle).color}
|
||||||
/>
|
testID={`${testID}-icon`}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -102,14 +106,18 @@ const Button = ({
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
hitSlop={hitSlop}
|
hitSlop={hitSlop}
|
||||||
>
|
>
|
||||||
<View style={textContainerStyle}>
|
<View
|
||||||
{icon}
|
style={styles.container}
|
||||||
|
testID={`${testID}-text-container`}
|
||||||
|
>
|
||||||
|
{!isIconOnTheRight && icon}
|
||||||
<Text
|
<Text
|
||||||
style={txtStyle}
|
style={[txtStyle]}
|
||||||
numberOfLines={1}
|
numberOfLines={1}
|
||||||
>
|
>
|
||||||
{text}
|
{text}
|
||||||
</Text>
|
</Text>
|
||||||
|
{isIconOnTheRight && icon}
|
||||||
</View>
|
</View>
|
||||||
</ElementButton>
|
</ElementButton>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -102,16 +102,12 @@ exports[`Loading Error should match snapshot 1`] = `
|
||||||
onStartShouldSetResponder={[Function]}
|
onStartShouldSetResponder={[Function]}
|
||||||
style={
|
style={
|
||||||
{
|
{
|
||||||
"alignItems": "center",
|
|
||||||
"backgroundColor": "#ffffff",
|
"backgroundColor": "#ffffff",
|
||||||
"borderRadius": 4,
|
"borderRadius": 4,
|
||||||
"flex": 0,
|
|
||||||
"height": 48,
|
|
||||||
"justifyContent": "center",
|
|
||||||
"marginTop": 24,
|
"marginTop": 24,
|
||||||
"opacity": 1,
|
"opacity": 1,
|
||||||
"paddingHorizontal": 24,
|
"paddingHorizontal": 24,
|
||||||
"paddingVertical": 14,
|
"paddingVertical": 12,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
|
@ -119,17 +115,10 @@ exports[`Loading Error should match snapshot 1`] = `
|
||||||
style={
|
style={
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
"alignItems": "center",
|
|
||||||
"fontFamily": "OpenSans-SemiBold",
|
"fontFamily": "OpenSans-SemiBold",
|
||||||
"fontWeight": "600",
|
|
||||||
"justifyContent": "center",
|
|
||||||
"padding": 1,
|
|
||||||
"textAlignVertical": "center",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fontSize": 16,
|
"fontSize": 16,
|
||||||
"lineHeight": 18,
|
"fontWeight": "600",
|
||||||
"marginTop": 1,
|
"lineHeight": 24,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"color": "#1c58d9",
|
"color": "#1c58d9",
|
||||||
|
|
|
||||||
52
app/components/menu_divider/index.test.tsx
Normal file
52
app/components/menu_divider/index.test.tsx
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import {render} from '@testing-library/react-native';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import {Preferences} from '@constants';
|
||||||
|
|
||||||
|
import MenuDivider from './index';
|
||||||
|
|
||||||
|
describe('components/menu_divider', () => {
|
||||||
|
const theme = Preferences.THEMES.denim;
|
||||||
|
|
||||||
|
it('should render divider with default margins', () => {
|
||||||
|
const {root} = render(<MenuDivider/>);
|
||||||
|
|
||||||
|
expect(root).toHaveStyle({
|
||||||
|
marginTop: 8,
|
||||||
|
marginBottom: 8,
|
||||||
|
marginLeft: 0,
|
||||||
|
marginRight: 0,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render divider with custom margins', () => {
|
||||||
|
const margins = {
|
||||||
|
marginTop: 16,
|
||||||
|
marginBottom: 24,
|
||||||
|
marginLeft: 12,
|
||||||
|
marginRight: 13,
|
||||||
|
};
|
||||||
|
|
||||||
|
const {root} = render(<MenuDivider {...margins}/>);
|
||||||
|
|
||||||
|
expect(root).toHaveStyle({
|
||||||
|
marginTop: 16,
|
||||||
|
marginBottom: 24,
|
||||||
|
marginLeft: 12,
|
||||||
|
marginRight: 13,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should apply correct theme styles', () => {
|
||||||
|
const {root} = render(<MenuDivider/>);
|
||||||
|
|
||||||
|
expect(root).toHaveStyle({
|
||||||
|
backgroundColor: theme.centerChannelColor,
|
||||||
|
height: 1,
|
||||||
|
opacity: 0.08,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
38
app/components/menu_divider/index.tsx
Normal file
38
app/components/menu_divider/index.tsx
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import React, {useMemo} from 'react';
|
||||||
|
import {View} from 'react-native';
|
||||||
|
|
||||||
|
import {useTheme} from '@context/theme';
|
||||||
|
import {makeStyleSheetFromTheme} from '@utils/theme';
|
||||||
|
|
||||||
|
const getStyleSheet = makeStyleSheetFromTheme((theme) => ({
|
||||||
|
divider: {
|
||||||
|
backgroundColor: theme.centerChannelColor,
|
||||||
|
height: 1,
|
||||||
|
opacity: 0.08,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
marginTop?: number;
|
||||||
|
marginBottom?: number;
|
||||||
|
marginLeft?: number;
|
||||||
|
marginRight?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Menu divider as per https://www.figma.com/design/ZV4NeVyUZoKfKRcRGyFJiT/Components---Mobile---Menu-Divider?node-id=1215-1535&t=qHU2DwCWm5cSbiq4-0
|
||||||
|
const MenuDivider = ({marginTop = 8, marginBottom = 8, marginLeft = 0, marginRight = 0}: Props) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
const styles = getStyleSheet(theme);
|
||||||
|
|
||||||
|
const style = useMemo(() => [
|
||||||
|
styles.divider,
|
||||||
|
{marginTop, marginBottom, marginLeft, marginRight},
|
||||||
|
], [styles.divider, marginTop, marginBottom, marginLeft, marginRight]);
|
||||||
|
|
||||||
|
return (<View style={style}/>);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MenuDivider;
|
||||||
|
|
@ -156,61 +156,50 @@ exports[`Section notice match snapshot 1`] = `
|
||||||
"borderColor": "#2089dc",
|
"borderColor": "#2089dc",
|
||||||
"borderRadius": 4,
|
"borderRadius": 4,
|
||||||
"borderWidth": 0,
|
"borderWidth": 0,
|
||||||
"flex": 0,
|
|
||||||
"flexDirection": "row",
|
"flexDirection": "row",
|
||||||
"height": 40,
|
|
||||||
"justifyContent": "center",
|
"justifyContent": "center",
|
||||||
"padding": 8,
|
"padding": 8,
|
||||||
"paddingHorizontal": 20,
|
"paddingHorizontal": 20,
|
||||||
"paddingVertical": 12,
|
"paddingVertical": 10,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<View
|
<View
|
||||||
style={
|
style={
|
||||||
[
|
{
|
||||||
{
|
"alignItems": "center",
|
||||||
"flexDirection": "row",
|
"flexDirection": "row",
|
||||||
},
|
"gap": 7,
|
||||||
{
|
|
||||||
"minHeight": 18,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Icon
|
|
||||||
color="#ffffff"
|
|
||||||
name="chevron-right"
|
|
||||||
size={18}
|
|
||||||
style={
|
|
||||||
{
|
|
||||||
"marginRight": 7,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
/>
|
}
|
||||||
|
testID="undefined-text-container"
|
||||||
|
>
|
||||||
|
<View>
|
||||||
|
<Icon
|
||||||
|
color="#ffffff"
|
||||||
|
name="chevron-right"
|
||||||
|
size={18}
|
||||||
|
testID="undefined-icon"
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
<Text
|
<Text
|
||||||
numberOfLines={1}
|
numberOfLines={1}
|
||||||
style={
|
style={
|
||||||
[
|
[
|
||||||
[
|
[
|
||||||
{
|
[
|
||||||
"alignItems": "center",
|
{
|
||||||
"fontFamily": "OpenSans-SemiBold",
|
"fontFamily": "OpenSans-SemiBold",
|
||||||
"fontWeight": "600",
|
"fontSize": 14,
|
||||||
"justifyContent": "center",
|
"fontWeight": "600",
|
||||||
"padding": 1,
|
"lineHeight": 20,
|
||||||
"textAlignVertical": "center",
|
},
|
||||||
},
|
{
|
||||||
{
|
"color": "#ffffff",
|
||||||
"fontSize": 14,
|
},
|
||||||
"lineHeight": 14,
|
],
|
||||||
"marginTop": 3,
|
undefined,
|
||||||
},
|
|
||||||
{
|
|
||||||
"color": "#ffffff",
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
undefined,
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
|
@ -278,61 +267,50 @@ exports[`Section notice match snapshot 1`] = `
|
||||||
"borderColor": "#2089dc",
|
"borderColor": "#2089dc",
|
||||||
"borderRadius": 4,
|
"borderRadius": 4,
|
||||||
"borderWidth": 0,
|
"borderWidth": 0,
|
||||||
"flex": 0,
|
|
||||||
"flexDirection": "row",
|
"flexDirection": "row",
|
||||||
"height": 40,
|
|
||||||
"justifyContent": "center",
|
"justifyContent": "center",
|
||||||
"padding": 8,
|
"padding": 8,
|
||||||
"paddingHorizontal": 20,
|
"paddingHorizontal": 20,
|
||||||
"paddingVertical": 12,
|
"paddingVertical": 10,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<View
|
<View
|
||||||
style={
|
style={
|
||||||
[
|
{
|
||||||
{
|
"alignItems": "center",
|
||||||
"flexDirection": "row",
|
"flexDirection": "row",
|
||||||
},
|
"gap": 7,
|
||||||
{
|
|
||||||
"minHeight": 18,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Icon
|
|
||||||
color="#1c58d9"
|
|
||||||
name="chevron-right"
|
|
||||||
size={18}
|
|
||||||
style={
|
|
||||||
{
|
|
||||||
"marginRight": 7,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
/>
|
}
|
||||||
|
testID="undefined-text-container"
|
||||||
|
>
|
||||||
|
<View>
|
||||||
|
<Icon
|
||||||
|
color="#1c58d9"
|
||||||
|
name="chevron-right"
|
||||||
|
size={18}
|
||||||
|
testID="undefined-icon"
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
<Text
|
<Text
|
||||||
numberOfLines={1}
|
numberOfLines={1}
|
||||||
style={
|
style={
|
||||||
[
|
[
|
||||||
[
|
[
|
||||||
{
|
[
|
||||||
"alignItems": "center",
|
{
|
||||||
"fontFamily": "OpenSans-SemiBold",
|
"fontFamily": "OpenSans-SemiBold",
|
||||||
"fontWeight": "600",
|
"fontSize": 14,
|
||||||
"justifyContent": "center",
|
"fontWeight": "600",
|
||||||
"padding": 1,
|
"lineHeight": 20,
|
||||||
"textAlignVertical": "center",
|
},
|
||||||
},
|
{
|
||||||
{
|
"color": "#1c58d9",
|
||||||
"fontSize": 14,
|
},
|
||||||
"lineHeight": 14,
|
],
|
||||||
"marginTop": 3,
|
undefined,
|
||||||
},
|
|
||||||
{
|
|
||||||
"color": "#1c58d9",
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
undefined,
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
|
@ -400,61 +378,50 @@ exports[`Section notice match snapshot 1`] = `
|
||||||
"borderColor": "#2089dc",
|
"borderColor": "#2089dc",
|
||||||
"borderRadius": 4,
|
"borderRadius": 4,
|
||||||
"borderWidth": 0,
|
"borderWidth": 0,
|
||||||
"flex": 0,
|
|
||||||
"flexDirection": "row",
|
"flexDirection": "row",
|
||||||
"height": 40,
|
|
||||||
"justifyContent": "center",
|
"justifyContent": "center",
|
||||||
"padding": 8,
|
"padding": 8,
|
||||||
"paddingHorizontal": 20,
|
"paddingHorizontal": 20,
|
||||||
"paddingVertical": 12,
|
"paddingVertical": 10,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<View
|
<View
|
||||||
style={
|
style={
|
||||||
[
|
{
|
||||||
{
|
"alignItems": "center",
|
||||||
"flexDirection": "row",
|
"flexDirection": "row",
|
||||||
},
|
"gap": 7,
|
||||||
{
|
|
||||||
"minHeight": 18,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Icon
|
|
||||||
color="#1c58d9"
|
|
||||||
name="chevron-right"
|
|
||||||
size={18}
|
|
||||||
style={
|
|
||||||
{
|
|
||||||
"marginRight": 7,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
/>
|
}
|
||||||
|
testID="undefined-text-container"
|
||||||
|
>
|
||||||
|
<View>
|
||||||
|
<Icon
|
||||||
|
color="#1c58d9"
|
||||||
|
name="chevron-right"
|
||||||
|
size={18}
|
||||||
|
testID="undefined-icon"
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
<Text
|
<Text
|
||||||
numberOfLines={1}
|
numberOfLines={1}
|
||||||
style={
|
style={
|
||||||
[
|
[
|
||||||
[
|
[
|
||||||
{
|
[
|
||||||
"alignItems": "center",
|
{
|
||||||
"fontFamily": "OpenSans-SemiBold",
|
"fontFamily": "OpenSans-SemiBold",
|
||||||
"fontWeight": "600",
|
"fontSize": 14,
|
||||||
"justifyContent": "center",
|
"fontWeight": "600",
|
||||||
"padding": 1,
|
"lineHeight": 20,
|
||||||
"textAlignVertical": "center",
|
},
|
||||||
},
|
{
|
||||||
{
|
"color": "#1c58d9",
|
||||||
"fontSize": 14,
|
},
|
||||||
"lineHeight": 14,
|
],
|
||||||
"marginTop": 3,
|
undefined,
|
||||||
},
|
|
||||||
{
|
|
||||||
"color": "#1c58d9",
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
undefined,
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -17,8 +17,8 @@ const SectionNoticeButton = ({
|
||||||
emphasis,
|
emphasis,
|
||||||
}: ButtonProps) => {
|
}: ButtonProps) => {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const leadingIcon = button.leadingIcon ? {iconName: button.leadingIcon, iconSize: 18} : {};
|
const leadingIcon = button.leadingIcon ? {iconName: button.leadingIcon} : {};
|
||||||
const trailingIcon = button.trailingIcon ? {iconName: button.trailingIcon, iconSize: 18} : {};
|
const trailingIcon = button.trailingIcon ? {iconName: button.trailingIcon} : {};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
|
|
|
||||||
|
|
@ -196,7 +196,7 @@ export default function SelectedUsers({
|
||||||
USERS_CHIPS_MAX_HEIGHT,
|
USERS_CHIPS_MAX_HEIGHT,
|
||||||
e.nativeEvent.layout.height,
|
e.nativeEvent.layout.height,
|
||||||
);
|
);
|
||||||
}, []);
|
}, [usersChipsHeight]);
|
||||||
|
|
||||||
const androidMaxHeight = Platform.select({
|
const androidMaxHeight = Platform.select({
|
||||||
android: {
|
android: {
|
||||||
|
|
@ -269,7 +269,6 @@ export default function SelectedUsers({
|
||||||
onPress={handlePress}
|
onPress={handlePress}
|
||||||
iconName={buttonIcon}
|
iconName={buttonIcon}
|
||||||
text={buttonText}
|
text={buttonText}
|
||||||
iconSize={20}
|
|
||||||
theme={theme}
|
theme={theme}
|
||||||
buttonType={isDisabled ? 'disabled' : 'default'}
|
buttonType={isDisabled ? 'disabled' : 'default'}
|
||||||
emphasis={'primary'}
|
emphasis={'primary'}
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,7 @@ export const PINNED_MESSAGES = 'PinnedMessages';
|
||||||
export const POST_OPTIONS = 'PostOptions';
|
export const POST_OPTIONS = 'PostOptions';
|
||||||
export const POST_PRIORITY_PICKER = 'PostPriorityPicker';
|
export const POST_PRIORITY_PICKER = 'PostPriorityPicker';
|
||||||
export const REACTIONS = 'Reactions';
|
export const REACTIONS = 'Reactions';
|
||||||
|
export const REPORT_PROBLEM = 'ReportProblem';
|
||||||
export const RESCHEDULE_DRAFT = 'RescheduleDraft';
|
export const RESCHEDULE_DRAFT = 'RescheduleDraft';
|
||||||
export const REVIEW_APP = 'ReviewApp';
|
export const REVIEW_APP = 'ReviewApp';
|
||||||
export const SAVED_MESSAGES = 'SavedMessages';
|
export const SAVED_MESSAGES = 'SavedMessages';
|
||||||
|
|
@ -136,6 +137,7 @@ export default {
|
||||||
POST_OPTIONS,
|
POST_OPTIONS,
|
||||||
POST_PRIORITY_PICKER,
|
POST_PRIORITY_PICKER,
|
||||||
REACTIONS,
|
REACTIONS,
|
||||||
|
REPORT_PROBLEM,
|
||||||
RESCHEDULE_DRAFT,
|
RESCHEDULE_DRAFT,
|
||||||
REVIEW_APP,
|
REVIEW_APP,
|
||||||
SAVED_MESSAGES,
|
SAVED_MESSAGES,
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,23 @@
|
||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
// See LICENSE.txt for license information.
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import {Platform} from 'react-native';
|
||||||
|
|
||||||
import {SYSTEM_IDENTIFIERS} from '@constants/database';
|
import {SYSTEM_IDENTIFIERS} from '@constants/database';
|
||||||
import DatabaseManager from '@database/manager';
|
import DatabaseManager from '@database/manager';
|
||||||
|
|
||||||
import {observeCanDownloadFiles, observeCanUploadFiles} from './system';
|
import {observeCanDownloadFiles, observeCanUploadFiles, observeReportAProblemMetadata} from './system';
|
||||||
|
|
||||||
import type ServerDataOperator from '@database/operator/server_data_operator';
|
import type ServerDataOperator from '@database/operator/server_data_operator';
|
||||||
import type {Database} from '@nozbe/watermelondb';
|
import type {Database} from '@nozbe/watermelondb';
|
||||||
|
|
||||||
|
jest.mock('expo-application', () => {
|
||||||
|
return {
|
||||||
|
nativeApplicationVersion: '1.2.3',
|
||||||
|
nativeBuildVersion: '456',
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
describe('observeCanUploadFiles', () => {
|
describe('observeCanUploadFiles', () => {
|
||||||
const serverUrl = 'baseHandler.test.com';
|
const serverUrl = 'baseHandler.test.com';
|
||||||
let database: Database;
|
let database: Database;
|
||||||
|
|
@ -252,3 +261,68 @@ describe('observeCanDownloadFiles', () => {
|
||||||
});
|
});
|
||||||
}, 1500);
|
}, 1500);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('observeReportAProblemMetadata', () => {
|
||||||
|
const serverUrl = 'baseHandler.test.com';
|
||||||
|
let database: Database;
|
||||||
|
let operator: ServerDataOperator;
|
||||||
|
let originalPlatform: typeof Platform.OS;
|
||||||
|
beforeEach(async () => {
|
||||||
|
await DatabaseManager.init([serverUrl]);
|
||||||
|
const serverDatabaseAndOperator = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||||
|
database = serverDatabaseAndOperator.database;
|
||||||
|
operator = serverDatabaseAndOperator.operator;
|
||||||
|
originalPlatform = Platform.OS;
|
||||||
|
|
||||||
|
// @ts-expect-error Platform.OS is mocked
|
||||||
|
Platform.OS = 'somePlatform';
|
||||||
|
});
|
||||||
|
afterEach(async () => {
|
||||||
|
await DatabaseManager.destroyServerDatabase(serverUrl);
|
||||||
|
Platform.OS = originalPlatform;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return correct metadata', (done) => {
|
||||||
|
operator.handleConfigs({
|
||||||
|
configs: [
|
||||||
|
{id: 'Version', value: '7.8.0'},
|
||||||
|
{id: 'BuildNumber', value: '123'},
|
||||||
|
],
|
||||||
|
prepareRecordsOnly: false,
|
||||||
|
configsToDelete: [],
|
||||||
|
}).then(() => {
|
||||||
|
operator.handleSystem({
|
||||||
|
systems: [
|
||||||
|
{id: SYSTEM_IDENTIFIERS.LICENSE, value: {IsLicensed: 'true', Compliance: 'true'}},
|
||||||
|
{id: SYSTEM_IDENTIFIERS.CURRENT_USER_ID, value: 'user1'},
|
||||||
|
{id: SYSTEM_IDENTIFIERS.CURRENT_TEAM_ID, value: 'team1'},
|
||||||
|
],
|
||||||
|
prepareRecordsOnly: false,
|
||||||
|
}).then(() => {
|
||||||
|
observeReportAProblemMetadata(database).subscribe((data) => {
|
||||||
|
expect(data).toEqual({
|
||||||
|
currentUserId: 'user1',
|
||||||
|
currentTeamId: 'team1',
|
||||||
|
serverVersion: '7.8.0 (Build 123)',
|
||||||
|
appVersion: '1.2.3 (Build 456)',
|
||||||
|
appPlatform: 'somePlatform',
|
||||||
|
});
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle empty or undefined values', (done) => {
|
||||||
|
observeReportAProblemMetadata(database).subscribe((data) => {
|
||||||
|
expect(data).toEqual({
|
||||||
|
currentUserId: '',
|
||||||
|
currentTeamId: '',
|
||||||
|
serverVersion: 'Unknown (Build Unknown)',
|
||||||
|
appVersion: '1.2.3 (Build 456)',
|
||||||
|
appPlatform: 'somePlatform',
|
||||||
|
});
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,8 @@
|
||||||
// See LICENSE.txt for license information.
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
import {Database, Q} from '@nozbe/watermelondb';
|
import {Database, Q} from '@nozbe/watermelondb';
|
||||||
|
import {nativeApplicationVersion, nativeBuildVersion} from 'expo-application';
|
||||||
|
import {Platform} from 'react-native';
|
||||||
import {of as of$, Observable, combineLatest} from 'rxjs';
|
import {of as of$, Observable, combineLatest} from 'rxjs';
|
||||||
import {switchMap, distinctUntilChanged} from 'rxjs/operators';
|
import {switchMap, distinctUntilChanged} from 'rxjs/operators';
|
||||||
|
|
||||||
|
|
@ -624,3 +626,24 @@ const observeIsSelfHosterStarter = (database: Database) => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const observeReportAProblemMetadata = (database: Database) => {
|
||||||
|
const currentUserId = observeCurrentUserId(database);
|
||||||
|
const currentTeamId = observeCurrentTeamId(database);
|
||||||
|
const serverVersion = observeConfigValue(database, 'Version');
|
||||||
|
const buildNumber = observeConfigValue(database, 'BuildNumber');
|
||||||
|
|
||||||
|
return combineLatest([
|
||||||
|
currentUserId,
|
||||||
|
currentTeamId,
|
||||||
|
serverVersion,
|
||||||
|
buildNumber,
|
||||||
|
]).pipe(
|
||||||
|
switchMap(([userId, teamId, version = 'Unknown', build = 'Unknown']) => of$({
|
||||||
|
currentUserId: userId,
|
||||||
|
currentTeamId: teamId,
|
||||||
|
serverVersion: `${version} (Build ${build})`,
|
||||||
|
appVersion: `${nativeApplicationVersion} (Build ${nativeBuildVersion})`,
|
||||||
|
appPlatform: Platform.OS,
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -99,7 +99,7 @@ const ChannelBookmarkAddOrEdit = ({
|
||||||
const setBookmarkToSave = useCallback((b?: ChannelBookmark) => {
|
const setBookmarkToSave = useCallback((b?: ChannelBookmark) => {
|
||||||
enableSaveButton((b?.type === 'link' && Boolean(b?.link_url)) || (b?.type === 'file' && Boolean(b.file_id)));
|
enableSaveButton((b?.type === 'link' && Boolean(b?.link_url)) || (b?.type === 'file' && Boolean(b.file_id)));
|
||||||
setBookmark(b);
|
setBookmark(b);
|
||||||
}, []);
|
}, [enableSaveButton]);
|
||||||
|
|
||||||
const handleError = useCallback((error: string, buttons?: AlertButton[]) => {
|
const handleError = useCallback((error: string, buttons?: AlertButton[]) => {
|
||||||
const title = original ? formatMessage({id: 'channel_bookmark.edit.failed_title', defaultMessage: 'Error editing bookmark'}) : formatMessage({id: 'channel_bookmark.add.failed_title', defaultMessage: 'Error adding bookmark'});
|
const title = original ? formatMessage({id: 'channel_bookmark.edit.failed_title', defaultMessage: 'Error editing bookmark'}) : formatMessage({id: 'channel_bookmark.add.failed_title', defaultMessage: 'Error adding bookmark'});
|
||||||
|
|
@ -115,7 +115,7 @@ const ChannelBookmarkAddOrEdit = ({
|
||||||
const enabled = Boolean(bookmark?.display_name &&
|
const enabled = Boolean(bookmark?.display_name &&
|
||||||
((bookmark?.type === 'link' && Boolean(bookmark?.link_url)) || (bookmark?.type === 'file' && Boolean(bookmark.file_id))));
|
((bookmark?.type === 'link' && Boolean(bookmark?.link_url)) || (bookmark?.type === 'file' && Boolean(bookmark.file_id))));
|
||||||
enableSaveButton(enabled);
|
enableSaveButton(enabled);
|
||||||
}, [bookmark, enableSaveButton, formatMessage]);
|
}, [bookmark?.display_name, bookmark?.file_id, bookmark?.link_url, bookmark?.type, enableSaveButton, formatMessage, original]);
|
||||||
|
|
||||||
const close = useCallback(() => {
|
const close = useCallback(() => {
|
||||||
return dismissModal({componentId});
|
return dismissModal({componentId});
|
||||||
|
|
@ -129,7 +129,7 @@ const ChannelBookmarkAddOrEdit = ({
|
||||||
}
|
}
|
||||||
|
|
||||||
handleError((res.error as Error).message);
|
handleError((res.error as Error).message);
|
||||||
}, [channelId, handleError, serverUrl]);
|
}, [channelId, close, handleError, serverUrl]);
|
||||||
|
|
||||||
const updateBookmark = useCallback(async (b: ChannelBookmark) => {
|
const updateBookmark = useCallback(async (b: ChannelBookmark) => {
|
||||||
const res = await editChannelBookmark(serverUrl, b);
|
const res = await editChannelBookmark(serverUrl, b);
|
||||||
|
|
@ -139,7 +139,7 @@ const ChannelBookmarkAddOrEdit = ({
|
||||||
}
|
}
|
||||||
|
|
||||||
handleError((res.error as Error).message);
|
handleError((res.error as Error).message);
|
||||||
}, [handleError, serverUrl]);
|
}, [close, handleError, serverUrl]);
|
||||||
|
|
||||||
const setLinkBookmark = useCallback((url: string, title: string, imageUrl: string) => {
|
const setLinkBookmark = useCallback((url: string, title: string, imageUrl: string) => {
|
||||||
const b: ChannelBookmark = {
|
const b: ChannelBookmark = {
|
||||||
|
|
@ -195,12 +195,12 @@ const ChannelBookmarkAddOrEdit = ({
|
||||||
if (emoji) {
|
if (emoji) {
|
||||||
addRecentReaction(serverUrl, [emoji]);
|
addRecentReaction(serverUrl, [emoji]);
|
||||||
}
|
}
|
||||||
}, [bookmark, enableSaveButton, serverUrl]);
|
}, [bookmark, enableSaveButton, original, serverUrl]);
|
||||||
|
|
||||||
const resetBookmark = useCallback(() => {
|
const resetBookmark = useCallback(() => {
|
||||||
setBookmarkToSave(original);
|
setBookmarkToSave(original);
|
||||||
setFile(originalFile);
|
setFile(originalFile);
|
||||||
}, [setBookmarkToSave]);
|
}, [original, originalFile, setBookmarkToSave]);
|
||||||
|
|
||||||
const onSaveBookmark = useCallback(async () => {
|
const onSaveBookmark = useCallback(async () => {
|
||||||
if (bookmark) {
|
if (bookmark) {
|
||||||
|
|
@ -213,7 +213,7 @@ const ChannelBookmarkAddOrEdit = ({
|
||||||
|
|
||||||
createBookmark(bookmark);
|
createBookmark(bookmark);
|
||||||
}
|
}
|
||||||
}, [bookmark, createBookmark, updateBookmark]);
|
}, [bookmark, createBookmark, enableSaveButton, original, updateBookmark]);
|
||||||
|
|
||||||
const handleDelete = useCallback(async () => {
|
const handleDelete = useCallback(async () => {
|
||||||
if (bookmark) {
|
if (bookmark) {
|
||||||
|
|
@ -235,7 +235,7 @@ const ChannelBookmarkAddOrEdit = ({
|
||||||
|
|
||||||
close();
|
close();
|
||||||
}
|
}
|
||||||
}, [bookmark, serverUrl, close]);
|
}, [bookmark, enableSaveButton, serverUrl, close, formatMessage]);
|
||||||
|
|
||||||
const onDelete = useCallback(async () => {
|
const onDelete = useCallback(async () => {
|
||||||
if (bookmark) {
|
if (bookmark) {
|
||||||
|
|
@ -255,7 +255,7 @@ const ChannelBookmarkAddOrEdit = ({
|
||||||
}],
|
}],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}, [bookmark, handleDelete]);
|
}, [bookmark, formatMessage, handleDelete]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
enableSaveButton(false);
|
enableSaveButton(false);
|
||||||
|
|
@ -307,9 +307,7 @@ const ChannelBookmarkAddOrEdit = ({
|
||||||
size='m'
|
size='m'
|
||||||
text='Delete bookmark'
|
text='Delete bookmark'
|
||||||
iconName='trash-can-outline'
|
iconName='trash-can-outline'
|
||||||
textStyle={styles.deleteText}
|
emphasis='tertiary'
|
||||||
backgroundStyle={styles.deleteBg}
|
|
||||||
iconSize={18}
|
|
||||||
onPress={onDelete}
|
onPress={onDelete}
|
||||||
theme={theme}
|
theme={theme}
|
||||||
disabled={isSaving}
|
disabled={isSaving}
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ import CompassIcon from '@components/compass_icon';
|
||||||
import FormattedText from '@components/formatted_text';
|
import FormattedText from '@components/formatted_text';
|
||||||
import {useServerUrl} from '@context/server';
|
import {useServerUrl} from '@context/server';
|
||||||
import {useTheme} from '@context/theme';
|
import {useTheme} from '@context/theme';
|
||||||
import {preventDoubleTap} from '@utils/tap';
|
import {usePreventDoubleTap} from '@hooks/utils';
|
||||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||||
import {typography} from '@utils/typography';
|
import {typography} from '@utils/typography';
|
||||||
|
|
||||||
|
|
@ -57,9 +57,9 @@ const MutedBanner = ({channelId}: Props) => {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const styles = getStyleSheet(theme);
|
const styles = getStyleSheet(theme);
|
||||||
|
|
||||||
const onPress = useCallback(preventDoubleTap(() => {
|
const onPress = usePreventDoubleTap(useCallback(() => {
|
||||||
toggleMuteChannel(serverUrl, channelId, false);
|
toggleMuteChannel(serverUrl, channelId, false);
|
||||||
}), [channelId, serverUrl]);
|
}, [channelId, serverUrl]));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Animated.View
|
<Animated.View
|
||||||
|
|
@ -83,18 +83,18 @@ const MutedBanner = ({channelId}: Props) => {
|
||||||
defaultMessage='You can change the notification settings, but you will not receive notifications until the channel is unmuted.'
|
defaultMessage='You can change the notification settings, but you will not receive notifications until the channel is unmuted.'
|
||||||
style={styles.contentText}
|
style={styles.contentText}
|
||||||
/>
|
/>
|
||||||
<Button
|
<View style={styles.button}>
|
||||||
buttonType='default'
|
<Button
|
||||||
onPress={onPress}
|
buttonType='default'
|
||||||
text={formatMessage({
|
onPress={onPress}
|
||||||
id: 'channel_notification_preferences.unmute_content',
|
text={formatMessage({
|
||||||
defaultMessage: 'Unmute channel',
|
id: 'channel_notification_preferences.unmute_content',
|
||||||
})}
|
defaultMessage: 'Unmute channel',
|
||||||
theme={theme}
|
})}
|
||||||
backgroundStyle={styles.button}
|
theme={theme}
|
||||||
iconName='bell-outline'
|
iconName='bell-outline'
|
||||||
iconSize={18}
|
/>
|
||||||
/>
|
</View>
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,6 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme: Theme) => {
|
||||||
color: theme.dndIndicator,
|
color: theme.dndIndicator,
|
||||||
},
|
},
|
||||||
loadingContainerStyle: {
|
loadingContainerStyle: {
|
||||||
marginRight: 10,
|
|
||||||
padding: 0,
|
padding: 0,
|
||||||
top: -2,
|
top: -2,
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -23,8 +23,7 @@ import {GALLERY_FOOTER_HEIGHT} from '@constants/gallery';
|
||||||
import {useServerUrl} from '@context/server';
|
import {useServerUrl} from '@context/server';
|
||||||
import {alertFailedToOpenDocument} from '@utils/document';
|
import {alertFailedToOpenDocument} from '@utils/document';
|
||||||
import {getFullErrorMessage} from '@utils/errors';
|
import {getFullErrorMessage} from '@utils/errors';
|
||||||
import {fileExists, getLocalFilePathFromFile, hasWriteStoragePermission} from '@utils/file';
|
import {fileExists, getLocalFilePathFromFile, hasWriteStoragePermission, pathWithPrefix} from '@utils/file';
|
||||||
import {pathWithPrefix} from '@utils/files';
|
|
||||||
import {galleryItemToFileInfo} from '@utils/gallery';
|
import {galleryItemToFileInfo} from '@utils/gallery';
|
||||||
import {logDebug} from '@utils/log';
|
import {logDebug} from '@utils/log';
|
||||||
import {typography} from '@utils/typography';
|
import {typography} from '@utils/typography';
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,6 @@ import Button from '@components/button';
|
||||||
import CompassIcon from '@components/compass_icon';
|
import CompassIcon from '@components/compass_icon';
|
||||||
import FormattedText from '@components/formatted_text';
|
import FormattedText from '@components/formatted_text';
|
||||||
import {Preferences} from '@constants';
|
import {Preferences} from '@constants';
|
||||||
import {buttonBackgroundStyle, buttonTextStyle} from '@utils/buttonStyles';
|
|
||||||
import {calculateDimensions} from '@utils/images';
|
import {calculateDimensions} from '@utils/images';
|
||||||
import {typography} from '@utils/typography';
|
import {typography} from '@utils/typography';
|
||||||
|
|
||||||
|
|
@ -121,9 +120,7 @@ const VideoError = ({filename, height, isDownloading, isRemote, onShouldHideCont
|
||||||
onPress={handleDownload}
|
onPress={handleDownload}
|
||||||
theme={Preferences.THEMES.onyx}
|
theme={Preferences.THEMES.onyx}
|
||||||
size={'lg'}
|
size={'lg'}
|
||||||
textStyle={buttonTextStyle(Preferences.THEMES.onyx, 'lg', 'primary', isDownloading ? 'disabled' : 'default')}
|
|
||||||
text={intl.formatMessage({id: 'video.download', defaultMessage: 'Download video'})}
|
text={intl.formatMessage({id: 'video.download', defaultMessage: 'Download video'})}
|
||||||
backgroundStyle={buttonBackgroundStyle(Preferences.THEMES.onyx, 'lg', 'primary', isDownloading ? 'disabled' : 'default')}
|
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -285,16 +285,12 @@ exports[`components/categories_list should render channels error 1`] = `
|
||||||
onStartShouldSetResponder={[Function]}
|
onStartShouldSetResponder={[Function]}
|
||||||
style={
|
style={
|
||||||
{
|
{
|
||||||
"alignItems": "center",
|
|
||||||
"backgroundColor": "#ffffff",
|
"backgroundColor": "#ffffff",
|
||||||
"borderRadius": 4,
|
"borderRadius": 4,
|
||||||
"flex": 0,
|
|
||||||
"height": 48,
|
|
||||||
"justifyContent": "center",
|
|
||||||
"marginTop": 24,
|
"marginTop": 24,
|
||||||
"opacity": 1,
|
"opacity": 1,
|
||||||
"paddingHorizontal": 24,
|
"paddingHorizontal": 24,
|
||||||
"paddingVertical": 14,
|
"paddingVertical": 12,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
|
@ -302,17 +298,10 @@ exports[`components/categories_list should render channels error 1`] = `
|
||||||
style={
|
style={
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
"alignItems": "center",
|
|
||||||
"fontFamily": "OpenSans-SemiBold",
|
"fontFamily": "OpenSans-SemiBold",
|
||||||
"fontWeight": "600",
|
|
||||||
"justifyContent": "center",
|
|
||||||
"padding": 1,
|
|
||||||
"textAlignVertical": "center",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fontSize": 16,
|
"fontSize": 16,
|
||||||
"lineHeight": 18,
|
"fontWeight": "600",
|
||||||
"marginTop": 1,
|
"lineHeight": 24,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"color": "#1c58d9",
|
"color": "#1c58d9",
|
||||||
|
|
@ -683,16 +672,12 @@ exports[`components/categories_list should render team error 1`] = `
|
||||||
onStartShouldSetResponder={[Function]}
|
onStartShouldSetResponder={[Function]}
|
||||||
style={
|
style={
|
||||||
{
|
{
|
||||||
"alignItems": "center",
|
|
||||||
"backgroundColor": "#ffffff",
|
"backgroundColor": "#ffffff",
|
||||||
"borderRadius": 4,
|
"borderRadius": 4,
|
||||||
"flex": 0,
|
|
||||||
"height": 48,
|
|
||||||
"justifyContent": "center",
|
|
||||||
"marginTop": 24,
|
"marginTop": 24,
|
||||||
"opacity": 1,
|
"opacity": 1,
|
||||||
"paddingHorizontal": 24,
|
"paddingHorizontal": 24,
|
||||||
"paddingVertical": 14,
|
"paddingVertical": 12,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
|
@ -700,17 +685,10 @@ exports[`components/categories_list should render team error 1`] = `
|
||||||
style={
|
style={
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
"alignItems": "center",
|
|
||||||
"fontFamily": "OpenSans-SemiBold",
|
"fontFamily": "OpenSans-SemiBold",
|
||||||
"fontWeight": "600",
|
|
||||||
"justifyContent": "center",
|
|
||||||
"padding": 1,
|
|
||||||
"textAlignVertical": "center",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fontSize": 16,
|
"fontSize": 16,
|
||||||
"lineHeight": 18,
|
"fontWeight": "600",
|
||||||
"marginTop": 1,
|
"lineHeight": 24,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"color": "#1c58d9",
|
"color": "#1c58d9",
|
||||||
|
|
|
||||||
196
app/screens/index.test.tsx
Normal file
196
app/screens/index.test.tsx
Normal file
|
|
@ -0,0 +1,196 @@
|
||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
import {Provider as EMMProvider} from '@mattermost/react-native-emm';
|
||||||
|
import {render} from '@testing-library/react-native';
|
||||||
|
import React from 'react';
|
||||||
|
import {IntlProvider} from 'react-intl';
|
||||||
|
import {Platform, Text, View} from 'react-native';
|
||||||
|
import {GestureHandlerRootView} from 'react-native-gesture-handler';
|
||||||
|
import {Navigation} from 'react-native-navigation';
|
||||||
|
import {SafeAreaProvider} from 'react-native-safe-area-context';
|
||||||
|
|
||||||
|
import {Screens} from '@constants';
|
||||||
|
import {withServerDatabase} from '@database/components';
|
||||||
|
|
||||||
|
import EditServer from './edit_server';
|
||||||
|
import InAppNotification from './in_app_notification';
|
||||||
|
import ReportProblem from './report_a_problem';
|
||||||
|
|
||||||
|
// Mock the dependencies
|
||||||
|
jest.mock('react-native-navigation', () => ({
|
||||||
|
Navigation: {
|
||||||
|
setLazyComponentRegistrator: jest.fn(),
|
||||||
|
registerComponent: jest.fn(),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Mock providers
|
||||||
|
jest.mock('react-intl', () => ({
|
||||||
|
...jest.requireActual('react-intl'),
|
||||||
|
IntlProvider: jest.fn(),
|
||||||
|
}));
|
||||||
|
jest.mocked(IntlProvider).mockImplementation((props) => (
|
||||||
|
<View
|
||||||
|
{...props}
|
||||||
|
testID='IntlProvider'
|
||||||
|
/>
|
||||||
|
) as any);
|
||||||
|
|
||||||
|
jest.mock('react-native-gesture-handler', () => ({
|
||||||
|
GestureHandlerRootView: jest.fn(),
|
||||||
|
}));
|
||||||
|
jest.mocked(GestureHandlerRootView).mockImplementation((props) => (
|
||||||
|
<View
|
||||||
|
{...props}
|
||||||
|
testID='GestureHandlerRootView'
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
|
||||||
|
jest.mock('react-native-safe-area-context', () => ({
|
||||||
|
SafeAreaProvider: jest.fn(),
|
||||||
|
}));
|
||||||
|
jest.mocked(SafeAreaProvider).mockImplementation((props) => (
|
||||||
|
<View
|
||||||
|
{...props}
|
||||||
|
testID='SafeAreaProvider'
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
|
||||||
|
jest.mock('@mattermost/react-native-emm', () => ({
|
||||||
|
Provider: jest.fn(),
|
||||||
|
}));
|
||||||
|
jest.mocked(EMMProvider).mockImplementation((props) => (
|
||||||
|
<View
|
||||||
|
{...props}
|
||||||
|
testID='EMMProvider'
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
|
||||||
|
jest.mock('@database/components', () => ({
|
||||||
|
withServerDatabase: jest.fn(),
|
||||||
|
}));
|
||||||
|
jest.mocked(withServerDatabase).mockImplementation((Component) => function MockedWithServerDatabase(props: any) {
|
||||||
|
return <View testID='withDatabase'><Component {...props}/></View>;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Mock some screen components
|
||||||
|
jest.mock('@screens/report_a_problem', () => ({
|
||||||
|
__esModule: true,
|
||||||
|
default: jest.fn(),
|
||||||
|
}));
|
||||||
|
jest.mocked(ReportProblem).mockImplementation((props) => <Text {...props}>{Screens.REPORT_PROBLEM}</Text>);
|
||||||
|
|
||||||
|
jest.mock('@screens/edit_server', () => ({
|
||||||
|
__esModule: true,
|
||||||
|
default: jest.fn(),
|
||||||
|
}));
|
||||||
|
jest.mocked(EditServer).mockImplementation((props) => <Text {...props}>{Screens.EDIT_SERVER}</Text>);
|
||||||
|
|
||||||
|
jest.mock('@screens/in_app_notification', () => ({
|
||||||
|
__esModule: true,
|
||||||
|
default: jest.fn(),
|
||||||
|
}));
|
||||||
|
jest.mocked(InAppNotification).mockImplementation((props) => <Text {...props}>{Screens.IN_APP_NOTIFICATION}</Text>);
|
||||||
|
|
||||||
|
describe('Screen Registration', () => {
|
||||||
|
let registrator: (screenName: string) => void;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
await require('./index');
|
||||||
|
registrator = jest.mocked(Navigation.setLazyComponentRegistrator).mock.calls[0][0];
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
const ttcc: Array<[string, {
|
||||||
|
withServerDatabase: boolean;
|
||||||
|
withGestures: boolean;
|
||||||
|
withSafeAreaInsets: boolean;
|
||||||
|
withManagedConfig: boolean;
|
||||||
|
withIntl: boolean;
|
||||||
|
platform: string | undefined;
|
||||||
|
}]> = [
|
||||||
|
[
|
||||||
|
Screens.REPORT_PROBLEM,
|
||||||
|
{
|
||||||
|
withServerDatabase: true,
|
||||||
|
withGestures: true,
|
||||||
|
withSafeAreaInsets: true,
|
||||||
|
withManagedConfig: true,
|
||||||
|
withIntl: false,
|
||||||
|
platform: undefined,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
Screens.EDIT_SERVER,
|
||||||
|
{
|
||||||
|
withServerDatabase: false,
|
||||||
|
withGestures: true,
|
||||||
|
withSafeAreaInsets: true,
|
||||||
|
withManagedConfig: true,
|
||||||
|
withIntl: true,
|
||||||
|
platform: undefined,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
Screens.IN_APP_NOTIFICATION,
|
||||||
|
{
|
||||||
|
withServerDatabase: false,
|
||||||
|
withGestures: false,
|
||||||
|
withSafeAreaInsets: true,
|
||||||
|
withManagedConfig: false,
|
||||||
|
withIntl: false,
|
||||||
|
platform: 'ios',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
Screens.IN_APP_NOTIFICATION,
|
||||||
|
{
|
||||||
|
withServerDatabase: false,
|
||||||
|
withGestures: false,
|
||||||
|
withSafeAreaInsets: false,
|
||||||
|
withManagedConfig: false,
|
||||||
|
withIntl: false,
|
||||||
|
platform: 'android',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
it.each(ttcc)('register screen %s when requested', (screenName, testCase) => {
|
||||||
|
const originalPlatform = Platform.OS;
|
||||||
|
if (testCase.platform) {
|
||||||
|
Platform.OS = testCase.platform as any;
|
||||||
|
}
|
||||||
|
|
||||||
|
registrator(screenName);
|
||||||
|
expect(Navigation.registerComponent).toHaveBeenCalledWith(
|
||||||
|
screenName,
|
||||||
|
expect.any(Function),
|
||||||
|
);
|
||||||
|
const ResultingComponent = jest.mocked(Navigation.registerComponent).mock.calls[0][1]();
|
||||||
|
const {getByTestId, getByText} = render(<ResultingComponent componentId={screenName}/>);
|
||||||
|
const endComponent = getByText(screenName);
|
||||||
|
expect(endComponent).toBeDefined();
|
||||||
|
expect(endComponent.props.componentId).toBe(screenName);
|
||||||
|
if (testCase.withGestures) {
|
||||||
|
expect(getByTestId('GestureHandlerRootView')).toBeDefined();
|
||||||
|
}
|
||||||
|
if (testCase.withSafeAreaInsets) {
|
||||||
|
expect(getByTestId('SafeAreaProvider')).toBeDefined();
|
||||||
|
}
|
||||||
|
if (testCase.withManagedConfig) {
|
||||||
|
expect(getByTestId('EMMProvider')).toBeDefined();
|
||||||
|
}
|
||||||
|
if (testCase.withServerDatabase) {
|
||||||
|
expect(getByTestId('withDatabase')).toBeDefined();
|
||||||
|
}
|
||||||
|
Platform.OS = originalPlatform;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles unknown screen names gracefully', () => {
|
||||||
|
registrator('UNKNOWN_SCREEN');
|
||||||
|
expect(Navigation.registerComponent).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
import {Provider as EMMProvider} from '@mattermost/react-native-emm';
|
import {Provider as EMMProvider} from '@mattermost/react-native-emm';
|
||||||
import React, {type ComponentType} from 'react';
|
import React, {type ComponentType} from 'react';
|
||||||
import {IntlProvider} from 'react-intl';
|
import {IntlProvider} from 'react-intl';
|
||||||
import {Platform, type StyleProp, type ViewStyle} from 'react-native';
|
import {Platform} from 'react-native';
|
||||||
import {GestureHandlerRootView} from 'react-native-gesture-handler';
|
import {GestureHandlerRootView} from 'react-native-gesture-handler';
|
||||||
import {Navigation} from 'react-native-navigation';
|
import {Navigation} from 'react-native-navigation';
|
||||||
import {SafeAreaProvider} from 'react-native-safe-area-context';
|
import {SafeAreaProvider} from 'react-native-safe-area-context';
|
||||||
|
|
@ -13,10 +13,10 @@ import {Screens} from '@constants';
|
||||||
import {withServerDatabase} from '@database/components';
|
import {withServerDatabase} from '@database/components';
|
||||||
import {DEFAULT_LOCALE, getTranslations} from '@i18n';
|
import {DEFAULT_LOCALE, getTranslations} from '@i18n';
|
||||||
|
|
||||||
const withGestures = (Screen: React.ComponentType, styles: StyleProp<ViewStyle>) => {
|
const withGestures = (Screen: React.ComponentType) => {
|
||||||
return function gestureHoc(props: any) {
|
return function gestureHoc(props: any) {
|
||||||
return (
|
return (
|
||||||
<GestureHandlerRootView style={[{flex: 1}, styles]}>
|
<GestureHandlerRootView style={{flex: 1}}>
|
||||||
<Screen {...props}/>
|
<Screen {...props}/>
|
||||||
</GestureHandlerRootView>
|
</GestureHandlerRootView>
|
||||||
);
|
);
|
||||||
|
|
@ -58,7 +58,6 @@ const withManagedConfig = (Screen: React.ComponentType) => {
|
||||||
|
|
||||||
Navigation.setLazyComponentRegistrator((screenName) => {
|
Navigation.setLazyComponentRegistrator((screenName) => {
|
||||||
let screen: any|undefined;
|
let screen: any|undefined;
|
||||||
let extraStyles: StyleProp<ViewStyle>;
|
|
||||||
switch (screenName) {
|
switch (screenName) {
|
||||||
case Screens.ABOUT:
|
case Screens.ABOUT:
|
||||||
screen = withServerDatabase(require('@screens/settings/about').default);
|
screen = withServerDatabase(require('@screens/settings/about').default);
|
||||||
|
|
@ -68,10 +67,7 @@ Navigation.setLazyComponentRegistrator((screenName) => {
|
||||||
break;
|
break;
|
||||||
case Screens.BOTTOM_SHEET:
|
case Screens.BOTTOM_SHEET:
|
||||||
screen = withServerDatabase(require('@screens/bottom_sheet').default);
|
screen = withServerDatabase(require('@screens/bottom_sheet').default);
|
||||||
Navigation.registerComponent(Screens.BOTTOM_SHEET, () =>
|
break;
|
||||||
withGestures(withSafeAreaInsets(withManagedConfig(screen)), undefined),
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
case Screens.BROWSE_CHANNELS:
|
case Screens.BROWSE_CHANNELS:
|
||||||
screen = withServerDatabase(require('@screens/browse_channels').default);
|
screen = withServerDatabase(require('@screens/browse_channels').default);
|
||||||
break;
|
break;
|
||||||
|
|
@ -199,6 +195,9 @@ Navigation.setLazyComponentRegistrator((screenName) => {
|
||||||
case Screens.REACTIONS:
|
case Screens.REACTIONS:
|
||||||
screen = withServerDatabase(require('@screens/reactions').default);
|
screen = withServerDatabase(require('@screens/reactions').default);
|
||||||
break;
|
break;
|
||||||
|
case Screens.REPORT_PROBLEM:
|
||||||
|
screen = withServerDatabase(require('@screens/report_a_problem').default);
|
||||||
|
break;
|
||||||
case Screens.RESCHEDULE_DRAFT:
|
case Screens.RESCHEDULE_DRAFT:
|
||||||
screen = withServerDatabase(require('@screens/reschedule_draft').default);
|
screen = withServerDatabase(require('@screens/reschedule_draft').default);
|
||||||
break;
|
break;
|
||||||
|
|
@ -301,7 +300,7 @@ Navigation.setLazyComponentRegistrator((screenName) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (screen) {
|
if (screen) {
|
||||||
Navigation.registerComponent(screenName, () => withGestures(withSafeAreaInsets(withManagedConfig(screen)), extraStyles));
|
Navigation.registerComponent(screenName, () => withGestures(withSafeAreaInsets(withManagedConfig(screen))));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -309,7 +308,7 @@ export function registerScreens() {
|
||||||
const homeScreen = require('@screens/home').default;
|
const homeScreen = require('@screens/home').default;
|
||||||
const serverScreen = require('@screens/server').default;
|
const serverScreen = require('@screens/server').default;
|
||||||
const onboardingScreen = require('@screens/onboarding').default;
|
const onboardingScreen = require('@screens/onboarding').default;
|
||||||
Navigation.registerComponent(Screens.ONBOARDING, () => withGestures(withIntl(withManagedConfig(onboardingScreen)), undefined));
|
Navigation.registerComponent(Screens.ONBOARDING, () => withGestures(withIntl(withManagedConfig(onboardingScreen))));
|
||||||
Navigation.registerComponent(Screens.SERVER, () => withSafeAreaInsets(withGestures(withIntl(withManagedConfig(serverScreen)), undefined)));
|
Navigation.registerComponent(Screens.SERVER, () => withSafeAreaInsets(withGestures(withIntl(withManagedConfig(serverScreen)))));
|
||||||
Navigation.registerComponent(Screens.HOME, () => withGestures(withSafeAreaInsets(withServerDatabase(withManagedConfig(homeScreen))), undefined));
|
Navigation.registerComponent(Screens.HOME, () => withGestures(withSafeAreaInsets(withServerDatabase(withManagedConfig(homeScreen)))));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ type Props = {
|
||||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
|
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
|
||||||
button: {
|
button: {
|
||||||
marginTop: 5,
|
marginTop: 5,
|
||||||
|
flex: 1,
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
},
|
},
|
||||||
|
|
@ -37,9 +38,6 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
width: 120,
|
width: 120,
|
||||||
},
|
},
|
||||||
signInButtonText: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
},
|
|
||||||
footerButtonsContainer: {
|
footerButtonsContainer: {
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
height: 120,
|
height: 120,
|
||||||
|
|
@ -131,7 +129,7 @@ const FooterButtons = ({
|
||||||
);
|
);
|
||||||
|
|
||||||
const signInButtonText = (
|
const signInButtonText = (
|
||||||
<Animated.View style={[styles.signInButtonText, opacitySignInTextStyle]}>
|
<Animated.View style={opacitySignInTextStyle}>
|
||||||
<FormattedText
|
<FormattedText
|
||||||
id='mobile.onboarding.sign_in_to_get_started'
|
id='mobile.onboarding.sign_in_to_get_started'
|
||||||
defaultMessage='Sign in to get started'
|
defaultMessage='Sign in to get started'
|
||||||
|
|
|
||||||
165
app/screens/report_a_problem/app_logs.test.tsx
Normal file
165
app/screens/report_a_problem/app_logs.test.tsx
Normal file
|
|
@ -0,0 +1,165 @@
|
||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import TurboLogger from '@mattermost/react-native-turbo-log';
|
||||||
|
import RNUtils from '@mattermost/rnutils';
|
||||||
|
import {fireEvent, waitFor, waitForElementToBeRemoved} from '@testing-library/react-native';
|
||||||
|
import React from 'react';
|
||||||
|
import {Platform} from 'react-native';
|
||||||
|
import Share from 'react-native-share';
|
||||||
|
|
||||||
|
import {renderWithIntl} from '@test/intl-test-helper';
|
||||||
|
import {deleteFile} from '@utils/file';
|
||||||
|
import {logDebug} from '@utils/log';
|
||||||
|
|
||||||
|
import AppLogs from './app_logs';
|
||||||
|
jest.mock('react-native-share', () => ({
|
||||||
|
open: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('@utils/file', () => ({
|
||||||
|
...jest.requireActual('@utils/file'),
|
||||||
|
deleteFile: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('screens/report_a_problem/app_logs', () => {
|
||||||
|
const file1 = 'log1.txt';
|
||||||
|
const file2 = 'log2.txt';
|
||||||
|
const logPaths = [`/path/to/${file1}`, `/path/to/${file2}`];
|
||||||
|
const zipPath = '/path/to/logs.zip';
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
jest.mocked(TurboLogger.getLogPaths).mockResolvedValue(logPaths);
|
||||||
|
jest.mocked(RNUtils.createZipFile).mockResolvedValue(zipPath);
|
||||||
|
|
||||||
|
// React TouchableOpacity seems to be causing warnings about state
|
||||||
|
// changes outside of an act. This is a workaround to silence the warnings.
|
||||||
|
// We keep nextTick as it is needed for the tests to work.
|
||||||
|
jest.useFakeTimers({
|
||||||
|
doNotFake: [
|
||||||
|
'nextTick',
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
Platform.OS = 'ios';
|
||||||
|
jest.useRealTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders loading state initially', async () => {
|
||||||
|
const {getByTestId, getByText} = renderWithIntl(
|
||||||
|
<AppLogs/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(getByTestId('logs-loading')).toBeTruthy();
|
||||||
|
expect(getByText('Download App Logs')).toBeDisabled();
|
||||||
|
|
||||||
|
// This removes a jest warning
|
||||||
|
await waitForElementToBeRemoved(() => getByTestId('logs-loading'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders log files after loading', async () => {
|
||||||
|
const {getByTestId, getByText} = renderWithIntl(
|
||||||
|
<AppLogs/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
await waitForElementToBeRemoved(() => getByTestId('logs-loading'));
|
||||||
|
|
||||||
|
expect(getByText(/Logs_/)).toBeTruthy();
|
||||||
|
expect(getByText('Download App Logs')).toBeEnabled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles download on iOS', async () => {
|
||||||
|
Platform.OS = 'ios';
|
||||||
|
const {getByText} = renderWithIntl(
|
||||||
|
<AppLogs/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(TurboLogger.getLogPaths).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
const downloadButton = getByText('Download App Logs');
|
||||||
|
fireEvent.press(downloadButton);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(RNUtils.createZipFile).toHaveBeenCalledWith(logPaths);
|
||||||
|
expect(Share.open).toHaveBeenCalledWith({
|
||||||
|
url: 'file://' + zipPath,
|
||||||
|
saveToFiles: true,
|
||||||
|
});
|
||||||
|
expect(deleteFile).toHaveBeenCalledWith(zipPath);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles download on Android', async () => {
|
||||||
|
Platform.OS = 'android';
|
||||||
|
const {getByText} = renderWithIntl(
|
||||||
|
<AppLogs/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(TurboLogger.getLogPaths).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
const downloadButton = getByText('Download App Logs');
|
||||||
|
fireEvent.press(downloadButton);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(RNUtils.createZipFile).toHaveBeenCalledWith(logPaths);
|
||||||
|
expect(RNUtils.saveFile).toHaveBeenCalledWith(zipPath);
|
||||||
|
expect(deleteFile).toHaveBeenCalledWith(zipPath);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles errors during zip creation', async () => {
|
||||||
|
jest.mocked(RNUtils.createZipFile).mockRejectedValue(new Error('Zip failed'));
|
||||||
|
const {getByText} = renderWithIntl(
|
||||||
|
<AppLogs/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(TurboLogger.getLogPaths).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
const downloadButton = getByText('Download App Logs');
|
||||||
|
fireEvent.press(downloadButton);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(RNUtils.createZipFile).toHaveBeenCalled();
|
||||||
|
expect(logDebug).toHaveBeenCalledWith('Failed to create save file', new Error('Zip failed'));
|
||||||
|
expect(deleteFile).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles errors during file deletion', async () => {
|
||||||
|
jest.mocked(deleteFile).mockRejectedValue(new Error('Delete failed'));
|
||||||
|
const {getByText} = renderWithIntl(
|
||||||
|
<AppLogs/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(TurboLogger.getLogPaths).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
const downloadButton = getByText('Download App Logs');
|
||||||
|
fireEvent.press(downloadButton);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(RNUtils.createZipFile).toHaveBeenCalled();
|
||||||
|
expect(deleteFile).toHaveBeenCalled();
|
||||||
|
expect(logDebug).toHaveBeenCalledWith('Failed to delete zip file', new Error('Delete failed'));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles empty log paths', async () => {
|
||||||
|
jest.mocked(TurboLogger.getLogPaths).mockResolvedValue([]);
|
||||||
|
const {getByText, getByTestId} = renderWithIntl(
|
||||||
|
<AppLogs/>,
|
||||||
|
);
|
||||||
|
await waitForElementToBeRemoved(() => getByTestId('logs-loading'));
|
||||||
|
expect(getByText('Download App Logs')).toBeDisabled();
|
||||||
|
});
|
||||||
|
});
|
||||||
114
app/screens/report_a_problem/app_logs.tsx
Normal file
114
app/screens/report_a_problem/app_logs.tsx
Normal file
|
|
@ -0,0 +1,114 @@
|
||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
import TurboLogger from '@mattermost/react-native-turbo-log';
|
||||||
|
import RNUtils from '@mattermost/rnutils';
|
||||||
|
import React, {useCallback, useEffect, useState} from 'react';
|
||||||
|
import {useIntl} from 'react-intl';
|
||||||
|
import {View, Text, ActivityIndicator, Platform} from 'react-native';
|
||||||
|
import Share from 'react-native-share';
|
||||||
|
|
||||||
|
import Button from '@components/button';
|
||||||
|
import {useTheme} from '@context/theme';
|
||||||
|
import {deleteFile, pathWithPrefix} from '@utils/file';
|
||||||
|
import {logDebug} from '@utils/log';
|
||||||
|
import {makeStyleSheetFromTheme} from '@utils/theme';
|
||||||
|
|
||||||
|
import LogFileItem from './log_file_item';
|
||||||
|
import {getCommonStyleSheet} from './styles';
|
||||||
|
|
||||||
|
const getStyleSheet = makeStyleSheetFromTheme((theme) => ({
|
||||||
|
...getCommonStyleSheet(theme),
|
||||||
|
|
||||||
|
buttonContainer: {
|
||||||
|
alignItems: 'flex-start',
|
||||||
|
},
|
||||||
|
container: {
|
||||||
|
gap: 16,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const AppLogs = () => {
|
||||||
|
const theme = useTheme();
|
||||||
|
const styles = getStyleSheet(theme);
|
||||||
|
const intl = useIntl();
|
||||||
|
|
||||||
|
const [logFiles, setLogFiles] = useState<string[]>([]);
|
||||||
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchLogs = async () => {
|
||||||
|
try {
|
||||||
|
const paths = await TurboLogger.getLogPaths();
|
||||||
|
setLogFiles(paths);
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchLogs();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleDownload = useCallback(async () => {
|
||||||
|
let zipFilePath = '';
|
||||||
|
try {
|
||||||
|
zipFilePath = await RNUtils.createZipFile(logFiles);
|
||||||
|
if (Platform.OS === 'android') {
|
||||||
|
await RNUtils.saveFile(zipFilePath);
|
||||||
|
} else {
|
||||||
|
await Share.open({
|
||||||
|
url: pathWithPrefix('file://', zipFilePath),
|
||||||
|
saveToFiles: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logDebug('Failed to create save file', error);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (zipFilePath) {
|
||||||
|
try {
|
||||||
|
await deleteFile(zipFilePath);
|
||||||
|
} catch (error) {
|
||||||
|
logDebug('Failed to delete zip file', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [logFiles]);
|
||||||
|
|
||||||
|
const hasLogs = logFiles.length > 0;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={styles.container}>
|
||||||
|
<Text style={styles.sectionTitle}>
|
||||||
|
{intl.formatMessage({
|
||||||
|
id: 'screen.report_problem.logs.title',
|
||||||
|
defaultMessage: 'APP LOGS:',
|
||||||
|
})}
|
||||||
|
</Text>
|
||||||
|
<View>
|
||||||
|
{isLoading ? (
|
||||||
|
<ActivityIndicator
|
||||||
|
testID='logs-loading'
|
||||||
|
color={theme.buttonBg}
|
||||||
|
size='large'
|
||||||
|
/>
|
||||||
|
) : (hasLogs && (
|
||||||
|
<LogFileItem/>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
<View style={styles.buttonContainer}>
|
||||||
|
<Button
|
||||||
|
onPress={handleDownload}
|
||||||
|
text={intl.formatMessage({
|
||||||
|
id: 'screen.report_problem.logs.download',
|
||||||
|
defaultMessage: 'Download App Logs',
|
||||||
|
})}
|
||||||
|
emphasis='tertiary'
|
||||||
|
theme={theme}
|
||||||
|
iconName='download-outline'
|
||||||
|
disabled={!hasLogs}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AppLogs;
|
||||||
110
app/screens/report_a_problem/copy_metadata.test.tsx
Normal file
110
app/screens/report_a_problem/copy_metadata.test.tsx
Normal file
|
|
@ -0,0 +1,110 @@
|
||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import Clipboard from '@react-native-clipboard/clipboard';
|
||||||
|
import {fireEvent} from '@testing-library/react-native';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import {SNACK_BAR_TYPE} from '@constants/snack_bar';
|
||||||
|
import {renderWithIntl} from '@test/intl-test-helper';
|
||||||
|
import {metadataToString} from '@utils/share_logs';
|
||||||
|
import {showSnackBar} from '@utils/snack_bar';
|
||||||
|
|
||||||
|
import CopyMetadata from './copy_metadata';
|
||||||
|
|
||||||
|
jest.mock('@react-native-clipboard/clipboard', () => ({
|
||||||
|
setString: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('@utils/snack_bar', () => ({
|
||||||
|
showSnackBar: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('screens/report_a_problem/copy_metadata', () => {
|
||||||
|
const metadata = {
|
||||||
|
currentUserId: 'user1',
|
||||||
|
currentTeamId: 'team1',
|
||||||
|
serverVersion: '7.8.0',
|
||||||
|
appVersion: '2.0.0',
|
||||||
|
appPlatform: 'ios',
|
||||||
|
};
|
||||||
|
const componentId = 'ReportProblem';
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders all metadata fields', () => {
|
||||||
|
const {getByText} = renderWithIntl(
|
||||||
|
<CopyMetadata
|
||||||
|
metadata={metadata}
|
||||||
|
componentId={componentId}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Check title
|
||||||
|
expect(getByText('METADATA:')).toBeTruthy();
|
||||||
|
|
||||||
|
// Check each metadata field is displayed
|
||||||
|
expect(getByText('Current User ID: user1')).toBeTruthy();
|
||||||
|
expect(getByText('Current Team ID: team1')).toBeTruthy();
|
||||||
|
expect(getByText('Server Version: 7.8.0')).toBeTruthy();
|
||||||
|
expect(getByText('App Version: 2.0.0')).toBeTruthy();
|
||||||
|
expect(getByText('App Platform: ios')).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('copies metadata to clipboard when copy button is pressed', () => {
|
||||||
|
const {getByText} = renderWithIntl(
|
||||||
|
<CopyMetadata
|
||||||
|
metadata={metadata}
|
||||||
|
componentId={componentId}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
const copyButton = getByText('Copy');
|
||||||
|
fireEvent.press(copyButton);
|
||||||
|
|
||||||
|
expect(Clipboard.setString).toHaveBeenCalledWith(
|
||||||
|
metadataToString(metadata),
|
||||||
|
);
|
||||||
|
expect(showSnackBar).toHaveBeenCalledWith({
|
||||||
|
barType: SNACK_BAR_TYPE.INFO_COPIED,
|
||||||
|
sourceScreen: componentId,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles empty metadata', () => {
|
||||||
|
const emptyMetadata = {
|
||||||
|
currentUserId: '',
|
||||||
|
currentTeamId: '',
|
||||||
|
serverVersion: '',
|
||||||
|
appVersion: '',
|
||||||
|
appPlatform: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
const {getByText} = renderWithIntl(
|
||||||
|
<CopyMetadata
|
||||||
|
metadata={emptyMetadata}
|
||||||
|
componentId={componentId}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Check that empty values are displayed correctly
|
||||||
|
expect(getByText('Current User ID: ')).toBeTruthy();
|
||||||
|
expect(getByText('Current Team ID: ')).toBeTruthy();
|
||||||
|
expect(getByText('Server Version: ')).toBeTruthy();
|
||||||
|
expect(getByText('App Version: ')).toBeTruthy();
|
||||||
|
expect(getByText('App Platform: ')).toBeTruthy();
|
||||||
|
|
||||||
|
const copyButton = getByText('Copy');
|
||||||
|
fireEvent.press(copyButton);
|
||||||
|
|
||||||
|
expect(Clipboard.setString).toHaveBeenCalledWith(
|
||||||
|
metadataToString(emptyMetadata),
|
||||||
|
);
|
||||||
|
expect(showSnackBar).toHaveBeenCalledWith({
|
||||||
|
barType: SNACK_BAR_TYPE.INFO_COPIED,
|
||||||
|
sourceScreen: componentId,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
78
app/screens/report_a_problem/copy_metadata.tsx
Normal file
78
app/screens/report_a_problem/copy_metadata.tsx
Normal file
|
|
@ -0,0 +1,78 @@
|
||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
import Clipboard from '@react-native-clipboard/clipboard';
|
||||||
|
import React, {useCallback} from 'react';
|
||||||
|
import {useIntl} from 'react-intl';
|
||||||
|
import {View, Text, StyleSheet} from 'react-native';
|
||||||
|
|
||||||
|
import Button from '@components/button';
|
||||||
|
import {SNACK_BAR_TYPE} from '@constants/snack_bar';
|
||||||
|
import {useTheme} from '@context/theme';
|
||||||
|
import {metadataToString, reportAProblemMessages} from '@utils/share_logs';
|
||||||
|
import {showSnackBar} from '@utils/snack_bar';
|
||||||
|
|
||||||
|
import {getCommonStyleSheet} from './styles';
|
||||||
|
|
||||||
|
import type {AvailableScreens} from '@typings/screens/navigation';
|
||||||
|
import type {ReportAProblemMetadata} from '@typings/screens/report_a_problem';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
metadata: ReportAProblemMetadata;
|
||||||
|
componentId: AvailableScreens;
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
buttonContainer: {
|
||||||
|
alignItems: 'flex-start',
|
||||||
|
},
|
||||||
|
container: {
|
||||||
|
gap: 16,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const CopyMetadata = ({
|
||||||
|
metadata,
|
||||||
|
componentId,
|
||||||
|
}: Props) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
const commonStyles = getCommonStyleSheet(theme);
|
||||||
|
const intl = useIntl();
|
||||||
|
|
||||||
|
const handleCopy = useCallback(() => {
|
||||||
|
Clipboard.setString(metadataToString(metadata));
|
||||||
|
showSnackBar({barType: SNACK_BAR_TYPE.INFO_COPIED, sourceScreen: componentId});
|
||||||
|
}, [componentId, metadata]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={styles.container}>
|
||||||
|
<Text style={commonStyles.sectionTitle}>
|
||||||
|
{intl.formatMessage({
|
||||||
|
id: 'screen.report_problem.metadata.title',
|
||||||
|
defaultMessage: 'METADATA:',
|
||||||
|
})}
|
||||||
|
</Text>
|
||||||
|
<View>
|
||||||
|
{Object.entries(metadata).map(([key, value]) => (
|
||||||
|
<Text
|
||||||
|
key={key}
|
||||||
|
style={commonStyles.bodyText}
|
||||||
|
numberOfLines={1}
|
||||||
|
>
|
||||||
|
{`${intl.formatMessage(reportAProblemMessages[key as keyof Props['metadata']])}: ${value}`}
|
||||||
|
</Text>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
<View style={styles.buttonContainer}>
|
||||||
|
<Button
|
||||||
|
onPress={handleCopy}
|
||||||
|
emphasis='tertiary'
|
||||||
|
iconName='content-copy'
|
||||||
|
theme={theme}
|
||||||
|
text={intl.formatMessage({id: 'report_a_problem.metadata.copy', defaultMessage: 'Copy'})}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CopyMetadata;
|
||||||
158
app/screens/report_a_problem/index.test.tsx
Normal file
158
app/screens/report_a_problem/index.test.tsx
Normal file
|
|
@ -0,0 +1,158 @@
|
||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import {Database} from '@nozbe/watermelondb';
|
||||||
|
import React, {type ComponentProps} from 'react';
|
||||||
|
import {View, Text} from 'react-native';
|
||||||
|
|
||||||
|
import {SYSTEM_IDENTIFIERS} from '@constants/database';
|
||||||
|
import DatabaseManager from '@database/manager';
|
||||||
|
import {renderWithEverything} from '@test/intl-test-helper';
|
||||||
|
|
||||||
|
import ReportProblem from './report_problem';
|
||||||
|
|
||||||
|
import enhanced from './index';
|
||||||
|
|
||||||
|
import type ServerDataOperator from '@database/operator/server_data_operator';
|
||||||
|
import type {ReportAProblemMetadata} from '@typings/screens/report_a_problem';
|
||||||
|
|
||||||
|
jest.mock('./report_problem', () => ({
|
||||||
|
__esModule: true,
|
||||||
|
default: jest.fn(),
|
||||||
|
}));
|
||||||
|
jest.mocked(ReportProblem).mockImplementation((props) => {
|
||||||
|
return (
|
||||||
|
<View>
|
||||||
|
{Object.keys(props).map((key) => {
|
||||||
|
if (key === 'metadata') {
|
||||||
|
return Object.keys(props[key]).map((metadataKey) => (
|
||||||
|
<Text
|
||||||
|
key={metadataKey}
|
||||||
|
testID={metadataKey}
|
||||||
|
>{`${props.metadata[metadataKey as keyof ReportAProblemMetadata]}`}</Text>
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Text
|
||||||
|
key={key}
|
||||||
|
testID={key}
|
||||||
|
>{`${props[key as keyof ComponentProps<typeof ReportProblem>]}`}</Text>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('screens/report_a_problem/index', () => {
|
||||||
|
const serverUrl = 'baseHandler.test.com';
|
||||||
|
let database: Database;
|
||||||
|
let operator: ServerDataOperator;
|
||||||
|
beforeEach(async () => {
|
||||||
|
await DatabaseManager.init([serverUrl]);
|
||||||
|
const serverDatabaseAndOperator = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||||
|
database = serverDatabaseAndOperator.database;
|
||||||
|
operator = serverDatabaseAndOperator.operator;
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await DatabaseManager.destroyServerDatabase(serverUrl);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle default values', async () => {
|
||||||
|
const Component = enhanced;
|
||||||
|
const {getByTestId} = renderWithEverything(<Component componentId={'ReportProblem'}/>, {database});
|
||||||
|
|
||||||
|
expect(getByTestId('currentUserId')).toHaveTextContent('');
|
||||||
|
expect(getByTestId('currentTeamId')).toHaveTextContent('');
|
||||||
|
expect(getByTestId('serverVersion')).toHaveTextContent('Unknown (Build Unknown)');
|
||||||
|
expect(getByTestId('appVersion')).toHaveTextContent('0.0.0 (Build 0)');
|
||||||
|
expect(getByTestId('appPlatform')).toHaveTextContent('ios');
|
||||||
|
expect(getByTestId('reportAProblemType')).toHaveTextContent('undefined');
|
||||||
|
expect(getByTestId('reportAProblemMail')).toHaveTextContent('undefined');
|
||||||
|
expect(getByTestId('reportAProblemLink')).toHaveTextContent('undefined');
|
||||||
|
expect(getByTestId('siteName')).toHaveTextContent('undefined');
|
||||||
|
expect(getByTestId('allowDownloadLogs')).toHaveTextContent('true');
|
||||||
|
expect(getByTestId('isLicensed')).toHaveTextContent('false');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should enhance ReportProblem with correct observables', async () => {
|
||||||
|
await operator.handleConfigs({
|
||||||
|
configs: [
|
||||||
|
{id: 'ReportAProblemType', value: 'email'},
|
||||||
|
{id: 'ReportAProblemMail', value: 'test@example.com'},
|
||||||
|
{id: 'ReportAProblemLink', value: 'https://example.com'},
|
||||||
|
{id: 'SiteName', value: 'Test Site'},
|
||||||
|
{id: 'AllowDownloadLogs', value: 'true'},
|
||||||
|
{id: 'Version', value: '7.8.0'},
|
||||||
|
{id: 'BuildNumber', value: '123'},
|
||||||
|
],
|
||||||
|
prepareRecordsOnly: false,
|
||||||
|
configsToDelete: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
await operator.handleSystem({
|
||||||
|
systems: [
|
||||||
|
{id: SYSTEM_IDENTIFIERS.LICENSE, value: {IsLicensed: 'false'}},
|
||||||
|
{id: SYSTEM_IDENTIFIERS.CURRENT_USER_ID, value: 'user1'},
|
||||||
|
{id: SYSTEM_IDENTIFIERS.CURRENT_TEAM_ID, value: 'team1'},
|
||||||
|
],
|
||||||
|
prepareRecordsOnly: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const Component = enhanced;
|
||||||
|
const {getByTestId} = renderWithEverything(<Component componentId={'ReportProblem'}/>, {database});
|
||||||
|
|
||||||
|
expect(getByTestId('currentUserId')).toHaveTextContent('user1');
|
||||||
|
expect(getByTestId('currentTeamId')).toHaveTextContent('team1');
|
||||||
|
expect(getByTestId('serverVersion')).toHaveTextContent('7.8.0 (Build 123)');
|
||||||
|
expect(getByTestId('appVersion')).toHaveTextContent('0.0.0 (Build 0)');
|
||||||
|
expect(getByTestId('appPlatform')).toHaveTextContent('ios');
|
||||||
|
expect(getByTestId('reportAProblemType')).toHaveTextContent('email');
|
||||||
|
expect(getByTestId('reportAProblemMail')).toHaveTextContent('test@example.com');
|
||||||
|
expect(getByTestId('reportAProblemLink')).toHaveTextContent('https://example.com');
|
||||||
|
expect(getByTestId('siteName')).toHaveTextContent('Test Site');
|
||||||
|
expect(getByTestId('allowDownloadLogs')).toHaveTextContent('true');
|
||||||
|
expect(getByTestId('isLicensed')).toHaveTextContent('false');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('different data should show different values', async () => {
|
||||||
|
await operator.handleConfigs({
|
||||||
|
configs: [
|
||||||
|
{id: 'ReportAProblemType', value: 'link'},
|
||||||
|
{id: 'ReportAProblemMail', value: 'test2@example.com'},
|
||||||
|
{id: 'ReportAProblemLink', value: 'https://example2.com'},
|
||||||
|
{id: 'SiteName', value: 'Test Site2'},
|
||||||
|
{id: 'AllowDownloadLogs', value: 'false'},
|
||||||
|
{id: 'Version', value: '7.8.1'},
|
||||||
|
{id: 'BuildNumber', value: '124'},
|
||||||
|
],
|
||||||
|
prepareRecordsOnly: false,
|
||||||
|
configsToDelete: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
await operator.handleSystem({
|
||||||
|
systems: [
|
||||||
|
{id: SYSTEM_IDENTIFIERS.LICENSE, value: {IsLicensed: 'false'}},
|
||||||
|
{id: SYSTEM_IDENTIFIERS.CURRENT_USER_ID, value: 'user2'},
|
||||||
|
{id: SYSTEM_IDENTIFIERS.CURRENT_TEAM_ID, value: 'team2'},
|
||||||
|
],
|
||||||
|
prepareRecordsOnly: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const Component = enhanced;
|
||||||
|
const {getByTestId} = renderWithEverything(<Component componentId={'ReportProblem'}/>, {database});
|
||||||
|
|
||||||
|
expect(getByTestId('currentUserId')).toHaveTextContent('user2');
|
||||||
|
expect(getByTestId('currentTeamId')).toHaveTextContent('team2');
|
||||||
|
expect(getByTestId('serverVersion')).toHaveTextContent('7.8.1 (Build 124)');
|
||||||
|
expect(getByTestId('appVersion')).toHaveTextContent('0.0.0 (Build 0)');
|
||||||
|
expect(getByTestId('appPlatform')).toHaveTextContent('ios');
|
||||||
|
expect(getByTestId('reportAProblemType')).toHaveTextContent('link');
|
||||||
|
expect(getByTestId('reportAProblemMail')).toHaveTextContent('test2@example.com');
|
||||||
|
expect(getByTestId('reportAProblemLink')).toHaveTextContent('https://example2.com');
|
||||||
|
expect(getByTestId('siteName')).toHaveTextContent('Test Site2');
|
||||||
|
expect(getByTestId('allowDownloadLogs')).toHaveTextContent('false');
|
||||||
|
expect(getByTestId('isLicensed')).toHaveTextContent('false');
|
||||||
|
});
|
||||||
|
});
|
||||||
25
app/screens/report_a_problem/index.ts
Normal file
25
app/screens/report_a_problem/index.ts
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
// 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/watermelondb/react';
|
||||||
|
import {switchMap} from '@nozbe/watermelondb/utils/rx';
|
||||||
|
import {of as of$} from 'rxjs';
|
||||||
|
|
||||||
|
import {observeConfigBooleanValue, observeConfigValue, observeLicense, observeReportAProblemMetadata} from '@queries/servers/system';
|
||||||
|
|
||||||
|
import ReportProblem from './report_problem';
|
||||||
|
|
||||||
|
const enhanced = withObservables([], ({database}) => {
|
||||||
|
return {
|
||||||
|
reportAProblemType: observeConfigValue(database, 'ReportAProblemType'),
|
||||||
|
reportAProblemMail: observeConfigValue(database, 'ReportAProblemMail'),
|
||||||
|
reportAProblemLink: observeConfigValue(database, 'ReportAProblemLink'),
|
||||||
|
siteName: observeConfigValue(database, 'SiteName'),
|
||||||
|
allowDownloadLogs: observeConfigBooleanValue(database, 'AllowDownloadLogs', true),
|
||||||
|
isLicensed: observeLicense(database).pipe(switchMap((license) => (license ? of$(license.IsLicensed) : of$(false)))),
|
||||||
|
metadata: observeReportAProblemMetadata(database),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
export default withDatabase(enhanced(ReportProblem));
|
||||||
48
app/screens/report_a_problem/log_file_item.test.tsx
Normal file
48
app/screens/report_a_problem/log_file_item.test.tsx
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import {render, waitFor} from '@testing-library/react-native';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import LogFileItem from './log_file_item';
|
||||||
|
|
||||||
|
jest.mock('@utils/file', () => ({
|
||||||
|
...jest.requireActual('@utils/file'),
|
||||||
|
getFileSize: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('screens/report_a_problem/log_file_item', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders log file item with correct filename', async () => {
|
||||||
|
const {getByText} = render(
|
||||||
|
<LogFileItem/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(getByText(/Logs_/)).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('displays file type', async () => {
|
||||||
|
const {getByText} = render(
|
||||||
|
<LogFileItem/>,
|
||||||
|
);
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(getByText('ZIP')).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('displays file icon', async () => {
|
||||||
|
const {getByTestId} = render(
|
||||||
|
<LogFileItem/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
const icon = getByTestId('log-file-icon');
|
||||||
|
expect(icon.props.name).toBe('file-zip-outline-large');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
45
app/screens/report_a_problem/log_file_item.tsx
Normal file
45
app/screens/report_a_problem/log_file_item.tsx
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
import React from 'react';
|
||||||
|
import {View, Text} from 'react-native';
|
||||||
|
|
||||||
|
import CompassIcon from '@components/compass_icon';
|
||||||
|
import {useTheme} from '@context/theme';
|
||||||
|
|
||||||
|
import {getCommonFileStyles} from './styles';
|
||||||
|
|
||||||
|
const getCurrentDate = () => {
|
||||||
|
const date = new Date();
|
||||||
|
const year = date.getFullYear();
|
||||||
|
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||||
|
const day = String(date.getDate()).padStart(2, '0');
|
||||||
|
return `${year}${month}${day}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const LogFileItem = () => {
|
||||||
|
const theme = useTheme();
|
||||||
|
const styles = getCommonFileStyles(theme);
|
||||||
|
|
||||||
|
const currentDate = getCurrentDate();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={styles.container}>
|
||||||
|
<CompassIcon
|
||||||
|
name='file-zip-outline-large'
|
||||||
|
size={40}
|
||||||
|
color={theme.centerChannelColor}
|
||||||
|
testID='log-file-icon'
|
||||||
|
/>
|
||||||
|
<View style={styles.header}>
|
||||||
|
<Text style={styles.name}>
|
||||||
|
{`Logs_${currentDate}`}
|
||||||
|
</Text>
|
||||||
|
<Text style={styles.type}>
|
||||||
|
{'ZIP'}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LogFileItem;
|
||||||
237
app/screens/report_a_problem/report_problem.test.tsx
Normal file
237
app/screens/report_a_problem/report_problem.test.tsx
Normal file
|
|
@ -0,0 +1,237 @@
|
||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import {act, fireEvent} from '@testing-library/react-native';
|
||||||
|
import React, {type ComponentProps} from 'react';
|
||||||
|
import {View} from 'react-native';
|
||||||
|
|
||||||
|
import {Screens} from '@constants';
|
||||||
|
import {renderWithIntl} from '@test/intl-test-helper';
|
||||||
|
import {logDebug} from '@utils/log';
|
||||||
|
import {emailLogs, getDefaultReportAProblemLink, shareLogs} from '@utils/share_logs';
|
||||||
|
import {tryOpenURL} from '@utils/url';
|
||||||
|
|
||||||
|
import AppLogs from './app_logs';
|
||||||
|
import ReportProblem from './report_problem';
|
||||||
|
|
||||||
|
jest.mock('@utils/share_logs', () => ({
|
||||||
|
...jest.requireActual('@utils/share_logs'),
|
||||||
|
shareLogs: jest.fn(),
|
||||||
|
emailLogs: jest.fn(),
|
||||||
|
getDefaultReportAProblemLink: jest.fn().mockReturnValue('default-link'),
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('@utils/url', () => ({
|
||||||
|
tryOpenURL: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('@screens/navigation', () => ({
|
||||||
|
popTopScreen: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
// We mock the app logs to simplify the testing and avoid
|
||||||
|
// warnings about updating component state outside of an act
|
||||||
|
jest.mock('@screens/report_a_problem/app_logs', () => ({
|
||||||
|
__esModule: true,
|
||||||
|
default: jest.fn(),
|
||||||
|
}));
|
||||||
|
jest.mocked(AppLogs).mockImplementation(() => <View testID='app-logs'/>);
|
||||||
|
|
||||||
|
describe('screens/report_a_problem/report_problem', () => {
|
||||||
|
const baseProps: ComponentProps<typeof ReportProblem> = {
|
||||||
|
componentId: Screens.REPORT_PROBLEM,
|
||||||
|
allowDownloadLogs: true,
|
||||||
|
isLicensed: true,
|
||||||
|
metadata: {
|
||||||
|
currentUserId: 'user1',
|
||||||
|
currentTeamId: 'team1',
|
||||||
|
serverVersion: '7.8.0',
|
||||||
|
appVersion: '2.0.0',
|
||||||
|
appPlatform: 'ios',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders with logs section when allowDownloadLogs is true', () => {
|
||||||
|
const {getByText, getByTestId} = renderWithIntl(
|
||||||
|
<ReportProblem {...baseProps}/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(getByText('Troubleshouting details')).toBeTruthy();
|
||||||
|
expect(getByText('When reporting a problem, share the metadata and app logs given below to help troubleshoot your problem faster')).toBeTruthy();
|
||||||
|
expect(getByTestId('app-logs')).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders without logs section when allowDownloadLogs is false', () => {
|
||||||
|
const props = {...baseProps, allowDownloadLogs: false};
|
||||||
|
const {getByText, queryByTestId} = renderWithIntl(
|
||||||
|
<ReportProblem {...props}/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(getByText('When reporting a problem, share the metadata given below to help troubleshoot your problem faster')).toBeTruthy();
|
||||||
|
expect(queryByTestId('app-logs')).not.toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles email report type and allows sharing logs', async () => {
|
||||||
|
const props = {
|
||||||
|
...baseProps,
|
||||||
|
reportAProblemType: 'email',
|
||||||
|
reportAProblemMail: 'test@example.com',
|
||||||
|
siteName: 'Test Site',
|
||||||
|
allowDownloadLogs: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
const {getByText} = renderWithIntl(
|
||||||
|
<ReportProblem {...props}/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
fireEvent.press(getByText('Report a problem'));
|
||||||
|
expect(emailLogs).toHaveBeenCalledWith(
|
||||||
|
props.metadata,
|
||||||
|
props.siteName,
|
||||||
|
props.reportAProblemMail,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles email report type and does not allow downloading logs', async () => {
|
||||||
|
const props = {
|
||||||
|
...baseProps,
|
||||||
|
reportAProblemType: 'email',
|
||||||
|
reportAProblemMail: 'test@example.com',
|
||||||
|
siteName: 'Test Site',
|
||||||
|
allowDownloadLogs: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
const {getByText} = renderWithIntl(
|
||||||
|
<ReportProblem {...props}/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
fireEvent.press(getByText('Report a problem'));
|
||||||
|
expect(emailLogs).toHaveBeenCalledWith(
|
||||||
|
props.metadata,
|
||||||
|
props.siteName,
|
||||||
|
props.reportAProblemMail,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles link report type', async () => {
|
||||||
|
const props = {
|
||||||
|
...baseProps,
|
||||||
|
reportAProblemType: 'link',
|
||||||
|
reportAProblemLink: 'https://example.com/report',
|
||||||
|
};
|
||||||
|
|
||||||
|
const {getByText} = renderWithIntl(
|
||||||
|
<ReportProblem {...props}/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
fireEvent.press(getByText('Report a problem'));
|
||||||
|
expect(tryOpenURL).toHaveBeenCalledWith(props.reportAProblemLink);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles missing report link', async () => {
|
||||||
|
const props = {
|
||||||
|
...baseProps,
|
||||||
|
reportAProblemType: 'link',
|
||||||
|
reportAProblemLink: undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
const {getByText} = renderWithIntl(
|
||||||
|
<ReportProblem {...props}/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
fireEvent.press(getByText('Report a problem'));
|
||||||
|
expect(logDebug).toHaveBeenCalledWith('Report a problem link is not set');
|
||||||
|
expect(tryOpenURL).toHaveBeenCalledWith('default-link');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles default report type when licensed', async () => {
|
||||||
|
const props = {
|
||||||
|
...baseProps,
|
||||||
|
reportAProblemType: 'default',
|
||||||
|
isLicensed: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
const {getByText} = renderWithIntl(
|
||||||
|
<ReportProblem {...props}/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
fireEvent.press(getByText('Report a problem'));
|
||||||
|
expect(getDefaultReportAProblemLink).toHaveBeenCalledWith(true);
|
||||||
|
expect(tryOpenURL).toHaveBeenCalledWith('default-link');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles default report type when not licensed', async () => {
|
||||||
|
const props = {
|
||||||
|
...baseProps,
|
||||||
|
reportAProblemType: 'default',
|
||||||
|
isLicensed: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
const {getByText} = renderWithIntl(
|
||||||
|
<ReportProblem {...props}/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
fireEvent.press(getByText('Report a problem'));
|
||||||
|
expect(getDefaultReportAProblemLink).toHaveBeenCalledWith(false);
|
||||||
|
expect(tryOpenURL).toHaveBeenCalledWith('default-link');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles legacy behavior when reportAProblemType is not defined', async () => {
|
||||||
|
const props = {
|
||||||
|
...baseProps,
|
||||||
|
reportAProblemType: undefined,
|
||||||
|
reportAProblemLink: 'https://example.com/report',
|
||||||
|
};
|
||||||
|
|
||||||
|
const {getByText} = renderWithIntl(
|
||||||
|
<ReportProblem {...props}/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
fireEvent.press(getByText('Report a problem'));
|
||||||
|
expect(tryOpenURL).toHaveBeenCalledWith(props.reportAProblemLink);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles legacy behavior with no link', async () => {
|
||||||
|
const props = {
|
||||||
|
...baseProps,
|
||||||
|
reportAProblemType: undefined,
|
||||||
|
reportAProblemLink: undefined,
|
||||||
|
reportAProblemMail: 'test@example.com',
|
||||||
|
siteName: 'Test Site',
|
||||||
|
};
|
||||||
|
|
||||||
|
const {getByText} = renderWithIntl(
|
||||||
|
<ReportProblem {...props}/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
fireEvent.press(getByText('Report a problem'));
|
||||||
|
expect(shareLogs).toHaveBeenCalledWith(
|
||||||
|
props.metadata,
|
||||||
|
props.siteName,
|
||||||
|
undefined,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
171
app/screens/report_a_problem/report_problem.tsx
Normal file
171
app/screens/report_a_problem/report_problem.tsx
Normal file
|
|
@ -0,0 +1,171 @@
|
||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
import React, {useCallback} from 'react';
|
||||||
|
import {useIntl} from 'react-intl';
|
||||||
|
import {View, Text, ScrollView} from 'react-native';
|
||||||
|
|
||||||
|
import Button from '@components/button';
|
||||||
|
import MenuDivider from '@components/menu_divider';
|
||||||
|
import {useTheme} from '@context/theme';
|
||||||
|
import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
|
||||||
|
import {popTopScreen} from '@screens/navigation';
|
||||||
|
import {logDebug} from '@utils/log';
|
||||||
|
import {emailLogs, getDefaultReportAProblemLink, shareLogs} from '@utils/share_logs';
|
||||||
|
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||||
|
import {typography} from '@utils/typography';
|
||||||
|
import {tryOpenURL} from '@utils/url';
|
||||||
|
|
||||||
|
import AppLogs from './app_logs';
|
||||||
|
import CopyMetadata from './copy_metadata';
|
||||||
|
import {getCommonStyleSheet} from './styles';
|
||||||
|
|
||||||
|
import type {AvailableScreens} from '@typings/screens/navigation';
|
||||||
|
import type {ReportAProblemMetadata} from '@typings/screens/report_a_problem';
|
||||||
|
export const REPORT_PROBLEM_CLOSE_BUTTON_ID = 'close-report-problem';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
componentId: AvailableScreens;
|
||||||
|
reportAProblemMail?: string;
|
||||||
|
reportAProblemLink?: string;
|
||||||
|
siteName?: string;
|
||||||
|
allowDownloadLogs: boolean;
|
||||||
|
reportAProblemType?: string;
|
||||||
|
isLicensed: boolean;
|
||||||
|
metadata: ReportAProblemMetadata;
|
||||||
|
}
|
||||||
|
|
||||||
|
const getStyleSheet = makeStyleSheetFromTheme((theme) => ({
|
||||||
|
...getCommonStyleSheet(theme),
|
||||||
|
container: {
|
||||||
|
flex: 1,
|
||||||
|
backgroundColor: theme.centerChannelBg,
|
||||||
|
paddingVertical: 20,
|
||||||
|
gap: 20,
|
||||||
|
},
|
||||||
|
body: {
|
||||||
|
paddingHorizontal: 20,
|
||||||
|
},
|
||||||
|
content: {
|
||||||
|
gap: 16,
|
||||||
|
},
|
||||||
|
detailsTitle: {
|
||||||
|
...typography('Heading', 200, 'SemiBold'),
|
||||||
|
color: theme.centerChannelColor,
|
||||||
|
},
|
||||||
|
detailsSection: {
|
||||||
|
gap: 8,
|
||||||
|
},
|
||||||
|
buttonContainer: {
|
||||||
|
paddingHorizontal: 20,
|
||||||
|
marginTop: 'auto',
|
||||||
|
borderTopWidth: 1,
|
||||||
|
borderColor: changeOpacity(theme.centerChannelColor, 0.08),
|
||||||
|
width: '100%',
|
||||||
|
paddingTop: 20,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const ReportProblem = ({
|
||||||
|
componentId,
|
||||||
|
reportAProblemMail,
|
||||||
|
reportAProblemLink,
|
||||||
|
siteName,
|
||||||
|
allowDownloadLogs,
|
||||||
|
reportAProblemType,
|
||||||
|
isLicensed,
|
||||||
|
metadata,
|
||||||
|
}: Props) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
const styles = getStyleSheet(theme);
|
||||||
|
const intl = useIntl();
|
||||||
|
|
||||||
|
const handleReport = useCallback(async () => {
|
||||||
|
switch (reportAProblemType) {
|
||||||
|
case 'email':
|
||||||
|
await emailLogs(metadata, siteName, reportAProblemMail, !allowDownloadLogs);
|
||||||
|
return;
|
||||||
|
case 'link': {
|
||||||
|
let linkToUse = reportAProblemLink;
|
||||||
|
if (!linkToUse) {
|
||||||
|
logDebug('Report a problem link is not set');
|
||||||
|
linkToUse = getDefaultReportAProblemLink(isLicensed);
|
||||||
|
}
|
||||||
|
tryOpenURL(linkToUse);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
case 'default': {
|
||||||
|
tryOpenURL(getDefaultReportAProblemLink(isLicensed));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Old servers where reportAProblemType is not defined
|
||||||
|
if (!reportAProblemLink) {
|
||||||
|
await shareLogs(metadata, siteName, undefined, false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
tryOpenURL(reportAProblemLink);
|
||||||
|
}, [reportAProblemType, reportAProblemLink, reportAProblemMail, metadata, siteName, allowDownloadLogs, isLicensed]);
|
||||||
|
|
||||||
|
const close = useCallback(() => {
|
||||||
|
popTopScreen(componentId);
|
||||||
|
}, [componentId]);
|
||||||
|
useAndroidHardwareBackHandler(componentId, close);
|
||||||
|
|
||||||
|
const descriptionText = allowDownloadLogs ? intl.formatMessage({
|
||||||
|
id: 'screen.report_problem.details.description',
|
||||||
|
defaultMessage: 'When reporting a problem, share the metadata and app logs given below to help troubleshoot your problem faster',
|
||||||
|
}) : intl.formatMessage({
|
||||||
|
id: 'screen.report_problem.details.description_without_logs',
|
||||||
|
defaultMessage: 'When reporting a problem, share the metadata given below to help troubleshoot your problem faster',
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={styles.container}>
|
||||||
|
<View style={styles.body}>
|
||||||
|
<ScrollView contentContainerStyle={styles.content}>
|
||||||
|
<View style={styles.detailsSection}>
|
||||||
|
<Text style={styles.detailsTitle}>
|
||||||
|
{intl.formatMessage({
|
||||||
|
id: 'screen.report_problem.details.title',
|
||||||
|
defaultMessage: 'Troubleshouting details',
|
||||||
|
})}
|
||||||
|
</Text>
|
||||||
|
<Text style={styles.bodyText}>
|
||||||
|
{descriptionText}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<MenuDivider/>
|
||||||
|
<CopyMetadata
|
||||||
|
metadata={metadata}
|
||||||
|
componentId={componentId}
|
||||||
|
/>
|
||||||
|
{allowDownloadLogs && (
|
||||||
|
<>
|
||||||
|
<MenuDivider/>
|
||||||
|
<AppLogs/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</ScrollView>
|
||||||
|
</View>
|
||||||
|
{reportAProblemType !== 'hidden' && (
|
||||||
|
<View style={styles.buttonContainer}>
|
||||||
|
<Button
|
||||||
|
theme={theme}
|
||||||
|
text={intl.formatMessage({
|
||||||
|
id: 'screen.report_problem.button',
|
||||||
|
defaultMessage: 'Report a problem',
|
||||||
|
})}
|
||||||
|
onPress={handleReport}
|
||||||
|
iconName='open-in-new'
|
||||||
|
size='lg'
|
||||||
|
isIconOnTheRight={true}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ReportProblem;
|
||||||
42
app/screens/report_a_problem/styles.tsx
Normal file
42
app/screens/report_a_problem/styles.tsx
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||||
|
import {typography} from '@utils/typography';
|
||||||
|
|
||||||
|
export const getCommonStyleSheet = makeStyleSheetFromTheme((theme) => ({
|
||||||
|
bodyText: {
|
||||||
|
...typography('Body', 200),
|
||||||
|
color: theme.centerChannelColor,
|
||||||
|
},
|
||||||
|
sectionTitle: {
|
||||||
|
...typography('Body', 75, 'SemiBold'),
|
||||||
|
color: theme.centerChannelColor,
|
||||||
|
opacity: 0.64,
|
||||||
|
textTransform: 'uppercase',
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const getCommonFileStyles = makeStyleSheetFromTheme((theme) => ({
|
||||||
|
container: {
|
||||||
|
backgroundColor: theme.centerChannelBg,
|
||||||
|
borderRadius: 4,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: changeOpacity(theme.centerChannelColor, 0.16),
|
||||||
|
padding: 12,
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 10,
|
||||||
|
},
|
||||||
|
header: {
|
||||||
|
flex: 1,
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
...typography('Body', 100, 'SemiBold'),
|
||||||
|
color: theme.centerChannelColor,
|
||||||
|
},
|
||||||
|
type: {
|
||||||
|
...typography('Body', 75),
|
||||||
|
color: changeOpacity(theme.centerChannelColor, 0.64),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
@ -18,11 +18,10 @@ import {SNACK_BAR_TYPE} from '@constants/snack_bar';
|
||||||
import {useServerUrl} from '@context/server';
|
import {useServerUrl} from '@context/server';
|
||||||
import {useTheme} from '@context/theme';
|
import {useTheme} from '@context/theme';
|
||||||
import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
|
import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
|
||||||
|
import {usePreventDoubleTap} from '@hooks/utils';
|
||||||
import {t} from '@i18n';
|
import {t} from '@i18n';
|
||||||
import {popTopScreen} from '@screens/navigation';
|
import {popTopScreen} from '@screens/navigation';
|
||||||
import {buttonBackgroundStyle, buttonTextStyle} from '@utils/buttonStyles';
|
|
||||||
import {showSnackBar} from '@utils/snack_bar';
|
import {showSnackBar} from '@utils/snack_bar';
|
||||||
import {preventDoubleTap} from '@utils/tap';
|
|
||||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||||
import {typography} from '@utils/typography';
|
import {typography} from '@utils/typography';
|
||||||
import {tryOpenURL} from '@utils/url';
|
import {tryOpenURL} from '@utils/url';
|
||||||
|
|
@ -153,27 +152,27 @@ const About = ({componentId, config, license}: AboutProps) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
tryOpenURL(url, onError);
|
tryOpenURL(url, onError);
|
||||||
}, []);
|
}, [intl]);
|
||||||
|
|
||||||
const handleAboutTeam = useCallback(preventDoubleTap(() => {
|
const handleAboutTeam = usePreventDoubleTap(useCallback(() => {
|
||||||
return openURL(Config.WebsiteURL);
|
return openURL(Config.WebsiteURL);
|
||||||
}), []);
|
}, [openURL]));
|
||||||
|
|
||||||
const handlePlatformNotice = useCallback(preventDoubleTap(() => {
|
const handlePlatformNotice = usePreventDoubleTap(useCallback(() => {
|
||||||
return openURL(Config.ServerNoticeURL);
|
return openURL(Config.ServerNoticeURL);
|
||||||
}), []);
|
}, [openURL]));
|
||||||
|
|
||||||
const handleMobileNotice = useCallback(preventDoubleTap(() => {
|
const handleMobileNotice = usePreventDoubleTap(useCallback(() => {
|
||||||
return openURL(Config.MobileNoticeURL);
|
return openURL(Config.MobileNoticeURL);
|
||||||
}), []);
|
}, [openURL]));
|
||||||
|
|
||||||
const handleTermsOfService = useCallback(preventDoubleTap(() => {
|
const handleTermsOfService = usePreventDoubleTap(useCallback(() => {
|
||||||
return openURL(AboutLinks.TERMS_OF_SERVICE);
|
return openURL(AboutLinks.TERMS_OF_SERVICE);
|
||||||
}), []);
|
}, [openURL]));
|
||||||
|
|
||||||
const handlePrivacyPolicy = useCallback(preventDoubleTap(() => {
|
const handlePrivacyPolicy = usePreventDoubleTap(useCallback(() => {
|
||||||
return openURL(AboutLinks.PRIVACY_POLICY);
|
return openURL(AboutLinks.PRIVACY_POLICY);
|
||||||
}), []);
|
}, [openURL]));
|
||||||
|
|
||||||
const serverVersion = useMemo(() => {
|
const serverVersion = useMemo(() => {
|
||||||
const buildNumber = config.BuildNumber;
|
const buildNumber = config.BuildNumber;
|
||||||
|
|
@ -210,7 +209,7 @@ const About = ({componentId, config, license}: AboutProps) => {
|
||||||
Clipboard.setString(copiedString);
|
Clipboard.setString(copiedString);
|
||||||
showSnackBar({barType: SNACK_BAR_TYPE.INFO_COPIED, sourceScreen: componentId});
|
showSnackBar({barType: SNACK_BAR_TYPE.INFO_COPIED, sourceScreen: componentId});
|
||||||
},
|
},
|
||||||
[intl, config, loadMetric],
|
[intl, config.BuildNumber, config.Version, config.SQLDriverName, config.SchemaVersion, loadMetric, componentId],
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -306,17 +305,17 @@ const About = ({componentId, config, license}: AboutProps) => {
|
||||||
{config.SchemaVersion}
|
{config.SchemaVersion}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
<Button
|
<View style={styles.copyInfoButtonContainer}>
|
||||||
theme={theme}
|
<Button
|
||||||
backgroundStyle={[buttonBackgroundStyle(theme, 'm', 'tertiary'), styles.copyInfoButtonContainer]}
|
theme={theme}
|
||||||
onPress={copyToClipboard}
|
onPress={copyToClipboard}
|
||||||
textStyle={buttonTextStyle(theme, 'm', 'tertiary', 'default')}
|
text={intl.formatMessage({id: 'settings.about.button.copyInfo', defaultMessage: 'Copy info'})}
|
||||||
text={intl.formatMessage({id: 'settings.about.button.copyInfo', defaultMessage: 'Copy info'})}
|
testID={'about.copy_info'}
|
||||||
testID={'about.copy_info'}
|
iconName='content-copy'
|
||||||
iconName='content-copy'
|
emphasis='tertiary'
|
||||||
iconSize={15}
|
size='m'
|
||||||
buttonType={'default'}
|
/>
|
||||||
/>
|
</View>
|
||||||
{license.IsLicensed === 'true' && (
|
{license.IsLicensed === 'true' && (
|
||||||
<View style={styles.licenseContainer}>
|
<View style={styles.licenseContainer}>
|
||||||
<FormattedText
|
<FormattedText
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
// See LICENSE.txt for license information.
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
import {t} from '@i18n';
|
import {defineMessages, type IntlShape} from 'react-intl';
|
||||||
|
|
||||||
import {goToScreen} from '@screens/navigation';
|
import {goToScreen} from '@screens/navigation';
|
||||||
import {typography} from '@utils/typography';
|
import {typography} from '@utils/typography';
|
||||||
|
|
||||||
import type {AvailableScreens} from '@typings/screens/navigation';
|
import type {AvailableScreens} from '@typings/screens/navigation';
|
||||||
import type {IntlShape} from 'react-intl';
|
|
||||||
|
|
||||||
export const getSaveButton = (buttonId: string, intl: IntlShape, color: string) => ({
|
export const getSaveButton = (buttonId: string, intl: IntlShape, color: string) => ({
|
||||||
color,
|
color,
|
||||||
|
|
@ -37,42 +37,109 @@ type SettingConfigDetails = {
|
||||||
testID?: string;
|
testID?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SettingOptionConfig: Record<string, SettingConfigDetails> = {
|
const messages = defineMessages({
|
||||||
notification: {
|
download_logs: {
|
||||||
defaultMessage: 'Notifications',
|
defaultMessage: 'Download app logs',
|
||||||
i18nId: t('general_settings.notifications'),
|
id: 'general_settings.download_logs',
|
||||||
icon: 'bell-outline',
|
|
||||||
testID: 'general_settings.notifications',
|
|
||||||
},
|
|
||||||
display: {
|
|
||||||
defaultMessage: 'Display',
|
|
||||||
i18nId: t('general_settings.display'),
|
|
||||||
icon: 'layers-outline',
|
|
||||||
testID: 'general_settings.display',
|
|
||||||
},
|
|
||||||
advanced_settings: {
|
|
||||||
defaultMessage: 'Advanced Settings',
|
|
||||||
i18nId: t('general_settings.advanced_settings'),
|
|
||||||
icon: 'tune',
|
|
||||||
testID: 'general_settings.advanced',
|
|
||||||
},
|
|
||||||
about: {
|
|
||||||
defaultMessage: 'About {appTitle}',
|
|
||||||
i18nId: t('general_settings.about'),
|
|
||||||
icon: 'information-outline',
|
|
||||||
testID: 'general_settings.about',
|
|
||||||
},
|
|
||||||
help: {
|
|
||||||
defaultMessage: 'Help',
|
|
||||||
i18nId: t('general_settings.help'),
|
|
||||||
testID: 'general_settings.help',
|
|
||||||
},
|
},
|
||||||
report_problem: {
|
report_problem: {
|
||||||
defaultMessage: 'Report a Problem',
|
defaultMessage: 'Report a Problem',
|
||||||
i18nId: t('general_settings.report_problem'),
|
id: 'general_settings.report_problem',
|
||||||
testID: 'general_settings.report_problem',
|
|
||||||
},
|
},
|
||||||
|
notifications: {
|
||||||
|
defaultMessage: 'Notifications',
|
||||||
|
id: 'general_settings.notifications',
|
||||||
|
},
|
||||||
|
display: {
|
||||||
|
defaultMessage: 'Display',
|
||||||
|
id: 'general_settings.display',
|
||||||
|
},
|
||||||
|
advanced_settings: {
|
||||||
|
defaultMessage: 'Advanced Settings',
|
||||||
|
id: 'general_settings.advanced_settings',
|
||||||
|
},
|
||||||
|
about: {
|
||||||
|
defaultMessage: 'About {appTitle}',
|
||||||
|
id: 'general_settings.about',
|
||||||
|
},
|
||||||
|
help: {
|
||||||
|
defaultMessage: 'Help',
|
||||||
|
id: 'general_settings.help',
|
||||||
|
},
|
||||||
|
push_notifications: {
|
||||||
|
defaultMessage: 'Push Notifications',
|
||||||
|
id: 'notification_settings.mobile',
|
||||||
|
},
|
||||||
|
call_notifications: {
|
||||||
|
defaultMessage: 'Call Notifications',
|
||||||
|
id: 'notification_settings.calls',
|
||||||
|
},
|
||||||
|
email: {
|
||||||
|
defaultMessage: 'Email',
|
||||||
|
id: 'notification_settings.email',
|
||||||
|
},
|
||||||
|
automatic_replies: {
|
||||||
|
defaultMessage: 'Automatic replies',
|
||||||
|
id: 'notification_settings.ooo_auto_responder',
|
||||||
|
},
|
||||||
|
clock_display: {
|
||||||
|
defaultMessage: 'Clock Display',
|
||||||
|
id: 'mobile.display_settings.clockDisplay',
|
||||||
|
},
|
||||||
|
crt: {
|
||||||
|
defaultMessage: 'Collapsed Reply Threads',
|
||||||
|
id: 'mobile.display_settings.crt',
|
||||||
|
},
|
||||||
|
theme: {
|
||||||
|
defaultMessage: 'Theme',
|
||||||
|
id: 'mobile.display_settings.theme',
|
||||||
|
},
|
||||||
|
timezone: {
|
||||||
|
defaultMessage: 'Timezone',
|
||||||
|
id: 'mobile.display_settings.timezone',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const SettingOptionConfig: Record<string, SettingConfigDetails> = {
|
||||||
|
notification: {
|
||||||
|
defaultMessage: messages.notifications.defaultMessage,
|
||||||
|
i18nId: messages.notifications.id,
|
||||||
|
icon: 'bell-outline',
|
||||||
|
testID: messages.notifications.id,
|
||||||
|
},
|
||||||
|
display: {
|
||||||
|
defaultMessage: messages.display.defaultMessage,
|
||||||
|
i18nId: messages.display.id,
|
||||||
|
icon: 'layers-outline',
|
||||||
|
testID: messages.display.id,
|
||||||
|
},
|
||||||
|
advanced_settings: {
|
||||||
|
defaultMessage: messages.advanced_settings.defaultMessage,
|
||||||
|
i18nId: messages.advanced_settings.id,
|
||||||
|
icon: 'tune',
|
||||||
|
testID: messages.advanced_settings.id,
|
||||||
|
},
|
||||||
|
about: {
|
||||||
|
defaultMessage: messages.about.defaultMessage,
|
||||||
|
i18nId: messages.about.id,
|
||||||
|
icon: 'information-outline',
|
||||||
|
testID: messages.about.id,
|
||||||
|
},
|
||||||
|
help: {
|
||||||
|
defaultMessage: messages.help.defaultMessage,
|
||||||
|
i18nId: messages.help.id,
|
||||||
|
testID: messages.help.id,
|
||||||
|
},
|
||||||
|
report_problem: {
|
||||||
|
defaultMessage: messages.report_problem.defaultMessage,
|
||||||
|
i18nId: messages.report_problem.id,
|
||||||
|
testID: messages.report_problem.id,
|
||||||
|
},
|
||||||
|
download_logs: {
|
||||||
|
defaultMessage: messages.download_logs.defaultMessage,
|
||||||
|
i18nId: messages.download_logs.id,
|
||||||
|
testID: messages.download_logs.id,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const NotificationsOptionConfig: Record<string, SettingConfigDetails> = {
|
export const NotificationsOptionConfig: Record<string, SettingConfigDetails> = {
|
||||||
|
|
@ -81,55 +148,55 @@ export const NotificationsOptionConfig: Record<string, SettingConfigDetails> = {
|
||||||
testID: 'notification_settings.mentions_replies',
|
testID: 'notification_settings.mentions_replies',
|
||||||
},
|
},
|
||||||
push_notification: {
|
push_notification: {
|
||||||
defaultMessage: 'Push Notifications',
|
defaultMessage: messages.push_notifications.defaultMessage,
|
||||||
i18nId: t('notification_settings.mobile'),
|
i18nId: messages.push_notifications.id,
|
||||||
icon: 'cellphone',
|
icon: 'cellphone',
|
||||||
testID: 'notification_settings.push_notification',
|
testID: messages.push_notifications.id,
|
||||||
},
|
},
|
||||||
call_notification: {
|
call_notification: {
|
||||||
defaultMessage: 'Call Notifications',
|
defaultMessage: messages.call_notifications.defaultMessage,
|
||||||
i18nId: t('notification_settings.calls'),
|
i18nId: messages.call_notifications.id,
|
||||||
icon: 'phone-in-talk',
|
icon: 'phone-in-talk',
|
||||||
testID: 'notification_settings.call_notification',
|
testID: messages.call_notifications.id,
|
||||||
},
|
},
|
||||||
email: {
|
email: {
|
||||||
defaultMessage: 'Email',
|
defaultMessage: messages.email.defaultMessage,
|
||||||
i18nId: t('notification_settings.email'),
|
i18nId: messages.email.id,
|
||||||
icon: 'email-outline',
|
icon: 'email-outline',
|
||||||
testID: 'notification_settings.email',
|
testID: messages.email.id,
|
||||||
},
|
},
|
||||||
automatic_dm_replies: {
|
automatic_dm_replies: {
|
||||||
defaultMessage: 'Automatic replies',
|
defaultMessage: messages.automatic_replies.defaultMessage,
|
||||||
i18nId: t('notification_settings.ooo_auto_responder'),
|
i18nId: messages.automatic_replies.id,
|
||||||
icon: 'reply-outline',
|
icon: 'reply-outline',
|
||||||
testID: 'notification_settings.automatic_dm_replies',
|
testID: messages.automatic_replies.id,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DisplayOptionConfig: Record<string, SettingConfigDetails> = {
|
export const DisplayOptionConfig: Record<string, SettingConfigDetails> = {
|
||||||
clock: {
|
clock: {
|
||||||
defaultMessage: 'Clock Display',
|
defaultMessage: messages.clock_display.defaultMessage,
|
||||||
i18nId: t('mobile.display_settings.clockDisplay'),
|
i18nId: messages.clock_display.id,
|
||||||
icon: 'clock-outline',
|
icon: 'clock-outline',
|
||||||
testID: 'display_settings.clock',
|
testID: messages.clock_display.id,
|
||||||
},
|
},
|
||||||
crt: {
|
crt: {
|
||||||
defaultMessage: 'Collapsed Reply Threads',
|
defaultMessage: messages.crt.defaultMessage,
|
||||||
i18nId: t('mobile.display_settings.crt'),
|
i18nId: messages.crt.id,
|
||||||
icon: 'message-text-outline',
|
icon: 'message-text-outline',
|
||||||
testID: 'display_settings.crt',
|
testID: messages.crt.id,
|
||||||
},
|
},
|
||||||
theme: {
|
theme: {
|
||||||
defaultMessage: 'Theme',
|
defaultMessage: messages.theme.defaultMessage,
|
||||||
i18nId: t('mobile.display_settings.theme'),
|
i18nId: messages.theme.id,
|
||||||
icon: 'palette-outline',
|
icon: 'palette-outline',
|
||||||
testID: 'display_settings.theme',
|
testID: messages.theme.id,
|
||||||
},
|
},
|
||||||
timezone: {
|
timezone: {
|
||||||
defaultMessage: 'Timezone',
|
defaultMessage: messages.timezone.defaultMessage,
|
||||||
i18nId: t('mobile.display_settings.timezone'),
|
i18nId: messages.timezone.id,
|
||||||
icon: 'globe',
|
icon: 'globe',
|
||||||
testID: 'display_settings.timezone',
|
testID: messages.timezone.id,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -165,46 +165,42 @@ exports[`SendTestNotificationNotice should match snapshot 1`] = `
|
||||||
"borderColor": "#2089dc",
|
"borderColor": "#2089dc",
|
||||||
"borderRadius": 4,
|
"borderRadius": 4,
|
||||||
"borderWidth": 0,
|
"borderWidth": 0,
|
||||||
"flex": 0,
|
|
||||||
"flexDirection": "row",
|
"flexDirection": "row",
|
||||||
"height": 40,
|
|
||||||
"justifyContent": "center",
|
"justifyContent": "center",
|
||||||
"padding": 8,
|
"padding": 8,
|
||||||
"paddingHorizontal": 20,
|
"paddingHorizontal": 20,
|
||||||
"paddingVertical": 12,
|
"paddingVertical": 10,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<View
|
<View
|
||||||
style={
|
style={
|
||||||
{
|
{
|
||||||
|
"alignItems": "center",
|
||||||
"flexDirection": "row",
|
"flexDirection": "row",
|
||||||
|
"gap": 7,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
testID="undefined-text-container"
|
||||||
>
|
>
|
||||||
<Text
|
<Text
|
||||||
numberOfLines={1}
|
numberOfLines={1}
|
||||||
style={
|
style={
|
||||||
[
|
[
|
||||||
[
|
[
|
||||||
{
|
[
|
||||||
"alignItems": "center",
|
{
|
||||||
"fontFamily": "OpenSans-SemiBold",
|
"fontFamily": "OpenSans-SemiBold",
|
||||||
"fontWeight": "600",
|
"fontSize": 14,
|
||||||
"justifyContent": "center",
|
"fontWeight": "600",
|
||||||
"padding": 1,
|
"lineHeight": 20,
|
||||||
"textAlignVertical": "center",
|
},
|
||||||
},
|
{
|
||||||
{
|
"color": "#ffffff",
|
||||||
"fontSize": 14,
|
},
|
||||||
"lineHeight": 14,
|
],
|
||||||
"marginTop": 3,
|
undefined,
|
||||||
},
|
|
||||||
{
|
|
||||||
"color": "#ffffff",
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
undefined,
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
|
@ -272,61 +268,50 @@ exports[`SendTestNotificationNotice should match snapshot 1`] = `
|
||||||
"borderColor": "#2089dc",
|
"borderColor": "#2089dc",
|
||||||
"borderRadius": 4,
|
"borderRadius": 4,
|
||||||
"borderWidth": 0,
|
"borderWidth": 0,
|
||||||
"flex": 0,
|
|
||||||
"flexDirection": "row",
|
"flexDirection": "row",
|
||||||
"height": 40,
|
|
||||||
"justifyContent": "center",
|
"justifyContent": "center",
|
||||||
"padding": 8,
|
"padding": 8,
|
||||||
"paddingHorizontal": 20,
|
"paddingHorizontal": 20,
|
||||||
"paddingVertical": 12,
|
"paddingVertical": 10,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<View
|
<View
|
||||||
style={
|
style={
|
||||||
[
|
{
|
||||||
{
|
"alignItems": "center",
|
||||||
"flexDirection": "row",
|
"flexDirection": "row",
|
||||||
},
|
"gap": 7,
|
||||||
{
|
|
||||||
"minHeight": 18,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Icon
|
|
||||||
color="#1c58d9"
|
|
||||||
name="open-in-new"
|
|
||||||
size={18}
|
|
||||||
style={
|
|
||||||
{
|
|
||||||
"marginRight": 7,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
/>
|
}
|
||||||
|
testID="undefined-text-container"
|
||||||
|
>
|
||||||
|
<View>
|
||||||
|
<Icon
|
||||||
|
color="#1c58d9"
|
||||||
|
name="open-in-new"
|
||||||
|
size={18}
|
||||||
|
testID="undefined-icon"
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
<Text
|
<Text
|
||||||
numberOfLines={1}
|
numberOfLines={1}
|
||||||
style={
|
style={
|
||||||
[
|
[
|
||||||
[
|
[
|
||||||
{
|
[
|
||||||
"alignItems": "center",
|
{
|
||||||
"fontFamily": "OpenSans-SemiBold",
|
"fontFamily": "OpenSans-SemiBold",
|
||||||
"fontWeight": "600",
|
"fontSize": 14,
|
||||||
"justifyContent": "center",
|
"fontWeight": "600",
|
||||||
"padding": 1,
|
"lineHeight": 20,
|
||||||
"textAlignVertical": "center",
|
},
|
||||||
},
|
{
|
||||||
{
|
"color": "#1c58d9",
|
||||||
"fontSize": 14,
|
},
|
||||||
"lineHeight": 14,
|
],
|
||||||
"marginTop": 3,
|
undefined,
|
||||||
},
|
|
||||||
{
|
|
||||||
"color": "#1c58d9",
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
undefined,
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
|
|
||||||
157
app/screens/settings/report_problem/index.test.tsx
Normal file
157
app/screens/settings/report_problem/index.test.tsx
Normal file
|
|
@ -0,0 +1,157 @@
|
||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import {Database} from '@nozbe/watermelondb';
|
||||||
|
import React, {type ComponentProps} from 'react';
|
||||||
|
import {View, Text} from 'react-native';
|
||||||
|
|
||||||
|
import {SYSTEM_IDENTIFIERS} from '@constants/database';
|
||||||
|
import DatabaseManager from '@database/manager';
|
||||||
|
import {renderWithEverything} from '@test/intl-test-helper';
|
||||||
|
|
||||||
|
import ReportProblem from './report_problem';
|
||||||
|
|
||||||
|
import enhanced from './index';
|
||||||
|
|
||||||
|
import type {ReportAProblemMetadata} from '@typings/screens/report_a_problem';
|
||||||
|
|
||||||
|
jest.mock('./report_problem', () => ({
|
||||||
|
__esModule: true,
|
||||||
|
default: jest.fn(),
|
||||||
|
}));
|
||||||
|
jest.mocked(ReportProblem).mockImplementation((props) => {
|
||||||
|
return (
|
||||||
|
<View>
|
||||||
|
{Object.keys(props).map((key) => {
|
||||||
|
if (key === 'metadata') {
|
||||||
|
return Object.keys(props[key]).map((metadataKey) => (
|
||||||
|
<Text
|
||||||
|
key={metadataKey}
|
||||||
|
testID={metadataKey}
|
||||||
|
>{`${props.metadata[metadataKey as keyof ReportAProblemMetadata]}`}</Text>
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Text
|
||||||
|
key={key}
|
||||||
|
testID={key}
|
||||||
|
>{`${props[key as keyof ComponentProps<typeof ReportProblem>]}`}</Text>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('screens/settings/report_problem/index', () => {
|
||||||
|
const serverUrl = 'baseHandler.test.com';
|
||||||
|
let database: Database;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await DatabaseManager.init([serverUrl]);
|
||||||
|
const serverDatabaseAndOperator = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||||
|
database = serverDatabaseAndOperator.database;
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await DatabaseManager.destroyServerDatabase(serverUrl);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle default values', async () => {
|
||||||
|
const Component = enhanced;
|
||||||
|
const {getByTestId} = renderWithEverything(
|
||||||
|
<Component/>,
|
||||||
|
{database},
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(getByTestId('currentUserId')).toHaveTextContent('');
|
||||||
|
expect(getByTestId('currentTeamId')).toHaveTextContent('');
|
||||||
|
expect(getByTestId('serverVersion')).toHaveTextContent('Unknown (Build Unknown)');
|
||||||
|
expect(getByTestId('appVersion')).toHaveTextContent('0.0.0 (Build 0)');
|
||||||
|
expect(getByTestId('appPlatform')).toHaveTextContent('ios');
|
||||||
|
expect(getByTestId('reportAProblemType')).toHaveTextContent('undefined');
|
||||||
|
expect(getByTestId('reportAProblemMail')).toHaveTextContent('undefined');
|
||||||
|
expect(getByTestId('siteName')).toHaveTextContent('undefined');
|
||||||
|
expect(getByTestId('allowDownloadLogs')).toHaveTextContent('true');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should enhance ReportProblem with correct observables', async () => {
|
||||||
|
const {operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||||
|
await operator.handleConfigs({
|
||||||
|
configs: [
|
||||||
|
{id: 'ReportAProblemType', value: 'email'},
|
||||||
|
{id: 'ReportAProblemMail', value: 'test@example.com'},
|
||||||
|
{id: 'SiteName', value: 'Test Site'},
|
||||||
|
{id: 'AllowDownloadLogs', value: 'true'},
|
||||||
|
{id: 'Version', value: '7.8.0'},
|
||||||
|
{id: 'BuildNumber', value: '123'},
|
||||||
|
],
|
||||||
|
prepareRecordsOnly: false,
|
||||||
|
configsToDelete: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
await operator.handleSystem({
|
||||||
|
systems: [
|
||||||
|
{id: SYSTEM_IDENTIFIERS.CURRENT_USER_ID, value: 'user1'},
|
||||||
|
{id: SYSTEM_IDENTIFIERS.CURRENT_TEAM_ID, value: 'team1'},
|
||||||
|
],
|
||||||
|
prepareRecordsOnly: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const Component = enhanced;
|
||||||
|
const {getByTestId} = renderWithEverything(
|
||||||
|
<Component/>,
|
||||||
|
{database},
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(getByTestId('currentUserId')).toHaveTextContent('user1');
|
||||||
|
expect(getByTestId('currentTeamId')).toHaveTextContent('team1');
|
||||||
|
expect(getByTestId('serverVersion')).toHaveTextContent('7.8.0 (Build 123)');
|
||||||
|
expect(getByTestId('appVersion')).toHaveTextContent('0.0.0 (Build 0)');
|
||||||
|
expect(getByTestId('appPlatform')).toHaveTextContent('ios');
|
||||||
|
expect(getByTestId('reportAProblemType')).toHaveTextContent('email');
|
||||||
|
expect(getByTestId('reportAProblemMail')).toHaveTextContent('test@example.com');
|
||||||
|
expect(getByTestId('siteName')).toHaveTextContent('Test Site');
|
||||||
|
expect(getByTestId('allowDownloadLogs')).toHaveTextContent('true');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('different data should show different values', async () => {
|
||||||
|
const {operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||||
|
await operator.handleConfigs({
|
||||||
|
configs: [
|
||||||
|
{id: 'ReportAProblemType', value: 'link'},
|
||||||
|
{id: 'ReportAProblemMail', value: 'test2@example.com'},
|
||||||
|
{id: 'SiteName', value: 'Test Site2'},
|
||||||
|
{id: 'AllowDownloadLogs', value: 'false'},
|
||||||
|
{id: 'Version', value: '7.8.1'},
|
||||||
|
{id: 'BuildNumber', value: '124'},
|
||||||
|
],
|
||||||
|
prepareRecordsOnly: false,
|
||||||
|
configsToDelete: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
await operator.handleSystem({
|
||||||
|
systems: [
|
||||||
|
{id: SYSTEM_IDENTIFIERS.CURRENT_USER_ID, value: 'user2'},
|
||||||
|
{id: SYSTEM_IDENTIFIERS.CURRENT_TEAM_ID, value: 'team2'},
|
||||||
|
],
|
||||||
|
prepareRecordsOnly: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const Component = enhanced;
|
||||||
|
const {getByTestId} = renderWithEverything(
|
||||||
|
<Component/>,
|
||||||
|
{database},
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(getByTestId('currentUserId')).toHaveTextContent('user2');
|
||||||
|
expect(getByTestId('currentTeamId')).toHaveTextContent('team2');
|
||||||
|
expect(getByTestId('serverVersion')).toHaveTextContent('7.8.1 (Build 124)');
|
||||||
|
expect(getByTestId('appVersion')).toHaveTextContent('0.0.0 (Build 0)');
|
||||||
|
expect(getByTestId('appPlatform')).toHaveTextContent('ios');
|
||||||
|
expect(getByTestId('reportAProblemType')).toHaveTextContent('link');
|
||||||
|
expect(getByTestId('reportAProblemMail')).toHaveTextContent('test2@example.com');
|
||||||
|
expect(getByTestId('siteName')).toHaveTextContent('Test Site2');
|
||||||
|
expect(getByTestId('allowDownloadLogs')).toHaveTextContent('false');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -3,26 +3,16 @@
|
||||||
|
|
||||||
import {withDatabase, withObservables} from '@nozbe/watermelondb/react';
|
import {withDatabase, withObservables} from '@nozbe/watermelondb/react';
|
||||||
|
|
||||||
import {observeConfigValue, observeCurrentUserId, observeCurrentTeamId} from '@queries/servers/system';
|
import {observeConfigBooleanValue, observeConfigValue, observeReportAProblemMetadata} from '@queries/servers/system';
|
||||||
|
|
||||||
import ReportProblem from './report_problem';
|
import ReportProblem from './report_problem';
|
||||||
|
|
||||||
import type {WithDatabaseArgs} from '@typings/database/database';
|
const enhanced = withObservables([], ({database}) => ({
|
||||||
|
allowDownloadLogs: observeConfigBooleanValue(database, 'AllowDownloadLogs', true),
|
||||||
const enhanced = withObservables([], ({database}: WithDatabaseArgs) => {
|
reportAProblemMail: observeConfigValue(database, 'ReportAProblemMail'),
|
||||||
const buildNumber = observeConfigValue(database, 'BuildNumber');
|
reportAProblemType: observeConfigValue(database, 'ReportAProblemType'),
|
||||||
const currentTeamId = observeCurrentTeamId(database);
|
siteName: observeConfigValue(database, 'SiteName'),
|
||||||
const currentUserId = observeCurrentUserId(database);
|
metadata: observeReportAProblemMetadata(database),
|
||||||
const supportEmail = observeConfigValue(database, 'SupportEmail');
|
}));
|
||||||
const version = observeConfigValue(database, 'Version');
|
|
||||||
|
|
||||||
return {
|
|
||||||
buildNumber,
|
|
||||||
currentTeamId,
|
|
||||||
currentUserId,
|
|
||||||
supportEmail,
|
|
||||||
version,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
export default withDatabase(enhanced(ReportProblem));
|
export default withDatabase(enhanced(ReportProblem));
|
||||||
|
|
|
||||||
123
app/screens/settings/report_problem/report_problem.test.tsx
Normal file
123
app/screens/settings/report_problem/report_problem.test.tsx
Normal file
|
|
@ -0,0 +1,123 @@
|
||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import {act, fireEvent} from '@testing-library/react-native';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import {Screens} from '@constants';
|
||||||
|
import {goToScreen} from '@screens/navigation';
|
||||||
|
import {renderWithIntl} from '@test/intl-test-helper';
|
||||||
|
import {emailLogs} from '@utils/share_logs';
|
||||||
|
|
||||||
|
import ReportProblem from './report_problem';
|
||||||
|
|
||||||
|
jest.mock('@screens/navigation', () => ({
|
||||||
|
goToScreen: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('@utils/share_logs', () => ({
|
||||||
|
emailLogs: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('screens/settings/report_problem/report_problem', () => {
|
||||||
|
const baseProps = {
|
||||||
|
allowDownloadLogs: true,
|
||||||
|
metadata: {
|
||||||
|
currentUserId: 'user1',
|
||||||
|
currentTeamId: 'team1',
|
||||||
|
serverVersion: '7.8.0',
|
||||||
|
appVersion: '2.0.0',
|
||||||
|
appPlatform: 'ios',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render report problem option', () => {
|
||||||
|
const {getByTestId} = renderWithIntl(
|
||||||
|
<ReportProblem {...baseProps}/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(getByTestId('settings.report_problem.option')).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not render when type is hidden', () => {
|
||||||
|
const props = {
|
||||||
|
...baseProps,
|
||||||
|
reportAProblemType: 'hidden',
|
||||||
|
};
|
||||||
|
|
||||||
|
const {queryByTestId} = renderWithIntl(
|
||||||
|
<ReportProblem {...props}/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(queryByTestId('settings.report_problem.option')).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should navigate to report problem screen when logs are allowed', async () => {
|
||||||
|
const props = {
|
||||||
|
...baseProps,
|
||||||
|
allowDownloadLogs: true,
|
||||||
|
reportAProblemType: 'email',
|
||||||
|
};
|
||||||
|
|
||||||
|
const {getByTestId} = renderWithIntl(
|
||||||
|
<ReportProblem {...props}/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
fireEvent.press(getByTestId('settings.report_problem.option'));
|
||||||
|
expect(goToScreen).toHaveBeenCalledWith(
|
||||||
|
Screens.REPORT_PROBLEM,
|
||||||
|
'Report a problem',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should navigate to report problem screen when type is not email', async () => {
|
||||||
|
const props = {
|
||||||
|
...baseProps,
|
||||||
|
allowDownloadLogs: false,
|
||||||
|
reportAProblemType: 'link',
|
||||||
|
};
|
||||||
|
|
||||||
|
const {getByTestId} = renderWithIntl(
|
||||||
|
<ReportProblem {...props}/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
fireEvent.press(getByTestId('settings.report_problem.option'));
|
||||||
|
expect(goToScreen).toHaveBeenCalledWith(
|
||||||
|
Screens.REPORT_PROBLEM,
|
||||||
|
'Report a problem',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should share logs directly when logs are not allowed and type is email', async () => {
|
||||||
|
const props = {
|
||||||
|
...baseProps,
|
||||||
|
allowDownloadLogs: false,
|
||||||
|
reportAProblemType: 'email',
|
||||||
|
reportAProblemMail: 'test@example.com',
|
||||||
|
siteName: 'Test Site',
|
||||||
|
};
|
||||||
|
|
||||||
|
const {getByTestId} = renderWithIntl(
|
||||||
|
<ReportProblem {...props}/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
fireEvent.press(getByTestId('settings.report_problem.option'));
|
||||||
|
expect(emailLogs).toHaveBeenCalledWith(
|
||||||
|
props.metadata,
|
||||||
|
props.siteName,
|
||||||
|
props.reportAProblemMail,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
expect(goToScreen).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -1,58 +1,73 @@
|
||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
// See LICENSE.txt for license information.
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
import TurboLogger from '@mattermost/react-native-turbo-log';
|
import React, {useCallback} from 'react';
|
||||||
import {nativeApplicationVersion, nativeBuildVersion} from 'expo-application';
|
import {defineMessages, useIntl} from 'react-intl';
|
||||||
import {deviceName} from 'expo-device';
|
|
||||||
import React from 'react';
|
|
||||||
import {Alert, Platform} from 'react-native';
|
|
||||||
import Share from 'react-native-share';
|
|
||||||
|
|
||||||
import SettingItem from '@components/settings/item';
|
import SettingItem from '@components/settings/item';
|
||||||
|
import {Screens} from '@constants';
|
||||||
import {useTheme} from '@context/theme';
|
import {useTheme} from '@context/theme';
|
||||||
import {pathWithPrefix} from '@utils/files';
|
import {goToScreen} from '@screens/navigation';
|
||||||
import {preventDoubleTap} from '@utils/tap';
|
import {emailLogs} from '@utils/share_logs';
|
||||||
|
|
||||||
|
import type {ReportAProblemMetadata} from '@typings/screens/report_a_problem';
|
||||||
|
|
||||||
type ReportProblemProps = {
|
type ReportProblemProps = {
|
||||||
buildNumber: string;
|
allowDownloadLogs?: boolean;
|
||||||
currentTeamId: string;
|
reportAProblemMail?: string;
|
||||||
currentUserId: string;
|
reportAProblemType?: string;
|
||||||
supportEmail: string;
|
siteName?: string;
|
||||||
version: string;
|
metadata: ReportAProblemMetadata;
|
||||||
siteName: string;
|
}
|
||||||
};
|
|
||||||
|
|
||||||
const ReportProblem = ({buildNumber, currentTeamId, currentUserId, siteName, supportEmail, version}: ReportProblemProps) => {
|
const messages = defineMessages({
|
||||||
|
downloadLogs: {id: 'report_problem.download_logs.title', defaultMessage: 'Download app logs'},
|
||||||
|
reportProblem: {id: 'report_problem.title', defaultMessage: 'Report a problem'},
|
||||||
|
});
|
||||||
|
|
||||||
|
const ReportProblem = ({
|
||||||
|
allowDownloadLogs,
|
||||||
|
reportAProblemMail,
|
||||||
|
reportAProblemType,
|
||||||
|
siteName,
|
||||||
|
metadata,
|
||||||
|
}: ReportProblemProps) => {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
const intl = useIntl();
|
||||||
|
const onlyAllowLogs = allowDownloadLogs && reportAProblemType === 'hidden';
|
||||||
|
const skipReportAProblemScreen = reportAProblemType === 'email' && !allowDownloadLogs;
|
||||||
|
|
||||||
const openEmailClient = preventDoubleTap(async () => {
|
const onPress = useCallback(() => {
|
||||||
try {
|
if (skipReportAProblemScreen) {
|
||||||
const logPaths = await TurboLogger.getLogPaths();
|
emailLogs(metadata, siteName, reportAProblemMail, true);
|
||||||
const attachments = logPaths.map((path) => pathWithPrefix('file://', path));
|
} else {
|
||||||
await Share.open({
|
const message = onlyAllowLogs ? messages.downloadLogs : messages.reportProblem;
|
||||||
subject: `Problem with ${siteName} React Native app`,
|
const title = intl.formatMessage(message);
|
||||||
email: supportEmail,
|
goToScreen(Screens.REPORT_PROBLEM, title);
|
||||||
failOnCancel: false,
|
|
||||||
urls: attachments,
|
|
||||||
message: [
|
|
||||||
'Please share a description of the problem:\n\n',
|
|
||||||
`Current User Id: ${currentUserId}`,
|
|
||||||
`Current Team Id: ${currentTeamId}`,
|
|
||||||
`Server Version: ${version} (Build ${buildNumber})`,
|
|
||||||
`App Version: ${nativeApplicationVersion} (Build ${nativeBuildVersion})`,
|
|
||||||
`App Platform: ${Platform.OS}`,
|
|
||||||
`Device Model: ${deviceName}`,
|
|
||||||
].join('\n'),
|
|
||||||
});
|
|
||||||
} catch (e: any) {
|
|
||||||
Alert.alert('Error', e.message);
|
|
||||||
}
|
}
|
||||||
});
|
}, [intl, metadata, onlyAllowLogs, reportAProblemMail, siteName, skipReportAProblemScreen]);
|
||||||
|
|
||||||
|
if (onlyAllowLogs) {
|
||||||
|
return (
|
||||||
|
<SettingItem
|
||||||
|
optionLabelTextStyle={{color: theme.linkColor}}
|
||||||
|
onPress={onPress}
|
||||||
|
optionName='download_logs'
|
||||||
|
separator={false}
|
||||||
|
testID='settings.download_logs.option'
|
||||||
|
type='default'
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (reportAProblemType === 'hidden') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SettingItem
|
<SettingItem
|
||||||
optionLabelTextStyle={{color: theme.linkColor}}
|
optionLabelTextStyle={{color: theme.linkColor}}
|
||||||
onPress={openEmailClient}
|
onPress={onPress}
|
||||||
optionName='report_problem'
|
optionName='report_problem'
|
||||||
separator={false}
|
separator={false}
|
||||||
testID='settings.report_problem.option'
|
testID='settings.report_problem.option'
|
||||||
|
|
|
||||||
|
|
@ -149,7 +149,7 @@ const Settings = ({componentId, helpLink, showHelp, siteName}: SettingsProps) =>
|
||||||
type='default'
|
type='default'
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
<ReportProblem siteName={siteName}/>
|
<ReportProblem/>
|
||||||
</SettingContainer>
|
</SettingContainer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -84,12 +84,13 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||||
color: theme.centerChannelColor,
|
color: theme.centerChannelColor,
|
||||||
...typography('Body', 200, 'Regular'),
|
...typography('Body', 200, 'Regular'),
|
||||||
},
|
},
|
||||||
firstButton: {
|
|
||||||
marginBottom: 10,
|
|
||||||
},
|
|
||||||
loadingContainer: {
|
loadingContainer: {
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
},
|
},
|
||||||
|
buttonContainer: {
|
||||||
|
flexDirection: 'column',
|
||||||
|
gap: 10,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -145,7 +146,7 @@ const TermsOfService = ({
|
||||||
onPress: retry,
|
onPress: retry,
|
||||||
}],
|
}],
|
||||||
);
|
);
|
||||||
}, [intl, closeTermsAndLogout]);
|
}, [siteName, intl, closeTermsAndLogout]);
|
||||||
|
|
||||||
const alertDecline = useCallback(() => {
|
const alertDecline = useCallback(() => {
|
||||||
Alert.alert(
|
Alert.alert(
|
||||||
|
|
@ -162,7 +163,7 @@ const TermsOfService = ({
|
||||||
onPress: closeTermsAndLogout,
|
onPress: closeTermsAndLogout,
|
||||||
}],
|
}],
|
||||||
);
|
);
|
||||||
}, [intl, siteName, closeTermsAndLogout]);
|
}, [intl, closeTermsAndLogout]);
|
||||||
|
|
||||||
const acceptTerms = useCallback(async () => {
|
const acceptTerms = useCallback(async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
@ -170,7 +171,7 @@ const TermsOfService = ({
|
||||||
if (error) {
|
if (error) {
|
||||||
alertError(acceptTerms);
|
alertError(acceptTerms);
|
||||||
}
|
}
|
||||||
}, [alertError, alertDecline, termsId, serverUrl, componentId]);
|
}, [alertError, termsId, serverUrl]);
|
||||||
|
|
||||||
const declineTerms = useCallback(async () => {
|
const declineTerms = useCallback(async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
@ -180,7 +181,7 @@ const TermsOfService = ({
|
||||||
} else {
|
} else {
|
||||||
alertDecline();
|
alertDecline();
|
||||||
}
|
}
|
||||||
}, [serverUrl, termsId, closeTermsAndLogout]);
|
}, [serverUrl, termsId, alertError, alertDecline]);
|
||||||
|
|
||||||
const onPressClose = useCallback(async () => {
|
const onPressClose = useCallback(async () => {
|
||||||
if (getTermsError) {
|
if (getTermsError) {
|
||||||
|
|
@ -227,20 +228,21 @@ const TermsOfService = ({
|
||||||
<Text style={styles.errorDescription}>
|
<Text style={styles.errorDescription}>
|
||||||
{intl.formatMessage({id: 'terms_of_service.error.description', defaultMessage: 'It was not possible to get the Terms of Service from the Server.'})}
|
{intl.formatMessage({id: 'terms_of_service.error.description', defaultMessage: 'It was not possible to get the Terms of Service from the Server.'})}
|
||||||
</Text>
|
</Text>
|
||||||
<Button
|
<View style={styles.buttonContainer}>
|
||||||
onPress={getTerms}
|
<Button
|
||||||
theme={theme}
|
onPress={getTerms}
|
||||||
text={intl.formatMessage({id: 'terms_of_service.error.retry', defaultMessage: 'Retry'})}
|
theme={theme}
|
||||||
size={'lg'}
|
text={intl.formatMessage({id: 'terms_of_service.error.retry', defaultMessage: 'Retry'})}
|
||||||
backgroundStyle={styles.firstButton}
|
size={'lg'}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
onPress={onPressClose}
|
onPress={onPressClose}
|
||||||
theme={theme}
|
theme={theme}
|
||||||
text={intl.formatMessage({id: 'terms_of_service.error.logout', defaultMessage: 'Logout'})}
|
text={intl.formatMessage({id: 'terms_of_service.error.logout', defaultMessage: 'Logout'})}
|
||||||
size={'lg'}
|
size={'lg'}
|
||||||
emphasis={'link'}
|
emphasis={'link'}
|
||||||
/>
|
/>
|
||||||
|
</View>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -267,7 +269,6 @@ const TermsOfService = ({
|
||||||
theme={theme}
|
theme={theme}
|
||||||
text={intl.formatMessage({id: 'terms_of_service.acceptButton', defaultMessage: 'Accept'})}
|
text={intl.formatMessage({id: 'terms_of_service.acceptButton', defaultMessage: 'Accept'})}
|
||||||
size={'lg'}
|
size={'lg'}
|
||||||
backgroundStyle={styles.firstButton}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
|
|
||||||
import Preferences from '@constants/preferences';
|
import Preferences from '@constants/preferences';
|
||||||
|
|
||||||
import {buttonBackgroundStyle, buttonStyles, getBackgroundStyles, buttonSizeStyles, buttonTextStyle, buttonTextStyles, buttonTextSizeStyles} from './buttonStyles';
|
import {buttonBackgroundStyle, buttonStyles, getBackgroundStyles, buttonSizeStyles, buttonTextStyle, buttonTextSizeStyles} from './buttonStyles';
|
||||||
import {changeOpacity} from './theme';
|
import {changeOpacity} from './theme';
|
||||||
|
|
||||||
describe('get the style of a button', () => {
|
describe('get the style of a button', () => {
|
||||||
|
|
@ -65,28 +65,24 @@ describe('get the style of a button', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('get the text style of a button', () => {
|
describe('get the text style of a button', () => {
|
||||||
const defaultTextStyle = buttonTextStyles.main;
|
|
||||||
const theme = Preferences.THEMES.denim;
|
const theme = Preferences.THEMES.denim;
|
||||||
|
|
||||||
test('button text default values', () => {
|
test('button text default values', () => {
|
||||||
const tests = [{
|
const tests = [{
|
||||||
getStyle: () => buttonTextStyle(theme),
|
getStyle: () => buttonTextStyle(theme),
|
||||||
expected: [
|
expected: [
|
||||||
defaultTextStyle,
|
|
||||||
buttonTextSizeStyles.m,
|
buttonTextSizeStyles.m,
|
||||||
{color: theme.buttonColor},
|
{color: theme.buttonColor},
|
||||||
],
|
],
|
||||||
}, {
|
}, {
|
||||||
getStyle: () => buttonTextStyle(theme, 'xs'),
|
getStyle: () => buttonTextStyle(theme, 'xs'),
|
||||||
expected: [
|
expected: [
|
||||||
defaultTextStyle,
|
|
||||||
buttonTextSizeStyles.xs,
|
buttonTextSizeStyles.xs,
|
||||||
{color: theme.buttonColor},
|
{color: theme.buttonColor},
|
||||||
],
|
],
|
||||||
}, {
|
}, {
|
||||||
getStyle: () => buttonTextStyle(theme, 'lg', 'secondary'),
|
getStyle: () => buttonTextStyle(theme, 'lg', 'secondary'),
|
||||||
expected: [
|
expected: [
|
||||||
defaultTextStyle,
|
|
||||||
buttonTextSizeStyles.lg,
|
buttonTextSizeStyles.lg,
|
||||||
{color: theme.buttonBg},
|
{color: theme.buttonBg},
|
||||||
],
|
],
|
||||||
|
|
@ -104,26 +100,29 @@ describe('get the text style of a button', () => {
|
||||||
const btnTypes: ButtonType[] = ['default', 'destructive', 'disabled', 'inverted'];
|
const btnTypes: ButtonType[] = ['default', 'destructive', 'disabled', 'inverted'];
|
||||||
|
|
||||||
const getColor = (type: ButtonType, emphasis: ButtonEmphasis) => {
|
const getColor = (type: ButtonType, emphasis: ButtonEmphasis) => {
|
||||||
let color: string = theme.buttonColor;
|
|
||||||
|
|
||||||
if (type === 'disabled') {
|
if (type === 'disabled') {
|
||||||
color = changeOpacity(theme.centerChannelColor, 0.32);
|
return changeOpacity(theme.centerChannelColor, 0.32);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((type === 'destructive' && emphasis !== 'primary')) {
|
if (type === 'destructive') {
|
||||||
color = theme.errorTextColor;
|
if (emphasis === 'primary') {
|
||||||
|
return theme.buttonColor;
|
||||||
|
}
|
||||||
|
return theme.errorTextColor;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((type === 'inverted' && emphasis === 'primary') ||
|
if (type === 'inverted') {
|
||||||
(type !== 'inverted' && emphasis !== 'primary')) {
|
if (emphasis === 'primary') {
|
||||||
color = theme.buttonBg;
|
return theme.buttonBg;
|
||||||
|
}
|
||||||
|
return theme.buttonColor;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (type === 'inverted' && emphasis === 'tertiary') {
|
if (emphasis === 'primary') {
|
||||||
color = theme.sidebarText;
|
return theme.buttonColor;
|
||||||
}
|
}
|
||||||
|
|
||||||
return color;
|
return theme.buttonBg;
|
||||||
};
|
};
|
||||||
|
|
||||||
btnEnphasis.forEach((emphasis) => {
|
btnEnphasis.forEach((emphasis) => {
|
||||||
|
|
@ -131,7 +130,6 @@ describe('get the text style of a button', () => {
|
||||||
btnTypes.forEach((type) => {
|
btnTypes.forEach((type) => {
|
||||||
const style = buttonTextStyle(theme, size, emphasis, type);
|
const style = buttonTextStyle(theme, size, emphasis, type);
|
||||||
expect(style).toEqual([
|
expect(style).toEqual([
|
||||||
defaultTextStyle,
|
|
||||||
buttonTextSizeStyles[size],
|
buttonTextSizeStyles[size],
|
||||||
{color: getColor(type, emphasis)},
|
{color: getColor(type, emphasis)},
|
||||||
]);
|
]);
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,8 @@ import {type StyleProp, StyleSheet, type TextStyle, type ViewStyle} from 'react-
|
||||||
|
|
||||||
import {blendColors, changeOpacity} from '@utils/theme';
|
import {blendColors, changeOpacity} from '@utils/theme';
|
||||||
|
|
||||||
|
import {typography} from './typography';
|
||||||
|
|
||||||
export const getBackgroundStyles = (theme: Theme): BackgroundStyles => {
|
export const getBackgroundStyles = (theme: Theme): BackgroundStyles => {
|
||||||
return {
|
return {
|
||||||
primary: {
|
primary: {
|
||||||
|
|
@ -288,32 +290,25 @@ export const getBackgroundStyles = (theme: Theme): BackgroundStyles => {
|
||||||
|
|
||||||
export const buttonSizeStyles: ButtonSizes = StyleSheet.create({
|
export const buttonSizeStyles: ButtonSizes = StyleSheet.create({
|
||||||
xs: {
|
xs: {
|
||||||
height: 24,
|
paddingVertical: 4,
|
||||||
paddingVertical: 6,
|
|
||||||
paddingHorizontal: 10,
|
paddingHorizontal: 10,
|
||||||
},
|
},
|
||||||
s: {
|
s: {
|
||||||
height: 32,
|
paddingVertical: 8,
|
||||||
paddingVertical: 10,
|
|
||||||
paddingHorizontal: 16,
|
paddingHorizontal: 16,
|
||||||
},
|
},
|
||||||
m: {
|
m: {
|
||||||
height: 40,
|
paddingVertical: 10,
|
||||||
paddingVertical: 12,
|
|
||||||
paddingHorizontal: 20,
|
paddingHorizontal: 20,
|
||||||
},
|
},
|
||||||
lg: {
|
lg: {
|
||||||
height: 48,
|
paddingVertical: 12,
|
||||||
paddingVertical: 14,
|
|
||||||
paddingHorizontal: 24,
|
paddingHorizontal: 24,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const buttonStyles = StyleSheet.create({
|
export const buttonStyles = StyleSheet.create({
|
||||||
main: {
|
main: {
|
||||||
flex: 0,
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
borderRadius: 4,
|
borderRadius: 4,
|
||||||
},
|
},
|
||||||
fullWidth: {
|
fullWidth: {
|
||||||
|
|
@ -321,41 +316,18 @@ export const buttonStyles = StyleSheet.create({
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const buttonTextStyles = StyleSheet.create({
|
|
||||||
main: {
|
|
||||||
fontFamily: 'OpenSans-SemiBold',
|
|
||||||
fontWeight: '600',
|
|
||||||
textAlignVertical: 'center',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
padding: 1,
|
|
||||||
},
|
|
||||||
underline: {
|
|
||||||
textDecorationLine: 'underline',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export const buttonTextSizeStyles = StyleSheet.create({
|
export const buttonTextSizeStyles = StyleSheet.create({
|
||||||
xs: {
|
xs: {
|
||||||
fontSize: 11,
|
...typography('Body', 50, 'SemiBold'),
|
||||||
lineHeight: 10,
|
|
||||||
letterSpacing: 0.02,
|
|
||||||
marginTop: 2,
|
|
||||||
},
|
},
|
||||||
s: {
|
s: {
|
||||||
fontSize: 12,
|
...typography('Body', 75, 'SemiBold'),
|
||||||
lineHeight: 12,
|
|
||||||
marginTop: 1,
|
|
||||||
},
|
},
|
||||||
m: {
|
m: {
|
||||||
fontSize: 14,
|
...typography('Body', 100, 'SemiBold'),
|
||||||
lineHeight: 14,
|
|
||||||
marginTop: 3,
|
|
||||||
},
|
},
|
||||||
lg: {
|
lg: {
|
||||||
fontSize: 16,
|
...typography('Body', 200, 'SemiBold'),
|
||||||
lineHeight: 18,
|
|
||||||
marginTop: 1,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -398,25 +370,31 @@ export const buttonTextStyle = (
|
||||||
emphasis: ButtonEmphasis = 'primary',
|
emphasis: ButtonEmphasis = 'primary',
|
||||||
type: ButtonType = 'default',
|
type: ButtonType = 'default',
|
||||||
): StyleProp<TextStyle> => {
|
): StyleProp<TextStyle> => {
|
||||||
// Color
|
return [buttonTextSizeStyles[size], {color: getColorByType(theme, type, emphasis)}];
|
||||||
let color: string = theme.buttonColor;
|
};
|
||||||
|
|
||||||
if (type === 'disabled') {
|
const getColorByType = (theme: Theme, type: ButtonType, emphasis: ButtonEmphasis) => {
|
||||||
color = changeOpacity(theme.centerChannelColor, 0.32);
|
if (type === 'disabled') {
|
||||||
}
|
return changeOpacity(theme.centerChannelColor, 0.32);
|
||||||
|
}
|
||||||
if ((type === 'destructive' && emphasis !== 'primary')) {
|
|
||||||
color = theme.errorTextColor;
|
if (type === 'destructive') {
|
||||||
}
|
if (emphasis === 'primary') {
|
||||||
|
return theme.buttonColor;
|
||||||
if ((type === 'inverted' && emphasis === 'primary') ||
|
}
|
||||||
(type !== 'inverted' && emphasis !== 'primary')) {
|
return theme.errorTextColor;
|
||||||
color = theme.buttonBg;
|
}
|
||||||
}
|
|
||||||
|
if (type === 'inverted') {
|
||||||
if (type === 'inverted' && emphasis === 'tertiary') {
|
if (emphasis === 'primary') {
|
||||||
color = theme.sidebarText;
|
return theme.buttonBg;
|
||||||
}
|
}
|
||||||
|
return theme.buttonColor;
|
||||||
return [buttonTextStyles.main, buttonTextSizeStyles[size], {color}];
|
}
|
||||||
|
|
||||||
|
if (emphasis === 'primary') {
|
||||||
|
return theme.buttonColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
return theme.buttonBg;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,31 @@ import {getIntlShape} from '@utils/general';
|
||||||
import {logError} from '@utils/log';
|
import {logError} from '@utils/log';
|
||||||
import {urlSafeBase64Encode} from '@utils/security';
|
import {urlSafeBase64Encode} from '@utils/security';
|
||||||
|
|
||||||
import {deleteFileCache, deleteFileCacheByDir, deleteV1Data, extractFileInfo, fileExists, fileMaxWarning, fileSizeWarning, filterFileExtensions, getAllFilesInCachesDirectory, getAllowedServerMaxFileSize, getExtensionFromContentDisposition, getExtensionFromMime, getFileType, getFormattedFileSize, getLocalFilePathFromFile, hasWriteStoragePermission, isDocument, isGif, isImage, isVideo, lookupMimeType, uploadDisabledWarning} from '.';
|
import {
|
||||||
|
deleteFileCache,
|
||||||
|
deleteFileCacheByDir,
|
||||||
|
deleteV1Data,
|
||||||
|
extractFileInfo,
|
||||||
|
fileExists,
|
||||||
|
fileMaxWarning,
|
||||||
|
fileSizeWarning,
|
||||||
|
filterFileExtensions,
|
||||||
|
getAllFilesInCachesDirectory,
|
||||||
|
getAllowedServerMaxFileSize,
|
||||||
|
getExtensionFromContentDisposition,
|
||||||
|
getExtensionFromMime,
|
||||||
|
getFileType,
|
||||||
|
getFormattedFileSize,
|
||||||
|
getLocalFilePathFromFile,
|
||||||
|
hasWriteStoragePermission,
|
||||||
|
isDocument,
|
||||||
|
isGif,
|
||||||
|
isImage,
|
||||||
|
isVideo,
|
||||||
|
lookupMimeType,
|
||||||
|
pathWithPrefix,
|
||||||
|
uploadDisabledWarning,
|
||||||
|
} from '.';
|
||||||
|
|
||||||
jest.mock('expo-file-system');
|
jest.mock('expo-file-system');
|
||||||
jest.mock('react-native', () => {
|
jest.mock('react-native', () => {
|
||||||
|
|
@ -272,5 +296,12 @@ describe('Image utils', () => {
|
||||||
expect(result.files).toEqual(expect.any(Array));
|
expect(result.files).toEqual(expect.any(Array));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('pathWithPrefix', () => {
|
||||||
|
it('should return correct path with prefix', () => {
|
||||||
|
expect(pathWithPrefix('file://', 'file://something')).toEqual('file://something');
|
||||||
|
expect(pathWithPrefix('file://', 'something')).toEqual('file://something');
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -573,3 +573,12 @@ export const getAllFilesInCachesDirectory = async (serverUrl: string) => {
|
||||||
return {error};
|
return {error};
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const pathWithPrefix = (prefix: string, path: string) => {
|
||||||
|
const p = path.startsWith(prefix) ? '' : prefix;
|
||||||
|
return `${p}${path}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const deleteFile = async (path: string) => {
|
||||||
|
await deleteAsync(pathWithPrefix('file://', path));
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
import testHelper from '@test/test_helper';
|
import testHelper from '@test/test_helper';
|
||||||
|
|
||||||
import {toMilliseconds} from './datetime';
|
import {toMilliseconds} from './datetime';
|
||||||
import {getNumberFileMenuOptions, getChannelNamesWithID, getOrderedFileInfos, getFileInfosIndexes, getOrderedGalleryItems, pathWithPrefix} from './files';
|
import {getNumberFileMenuOptions, getChannelNamesWithID, getOrderedFileInfos, getFileInfosIndexes, getOrderedGalleryItems} from './files';
|
||||||
|
|
||||||
import type ChannelModel from '@typings/database/models/servers/channel';
|
import type ChannelModel from '@typings/database/models/servers/channel';
|
||||||
|
|
||||||
|
|
@ -102,9 +102,4 @@ describe('Files utils', () => {
|
||||||
const types = result.map((f) => f.type);
|
const types = result.map((f) => f.type);
|
||||||
expect(types).toEqual(['file', 'image', 'video']);
|
expect(types).toEqual(['file', 'image', 'video']);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('pathWithPrefix', () => {
|
|
||||||
expect(pathWithPrefix('file://', 'file://something')).toEqual('file://something');
|
|
||||||
expect(pathWithPrefix('file://', 'something')).toEqual('file://something');
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -42,8 +42,3 @@ export const getFileInfosIndexes = (orderedFilesForGallery: FileInfo[]) => {
|
||||||
export const getOrderedGalleryItems = (orderedFileInfos: FileInfo[]) => {
|
export const getOrderedGalleryItems = (orderedFileInfos: FileInfo[]) => {
|
||||||
return orderedFileInfos.map((f) => fileToGalleryItem(f, f.user_id));
|
return orderedFileInfos.map((f) => fileToGalleryItem(f, f.user_id));
|
||||||
};
|
};
|
||||||
|
|
||||||
export const pathWithPrefix = (prefix: string, path: string) => {
|
|
||||||
const p = path.startsWith(prefix) ? '' : prefix;
|
|
||||||
return `${p}${path}`;
|
|
||||||
};
|
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,12 @@ jest.mock('@sentry/react-native', () => ({
|
||||||
addBreadcrumb: jest.fn(),
|
addBreadcrumb: jest.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
// We need to get back the original functions
|
||||||
|
// since we are mocking them in the setup file
|
||||||
|
jest.mock('./log', () => ({
|
||||||
|
...jest.requireActual('./log'),
|
||||||
|
}));
|
||||||
|
|
||||||
// Mock console methods
|
// Mock console methods
|
||||||
const originalConsole = global.console;
|
const originalConsole = global.console;
|
||||||
|
|
||||||
|
|
|
||||||
250
app/utils/share_logs.test.ts
Normal file
250
app/utils/share_logs.test.ts
Normal file
|
|
@ -0,0 +1,250 @@
|
||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import TurboLogger from '@mattermost/react-native-turbo-log';
|
||||||
|
import {Alert, Platform} from 'react-native';
|
||||||
|
import Share from 'react-native-share';
|
||||||
|
|
||||||
|
import {shareLogs, getDefaultReportAProblemLink, metadataToString, emailLogs} from './share_logs';
|
||||||
|
import {tryOpenURL} from './url';
|
||||||
|
|
||||||
|
jest.mock('react-native-share', () => ({
|
||||||
|
open: jest.fn(),
|
||||||
|
shareSingle: jest.fn(),
|
||||||
|
Social: {
|
||||||
|
EMAIL: 'email',
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('./url', () => ({
|
||||||
|
tryOpenURL: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('react-native/Libraries/Alert/Alert', () => ({
|
||||||
|
alert: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('shareLogs', () => {
|
||||||
|
const metadata = {
|
||||||
|
currentUserId: 'user1',
|
||||||
|
currentTeamId: 'team1',
|
||||||
|
serverVersion: '1.0.0',
|
||||||
|
appVersion: '2.0.0',
|
||||||
|
appPlatform: 'ios',
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
|
||||||
|
const logPaths = ['/path/to/log1', '/path/to/log2'];
|
||||||
|
jest.mocked(TurboLogger.getLogPaths).mockResolvedValue(logPaths);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should share logs with attachments when excludeLogs is false', async () => {
|
||||||
|
await shareLogs(metadata, 'My Site', 'support@example.com', false);
|
||||||
|
|
||||||
|
expect(Share.open).toHaveBeenCalledWith(expect.objectContaining({
|
||||||
|
subject: 'Problem with My Site React Native app',
|
||||||
|
email: 'support@example.com',
|
||||||
|
urls: ['file:///path/to/log1', 'file:///path/to/log2'],
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle an empty list of log paths', async () => {
|
||||||
|
jest.mocked(TurboLogger.getLogPaths).mockResolvedValue([]);
|
||||||
|
|
||||||
|
await shareLogs(metadata, 'My Site', 'support@example.com', false);
|
||||||
|
|
||||||
|
expect(Share.open).toHaveBeenCalledWith(expect.objectContaining({
|
||||||
|
subject: 'Problem with My Site React Native app',
|
||||||
|
email: 'support@example.com',
|
||||||
|
urls: undefined,
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should share without logs when excludeLogs is true', async () => {
|
||||||
|
await shareLogs(metadata, 'My Site', 'support@example.com', true);
|
||||||
|
|
||||||
|
expect(Share.open).toHaveBeenCalledWith(expect.objectContaining({
|
||||||
|
subject: 'Problem with My Site React Native app',
|
||||||
|
email: 'support@example.com',
|
||||||
|
urls: undefined,
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle errors', async () => {
|
||||||
|
const error = new Error('Share failed');
|
||||||
|
jest.mocked(Share.open).mockRejectedValue(error);
|
||||||
|
|
||||||
|
await shareLogs(metadata, 'My Site', 'support@example.com');
|
||||||
|
|
||||||
|
expect(Alert.alert).toHaveBeenCalledWith('Error', 'Error: Share failed');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should pass the correct metadata to the share function', async () => {
|
||||||
|
await shareLogs(metadata, 'My Site', 'support@example.com', false);
|
||||||
|
|
||||||
|
expect(Share.open).toHaveBeenCalledWith(expect.objectContaining({
|
||||||
|
message: expect.stringContaining('Current User ID: user1'),
|
||||||
|
}));
|
||||||
|
|
||||||
|
expect(Share.open).toHaveBeenCalledWith(expect.objectContaining({
|
||||||
|
message: expect.stringContaining('Current Team ID: team1'),
|
||||||
|
}));
|
||||||
|
|
||||||
|
expect(Share.open).toHaveBeenCalledWith(expect.objectContaining({
|
||||||
|
message: expect.stringContaining('Server Version: 1.0.0'),
|
||||||
|
}));
|
||||||
|
|
||||||
|
expect(Share.open).toHaveBeenCalledWith(expect.objectContaining({
|
||||||
|
message: expect.stringContaining('App Version: 2.0.0'),
|
||||||
|
}));
|
||||||
|
|
||||||
|
expect(Share.open).toHaveBeenCalledWith(expect.objectContaining({
|
||||||
|
message: expect.stringContaining('App Platform: ios'),
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('emailLogs', () => {
|
||||||
|
const metadata = {
|
||||||
|
currentUserId: 'user1',
|
||||||
|
currentTeamId: 'team1',
|
||||||
|
serverVersion: '1.0.0',
|
||||||
|
appVersion: '2.0.0',
|
||||||
|
appPlatform: 'ios',
|
||||||
|
};
|
||||||
|
|
||||||
|
let originalPlatform: typeof Platform.OS;
|
||||||
|
beforeAll(() => {
|
||||||
|
originalPlatform = Platform.OS;
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(() => {
|
||||||
|
Platform.OS = originalPlatform;
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('android', () => {
|
||||||
|
beforeAll(() => {
|
||||||
|
Platform.OS = 'android';
|
||||||
|
});
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
|
||||||
|
const logPaths = ['/path/to/log1', '/path/to/log2'];
|
||||||
|
jest.mocked(TurboLogger.getLogPaths).mockResolvedValue(logPaths);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should share logs with attachments when excludeLogs is false', async () => {
|
||||||
|
await emailLogs(metadata, 'My Site', 'support@example.com', false);
|
||||||
|
|
||||||
|
expect(Share.shareSingle).toHaveBeenCalledWith(expect.objectContaining({
|
||||||
|
subject: 'Problem with My Site React Native app',
|
||||||
|
email: 'support@example.com',
|
||||||
|
urls: ['file:///path/to/log1', 'file:///path/to/log2'],
|
||||||
|
social: Share.Social.EMAIL,
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle an empty list of log paths', async () => {
|
||||||
|
jest.mocked(TurboLogger.getLogPaths).mockResolvedValue([]);
|
||||||
|
|
||||||
|
await emailLogs(metadata, 'My Site', 'support@example.com', false);
|
||||||
|
|
||||||
|
expect(Share.shareSingle).toHaveBeenCalledWith(expect.objectContaining({
|
||||||
|
subject: 'Problem with My Site React Native app',
|
||||||
|
email: 'support@example.com',
|
||||||
|
urls: undefined,
|
||||||
|
social: Share.Social.EMAIL,
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should share without logs when excludeLogs is true', async () => {
|
||||||
|
await emailLogs(metadata, 'My Site', 'support@example.com', true);
|
||||||
|
|
||||||
|
expect(Share.shareSingle).toHaveBeenCalledWith(expect.objectContaining({
|
||||||
|
subject: 'Problem with My Site React Native app',
|
||||||
|
email: 'support@example.com',
|
||||||
|
urls: undefined,
|
||||||
|
social: Share.Social.EMAIL,
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle errors', async () => {
|
||||||
|
const error = new Error('Share failed');
|
||||||
|
jest.mocked(Share.shareSingle).mockRejectedValue(error);
|
||||||
|
|
||||||
|
await emailLogs(metadata, 'My Site', 'support@example.com');
|
||||||
|
|
||||||
|
expect(Alert.alert).toHaveBeenCalledWith('Error', 'Error: Share failed');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should pass the correct metadata to the share function', async () => {
|
||||||
|
await emailLogs(metadata, 'My Site', 'support@example.com', false);
|
||||||
|
|
||||||
|
expect(Share.shareSingle).toHaveBeenCalledWith(expect.objectContaining({
|
||||||
|
message: expect.stringContaining('Current User ID: user1'),
|
||||||
|
}));
|
||||||
|
|
||||||
|
expect(Share.shareSingle).toHaveBeenCalledWith(expect.objectContaining({
|
||||||
|
message: expect.stringContaining('Current Team ID: team1'),
|
||||||
|
}));
|
||||||
|
|
||||||
|
expect(Share.shareSingle).toHaveBeenCalledWith(expect.objectContaining({
|
||||||
|
message: expect.stringContaining('Server Version: 1.0.0'),
|
||||||
|
}));
|
||||||
|
|
||||||
|
expect(Share.shareSingle).toHaveBeenCalledWith(expect.objectContaining({
|
||||||
|
message: expect.stringContaining('App Version: 2.0.0'),
|
||||||
|
}));
|
||||||
|
|
||||||
|
expect(Share.shareSingle).toHaveBeenCalledWith(expect.objectContaining({
|
||||||
|
message: expect.stringContaining('App Platform: ios'),
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('ios', () => {
|
||||||
|
beforeAll(() => {
|
||||||
|
Platform.OS = 'ios';
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should open the correct mailto link', async () => {
|
||||||
|
await emailLogs(metadata, 'My Site', 'support@example.com', false);
|
||||||
|
|
||||||
|
expect(tryOpenURL).toHaveBeenCalledTimes(1);
|
||||||
|
expect(tryOpenURL).toHaveBeenCalledWith(expect.stringMatching(/^mailto:support@example\.com\?subject=Problem%20with%20My%20Site%20React%20Native%20app&body=/));
|
||||||
|
expect(tryOpenURL).toHaveBeenCalledWith(expect.stringContaining(encodeURIComponent('Current User ID: user1')));
|
||||||
|
expect(tryOpenURL).toHaveBeenCalledWith(expect.stringContaining(encodeURIComponent('Current Team ID: team1')));
|
||||||
|
expect(tryOpenURL).toHaveBeenCalledWith(expect.stringContaining(encodeURIComponent('Server Version: 1.0.0')));
|
||||||
|
expect(tryOpenURL).toHaveBeenCalledWith(expect.stringContaining(encodeURIComponent('App Version: 2.0.0')));
|
||||||
|
expect(tryOpenURL).toHaveBeenCalledWith(expect.stringContaining(encodeURIComponent('App Platform: ios')));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getDefaultReportAProblemLink', () => {
|
||||||
|
it('should return licensed link when isLicensed is true', () => {
|
||||||
|
const link = getDefaultReportAProblemLink(true);
|
||||||
|
expect(link).toBe('https://mattermost.com/pl/report_a_problem_licensed');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return unlicensed link when isLicensed is false', () => {
|
||||||
|
const link = getDefaultReportAProblemLink(false);
|
||||||
|
expect(link).toBe('https://mattermost.com/pl/report_a_problem_unlicensed');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('metadataToString', () => {
|
||||||
|
it('should return a string with the correct metadata', () => {
|
||||||
|
const metadata = {
|
||||||
|
currentUserId: 'user1',
|
||||||
|
currentTeamId: 'team1',
|
||||||
|
serverVersion: '1.0.0',
|
||||||
|
appVersion: '2.0.0',
|
||||||
|
appPlatform: 'ios',
|
||||||
|
};
|
||||||
|
|
||||||
|
const string = metadataToString(metadata);
|
||||||
|
expect(string).toBe('Current User ID: user1\nCurrent Team ID: team1\nServer Version: 1.0.0\nApp Version: 2.0.0\nApp Platform: ios');
|
||||||
|
});
|
||||||
|
});
|
||||||
92
app/utils/share_logs.ts
Normal file
92
app/utils/share_logs.ts
Normal file
|
|
@ -0,0 +1,92 @@
|
||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
import TurboLogger from '@mattermost/react-native-turbo-log';
|
||||||
|
import {defineMessages} from 'react-intl';
|
||||||
|
import {Alert, Platform} from 'react-native';
|
||||||
|
import Share from 'react-native-share';
|
||||||
|
|
||||||
|
import {pathWithPrefix} from '@utils/file';
|
||||||
|
|
||||||
|
import {tryOpenURL} from './url';
|
||||||
|
|
||||||
|
import type {ReportAProblemMetadata} from '@typings/screens/report_a_problem';
|
||||||
|
|
||||||
|
export const shareLogs = async (metadata: ReportAProblemMetadata, siteName: string | undefined, reportAProblemMail: string | undefined, excludeLogs: boolean = false) => {
|
||||||
|
try {
|
||||||
|
const logPaths = await TurboLogger.getLogPaths();
|
||||||
|
const attachments = excludeLogs ? [] : logPaths.map((path) => pathWithPrefix('file://', path));
|
||||||
|
await Share.open({
|
||||||
|
subject: `Problem with ${siteName} React Native app`,
|
||||||
|
email: reportAProblemMail,
|
||||||
|
failOnCancel: false,
|
||||||
|
urls: attachments.length ? attachments : undefined,
|
||||||
|
message: [
|
||||||
|
'Please share a description of the problem:\n\n',
|
||||||
|
metadataToString(metadata),
|
||||||
|
].join('\n'),
|
||||||
|
});
|
||||||
|
} catch (e: unknown) {
|
||||||
|
Alert.alert('Error', `${e}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const emailLogs = async (metadata: ReportAProblemMetadata, siteName: string | undefined, reportAProblemMail: string | undefined, excludeLogs: boolean = false) => {
|
||||||
|
try {
|
||||||
|
if (Platform.OS === 'ios') {
|
||||||
|
// iOS does not support sharing with different mail apps, so we use a mailto link
|
||||||
|
|
||||||
|
const subject = `Problem with ${siteName || 'Mattermost'} React Native app`;
|
||||||
|
const body = metadataToString(metadata);
|
||||||
|
const url = `mailto:${reportAProblemMail}?subject=${encodeURIComponent(subject)}&body=${encodeURIComponent(body)}`;
|
||||||
|
tryOpenURL(url);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const logPaths = await TurboLogger.getLogPaths();
|
||||||
|
const attachments = excludeLogs ? [] : logPaths.map((path) => pathWithPrefix('file://', path));
|
||||||
|
await Share.shareSingle({
|
||||||
|
social: Share.Social.EMAIL as any, // The type is not correct in the library
|
||||||
|
subject: `Problem with ${siteName} React Native app`,
|
||||||
|
email: reportAProblemMail,
|
||||||
|
urls: attachments.length ? attachments : undefined,
|
||||||
|
message: [
|
||||||
|
'Please share a description of the problem:\n\n',
|
||||||
|
metadataToString(metadata),
|
||||||
|
].join('\n'),
|
||||||
|
});
|
||||||
|
} catch (e: unknown) {
|
||||||
|
Alert.alert('Error', `${e}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getDefaultReportAProblemLink = (isLicensed: boolean) => {
|
||||||
|
return isLicensed ? 'https://mattermost.com/pl/report_a_problem_licensed' : 'https://mattermost.com/pl/report_a_problem_unlicensed';
|
||||||
|
};
|
||||||
|
|
||||||
|
export function metadataToString(metadata: ReportAProblemMetadata): string {
|
||||||
|
return Object.entries(metadata).
|
||||||
|
map(([key, value]) => `${reportAProblemMessages[key as keyof ReportAProblemMetadata].defaultMessage}: ${value}`).
|
||||||
|
join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
export const reportAProblemMessages = defineMessages({
|
||||||
|
currentUserId: {
|
||||||
|
id: 'report_a_problem.metadata.currentUserId',
|
||||||
|
defaultMessage: 'Current User ID',
|
||||||
|
},
|
||||||
|
currentTeamId: {
|
||||||
|
id: 'report_a_problem.metadata.currentTeamId',
|
||||||
|
defaultMessage: 'Current Team ID',
|
||||||
|
},
|
||||||
|
serverVersion: {
|
||||||
|
id: 'report_a_problem.metadata.serverVersion',
|
||||||
|
defaultMessage: 'Server Version',
|
||||||
|
},
|
||||||
|
appVersion: {
|
||||||
|
id: 'report_a_problem.metadata.appVersion',
|
||||||
|
defaultMessage: 'App Version',
|
||||||
|
},
|
||||||
|
appPlatform: {
|
||||||
|
id: 'report_a_problem.metadata.appPlatform',
|
||||||
|
defaultMessage: 'App Platform',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
@ -380,6 +380,7 @@
|
||||||
"general_settings.about": "About {appTitle}",
|
"general_settings.about": "About {appTitle}",
|
||||||
"general_settings.advanced_settings": "Advanced Settings",
|
"general_settings.advanced_settings": "Advanced Settings",
|
||||||
"general_settings.display": "Display",
|
"general_settings.display": "Display",
|
||||||
|
"general_settings.download_logs": "Download app logs",
|
||||||
"general_settings.help": "Help",
|
"general_settings.help": "Help",
|
||||||
"general_settings.notifications": "Notifications",
|
"general_settings.notifications": "Notifications",
|
||||||
"general_settings.report_problem": "Report a problem",
|
"general_settings.report_problem": "Report a problem",
|
||||||
|
|
@ -1008,6 +1009,14 @@
|
||||||
"rate.error.title": "Error",
|
"rate.error.title": "Error",
|
||||||
"rate.subtitle": "Let us know what you think.",
|
"rate.subtitle": "Let us know what you think.",
|
||||||
"rate.title": "Enjoying Mattermost?",
|
"rate.title": "Enjoying Mattermost?",
|
||||||
|
"report_a_problem.metadata.appPlatform": "App Platform",
|
||||||
|
"report_a_problem.metadata.appVersion": "App Version",
|
||||||
|
"report_a_problem.metadata.copy": "Copy",
|
||||||
|
"report_a_problem.metadata.currentTeamId": "Current Team ID",
|
||||||
|
"report_a_problem.metadata.currentUserId": "Current User ID",
|
||||||
|
"report_a_problem.metadata.serverVersion": "Server Version",
|
||||||
|
"report_problem.download_logs.title": "Download app logs",
|
||||||
|
"report_problem.title": "Report a problem",
|
||||||
"requested_ack.title": "Request Acknowledgements",
|
"requested_ack.title": "Request Acknowledgements",
|
||||||
"saved_messages.empty.paragraph": "To save something for later, long-press on a message and choose Save from the menu. Saved messages are only visible to you.",
|
"saved_messages.empty.paragraph": "To save something for later, long-press on a message and choose Save from the menu. Saved messages are only visible to you.",
|
||||||
"saved_messages.empty.title": "No saved messages yet",
|
"saved_messages.empty.title": "No saved messages yet",
|
||||||
|
|
@ -1060,6 +1069,13 @@
|
||||||
"screen.channel_files.results.filter.title": "Filter by file type",
|
"screen.channel_files.results.filter.title": "Filter by file type",
|
||||||
"screen.mentions.subtitle": "Messages you've been mentioned in",
|
"screen.mentions.subtitle": "Messages you've been mentioned in",
|
||||||
"screen.mentions.title": "Recent Mentions",
|
"screen.mentions.title": "Recent Mentions",
|
||||||
|
"screen.report_problem.button": "Report a problem",
|
||||||
|
"screen.report_problem.details.description": "When reporting a problem, share the metadata and app logs given below to help troubleshoot your problem faster",
|
||||||
|
"screen.report_problem.details.description_without_logs": "When reporting a problem, share the metadata given below to help troubleshoot your problem faster",
|
||||||
|
"screen.report_problem.details.title": "Troubleshouting details",
|
||||||
|
"screen.report_problem.logs.download": "Download App Logs",
|
||||||
|
"screen.report_problem.logs.title": "APP LOGS:",
|
||||||
|
"screen.report_problem.metadata.title": "METADATA:",
|
||||||
"screen.saved_messages.subtitle": "All messages you've saved for follow up",
|
"screen.saved_messages.subtitle": "All messages you've saved for follow up",
|
||||||
"screen.saved_messages.title": "Saved Messages",
|
"screen.saved_messages.title": "Saved Messages",
|
||||||
"screen.search.header.files": "Files",
|
"screen.search.header.files": "Files",
|
||||||
|
|
|
||||||
|
|
@ -64,8 +64,75 @@ class RNUtilsModuleImpl(private val reactContext: ReactApplicationContext) {
|
||||||
promise?.resolve(result)
|
promise?.resolve(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun createZipFile(paths: List<String>, promise: Promise?) {
|
||||||
|
var zipFile: java.io.File? = null
|
||||||
|
var fos: java.io.FileOutputStream? = null
|
||||||
|
var zos: java.util.zip.ZipOutputStream? = null
|
||||||
|
|
||||||
|
try {
|
||||||
|
val dateFormat = java.text.SimpleDateFormat("yyyyMMdd_HHmmss", java.util.Locale.getDefault())
|
||||||
|
val date = java.util.Date()
|
||||||
|
val fileName = "Logs_${dateFormat.format(date)}.zip"
|
||||||
|
val tempDir = reactContext.cacheDir
|
||||||
|
zipFile = java.io.File(tempDir, fileName)
|
||||||
|
|
||||||
|
fos = java.io.FileOutputStream(zipFile)
|
||||||
|
zos = java.util.zip.ZipOutputStream(fos)
|
||||||
|
|
||||||
|
paths.forEach { path ->
|
||||||
|
val file = java.io.File(path)
|
||||||
|
if (file.exists()) {
|
||||||
|
var fis: java.io.FileInputStream? = null
|
||||||
|
try {
|
||||||
|
fis = java.io.FileInputStream(file)
|
||||||
|
val zipEntry = java.util.zip.ZipEntry(file.name)
|
||||||
|
zos.putNextEntry(zipEntry)
|
||||||
|
|
||||||
|
val buffer = ByteArray(1024)
|
||||||
|
var length: Int
|
||||||
|
while (fis.read(buffer).also { length = it } >= 0) {
|
||||||
|
zos.write(buffer, 0, length)
|
||||||
|
}
|
||||||
|
|
||||||
|
zos.closeEntry()
|
||||||
|
} finally {
|
||||||
|
fis?.close()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Clean up if a file is missing
|
||||||
|
cleanupResources(zos, fos, zipFile)
|
||||||
|
promise?.reject("File does not exist: $path")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
zos.close()
|
||||||
|
fos.close()
|
||||||
|
promise?.resolve(zipFile.absolutePath)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
// Clean up on any error
|
||||||
|
cleanupResources(zos, fos, zipFile)
|
||||||
|
promise?.reject("Error creating ZIP file", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun cleanupResources(
|
||||||
|
zos: java.util.zip.ZipOutputStream?,
|
||||||
|
fos: java.io.FileOutputStream?,
|
||||||
|
zipFile: java.io.File?
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
zos?.close()
|
||||||
|
fos?.close()
|
||||||
|
zipFile?.delete()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
// Log cleanup errors but don't throw as we're already in error handling
|
||||||
|
android.util.Log.e("RNUtils", "Error cleaning up resources", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun saveFile(filePath: String?, promise: Promise?) {
|
fun saveFile(filePath: String?, promise: Promise?) {
|
||||||
val task = SaveDataTask(reactContext)
|
val task = SaveDataTask.getInstance(reactContext)
|
||||||
task.saveFile(filePath, promise)
|
task.saveFile(filePath, promise)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,76 +11,102 @@ import com.facebook.react.bridge.BaseActivityEventListener
|
||||||
import com.facebook.react.bridge.Promise
|
import com.facebook.react.bridge.Promise
|
||||||
import com.facebook.react.bridge.ReactApplicationContext
|
import com.facebook.react.bridge.ReactApplicationContext
|
||||||
import com.mattermost.rnutils.enums.Events
|
import com.mattermost.rnutils.enums.Events
|
||||||
|
import io.reactivex.rxjava3.core.Single
|
||||||
|
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||||
|
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.FileInputStream
|
import java.io.FileInputStream
|
||||||
import java.io.FileOutputStream
|
import java.io.FileOutputStream
|
||||||
import java.lang.ref.WeakReference
|
import java.lang.ref.WeakReference
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
import java.util.Objects
|
import java.util.Objects
|
||||||
import java.util.concurrent.Executors
|
|
||||||
|
|
||||||
open class SaveDataTask(val reactContext: ReactApplicationContext) {
|
open class SaveDataTask(private val reactContext: ReactApplicationContext) {
|
||||||
private var mPickerPromise: Promise? = null
|
private var mPickerPromise: Promise? = null
|
||||||
private var fileContent: String? = null
|
private var fileContent: String? = null
|
||||||
|
|
||||||
private val weakContext = WeakReference(reactContext.applicationContext)
|
private val weakContext = WeakReference(reactContext.applicationContext)
|
||||||
private val myExecutor = Executors.newSingleThreadExecutor()
|
|
||||||
|
|
||||||
private lateinit var mActivityEventListener: ActivityEventListener
|
private lateinit var mActivityEventListener: ActivityEventListener
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val SAVE_REQUEST: Int = 38641
|
const val SAVE_REQUEST: Int = 38641
|
||||||
}
|
|
||||||
|
|
||||||
init {
|
// Store a single instance of the listener
|
||||||
mActivityEventListener = object : BaseActivityEventListener() {
|
private var activityEventListener: ActivityEventListener? = null
|
||||||
override fun onActivityResult(activity: Activity, requestCode: Int, resultCode: Int, intent: Intent?) {
|
private var instance: SaveDataTask? = null
|
||||||
if (requestCode == SAVE_REQUEST) {
|
|
||||||
if (resultCode == Activity.RESULT_CANCELED) {
|
|
||||||
mPickerPromise?.reject(Events.SAVE_ERROR_EVENT.event, "Save operation cancelled")
|
|
||||||
} else if (resultCode == Activity.RESULT_OK) {
|
|
||||||
val uri = intent!!.data
|
|
||||||
if (uri == null) {
|
|
||||||
mPickerPromise?.reject(Events.SAVE_ERROR_EVENT.event, "No data found")
|
|
||||||
} else {
|
|
||||||
try {
|
|
||||||
fileContent?.let { save(it, uri) }
|
|
||||||
mPickerPromise?.resolve(uri.toString())
|
|
||||||
} catch (e: java.lang.Exception) {
|
|
||||||
mPickerPromise?.reject(Events.SAVE_ERROR_EVENT.event, e.message)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
mPickerPromise = null
|
fun getInstance(reactContext: ReactApplicationContext): SaveDataTask {
|
||||||
reactContext.removeActivityEventListener(mActivityEventListener)
|
if (instance == null) {
|
||||||
}
|
instance = SaveDataTask(reactContext)
|
||||||
|
registerListener(reactContext)
|
||||||
}
|
}
|
||||||
|
return instance!!
|
||||||
}
|
}
|
||||||
|
|
||||||
reactContext.addActivityEventListener(mActivityEventListener)
|
private fun registerListener(reactContext: ReactApplicationContext) {
|
||||||
}
|
if (activityEventListener == null) {
|
||||||
|
activityEventListener = object : BaseActivityEventListener() {
|
||||||
private fun save(fromFile: String, toFile: Uri) {
|
override fun onActivityResult(activity: Activity, requestCode: Int, resultCode: Int, intent: Intent?) {
|
||||||
myExecutor.execute {
|
if (requestCode == SAVE_REQUEST) {
|
||||||
try {
|
val taskInstance = instance // Ensure we access the latest instance
|
||||||
val pfd = weakContext.get()?.contentResolver?.openFileDescriptor(toFile, "w")
|
if (taskInstance?.mPickerPromise != null) {
|
||||||
val input = File(fromFile)
|
if (resultCode == Activity.RESULT_CANCELED) {
|
||||||
FileInputStream(input).use { fileInputStream ->
|
taskInstance.mPickerPromise?.reject(Events.SAVE_ERROR_EVENT.event, "Save operation cancelled")
|
||||||
if (pfd != null) {
|
taskInstance.mPickerPromise = null
|
||||||
FileOutputStream(pfd.fileDescriptor).use { fileOutputStream ->
|
} else if (resultCode == Activity.RESULT_OK) {
|
||||||
val source = fileInputStream.channel
|
val uri = intent?.data
|
||||||
val dest = fileOutputStream.channel
|
if (uri == null) {
|
||||||
dest.transferFrom(source, 0, source.size())
|
taskInstance.mPickerPromise?.reject(Events.SAVE_ERROR_EVENT.event, "No data found")
|
||||||
source.close()
|
taskInstance.mPickerPromise = null
|
||||||
dest.close()
|
} else {
|
||||||
|
taskInstance.save(taskInstance.fileContent!!, uri)
|
||||||
|
.subscribeOn(Schedulers.io())
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe({ success ->
|
||||||
|
if (success) {
|
||||||
|
taskInstance.mPickerPromise?.resolve(uri.toString())
|
||||||
|
} else {
|
||||||
|
taskInstance.mPickerPromise?.reject(Events.SAVE_ERROR_EVENT.event, "Save failed")
|
||||||
|
}
|
||||||
|
taskInstance.mPickerPromise = null
|
||||||
|
}, { error ->
|
||||||
|
taskInstance.mPickerPromise?.reject(Events.SAVE_ERROR_EVENT.event, error.message)
|
||||||
|
taskInstance.mPickerPromise = null
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pfd?.close()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
e.printStackTrace()
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
reactContext.addActivityEventListener(activityEventListener!!)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun save(fromFile: String, toFile: Uri): Single<Boolean> {
|
||||||
|
return Single.create { emitter ->
|
||||||
|
try {
|
||||||
|
val pfd = weakContext.get()?.contentResolver?.openFileDescriptor(toFile, "w")
|
||||||
|
val input = File(fromFile)
|
||||||
|
FileInputStream(input).use { fileInputStream ->
|
||||||
|
if (pfd != null) {
|
||||||
|
FileOutputStream(pfd.fileDescriptor).use { fileOutputStream ->
|
||||||
|
val source = fileInputStream.channel
|
||||||
|
val dest = fileOutputStream.channel
|
||||||
|
dest.transferFrom(source, 0, source.size())
|
||||||
|
source.close()
|
||||||
|
dest.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pfd?.close()
|
||||||
|
emitter.onSuccess(true)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
emitter.onSuccess(false)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -148,4 +174,4 @@ open class SaveDataTask(val reactContext: ReactApplicationContext) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -3,6 +3,7 @@ package com.mattermost.rnutils
|
||||||
import com.facebook.react.bridge.Promise
|
import com.facebook.react.bridge.Promise
|
||||||
import com.facebook.react.bridge.ReactApplicationContext
|
import com.facebook.react.bridge.ReactApplicationContext
|
||||||
import com.facebook.react.bridge.WritableMap
|
import com.facebook.react.bridge.WritableMap
|
||||||
|
import com.facebook.react.bridge.ReadableArray
|
||||||
|
|
||||||
class RNUtilsModule(val reactContext: ReactApplicationContext) : NativeRNUtilsSpec(reactContext) {
|
class RNUtilsModule(val reactContext: ReactApplicationContext) : NativeRNUtilsSpec(reactContext) {
|
||||||
private var implementation: RNUtilsModuleImpl = RNUtilsModuleImpl(reactContext)
|
private var implementation: RNUtilsModuleImpl = RNUtilsModuleImpl(reactContext)
|
||||||
|
|
@ -75,4 +76,9 @@ class RNUtilsModule(val reactContext: ReactApplicationContext) : NativeRNUtilsSp
|
||||||
override fun setSoftKeyboardToAdjustNothing() {
|
override fun setSoftKeyboardToAdjustNothing() {
|
||||||
implementation.setSoftKeyboardToAdjustNothing()
|
implementation.setSoftKeyboardToAdjustNothing()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun createZipFile(paths: ReadableArray, promise: Promise?) {
|
||||||
|
val pathList = paths.toArrayList().map { it.toString() }
|
||||||
|
implementation.createZipFile(pathList, promise)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import com.facebook.react.bridge.ReactApplicationContext
|
||||||
import com.facebook.react.bridge.ReactContextBaseJavaModule
|
import com.facebook.react.bridge.ReactContextBaseJavaModule
|
||||||
import com.facebook.react.bridge.ReactMethod
|
import com.facebook.react.bridge.ReactMethod
|
||||||
import com.facebook.react.bridge.WritableMap
|
import com.facebook.react.bridge.WritableMap
|
||||||
|
import com.facebook.react.bridge.ReadableArray
|
||||||
|
|
||||||
class RNUtilsModule(context: ReactApplicationContext) :
|
class RNUtilsModule(context: ReactApplicationContext) :
|
||||||
ReactContextBaseJavaModule(context) {
|
ReactContextBaseJavaModule(context) {
|
||||||
|
|
@ -108,4 +109,10 @@ class RNUtilsModule(context: ReactApplicationContext) :
|
||||||
fun setSoftKeyboardToAdjustNothing() {
|
fun setSoftKeyboardToAdjustNothing() {
|
||||||
implementation.setSoftKeyboardToAdjustNothing()
|
implementation.setSoftKeyboardToAdjustNothing()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ReactMethod
|
||||||
|
fun createZipFile(paths: ReadableArray, promise: Promise?) {
|
||||||
|
val pathList = paths.toArrayList().map { it.toString() }
|
||||||
|
implementation.createZipFile(pathList, promise)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -131,6 +131,12 @@ RCT_REMAP_METHOD(setSoftKeyboardToAdjustNothing, setAdjustNothing) {
|
||||||
[self setSoftKeyboardToAdjustNothing];
|
[self setSoftKeyboardToAdjustNothing];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
RCT_EXPORT_METHOD(createZipFile:(NSArray<NSString *> *)paths
|
||||||
|
withResolver:(RCTPromiseResolveBlock)resolve
|
||||||
|
withRejecter:(RCTPromiseRejectBlock)reject) {
|
||||||
|
[self createZipFile:paths resolve:resolve reject:reject];
|
||||||
|
}
|
||||||
|
|
||||||
// Don't compile this code when we build for the old architecture.
|
// Don't compile this code when we build for the old architecture.
|
||||||
#ifdef RCT_NEW_ARCH_ENABLED
|
#ifdef RCT_NEW_ARCH_ENABLED
|
||||||
- (std::shared_ptr<TurboModule>)getTurboModule:
|
- (std::shared_ptr<TurboModule>)getTurboModule:
|
||||||
|
|
@ -224,6 +230,16 @@ RCT_REMAP_METHOD(setSoftKeyboardToAdjustNothing, setAdjustNothing) {
|
||||||
resolve(@"");
|
resolve(@"");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
- (void)createZipFile:(NSArray<NSString *> *)paths resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject {
|
||||||
|
NSDictionary *result = [wrapper createZipFileWithSourcePaths:paths];
|
||||||
|
|
||||||
|
if ([[result objectForKey:@"success"] boolValue]) {
|
||||||
|
resolve([result objectForKey:@"zipFilePath"]);
|
||||||
|
} else {
|
||||||
|
reject(@"create_zip_error", [result objectForKey:@"error"], nil);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
-(void)setSoftKeyboardToAdjustResize {
|
-(void)setSoftKeyboardToAdjustResize {
|
||||||
// Do nothing as it does not apply to iOS
|
// Do nothing as it does not apply to iOS
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -274,6 +274,77 @@ import React
|
||||||
UINavigationController.attemptRotationToDeviceOrientation()
|
UINavigationController.attemptRotationToDeviceOrientation()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@objc public func createZipFile(sourcePaths: [String]) -> Dictionary<String, Any> {
|
||||||
|
let fileManager = FileManager.default
|
||||||
|
let dateFormatter = DateFormatter()
|
||||||
|
dateFormatter.dateFormat = "yyyy-MM-dd-HHmmss"
|
||||||
|
let currentDate = dateFormatter.string(from: Date())
|
||||||
|
let destinationURL = fileManager.temporaryDirectory.appendingPathComponent("Logs_\(currentDate).zip")
|
||||||
|
let tempDirectory = fileManager.temporaryDirectory.appendingPathComponent(UUID().uuidString)
|
||||||
|
|
||||||
|
do {
|
||||||
|
try fileManager.createDirectory(at: destinationURL.deletingLastPathComponent(), withIntermediateDirectories: true, attributes: nil)
|
||||||
|
|
||||||
|
let coordinator = NSFileCoordinator()
|
||||||
|
var coordinatorError: NSError?
|
||||||
|
var zipFilePath: String?
|
||||||
|
|
||||||
|
try fileManager.createDirectory(at: tempDirectory, withIntermediateDirectories: true, attributes: nil)
|
||||||
|
|
||||||
|
// Copy files to temp directory
|
||||||
|
for sourcePath in sourcePaths {
|
||||||
|
let sourceURL = URL(fileURLWithPath: sourcePath)
|
||||||
|
let destinationTempURL = tempDirectory.appendingPathComponent(sourceURL.lastPathComponent)
|
||||||
|
do {
|
||||||
|
try fileManager.copyItem(at: sourceURL, to: destinationTempURL)
|
||||||
|
} catch {
|
||||||
|
// Clean up temp directory if copy fails
|
||||||
|
try? fileManager.removeItem(at: tempDirectory)
|
||||||
|
return [
|
||||||
|
"error": "Failed to copy file: \(error.localizedDescription)",
|
||||||
|
"success": false
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var moveError: Error?
|
||||||
|
coordinator.coordinate(readingItemAt: tempDirectory, options: [.forUploading], error: &coordinatorError) { (zipURL) in
|
||||||
|
do {
|
||||||
|
try fileManager.moveItem(at: zipURL, to: destinationURL)
|
||||||
|
zipFilePath = destinationURL.path
|
||||||
|
} catch let error {
|
||||||
|
moveError = error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean up temp directory regardless of success or failure
|
||||||
|
try? fileManager.removeItem(at: tempDirectory)
|
||||||
|
|
||||||
|
if let error = moveError ?? coordinatorError {
|
||||||
|
// Clean up destination file if move failed
|
||||||
|
try? fileManager.removeItem(at: destinationURL)
|
||||||
|
return [
|
||||||
|
"error": error.localizedDescription,
|
||||||
|
"success": false
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
"error": "",
|
||||||
|
"success": true,
|
||||||
|
"zipFilePath": zipFilePath ?? ""
|
||||||
|
]
|
||||||
|
} catch {
|
||||||
|
// Clean up both temp directory and destination file in case of any other errors
|
||||||
|
try? fileManager.removeItem(at: tempDirectory)
|
||||||
|
try? fileManager.removeItem(at: destinationURL)
|
||||||
|
return [
|
||||||
|
"error": error.localizedDescription,
|
||||||
|
"success": false
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -73,6 +73,8 @@ export interface Spec extends TurboModule {
|
||||||
|
|
||||||
setSoftKeyboardToAdjustResize(): void;
|
setSoftKeyboardToAdjustResize(): void;
|
||||||
setSoftKeyboardToAdjustNothing(): void;
|
setSoftKeyboardToAdjustNothing(): void;
|
||||||
|
|
||||||
|
createZipFile: (paths: string[]) => Promise<string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default TurboModuleRegistry.getEnforcing<Spec>('RNUtils');
|
export default TurboModuleRegistry.getEnforcing<Spec>('RNUtils');
|
||||||
|
|
|
||||||
16
patches/expo-file-system+18.0.6.patch
Normal file
16
patches/expo-file-system+18.0.6.patch
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
diff --git a/node_modules/expo-file-system/ios/FileSystemModule.swift b/node_modules/expo-file-system/ios/FileSystemModule.swift
|
||||||
|
index d564ae7..ace718c 100644
|
||||||
|
--- a/node_modules/expo-file-system/ios/FileSystemModule.swift
|
||||||
|
+++ b/node_modules/expo-file-system/ios/FileSystemModule.swift
|
||||||
|
@@ -80,7 +80,10 @@ public final class FileSystemModule: Module {
|
||||||
|
guard url.isFileURL else {
|
||||||
|
throw InvalidFileUrlException(url)
|
||||||
|
}
|
||||||
|
- try ensurePathPermission(appContext, path: url.appendingPathComponent("..").path, flag: .write)
|
||||||
|
+
|
||||||
|
+ // This line was impeding us to delete files in the tmp folder,
|
||||||
|
+ // since the ensure permission was mistakenly returning false.
|
||||||
|
+ //try ensurePathPermission(appContext, path: url.appendingPathComponent("..").path, flag: .write)
|
||||||
|
try removeFile(path: url.path, idempotent: options.idempotent)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -69,6 +69,10 @@ jest.mock('expo-web-browser', () => ({
|
||||||
})),
|
})),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
jest.mock('@mattermost/react-native-turbo-log', () => ({
|
||||||
|
getLogPaths: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
jest.mock('@nozbe/watermelondb/utils/common/randomId/randomId', () => ({}));
|
jest.mock('@nozbe/watermelondb/utils/common/randomId/randomId', () => ({}));
|
||||||
|
|
||||||
jest.mock('@nozbe/watermelondb/react/withObservables/garbageCollector', () => {
|
jest.mock('@nozbe/watermelondb/react/withObservables/garbageCollector', () => {
|
||||||
|
|
@ -196,6 +200,9 @@ jest.doMock('react-native', () => {
|
||||||
removeThreadNotifications: jest.fn().mockImplementation(),
|
removeThreadNotifications: jest.fn().mockImplementation(),
|
||||||
removeServerNotifications: jest.fn().mockImplementation(),
|
removeServerNotifications: jest.fn().mockImplementation(),
|
||||||
|
|
||||||
|
createZipFile: jest.fn(),
|
||||||
|
saveFile: jest.fn(),
|
||||||
|
|
||||||
unlockOrientation: jest.fn(),
|
unlockOrientation: jest.fn(),
|
||||||
getWindowDimensions: jest.fn().mockReturnValue({width: 426, height: 952}),
|
getWindowDimensions: jest.fn().mockReturnValue({width: 426, height: 952}),
|
||||||
},
|
},
|
||||||
|
|
@ -450,6 +457,13 @@ jest.mock('react-native-haptic-feedback', () => {
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
jest.mock('@utils/log', () => ({
|
||||||
|
logError: jest.fn(),
|
||||||
|
logDebug: jest.fn(),
|
||||||
|
logInfo: jest.fn(),
|
||||||
|
logWarning: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
declare const global: {
|
declare const global: {
|
||||||
requestAnimationFrame: (callback: () => void) => void;
|
requestAnimationFrame: (callback: () => void) => void;
|
||||||
performance: {
|
performance: {
|
||||||
|
|
@ -483,3 +497,7 @@ console.warn = filterStackTrace(colors.yellow, '⚠️ Warning:');
|
||||||
console.error = filterStackTrace(colors.red, '🚨 Error:');
|
console.error = filterStackTrace(colors.red, '🚨 Error:');
|
||||||
console.log = filterStackTrace(colors.cyan, '📢 Log:');
|
console.log = filterStackTrace(colors.cyan, '📢 Log:');
|
||||||
console.debug = filterStackTrace(colors.blue, '🐞 Debug:');
|
console.debug = filterStackTrace(colors.blue, '🐞 Debug:');
|
||||||
|
|
||||||
|
// Silence warnings about missing EXPO_OS environment variable
|
||||||
|
// on tests
|
||||||
|
process.env.EXPO_OS = 'ios'; // eslint-disable-line no-process-env
|
||||||
|
|
|
||||||
3
types/api/config.d.ts
vendored
3
types/api/config.d.ts
vendored
|
|
@ -7,6 +7,7 @@ interface ClientConfig {
|
||||||
AllowCustomThemes: string;
|
AllowCustomThemes: string;
|
||||||
AllowEditPost: string;
|
AllowEditPost: string;
|
||||||
AllowedThemes: string;
|
AllowedThemes: string;
|
||||||
|
AllowDownloadLogs: string;
|
||||||
AndroidAppDownloadLink: string;
|
AndroidAppDownloadLink: string;
|
||||||
AndroidLatestVersion: string;
|
AndroidLatestVersion: string;
|
||||||
AndroidMinVersion: string;
|
AndroidMinVersion: string;
|
||||||
|
|
@ -172,6 +173,8 @@ interface ClientConfig {
|
||||||
PersistentNotificationIntervalMinutes: string;
|
PersistentNotificationIntervalMinutes: string;
|
||||||
PrivacyPolicyLink: string;
|
PrivacyPolicyLink: string;
|
||||||
ReportAProblemLink: string;
|
ReportAProblemLink: string;
|
||||||
|
ReportAProblemMail: string;
|
||||||
|
ReportAProblemType: string;
|
||||||
RequireEmailVerification: string;
|
RequireEmailVerification: string;
|
||||||
RestrictDirectMessage: string;
|
RestrictDirectMessage: string;
|
||||||
RunJobs: string;
|
RunJobs: string;
|
||||||
|
|
|
||||||
10
types/screens/report_a_problem.ts
Normal file
10
types/screens/report_a_problem.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
export type ReportAProblemMetadata = {
|
||||||
|
currentUserId: string;
|
||||||
|
currentTeamId: string;
|
||||||
|
serverVersion: string;
|
||||||
|
appVersion: string;
|
||||||
|
appPlatform: string;
|
||||||
|
};
|
||||||
Loading…
Reference in a new issue