Merge branch 'develop' into appium-v2

This commit is contained in:
GleidsonDaniel 2022-05-23 11:29:32 -03:00
commit 4af0d7a881
138 changed files with 1447 additions and 1511 deletions

View File

@ -150,7 +150,7 @@ commands:
if [[ $CIRCLE_JOB == "android-build-official" ]]; then
./gradlew bundleOfficialPlayRelease
fi
if [[ $CIRCLE_JOB == "android-build-experimental" ]]; then
if [[ $CIRCLE_JOB == "android-build-experimental" || "android-automatic-build-experimental" ]]; then
./gradlew bundleExperimentalPlayRelease
fi
if [[ ! $KEYSTORE ]]; then
@ -169,7 +169,7 @@ commands:
--source-map=android/app/build/generated/sourcemaps/react/officialPlay/release/app.bundle.map \
--bundle android/app/build/generated/assets/react/officialPlay/release/app.bundle
fi
if [[ $CIRCLE_JOB == "android-build-experimental" ]]; then
if [[ $CIRCLE_JOB == "android-build-experimental" || "android-automatic-build-experimental" ]]; then
npx bugsnag-source-maps upload-react-native \
--api-key=$BUGSNAG_KEY \
--app-version-code=$CIRCLE_BUILD_NUM \
@ -379,7 +379,19 @@ jobs:
resource_class: large
steps:
- android-build
# Android automatic builds
android-automatic-build-experimental:
<<: *defaults
docker:
- image: circleci/android:api-29-node
environment:
<<: *android-env
<<: *bash-env
resource_class: large
steps:
- android-build
android-build-official:
<<: *defaults
docker:
@ -485,6 +497,10 @@ workflows:
type: approval
requires:
- lint-testunit
filters:
branches:
ignore:
- develop
- android-build-experimental:
requires:
- android-hold-build-experimental
@ -505,7 +521,7 @@ workflows:
- android-google-play-production-experimental:
requires:
- android-hold-google-play-production-experimental
# Android Official
- android-hold-build-official:
type: approval
@ -521,3 +537,15 @@ workflows:
- android-google-play-beta-official:
requires:
- android-hold-google-play-beta-official
# Android Automatic Experimental
- android-automatic-build-experimental:
filters:
branches:
only:
- develop
requires:
- lint-testunit
- android-google-play-production-experimental:
requires:
- android-automatic-build-experimental

View File

@ -1,33 +1,21 @@
import { useBackHandler } from '@react-native-community/hooks';
import * as Haptics from 'expo-haptics';
import React, { forwardRef, isValidElement, useEffect, useImperativeHandle, useRef, useState } from 'react';
import { Keyboard, Text } from 'react-native';
import { HandlerStateChangeEventPayload, State, TapGestureHandler } from 'react-native-gesture-handler';
import Animated, { Easing, Extrapolate, interpolateNode, Value } from 'react-native-reanimated';
import React, { forwardRef, isValidElement, useEffect, useImperativeHandle, useRef, useState, useCallback } from 'react';
import { Keyboard } from 'react-native';
import { Easing } from 'react-native-reanimated';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import ScrollBottomSheet from 'react-native-scroll-bottom-sheet';
import BottomSheet, { BottomSheetBackdrop } from '@gorhom/bottom-sheet';
import { themes } from '../../lib/constants';
import { useDimensions, useOrientation } from '../../dimensions';
import I18n from '../../i18n';
import { useTheme } from '../../theme';
import { isIOS, isTablet } from '../../utils/deviceInfo';
import * as List from '../List';
import { Button } from './Button';
import { Handle } from './Handle';
import { IActionSheetItem, Item } from './Item';
import { TActionSheetOptions, TActionSheetOptionsItem } from './Provider';
import { TActionSheetOptions } from './Provider';
import BottomSheetContent from './BottomSheetContent';
import styles, { ITEM_HEIGHT } from './styles';
import { testProps } from '../../lib/methods/testProps';
const getItemLayout = (data: TActionSheetOptionsItem[] | null | undefined, index: number) => ({
length: ITEM_HEIGHT,
offset: ITEM_HEIGHT * index,
index
});
const HANDLE_HEIGHT = isIOS ? 40 : 56;
const MAX_SNAP_HEIGHT = 16;
const MIN_SNAP_HEIGHT = 16;
const CANCEL_HEIGHT = 64;
const ANIMATION_DURATION = 250;
@ -40,27 +28,26 @@ const ANIMATION_CONFIG = {
const ActionSheet = React.memo(
forwardRef(({ children }: { children: React.ReactElement }, ref) => {
const { theme } = useTheme();
const bottomSheetRef = useRef<ScrollBottomSheet<TActionSheetOptionsItem>>(null);
const { colors } = useTheme();
const bottomSheetRef = useRef<BottomSheet>(null);
const [data, setData] = useState<TActionSheetOptions>({} as TActionSheetOptions);
const [isVisible, setVisible] = useState(false);
const { height } = useDimensions();
const { isLandscape } = useOrientation();
const insets = useSafeAreaInsets();
const maxSnap = Math.max(
height -
// Items height
ITEM_HEIGHT * (data?.options?.length || 0) -
const maxSnap = Math.min(
// Items height
ITEM_HEIGHT * (data?.options?.length || 0) +
// Handle height
HANDLE_HEIGHT -
HANDLE_HEIGHT +
// Custom header height
(data?.headerHeight || 0) -
(data?.headerHeight || 0) +
// Insets bottom height (Notch devices)
insets.bottom -
insets.bottom +
// Cancel button height
(data?.hasCancel ? CANCEL_HEIGHT : 0),
MAX_SNAP_HEIGHT
height - MIN_SNAP_HEIGHT
);
/*
@ -70,14 +57,13 @@ const ActionSheet = React.memo(
* we'll provide more one snap
* that point 50% of the whole screen
*/
const snaps = height - maxSnap > height * 0.6 && !isLandscape ? [maxSnap, height * 0.5, height] : [maxSnap, height];
const openedSnapIndex = snaps.length > 2 ? 1 : 0;
const closedSnapIndex = snaps.length - 1;
const snaps = maxSnap > height * 0.6 && !isLandscape && !data.snaps ? [height * 0.5, maxSnap] : [maxSnap];
const toggleVisible = () => setVisible(!isVisible);
const hide = () => {
bottomSheetRef.current?.snapTo(closedSnapIndex);
bottomSheetRef.current?.close();
toggleVisible();
};
const show = (options: TActionSheetOptions) => {
@ -85,12 +71,6 @@ const ActionSheet = React.memo(
toggleVisible();
};
const onBackdropPressed = ({ nativeEvent }: { nativeEvent: HandlerStateChangeEventPayload }) => {
if (nativeEvent.oldState === State.ACTIVE) {
hide();
}
};
useBackHandler(() => {
if (isVisible) {
hide();
@ -102,7 +82,6 @@ const ActionSheet = React.memo(
if (isVisible) {
Keyboard.dismiss();
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
bottomSheetRef.current?.snapTo(openedSnapIndex);
}
}, [isVisible]);
@ -123,26 +102,18 @@ const ActionSheet = React.memo(
</>
);
const renderFooter = () =>
data?.hasCancel ? (
<Button
onPress={hide}
style={[styles.button, { backgroundColor: themes[theme].auxiliaryBackground }]}
// TODO: Remove when migrate Touch
theme={theme}
accessibilityLabel={I18n.t('Cancel')}>
<Text style={[styles.text, { color: themes[theme].bodyText }]}>{I18n.t('Cancel')}</Text>
</Button>
) : null;
const renderItem = ({ item }: { item: IActionSheetItem['item'] }) => <Item item={item} hide={hide} />;
const animatedPosition = React.useRef(new Value(0));
const opacity = interpolateNode(animatedPosition.current, {
inputRange: [0, 1],
outputRange: [0, themes[theme].backdropOpacity],
extrapolate: Extrapolate.CLAMP
}) as any; // The function's return differs from the expected type of opacity, however this problem is something related to lib, maybe when updating the types will be fixed.
const renderBackdrop = useCallback(
props => (
<BottomSheetBackdrop
{...props}
appearsOnIndex={0}
// Backdrop should be visible all the time bottom sheet is open
disappearsOnIndex={-1}
opacity={colors.backdropOpacity}
/>
),
[]
);
const bottomSheet = isLandscape || isTablet ? styles.bottomSheet : {};
@ -150,42 +121,19 @@ const ActionSheet = React.memo(
<>
{children}
{isVisible && (
<>
<TapGestureHandler onHandlerStateChange={onBackdropPressed}>
<Animated.View
{...testProps('action-sheet-backdrop')}
style={[
styles.backdrop,
{
backgroundColor: themes[theme].backdropColor,
opacity
}
]}
/>
</TapGestureHandler>
<ScrollBottomSheet<TActionSheetOptionsItem>
{...testProps('action-sheet')}
ref={bottomSheetRef}
componentType='FlatList'
snapPoints={snaps}
initialSnapIndex={closedSnapIndex}
renderHandle={renderHandle}
onSettle={index => index === closedSnapIndex && toggleVisible()}
animatedPosition={animatedPosition.current}
containerStyle={{ ...styles.container, ...bottomSheet, backgroundColor: themes[theme].focusedBackground }}
animationConfig={ANIMATION_CONFIG}
data={data.options}
renderItem={renderItem}
keyExtractor={item => item.title}
style={{ backgroundColor: themes[theme].focusedBackground }}
contentContainerStyle={styles.content}
ItemSeparatorComponent={List.Separator}
ListHeaderComponent={List.Separator}
ListFooterComponent={renderFooter}
getItemLayout={getItemLayout}
removeClippedSubviews={isIOS}
/>
</>
<BottomSheet
ref={bottomSheetRef}
snapPoints={data?.snaps ? data.snaps : snaps}
animationConfigs={ANIMATION_CONFIG}
animateOnMount={true}
backdropComponent={renderBackdrop}
handleComponent={renderHandle}
enablePanDownToClose
style={{ ...styles.container, ...bottomSheet }}
backgroundStyle={{ backgroundColor: colors.focusedBackground }}
onChange={index => index === -1 && toggleVisible()}>
<BottomSheetContent options={data?.options} hide={hide} children={data?.children} hasCancel={data?.hasCancel} />
</BottomSheet>
)}
</>
);

View File

@ -0,0 +1,58 @@
import { Text } from 'react-native';
import React from 'react';
import { BottomSheetView, BottomSheetFlatList } from '@gorhom/bottom-sheet';
import { Button } from './Button';
import I18n from '../../i18n';
import { useTheme } from '../../theme';
import { IActionSheetItem, Item } from './Item';
import { TActionSheetOptionsItem } from './Provider';
import styles from './styles';
import * as List from '../List';
interface IBottomSheetContentProps {
hasCancel?: boolean;
options?: TActionSheetOptionsItem[];
hide: () => void;
children?: React.ReactElement | null;
}
const BottomSheetContent = React.memo(({ options, hasCancel, hide, children }: IBottomSheetContentProps) => {
const { theme, colors } = useTheme();
const renderFooter = () =>
hasCancel ? (
<Button
onPress={hide}
style={[styles.button, { backgroundColor: colors.auxiliaryBackground }]}
// TODO: Remove when migrate Touch
theme={theme}
accessibilityLabel={I18n.t('Cancel')}>
<Text style={[styles.text, { color: colors.bodyText }]}>{I18n.t('Cancel')}</Text>
</Button>
) : null;
const renderItem = ({ item }: { item: IActionSheetItem['item'] }) => <Item item={item} hide={hide} />;
if (options) {
return (
<BottomSheetFlatList
data={options}
refreshing={false}
keyExtractor={item => item.title}
bounces={true}
renderItem={renderItem}
style={{ backgroundColor: colors.focusedBackground }}
keyboardDismissMode='interactive'
indicatorStyle='black'
contentContainerStyle={styles.content}
ItemSeparatorComponent={List.Separator}
ListHeaderComponent={List.Separator}
ListFooterComponent={renderFooter}
/>
);
}
return <BottomSheetView>{children}</BottomSheetView>;
});
export default BottomSheetContent;

View File

@ -13,10 +13,13 @@ export type TActionSheetOptionsItem = {
};
export type TActionSheetOptions = {
options: TActionSheetOptionsItem[];
options?: TActionSheetOptionsItem[];
headerHeight?: number;
customHeader?: React.ReactElement | null;
hasCancel?: boolean;
type?: string;
children?: React.ReactElement | null;
snaps?: string[] | number[];
};
interface IActionSheetProvider {
showActionSheet: (item: TActionSheetOptions) => void;

View File

@ -46,8 +46,7 @@ export default StyleSheet.create({
},
bottomSheet: {
width: '50%',
alignSelf: 'center',
left: '25%'
marginHorizontal: '25%'
},
button: {
marginHorizontal: 16,

View File

@ -0,0 +1,28 @@
import React from 'react';
import { storiesOf } from '@storybook/react-native';
import Button from '.';
const buttonProps = {
title: 'Press me!',
type: 'primary',
onPress: () => {},
testID: 'testButton',
fontSize: 16,
style: {
padding: 10,
justifyContent: 'center'
}
};
const stories = storiesOf('Button', module);
stories.add('primary button', () => <Button {...buttonProps} />);
stories.add('secondary button', () => <Button {...buttonProps} type='secondary' />);
stories.add('loading button', () => <Button loading {...buttonProps} />);
stories.add('disabled button', () => <Button disabled {...buttonProps} />);
stories.add('disabled loading button', () => <Button disabled loading {...buttonProps} />);

View File

@ -0,0 +1,71 @@
import React from 'react';
import { View } from 'react-native';
import { fireEvent, render } from '@testing-library/react-native';
import Button from '.';
const onPressMock = jest.fn();
const testProps = {
title: 'Press me!',
type: 'primary',
onPress: onPressMock,
testID: 'testButton',
initialText: 'Initial text',
textAfterPress: 'Button pressed!'
};
const TestButton = ({ loading = false, disabled = false }) => (
<View>
<Button
title={testProps.title}
type={testProps.title}
onPress={testProps.onPress}
testID={testProps.testID}
accessibilityLabel={testProps.title}
disabled={disabled}
loading={loading}
/>
</View>
);
describe('ButtonTests', () => {
test('rendered', async () => {
const { findByTestId } = render(<TestButton />);
const Button = await findByTestId(testProps.testID);
expect(Button).toBeTruthy();
});
test('rendered with correct title', async () => {
const { findByText } = render(<TestButton />);
const ButtonTitle = await findByText(testProps.title);
expect(ButtonTitle).toBeTruthy();
expect(ButtonTitle.props.children).toEqual(testProps.title);
});
test('find button using accessibilityLabel', async () => {
const { findByA11yLabel } = render(<TestButton />);
const Button = await findByA11yLabel(testProps.title);
expect(Button).toBeTruthy();
});
test('title not visible while loading', async () => {
const { queryByText } = render(<TestButton loading={true} />);
const ButtonTitle = await queryByText(testProps.title);
expect(ButtonTitle).toBeNull();
});
test('should not trigger onPress on disabled button', async () => {
const { findByTestId } = render(<TestButton disabled={true} />);
const Button = await findByTestId(testProps.testID);
fireEvent.press(Button);
expect(onPressMock).not.toHaveBeenCalled();
});
test('should trigger onPress function on button press', async () => {
const { findByTestId } = render(<TestButton />);
const Button = await findByTestId(testProps.testID);
fireEvent.press(Button);
expect(onPressMock).toHaveBeenCalled();
});
});

View File

@ -0,0 +1,11 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Storyshots Button disabled button 1`] = `"{\\"type\\":\\"View\\",\\"props\\":{\\"accessible\\":true,\\"accessibilityLabel\\":\\"testButton\\",\\"testID\\":\\"testButton\\",\\"focusable\\":true,\\"style\\":{\\"paddingHorizontal\\":14,\\"justifyContent\\":\\"center\\",\\"height\\":48,\\"borderRadius\\":2,\\"marginBottom\\":12,\\"backgroundColor\\":\\"#1d74f5\\",\\"opacity\\":0.3,\\"padding\\":10}},\\"children\\":[{\\"type\\":\\"Text\\",\\"props\\":{\\"style\\":[{\\"textAlign\\":\\"center\\",\\"backgroundColor\\":\\"transparent\\",\\"fontFamily\\":\\"System\\",\\"fontWeight\\":\\"500\\"},{\\"color\\":\\"#ffffff\\",\\"fontSize\\":16},null],\\"accessibilityLabel\\":\\"Press me!\\"},\\"children\\":[\\"Press me!\\"]}]}"`;
exports[`Storyshots Button disabled loading button 1`] = `"{\\"type\\":\\"View\\",\\"props\\":{\\"accessible\\":true,\\"accessibilityLabel\\":\\"testButton\\",\\"testID\\":\\"testButton\\",\\"focusable\\":true,\\"style\\":{\\"paddingHorizontal\\":14,\\"justifyContent\\":\\"center\\",\\"height\\":48,\\"borderRadius\\":2,\\"marginBottom\\":12,\\"backgroundColor\\":\\"#1d74f5\\",\\"opacity\\":0.3,\\"padding\\":10}},\\"children\\":[{\\"type\\":\\"ActivityIndicator\\",\\"props\\":{\\"animating\\":true,\\"color\\":\\"#ffffff\\",\\"hidesWhenStopped\\":true,\\"size\\":\\"small\\",\\"style\\":[{\\"padding\\":16,\\"flex\\":1},null]},\\"children\\":null}]}"`;
exports[`Storyshots Button loading button 1`] = `"{\\"type\\":\\"View\\",\\"props\\":{\\"accessible\\":true,\\"accessibilityLabel\\":\\"testButton\\",\\"testID\\":\\"testButton\\",\\"focusable\\":true,\\"style\\":{\\"paddingHorizontal\\":14,\\"justifyContent\\":\\"center\\",\\"height\\":48,\\"borderRadius\\":2,\\"marginBottom\\":12,\\"backgroundColor\\":\\"#1d74f5\\",\\"padding\\":10,\\"opacity\\":1}},\\"children\\":[{\\"type\\":\\"ActivityIndicator\\",\\"props\\":{\\"animating\\":true,\\"color\\":\\"#ffffff\\",\\"hidesWhenStopped\\":true,\\"size\\":\\"small\\",\\"style\\":[{\\"padding\\":16,\\"flex\\":1},null]},\\"children\\":null}]}"`;
exports[`Storyshots Button primary button 1`] = `"{\\"type\\":\\"View\\",\\"props\\":{\\"accessible\\":true,\\"accessibilityLabel\\":\\"testButton\\",\\"testID\\":\\"testButton\\",\\"focusable\\":true,\\"style\\":{\\"paddingHorizontal\\":14,\\"justifyContent\\":\\"center\\",\\"height\\":48,\\"borderRadius\\":2,\\"marginBottom\\":12,\\"backgroundColor\\":\\"#1d74f5\\",\\"padding\\":10,\\"opacity\\":1}},\\"children\\":[{\\"type\\":\\"Text\\",\\"props\\":{\\"style\\":[{\\"textAlign\\":\\"center\\",\\"backgroundColor\\":\\"transparent\\",\\"fontFamily\\":\\"System\\",\\"fontWeight\\":\\"500\\"},{\\"color\\":\\"#ffffff\\",\\"fontSize\\":16},null],\\"accessibilityLabel\\":\\"Press me!\\"},\\"children\\":[\\"Press me!\\"]}]}"`;
exports[`Storyshots Button secondary button 1`] = `"{\\"type\\":\\"View\\",\\"props\\":{\\"accessible\\":true,\\"accessibilityLabel\\":\\"testButton\\",\\"testID\\":\\"testButton\\",\\"focusable\\":true,\\"style\\":{\\"paddingHorizontal\\":14,\\"justifyContent\\":\\"center\\",\\"height\\":48,\\"borderRadius\\":2,\\"marginBottom\\":12,\\"backgroundColor\\":\\"#ffffff\\",\\"padding\\":10,\\"opacity\\":1}},\\"children\\":[{\\"type\\":\\"Text\\",\\"props\\":{\\"style\\":[{\\"textAlign\\":\\"center\\",\\"backgroundColor\\":\\"transparent\\",\\"fontFamily\\":\\"System\\",\\"fontWeight\\":\\"500\\"},{\\"color\\":\\"#2f343d\\",\\"fontSize\\":16},null],\\"accessibilityLabel\\":\\"Press me!\\"},\\"children\\":[\\"Press me!\\"]}]}"`;

View File

@ -1,26 +1,21 @@
import React from 'react';
import { ButtonProps, StyleSheet, Text } from 'react-native';
import Touchable from 'react-native-platform-touchable';
import { StyleProp, StyleSheet, Text, TextStyle } from 'react-native';
import Touchable, { PlatformTouchableProps } from 'react-native-platform-touchable';
import { TSupportedThemes } from '../../theme';
import { themes } from '../../lib/constants';
import { testProps } from '../../lib/methods/testProps';
import { useTheme } from '../../theme';
import sharedStyles from '../../views/Styles';
import ActivityIndicator from '../ActivityIndicator';
interface IButtonProps extends ButtonProps {
interface IButtonProps extends PlatformTouchableProps {
title: string;
type: string;
onPress(): void;
disabled: boolean;
backgroundColor: string;
loading: boolean;
theme: TSupportedThemes;
color: string;
fontSize: any;
style: any;
styleText?: any;
testID: string;
onPress: () => void;
type?: string;
backgroundColor?: string;
loading?: boolean;
color?: string;
fontSize?: number;
styleText?: StyleProp<TextStyle>[];
}
const styles = StyleSheet.create({
@ -32,7 +27,6 @@ const styles = StyleSheet.create({
marginBottom: 12
},
text: {
fontSize: 16,
...sharedStyles.textMedium,
...sharedStyles.textAlignCenter
},
@ -41,60 +35,50 @@ const styles = StyleSheet.create({
}
});
export default class Button extends React.PureComponent<Partial<IButtonProps>, any> {
static defaultProps = {
title: 'Press me!',
type: 'primary',
onPress: () => alert('It works!'),
disabled: false,
loading: false
};
const Button = ({
type = 'primary',
disabled = false,
loading = false,
fontSize = 16,
title,
onPress,
backgroundColor,
color,
style,
styleText,
testID,
...otherProps
}: IButtonProps): React.ReactElement => {
const { colors } = useTheme();
const isPrimary = type === 'primary';
render() {
const {
title,
type,
onPress,
disabled,
backgroundColor,
color,
loading,
style,
theme,
fontSize,
styleText,
testID,
...otherProps
} = this.props;
const isPrimary = type === 'primary';
let textColor = isPrimary ? themes[theme!].buttonText : themes[theme!].bodyText;
if (color) {
textColor = color;
}
return (
<Touchable
onPress={onPress}
disabled={disabled || loading}
style={[
styles.container,
backgroundColor
? { backgroundColor }
: { backgroundColor: isPrimary ? themes[theme!].actionTintColor : themes[theme!].backgroundColor },
disabled && styles.disabled,
style
]}
{...otherProps}
{...testProps((testID || title) as string)}>
{loading ? (
<ActivityIndicator color={textColor} />
) : (
<Text style={[styles.text, { color: textColor }, fontSize && { fontSize }, styleText]} accessibilityLabel={title}>
{title}
</Text>
)}
</Touchable>
);
let textColor = isPrimary ? colors.buttonText : colors.bodyText;
if (color) {
textColor = color;
}
}
return (
<Touchable
onPress={onPress}
disabled={disabled || loading}
style={[
styles.container,
backgroundColor ? { backgroundColor } : { backgroundColor: isPrimary ? colors.actionTintColor : colors.backgroundColor },
disabled && styles.disabled,
style
]}
accessibilityLabel={title}
{...otherProps}
{...testProps((testID || title) as string)}>
{loading ? (
<ActivityIndicator color={textColor} />
) : (
<Text style={[styles.text, { color: textColor, fontSize }, styleText]} accessibilityLabel={title}>
{title}
</Text>
)}
</Touchable>
);
};
export default Button;

View File

@ -327,7 +327,6 @@ class LoginServices extends React.PureComponent<ILoginServicesProps, ILoginServi
title={collapsed ? I18n.t('Onboarding_more_options') : I18n.t('Onboarding_less_options')}
type='secondary'
onPress={this.toggleServices}
theme={theme}
style={styles.options}
color={themes[theme].actionTintColor}
/>

View File

@ -9,7 +9,7 @@ import { Q } from '@nozbe/watermelondb';
import { TouchableWithoutFeedback } from 'react-native-gesture-handler';
import { generateTriggerId } from '../../lib/methods/actions';
import TextInput, { IThemedTextInput } from '../../presentation/TextInput';
import TextInput, { IThemedTextInput } from '../TextInput';
import { userTyping as userTypingAction } from '../../actions/room';
import styles from './styles';
import database from '../../lib/database';
@ -678,7 +678,12 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
canUploadFile = (file: any) => {
const { permissionToUpload } = this.state;
const { FileUpload_MediaTypeWhiteList, FileUpload_MaxFileSize } = this.props;
const result = canUploadFile(file, FileUpload_MediaTypeWhiteList, FileUpload_MaxFileSize, permissionToUpload);
const result = canUploadFile({
file,
allowList: FileUpload_MediaTypeWhiteList,
maxFileSize: FileUpload_MaxFileSize,
permissionToUploadFile: permissionToUpload
});
if (result.success) {
return true;
}
@ -726,7 +731,7 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
chooseFile = async () => {
logEvent(events.ROOM_BOX_ACTION_FILE);
try {
const res = await DocumentPicker.pick({
const res = await DocumentPicker.pickSingle({
type: [DocumentPicker.types.allFiles]
});
const file = {

View File

@ -1,10 +1,10 @@
import React from 'react';
import { StyleProp, ViewStyle } from 'react-native';
import { SvgUri } from 'react-native-svg';
import { useSelector } from 'react-redux';
import { OmnichannelSourceType, IApplicationState, IOmnichannelSource } from '../../definitions';
import { OmnichannelSourceType, IOmnichannelSource } from '../../definitions';
import { STATUS_COLORS } from '../../lib/constants';
import { useAppSelector } from '../../lib/hooks';
import { CustomIcon, TIconsName } from '../CustomIcon';
interface IIconMap {
@ -29,8 +29,8 @@ interface IOmnichannelRoomIconProps {
}
export const OmnichannelRoomIcon = ({ size, style, sourceType, status }: IOmnichannelRoomIconProps) => {
const baseUrl = useSelector((state: IApplicationState) => state.server?.server);
const connected = useSelector((state: IApplicationState) => state.meteor?.connected);
const baseUrl = useAppSelector(state => state.server?.server);
const connected = useAppSelector(state => state.meteor?.connected);
if (sourceType?.type === OmnichannelSourceType.APP && sourceType.id && sourceType.sidebarIcon && connected) {
return (

View File

@ -5,7 +5,7 @@ import Touchable from 'react-native-platform-touchable';
import { themes } from '../lib/constants';
import I18n from '../i18n';
import { CustomIcon } from './CustomIcon';
import TextInput from '../presentation/TextInput';
import TextInput from './TextInput';
import { useTheme } from '../theme';
import { isIOS } from '../utils/deviceInfo';
import sharedStyles from '../views/Styles';

View File

@ -5,7 +5,7 @@ import I18n from '../i18n';
import { useTheme } from '../theme';
import sharedStyles from '../views/Styles';
import { themes } from '../lib/constants';
import TextInput from '../presentation/TextInput';
import TextInput from './TextInput';
import { isIOS, isTablet } from '../utils/deviceInfo';
import { useOrientation } from '../dimensions';
import { testProps } from '../lib/methods/testProps';

View File

@ -1,12 +1,12 @@
import React from 'react';
import { useSelector } from 'react-redux';
import { IApplicationState, TUserStatus } from '../../definitions';
import { TUserStatus } from '../../definitions';
import Status from './Status';
import { IStatus } from './definition';
import { useAppSelector } from '../../lib/hooks';
const StatusContainer = ({ id, style, size = 32, ...props }: Omit<IStatus, 'status'>): React.ReactElement => {
const status = useSelector((state: IApplicationState) =>
const status = useAppSelector(state =>
state.meteor.connected ? state.activeUsers[id] && state.activeUsers[id].status : 'loading'
) as TUserStatus;
return <Status size={size} style={style} status={status} {...props} />;

View File

@ -2,13 +2,13 @@ import React from 'react';
import { StyleProp, StyleSheet, Text, TextInputProps, TextInput as RNTextInput, TextStyle, View, ViewStyle } from 'react-native';
import Touchable from 'react-native-platform-touchable';
import sharedStyles from '../views/Styles';
import TextInput from '../presentation/TextInput';
import { themes } from '../lib/constants';
import { CustomIcon, TIconsName } from './CustomIcon';
import ActivityIndicator from './ActivityIndicator';
import { testProps } from '../lib/methods/testProps';
import { TSupportedThemes } from '../theme';
import sharedStyles from '../../views/Styles';
import TextInput from './index';
import { themes } from '../../lib/constants';
import { CustomIcon, TIconsName } from '../CustomIcon';
import ActivityIndicator from '../ActivityIndicator';
import { TSupportedThemes } from '../../theme';
import { testProps } from '../../lib/methods/testProps';
const styles = StyleSheet.create({
error: {
@ -71,7 +71,7 @@ interface IRCTextInputState {
showPassword: boolean;
}
export default class RCTextInput extends React.PureComponent<IRCTextInputProps, IRCTextInputState> {
export default class FormTextInput extends React.PureComponent<IRCTextInputProps, IRCTextInputState> {
static defaultProps = {
error: {},
theme: 'light'

View File

@ -3,7 +3,7 @@ import React from 'react';
import { storiesOf } from '@storybook/react-native';
import { View, StyleSheet } from 'react-native';
import TextInput from './TextInput';
import FormTextInput from './FormTextInput';
const styles = StyleSheet.create({
paddingHorizontal: {
@ -23,9 +23,9 @@ const theme = 'light';
stories.add('Short and Long Text', () => (
<>
<View style={styles.paddingHorizontal}>
<TextInput label='Short Text' placeholder='placeholder' value={item.name} theme={theme} />
<FormTextInput label='Short Text' placeholder='placeholder' value={item.name} theme={theme} />
<TextInput label='Long Text' placeholder='placeholder' value={item.longText} theme={theme} />
<FormTextInput label='Long Text' placeholder='placeholder' value={item.longText} theme={theme} />
</View>
</>
));

View File

@ -1,10 +1,10 @@
import React from 'react';
import { I18nManager, StyleProp, StyleSheet, TextInput, TextStyle } from 'react-native';
import { IRCTextInputProps } from '../containers/TextInput';
import { themes } from '../lib/constants';
import { testProps } from '../lib/methods/testProps';
import { TSupportedThemes } from '../theme';
import { IRCTextInputProps } from './FormTextInput';
import { themes } from '../../lib/constants';
import { TSupportedThemes } from '../../theme';
import { testProps } from '../../lib/methods/testProps';
const styles = StyleSheet.create({
input: {

View File

@ -6,7 +6,7 @@ import Modal from 'react-native-modal';
import useDeepCompareEffect from 'use-deep-compare-effect';
import { connect } from 'react-redux';
import TextInput from '../TextInput';
import FormTextInput from '../TextInput/FormTextInput';
import I18n from '../../i18n';
import EventEmitter from '../../utils/events';
import { useTheme } from '../../theme';
@ -114,7 +114,7 @@ const TwoFactor = React.memo(({ isMasterDetail }: { isMasterDetail: boolean }) =
]}>
<Text style={[styles.title, { color }]}>{I18n.t(method?.title || 'Two_Factor_Authentication')}</Text>
{method?.text ? <Text style={[styles.subtitle, { color }]}>{I18n.t(method.text)}</Text> : null}
<TextInput
<FormTextInput
value={code}
theme={theme}
inputRef={(e: any) => InteractionManager.runAfterInteractions(() => e?.getNativeRef()?.focus())}
@ -139,16 +139,8 @@ const TwoFactor = React.memo(({ isMasterDetail }: { isMasterDetail: boolean }) =
backgroundColor={themes[theme].chatComponentBackground}
style={styles.button}
onPress={onCancel}
theme={theme}
/>
<Button
title={I18n.t('Send')}
type='primary'
style={styles.button}
onPress={onSubmit}
theme={theme}
testID='two-factor-send'
/>
<Button title={I18n.t('Send')} type='primary' style={styles.button} onPress={onSubmit} testID='two-factor-send' />
</View>
</View>
</View>

View File

@ -4,10 +4,8 @@ import { BLOCK_CONTEXT } from '@rocket.chat/ui-kit';
import Button from '../Button';
import I18n from '../../i18n';
import { IActions } from './interfaces';
import { useTheme } from '../../theme';
export const Actions = ({ blockId, appId, elements, parser }: IActions) => {
const { theme } = useTheme();
const [showMoreVisible, setShowMoreVisible] = useState(() => elements && elements.length > 5);
const renderedElements = showMoreVisible ? elements?.slice(0, 5) : elements;
@ -18,7 +16,7 @@ export const Actions = ({ blockId, appId, elements, parser }: IActions) => {
return (
<>
<Elements />
{showMoreVisible && <Button theme={theme} title={I18n.t('Show_more')} onPress={() => setShowMoreVisible(false)} />}
{showMoreVisible && <Button title={I18n.t('Show_more')} onPress={() => setShowMoreVisible(false)} />}
</>
);
};

View File

@ -56,9 +56,7 @@ export const DatePicker = ({ element, language, action, context, loading, value,
}
};
let button = placeholder ? (
<Button title={textParser([placeholder])} onPress={() => onShow(!show)} loading={loading} theme={theme} />
) : null;
let button = placeholder ? <Button title={textParser([placeholder])} onPress={() => onShow(!show)} loading={loading} /> : null;
if (context === BLOCK_CONTEXT.FORM) {
button = (

View File

@ -13,7 +13,7 @@ import {
import { BLOCK_CONTEXT } from '@rocket.chat/ui-kit';
import Button from '../../Button';
import TextInput from '../../TextInput';
import FormTextInput from '../../TextInput/FormTextInput';
import { textParser } from '../utils';
import { themes } from '../../../lib/constants';
import I18n from '../../../i18n';
@ -139,7 +139,7 @@ export const MultiSelect = React.memo(
return (
<View style={[styles.modal, { backgroundColor: themes[theme].backgroundColor }]}>
<View style={[styles.content, { backgroundColor: themes[theme].backgroundColor }]}>
<TextInput
<FormTextInput
testID='multi-select-search'
onChangeText={onSearch || onSearchChange}
placeholder={I18n.t('Search')}
@ -157,7 +157,7 @@ export const MultiSelect = React.memo(
});
let button = multiselect ? (
<Button title={`${selected.length} selecteds`} onPress={onShow} loading={loading} theme={theme} />
<Button title={`${selected.length} selecteds`} onPress={onShow} loading={loading} />
) : (
<Input
onPress={onShow}

View File

@ -5,7 +5,7 @@ import { BLOCK_CONTEXT, UiKitParserMessage, UiKitParserModal, uiKitMessage, uiKi
import Markdown, { MarkdownPreview } from '../markdown';
import Button from '../Button';
import TextInput from '../TextInput';
import FormTextInput from '../TextInput/FormTextInput';
import { textParser, useBlockContext } from './utils';
import { themes } from '../../lib/constants';
import sharedStyles from '../../views/Styles';
@ -63,7 +63,6 @@ class MessageParser extends UiKitParserMessage {
button(element: IButton, context: BlockContext) {
const { text, value, actionId, style } = element;
const [{ loading }, action] = useBlockContext(element, context);
const { theme } = useContext(ThemeContext);
return (
<Button
key={actionId}
@ -72,7 +71,6 @@ class MessageParser extends UiKitParserMessage {
loading={loading}
onPress={() => action({ value })}
style={styles.button}
theme={theme}
/>
);
}
@ -171,7 +169,7 @@ class ModalParser extends UiKitParserModal {
const { theme } = useContext(ThemeContext);
const { multiline, actionId, placeholder } = element;
return (
<TextInput
<FormTextInput
key={actionId}
placeholder={plainText(placeholder)}
multiline={multiline}

View File

@ -1,10 +1,9 @@
import React, { PureComponent } from 'react';
import { Image, StyleProp, Text, TextStyle } from 'react-native';
import { Node, Parser } from 'commonmark';
import { Parser } from 'commonmark';
import Renderer from 'commonmark-react-renderer';
import { MarkdownAST } from '@rocket.chat/message-parser';
import I18n from '../../i18n';
import MarkdownLink from './Link';
import MarkdownList from './List';
import MarkdownListItem from './ListItem';
@ -37,7 +36,6 @@ interface IMarkdownProps {
baseUrl?: string;
username?: string;
tmid?: string;
isEdited?: boolean;
numberOfLines?: number;
customEmojis?: boolean;
useRealName?: boolean;
@ -133,9 +131,7 @@ class Markdown extends PureComponent<IMarkdownProps, any> {
table: this.renderTable,
table_row: this.renderTableRow,
table_cell: this.renderTableCell,
editedIndicator: this.renderEditedIndicator
table_cell: this.renderTableCell
},
renderParagraphsInLists: true
});
@ -145,21 +141,6 @@ class Markdown extends PureComponent<IMarkdownProps, any> {
return !!enableMessageParser && !!md;
}
editedMessage = (ast: any) => {
const { isEdited } = this.props;
if (isEdited) {
const editIndicatorNode = new Node('edited_indicator');
if (ast.lastChild && ['heading', 'paragraph'].includes(ast.lastChild.type)) {
ast.lastChild.appendChild(editIndicatorNode);
} else {
const node = new Node('paragraph');
node.appendChild(editIndicatorNode);
ast.appendChild(node);
}
}
};
renderText = ({ context, literal }: { context: []; literal: string }) => {
const { numberOfLines, style = [] } = this.props;
const defaultStyle = [this.isMessageContainsOnlyEmoji ? styles.textBig : {}, ...context.map(type => styles[type])];
@ -274,11 +255,6 @@ class Markdown extends PureComponent<IMarkdownProps, any> {
return <Image style={styles.inlineImage} source={{ uri: encodeURI(src) }} />;
};
renderEditedIndicator = () => {
const { theme } = this.props;
return <Text style={[styles.edited, { color: themes[theme].auxiliaryText }]}> ({I18n.t('edited')})</Text>;
};
renderHeading = ({ children, level }: any) => {
const { numberOfLines, theme } = this.props;
// @ts-ignore
@ -373,7 +349,6 @@ class Markdown extends PureComponent<IMarkdownProps, any> {
let ast = parser.parse(m);
ast = mergeTextNodes(ast);
this.isMessageContainsOnlyEmoji = isOnlyEmoji(m) && emojiCount(m) <= 3;
this.editedMessage(ast);
return this.renderer.render(ast);
}
}

View File

@ -94,10 +94,6 @@ export default StyleSheet.create({
fontSize: 16,
...sharedStyles.textRegular
},
edited: {
fontSize: 14,
...sharedStyles.textRegular
},
heading1: {
...sharedStyles.textBold,
fontSize: 24

View File

@ -43,7 +43,7 @@ const AttachedActions = ({ attachment }: { attachment: IAttachment }) => {
};
if (element.type === 'button') {
return <Button theme={theme} onPress={onPress} title={element.text} />;
return <Button onPress={onPress} title={element.text} />;
}
return null;

View File

@ -59,7 +59,6 @@ const Content = React.memo(
getCustomEmoji={props.getCustomEmoji}
enableMessageParser={user.enableMessageParserEarlyAdoption}
username={user.username}
isEdited={props.isEdited}
channels={props.channels}
mentions={props.mentions}
navToRoomInfo={props.navToRoomInfo}
@ -72,7 +71,7 @@ const Content = React.memo(
}
// If this is a encrypted message and is not a preview
if (props.type === E2E_MESSAGE_TYPE && !isPreview) {
if (props.type === E2E_MESSAGE_TYPE && !isPreview && !props.isHeader) {
content = (
<View style={styles.flex}>
<View style={styles.contentContainer}>{content}</View>

View File

@ -0,0 +1,23 @@
import React, { memo } from 'react';
import { View } from 'react-native';
import { CustomIcon } from '../CustomIcon';
import { useTheme } from '../../theme';
import { themes } from '../../lib/constants';
import styles from './styles';
const Edited = memo(({ isEdited }: { isEdited: boolean }) => {
const { theme } = useTheme();
if (!isEdited) {
return null;
}
return (
<View style={styles.leftIcons}>
<CustomIcon name='edit' size={16} color={themes[theme].auxiliaryText} />
</View>
);
});
export default Edited;

View File

@ -17,7 +17,7 @@ const Encrypted = React.memo(({ type }: { type: string }) => {
}
return (
<Touchable onPress={onEncryptedPress} style={styles.encrypted} hitSlop={BUTTON_HIT_SLOP}>
<Touchable onPress={onEncryptedPress} style={styles.leftIcons} hitSlop={BUTTON_HIT_SLOP}>
<CustomIcon name='encrypted' size={16} color={themes[theme].auxiliaryText} />
</Touchable>
);

View File

@ -20,6 +20,8 @@ import CallButton from './CallButton';
import { themes } from '../../lib/constants';
import { IMessage, IMessageInner, IMessageTouchable } from './interfaces';
import { useTheme } from '../../theme';
import Edited from './Edited';
import MessageError from './MessageError';
const MessageInner = React.memo((props: IMessageInner) => {
const { attachments } = props;
@ -102,6 +104,12 @@ const Message = React.memo((props: IMessage) => {
<View style={[styles.messageContent, props.isHeader && styles.messageContentWithHeader]}>
<MessageInner {...props} />
</View>
{!props.isHeader ? (
<>
<Edited isEdited={props.isEdited} />
<MessageError hasError={props.hasError} />
</>
) : null}
<ReadReceipt isReadReceiptEnabled={props.isReadReceiptEnabled} unread={props.unread || false} />
</View>
</View>

View File

@ -18,8 +18,8 @@ const MessageError = React.memo(
}
return (
<Touchable onPress={onErrorPress} style={styles.errorButton} hitSlop={BUTTON_HIT_SLOP}>
<CustomIcon name='warning' color={themes[theme].dangerColor} size={18} />
<Touchable onPress={onErrorPress} style={styles.leftIcons} hitSlop={BUTTON_HIT_SLOP}>
<CustomIcon name='warning' color={themes[theme].dangerColor} size={16} />
</Touchable>
);
},

View File

@ -8,7 +8,7 @@ import { useTheme } from '../../theme';
const ReadReceipt = React.memo(({ isReadReceiptEnabled, unread }: { isReadReceiptEnabled?: boolean; unread: boolean }) => {
const { theme } = useTheme();
if (isReadReceiptEnabled && !unread && unread !== null) {
return <CustomIcon name='check' color={themes[theme].tintColor} size={15} style={styles.readReceipt} />;
return <CustomIcon name='check' color={themes[theme].tintColor} size={16} style={[styles.leftIcons, styles.readReceipt]} />;
}
return null;
});

View File

@ -11,12 +11,12 @@ import sharedStyles from '../../views/Styles';
import { themes } from '../../lib/constants';
import MessageContext from './Context';
import { fileDownloadAndPreview } from '../../utils/fileDownload';
import { IAttachment } from '../../definitions/IAttachment';
import { TGetCustomEmoji } from '../../definitions/IEmoji';
import { IAttachment, TGetCustomEmoji } from '../../definitions';
import RCActivityIndicator from '../ActivityIndicator';
import Attachments from './Attachments';
import { TSupportedThemes, useTheme } from '../../theme';
import { formatAttachmentUrl } from '../../lib/methods/helpers/formatAttachmentUrl';
import messageStyles from './styles';
const styles = StyleSheet.create({
button: {
@ -44,15 +44,9 @@ const styles = StyleSheet.create({
marginBottom: 8
},
author: {
flex: 1,
fontSize: 16,
...sharedStyles.textMedium
},
time: {
fontSize: 12,
marginLeft: 8,
...sharedStyles.textRegular
},
fieldsContainer: {
flex: 1,
flexWrap: 'wrap',
@ -106,8 +100,8 @@ const Title = React.memo(
{attachment.author_name ? (
<Text style={[styles.author, { color: themes[theme].auxiliaryTintColor }]}>{attachment.author_name}</Text>
) : null}
{time ? <Text style={[messageStyles.time, { color: themes[theme].auxiliaryTintColor }]}>{time}</Text> : null}
{attachment.title ? <Text style={[styles.title, { color: themes[theme].bodyText }]}>{attachment.title}</Text> : null}
{time ? <Text style={[styles.time, { color: themes[theme].auxiliaryTintColor }]}>{time}</Text> : null}
</View>
);
}

View File

@ -11,6 +11,8 @@ import MessageContext from './Context';
import { SYSTEM_MESSAGE_TYPES_WITH_AUTHOR_NAME } from './utils';
import { SubscriptionType } from '../../definitions';
import { IRoomInfoParam } from '../../views/SearchMessagesView';
import Edited from './Edited';
import Encrypted from './Encrypted';
const styles = StyleSheet.create({
container: {
@ -19,7 +21,12 @@ const styles = StyleSheet.create({
justifyContent: 'space-between',
alignItems: 'center'
},
actionIcons: {
flexDirection: 'row',
alignItems: 'center'
},
username: {
flexShrink: 1,
fontSize: 16,
lineHeight: 22,
...sharedStyles.textMedium
@ -29,7 +36,6 @@ const styles = StyleSheet.create({
...sharedStyles.textMedium
},
titleContainer: {
flexShrink: 1,
flexDirection: 'row',
alignItems: 'center'
},
@ -41,7 +47,7 @@ const styles = StyleSheet.create({
interface IMessageUser {
isHeader?: boolean;
hasError?: boolean;
hasError: boolean;
useRealName?: boolean;
author?: {
_id: string;
@ -53,10 +59,11 @@ interface IMessageUser {
timeFormat?: string;
navToRoomInfo?: (navParam: IRoomInfoParam) => void;
type: string;
isEdited: boolean;
}
const User = React.memo(
({ isHeader, useRealName, author, alias, ts, timeFormat, hasError, navToRoomInfo, type, ...props }: IMessageUser) => {
({ isHeader, useRealName, author, alias, ts, timeFormat, hasError, navToRoomInfo, type, isEdited, ...props }: IMessageUser) => {
const { user } = useContext(MessageContext);
const { theme } = useTheme();
@ -99,9 +106,13 @@ const User = React.memo(
<Text style={[styles.username, { color: themes[theme].titleText }]} numberOfLines={1}>
{textContent}
</Text>
<Text style={[messageStyles.time, { color: themes[theme].auxiliaryText }]}>{time}</Text>
</TouchableOpacity>
<Text style={[messageStyles.time, { color: themes[theme].auxiliaryTintColor }]}>{time}</Text>
{hasError ? <MessageError hasError={hasError} {...props} /> : null}
<View style={styles.actionIcons}>
<Encrypted type={type} />
<Edited isEdited={isEdited} />
<MessageError hasError={hasError} {...props} />
</View>
</View>
);
}

View File

@ -256,7 +256,7 @@ class MessageContainer extends React.Component<IMessageContainerProps, IMessageC
get isInfo(): string | boolean {
const { item } = this.props;
if (['e2e', 'discussion-created'].includes(item.t)) {
if (['e2e', 'discussion-created', 'jitsi_call_started'].includes(item.t)) {
return false;
}
return item.t;

View File

@ -60,6 +60,8 @@ export interface IMessageContent {
isIgnored: boolean;
type: string;
comment?: string;
hasError: boolean;
isHeader: boolean;
}
export interface IMessageEmoji {

View File

@ -74,10 +74,6 @@ export default StyleSheet.create({
avatarSmall: {
marginLeft: 16
},
errorButton: {
paddingLeft: 10,
paddingVertical: 5
},
buttonContainer: {
marginTop: 8,
flexDirection: 'row',
@ -133,7 +129,7 @@ export default StyleSheet.create({
...sharedStyles.textRegular
},
time: {
fontSize: 12,
fontSize: 13,
marginLeft: 8,
...sharedStyles.textRegular
},
@ -167,12 +163,13 @@ export default StyleSheet.create({
threadBell: {
marginLeft: 8
},
leftIcons: {
paddingLeft: 5,
paddingVertical: 5
},
readReceipt: {
lineHeight: 20
},
encrypted: {
justifyContent: 'center'
},
threadDetails: {
flex: 1,
marginLeft: 12

View File

@ -56,3 +56,14 @@ export interface IServerAttachment {
url: string;
user: Pick<IUser, '_id' | 'username' | 'name'>;
}
export interface IShareAttachment {
filename: string;
description?: string;
size: number;
mime?: string;
path: string;
canUpload: boolean;
error?: any;
uri: string;
}

View File

@ -18,4 +18,7 @@ export interface ICredentials {
fullName?: AppleAuthenticationFullName | null;
email?: string | null;
identityToken?: string | null;
credentialToken?: string;
saml?: boolean;
cas?: { credentialToken?: string };
}

View File

@ -22,7 +22,7 @@ export interface IVisitorEmail {
export interface ILivechatVisitor extends IRocketChatRecord {
username: string;
ts: Date;
ts: Date | string;
token: string;
department?: string;
name?: string;

View File

@ -89,11 +89,7 @@ export interface IMessageFromServer {
drid?: string;
dcount?: number;
dml: string | Date;
starred?:
| {
_id: string;
}
| boolean;
starred?: boolean;
pinned?: boolean;
pinnedAt?: string | Date;
pinnedBy?: {
@ -120,11 +116,6 @@ export interface IMessage extends IMessageFromServer {
emoji?: string;
status?: number;
pinned?: boolean;
starred?:
| {
_id: string;
}
| boolean;
editedBy?: IEditedBy;
reactions?: IReaction[];
role?: string;
@ -144,6 +135,8 @@ export interface IMessage extends IMessageFromServer {
tshow?: boolean;
comment?: string;
subscription?: { id: string };
user?: string;
editedAt?: string | Date;
}
export type TMessageModel = IMessage & Model;

View File

@ -1,19 +0,0 @@
import rocketchat from '../lib/rocketchat';
export type TRocketChat = typeof rocketchat;
export interface IRocketChat extends TRocketChat {
closeListener: any;
usersListener: any;
notifyAllListener: any;
rolesListener: any;
notifyLoggedListener: any;
activeUsers: any;
_setUserTimer: any;
connectedListener: any;
connectingListener: any;
connectTimeout: any;
sdk: any;
activeUsersSubTimeout: any;
roomsSub: any;
}

View File

@ -59,6 +59,20 @@ export interface IRoom {
waitingResponse?: boolean;
}
export interface IRoomSettings {
roomName?: string;
roomAvatar?: string;
roomDescription?: string;
roomTopic?: string;
roomAnnouncement?: string;
roomType?: SubscriptionType;
readOnly?: boolean;
reactWhenReadOnly?: boolean;
systemMessages?: string[];
joinCode?: string;
encrypted?: boolean;
}
export enum OmnichannelSourceType {
WIDGET = 'widget',
EMAIL = 'email',

View File

@ -22,7 +22,6 @@ export * from './IUser';
export * from './IServer';
export * from './ILoggedUser';
export * from './IServerHistory';
export * from './IRocketChat';
export * from './ICertificate';
export * from './IUrl';
export * from './ICredentials';

1
app/lib/hooks/index.ts Normal file
View File

@ -0,0 +1 @@
export * from './useAppSelector';

View File

@ -0,0 +1,5 @@
import { TypedUseSelectorHook, useSelector } from 'react-redux';
import { IApplicationState } from '../../definitions';
export const useAppSelector: TypedUseSelectorHook<IApplicationState> = useSelector;

View File

@ -6,20 +6,24 @@ import { store as reduxStore } from '../store/auxStore';
import { setActiveUsers } from '../../actions/activeUsers';
import { setUser } from '../../actions/login';
import database from '../database';
import { IRocketChat, IUser } from '../../definitions';
import { IUser } from '../../definitions';
import sdk from '../services/sdk';
import { compareServerVersion } from './helpers/compareServerVersion';
export function subscribeUsersPresence(this: IRocketChat) {
export const _activeUsersSubTimeout: { activeUsersSubTimeout: boolean | ReturnType<typeof setTimeout> } = {
activeUsersSubTimeout: false
};
export function subscribeUsersPresence() {
const serverVersion = reduxStore.getState().server.version as string;
// if server is lower than 1.1.0
if (compareServerVersion(serverVersion, 'lowerThan', '1.1.0')) {
if (this.activeUsersSubTimeout) {
clearTimeout(this.activeUsersSubTimeout);
this.activeUsersSubTimeout = false;
if (_activeUsersSubTimeout.activeUsersSubTimeout) {
clearTimeout(_activeUsersSubTimeout.activeUsersSubTimeout as number);
_activeUsersSubTimeout.activeUsersSubTimeout = false;
}
this.activeUsersSubTimeout = setTimeout(() => {
_activeUsersSubTimeout.activeUsersSubTimeout = setTimeout(() => {
sdk.subscribe('activeUsers');
}, 5000);
} else if (compareServerVersion(serverVersion, 'lowerThan', '4.1.0')) {

View File

@ -0,0 +1,4 @@
import { LISTENER } from '../../../containers/Toast';
import EventEmitter from '../../../utils/events';
export const showToast = (message: string): void => EventEmitter.emit(LISTENER, { message });

View File

@ -34,3 +34,4 @@ export * from './userPreferences';
export * from './userPreferencesMethods';
export * from './crashReport';
export * from './parseSettings';
export * from './subscribeRooms';

View File

@ -8,11 +8,13 @@ import { BASIC_AUTH_KEY } from '../../utils/fetch';
import database, { getDatabase } from '../database';
import { isSsl } from '../../utils/url';
import log from '../../utils/log';
import { ICertificate, IRocketChat } from '../../definitions';
import { ICertificate } from '../../definitions';
import sdk from '../services/sdk';
import { CURRENT_SERVER, E2E_PRIVATE_KEY, E2E_PUBLIC_KEY, E2E_RANDOM_PASSWORD_KEY, TOKEN_KEY } from '../constants';
import UserPreferences from './userPreferences';
import { Services } from '../services';
import { roomsSubscription } from './subscriptions/rooms';
import { _activeUsersSubTimeout } from '.';
function removeServerKeys({ server, userId }: { server: string; userId?: string | null }) {
UserPreferences.removeItem(`${TOKEN_KEY}-${server}`);
@ -98,15 +100,14 @@ export async function removeServer({ server }: { server: string }): Promise<void
}
}
export async function logout(this: IRocketChat, { server }: { server: string }): Promise<void> {
if (this.roomsSub) {
this.roomsSub.stop();
this.roomsSub = null;
export async function logout({ server }: { server: string }): Promise<void> {
if (roomsSubscription?.stop) {
roomsSubscription.stop();
}
if (this.activeUsersSubTimeout) {
clearTimeout(this.activeUsersSubTimeout);
this.activeUsersSubTimeout = false;
if (_activeUsersSubTimeout.activeUsersSubTimeout) {
clearTimeout(_activeUsersSubTimeout.activeUsersSubTimeout as number);
_activeUsersSubTimeout.activeUsersSubTimeout = false;
}
try {

View File

@ -2,16 +2,35 @@ import { InteractionManager } from 'react-native';
import { setActiveUsers } from '../../actions/activeUsers';
import { setUser } from '../../actions/login';
import { IUser } from '../../definitions';
import { store as reduxStore } from '../store/auxStore';
import { compareServerVersion } from './helpers/compareServerVersion';
// TODO
export function _setUser(this: any, ddpMessage: { fields: any; id: any; cleared: any }) {
this.activeUsers = this.activeUsers || {};
export interface IActiveUsers {
[key: string]: { status: string; statusText?: string } | string | boolean;
msg: string;
collection: string;
id: string;
cleared: boolean;
fields: {
emails: {
address: string;
verified: boolean;
}[];
username: string;
status: string;
};
}
export const _activeUsers = { activeUsers: {} as IActiveUsers };
export const _setUserTimer: { setUserTimer: null | ReturnType<typeof setTimeout> } = { setUserTimer: null };
export function _setUser(ddpMessage: IActiveUsers): void {
_activeUsers.activeUsers = _activeUsers.activeUsers || {};
const { user } = reduxStore.getState().login;
if (ddpMessage.fields && user && user.id === ddpMessage.id) {
reduxStore.dispatch(setUser(ddpMessage.fields));
reduxStore.dispatch(setUser(ddpMessage.fields as Partial<IUser>));
}
if (ddpMessage.cleared && user && user.id === ddpMessage.id) {
@ -20,21 +39,22 @@ export function _setUser(this: any, ddpMessage: { fields: any; id: any; cleared:
const serverVersion = reduxStore.getState().server.version;
if (compareServerVersion(serverVersion, 'lowerThan', '4.1.0')) {
if (!this._setUserTimer) {
this._setUserTimer = setTimeout(() => {
const activeUsersBatch = this.activeUsers;
if (!_setUserTimer.setUserTimer) {
_setUserTimer.setUserTimer = setTimeout(() => {
const activeUsersBatch = _activeUsers.activeUsers;
InteractionManager.runAfterInteractions(() => {
// @ts-ignore
reduxStore.dispatch(setActiveUsers(activeUsersBatch));
});
this._setUserTimer = null;
return (this.activeUsers = {});
_setUserTimer.setUserTimer = null;
_activeUsers.activeUsers = {} as IActiveUsers;
}, 10000);
}
}
if (!ddpMessage.fields) {
this.activeUsers[ddpMessage.id] = { status: 'offline' };
_activeUsers.activeUsers[ddpMessage.id] = { status: 'offline' };
} else if (ddpMessage.fields.status) {
this.activeUsers[ddpMessage.id] = { status: ddpMessage.fields.status };
_activeUsers.activeUsers[ddpMessage.id] = { status: ddpMessage.fields.status };
}
}

View File

@ -1,22 +1,18 @@
import log from '../../utils/log';
import subscribeRoomsTmp from './subscriptions/rooms';
import subscribeRoomsTmp, { roomsSubscription } from './subscriptions/rooms';
// TODO: remove this
export async function subscribeRooms(this: any) {
if (!this.roomsSub) {
export async function subscribeRooms(): Promise<void> {
if (!roomsSubscription?.stop) {
try {
// TODO: We need to change this naming. Maybe move this logic to the SDK?
this.roomsSub = await subscribeRoomsTmp.call(this);
await subscribeRoomsTmp();
} catch (e) {
log(e);
}
}
}
// TODO: remove this
export function unsubscribeRooms(this: any) {
if (this.roomsSub) {
this.roomsSub.stop();
this.roomsSub = null;
export function unsubscribeRooms(): void {
if (roomsSubscription?.stop) {
roomsSubscription.stop();
}
}

View File

@ -44,6 +44,8 @@ let queue: { [key: string]: ISubscription | IRoom } = {};
let subTimer: number | null | false = null;
const WINDOW_TIME = 500;
export let roomsSubscription: { stop: () => void } | null = null;
const createOrUpdateSubscription = async (subscription: ISubscription, room: IServerRoom | IRoom) => {
try {
const db = database.active;
@ -404,6 +406,7 @@ export default function subscribeRooms() {
clearTimeout(subTimer);
subTimer = false;
}
roomsSubscription = null;
};
streamListener = sdk.onStreamData('stream-notify-user', handleStreamMessageReceived);
@ -412,10 +415,8 @@ export default function subscribeRooms() {
// set the server that started this task
subServer = sdk.current.client.host;
sdk.current.subscribeNotifyUser().catch((e: unknown) => console.log(e));
return {
stop: () => stop()
};
roomsSubscription = { stop: () => stop() };
return null;
} catch (e) {
log(e);
return Promise.reject();

View File

@ -1,13 +1,13 @@
import { useEffect, useState } from 'react';
import { useSelector } from 'react-redux';
import { IApplicationState, TServerModel } from '../../definitions';
import database from '../database';
import { useAppSelector } from '../hooks';
export default function useServer() {
const [server, setServer] = useState<TServerModel | null>(null);
const shareServer = useSelector((state: IApplicationState) => state.share.server.server);
const appServer = useSelector((state: IApplicationState) => state.server.server);
const shareServer = useAppSelector((state: IApplicationState) => state.share.server.server);
const appServer = useAppSelector((state: IApplicationState) => state.server.server);
useEffect(() => {
async function init() {

View File

@ -3,7 +3,7 @@ import EJSON from 'ejson';
import { store } from '../store/auxStore';
import { deepLinkingOpen } from '../../actions/deepLinking';
import { isFDroidBuild } from '../constants';
import PushNotification from './push';
import { deviceToken, pushNotificationConfigure, setNotificationsBadgeCount } from './push';
import { INotification, SubscriptionType } from '../../definitions';
interface IEjson {
@ -47,11 +47,11 @@ export const onNotification = (push: INotification): void => {
}
};
export const getDeviceToken = (): string => PushNotification.getDeviceToken();
export const setBadgeCount = (count?: number): void => PushNotification.setBadgeCount(count);
export const getDeviceToken = (): string => deviceToken;
export const setBadgeCount = (count?: number): void => setNotificationsBadgeCount(count);
export const initializePushNotifications = (): Promise<INotification> | undefined => {
if (!isFDroidBuild) {
setBadgeCount();
return PushNotification.configure(onNotification);
return pushNotificationConfigure(onNotification);
}
};

View File

@ -13,76 +13,62 @@ import { isIOS } from '../../utils/deviceInfo';
import { store as reduxStore } from '../store/auxStore';
import I18n from '../../i18n';
class PushNotification {
onNotification: (notification: any) => void;
deviceToken: string;
constructor() {
this.onNotification = () => {};
this.deviceToken = '';
if (isIOS) {
// init
Notifications.ios.registerRemoteNotifications();
export let deviceToken = '';
// setCategories
const notificationAction = new NotificationAction('REPLY_ACTION', 'background', I18n.t('Reply'), true, {
buttonTitle: I18n.t('Reply'),
placeholder: I18n.t('Type_message')
});
const notificationCategory = new NotificationCategory('MESSAGE', [notificationAction]);
Notifications.setCategories([notificationCategory]);
export const setNotificationsBadgeCount = (count = 0): void => {
if (isIOS) {
Notifications.ios.setBadgeCount(count);
}
};
export const pushNotificationConfigure = (onNotification: (notification: INotification) => void): Promise<any> => {
if (isIOS) {
// init
Notifications.ios.registerRemoteNotifications();
// setCategories
const notificationAction = new NotificationAction('REPLY_ACTION', 'background', I18n.t('Reply'), true, {
buttonTitle: I18n.t('Reply'),
placeholder: I18n.t('Type_message')
});
const notificationCategory = new NotificationCategory('MESSAGE', [notificationAction]);
Notifications.setCategories([notificationCategory]);
} else {
// init
Notifications.android.registerRemoteNotifications();
}
Notifications.events().registerRemoteNotificationsRegistered((event: Registered) => {
deviceToken = event.deviceToken;
});
Notifications.events().registerRemoteNotificationsRegistrationFailed((event: RegistrationError) => {
// TODO: Handle error
console.log(event);
});
Notifications.events().registerNotificationReceivedForeground(
(notification: Notification, completion: (response: NotificationCompletion) => void) => {
completion({ alert: false, sound: false, badge: false });
}
);
Notifications.events().registerNotificationOpened((notification: Notification, completion: () => void) => {
if (isIOS) {
const { background } = reduxStore.getState().app;
if (background) {
onNotification(notification);
}
} else {
// init
Notifications.android.registerRemoteNotifications();
onNotification(notification);
}
completion();
});
Notifications.events().registerRemoteNotificationsRegistered((event: Registered) => {
this.deviceToken = event.deviceToken;
});
Notifications.events().registerRemoteNotificationsRegistrationFailed((event: RegistrationError) => {
// TODO: Handle error
console.log(event);
});
Notifications.events().registerNotificationReceivedForeground(
(notification: Notification, completion: (response: NotificationCompletion) => void) => {
completion({ alert: false, sound: false, badge: false });
}
);
Notifications.events().registerNotificationOpened((notification: Notification, completion: () => void) => {
if (isIOS) {
const { background } = reduxStore.getState().app;
if (background) {
this.onNotification(notification);
}
} else {
this.onNotification(notification);
}
completion();
});
Notifications.events().registerNotificationReceivedBackground(
(notification: Notification, completion: (response: any) => void) => {
completion({ alert: true, sound: true, badge: false });
}
);
}
getDeviceToken() {
return this.deviceToken;
}
setBadgeCount = (count = 0) => {
if (isIOS) {
Notifications.ios.setBadgeCount(count);
Notifications.events().registerNotificationReceivedBackground(
(notification: Notification, completion: (response: any) => void) => {
completion({ alert: true, sound: true, badge: false });
}
};
);
configure(onNotification: (notification: INotification) => void): Promise<any> {
this.onNotification = onNotification;
return Notifications.getInitialNotification();
}
}
export default new PushNotification();
return Notifications.getInitialNotification();
};

View File

@ -1,16 +0,0 @@
import { _setUser } from './methods/setUser';
import { logout } from './methods/logout';
import { subscribeRooms, unsubscribeRooms } from './methods/subscribeRooms';
import { subscribeUsersPresence } from './methods/getUsersPresence';
import { connect } from './services/connect';
const RocketChat = {
logout,
subscribeRooms,
unsubscribeRooms,
_setUser,
subscribeUsersPresence,
connect
};
export default RocketChat;

View File

@ -1,5 +1,5 @@
import RNFetchBlob from 'rn-fetch-blob';
import { settings as RocketChatSettings } from '@rocket.chat/sdk';
import { settings as RocketChatSettings, Rocketchat as RocketchatClient } from '@rocket.chat/sdk';
import { sanitizedRaw } from '@nozbe/watermelondb/RawRecord';
import { InteractionManager } from 'react-native';
import { Q } from '@nozbe/watermelondb';
@ -14,8 +14,7 @@ import { store } from '../store/auxStore';
import { loginRequest, setLoginServices, setUser } from '../../actions/login';
import sdk from './sdk';
import I18n from '../../i18n';
import RocketChat from '../rocketchat';
import { ICredentials, ILoggedUser, IRocketChat, STATUSES } from '../../definitions';
import { ICredentials, ILoggedUser, STATUSES } from '../../definitions';
import { isIOS } from '../../utils/deviceInfo';
import { connectRequest, connectSuccess, disconnect as disconnectAction } from '../../actions/connect';
import { updatePermission } from '../../actions/permissions';
@ -24,7 +23,8 @@ import { updateSettings } from '../../actions/settings';
import { defaultSettings, MIN_ROCKETCHAT_VERSION } from '../constants';
import { compareServerVersion } from '../methods/helpers/compareServerVersion';
import { onRolesChanged } from '../methods/getRoles';
import { getSettings } from '../methods';
import { isSsl } from '../../utils/url';
import { getSettings, IActiveUsers, unsubscribeRooms, _activeUsers, _setUser, _setUserTimer } from '../methods';
interface IServices {
[index: string]: string | boolean;
@ -35,11 +35,15 @@ interface IServices {
service: string;
}
// FIXME: Remove `this` context
function connect(
this: IRocketChat,
{ server, logoutOnError = false }: { server: string; logoutOnError: boolean }
): Promise<void> {
let connectingListener: any;
let connectedListener: any;
let closeListener: any;
let usersListener: any;
let notifyAllListener: any;
let rolesListener: any;
let notifyLoggedListener: any;
function connect({ server, logoutOnError = false }: { server: string; logoutOnError: boolean }): Promise<void> {
return new Promise<void>(resolve => {
if (sdk.current?.client?.host === server) {
return resolve();
@ -49,39 +53,40 @@ function connect(
store.dispatch(connectRequest());
if (this.connectTimeout) {
clearTimeout(this.connectTimeout);
// It's not called anywhere else
// if (this.connectTimeout) {
// clearTimeout(this.connectTimeout);
// }
if (connectingListener) {
connectingListener.then(stopListener);
}
if (this.connectingListener) {
this.connectingListener.then(stopListener);
if (connectedListener) {
connectedListener.then(stopListener);
}
if (this.connectedListener) {
this.connectedListener.then(stopListener);
if (closeListener) {
closeListener.then(stopListener);
}
if (this.closeListener) {
this.closeListener.then(stopListener);
if (usersListener) {
usersListener.then(stopListener);
}
if (this.usersListener) {
this.usersListener.then(stopListener);
if (notifyAllListener) {
notifyAllListener.then(stopListener);
}
if (this.notifyAllListener) {
this.notifyAllListener.then(stopListener);
if (rolesListener) {
rolesListener.then(stopListener);
}
if (this.rolesListener) {
this.rolesListener.then(stopListener);
if (notifyLoggedListener) {
notifyLoggedListener.then(stopListener);
}
if (this.notifyLoggedListener) {
this.notifyLoggedListener.then(stopListener);
}
this.unsubscribeRooms();
unsubscribeRooms();
EventEmitter.emit('INQUIRY_UNSUBSCRIBE');
@ -97,11 +102,11 @@ function connect(
console.log('connect error', err);
});
this.connectingListener = sdk.current.onStreamData('connecting', () => {
connectingListener = sdk.current.onStreamData('connecting', () => {
store.dispatch(connectRequest());
});
this.connectedListener = sdk.current.onStreamData('connected', () => {
connectedListener = sdk.current.onStreamData('connected', () => {
const { connected } = store.getState().meteor;
if (connected) {
return;
@ -113,16 +118,16 @@ function connect(
}
});
this.closeListener = sdk.current.onStreamData('close', () => {
closeListener = sdk.current.onStreamData('close', () => {
store.dispatch(disconnectAction());
});
this.usersListener = sdk.current.onStreamData(
usersListener = sdk.current.onStreamData(
'users',
protectedFunction((ddpMessage: any) => RocketChat._setUser(ddpMessage))
protectedFunction((ddpMessage: any) => _setUser(ddpMessage))
);
this.notifyAllListener = sdk.current.onStreamData(
notifyAllListener = sdk.current.onStreamData(
'stream-notify-all',
protectedFunction(async (ddpMessage: { fields: { args?: any; eventName: string } }) => {
const { eventName } = ddpMessage.fields;
@ -150,7 +155,7 @@ function connect(
})
);
this.rolesListener = sdk.current.onStreamData(
rolesListener = sdk.current.onStreamData(
'stream-roles',
protectedFunction((ddpMessage: any) => onRolesChanged(ddpMessage))
);
@ -171,27 +176,29 @@ function connect(
}
});
this.notifyLoggedListener = sdk.current.onStreamData(
notifyLoggedListener = sdk.current.onStreamData(
'stream-notify-logged',
protectedFunction(async (ddpMessage: { fields: { args?: any; eventName?: any } }) => {
const { eventName } = ddpMessage.fields;
// `user-status` event is deprecated after RC 4.1 in favor of `stream-user-presence/${uid}`
if (/user-status/.test(eventName)) {
this.activeUsers = this.activeUsers || {};
if (!this._setUserTimer) {
this._setUserTimer = setTimeout(() => {
const activeUsersBatch = this.activeUsers;
_activeUsers.activeUsers = _activeUsers.activeUsers || {};
if (!_setUserTimer.setUserTimer) {
_setUserTimer.setUserTimer = setTimeout(() => {
const activeUsersBatch = _activeUsers.activeUsers;
InteractionManager.runAfterInteractions(() => {
// @ts-ignore
store.dispatch(setActiveUsers(activeUsersBatch));
});
this._setUserTimer = null;
return (this.activeUsers = {});
_setUserTimer.setUserTimer = null;
_activeUsers.activeUsers = {} as IActiveUsers;
return null;
}, 10000);
}
const userStatus = ddpMessage.fields.args[0];
const [id, , status, statusText] = userStatus;
this.activeUsers[id] = { status: STATUSES[status], statusText };
_activeUsers.activeUsers[id] = { status: STATUSES[status], statusText };
const { user: loggedUser } = store.getState().login;
if (loggedUser && loggedUser.id === id) {
@ -419,10 +426,10 @@ async function getServerInfo(server: string) {
}
async function getWebsocketInfo({ server }: { server: string }) {
sdk.initialize(server);
const websocketSdk = new RocketchatClient({ host: server, protocol: 'ddp', useSsl: isSsl(server) });
try {
await sdk.current.connect();
await websocketSdk.connect();
} catch (err: any) {
if (err.message && err.message.includes('400')) {
return {
@ -432,7 +439,7 @@ async function getWebsocketInfo({ server }: { server: string }) {
}
}
sdk.disconnect();
websocketSdk.disconnect();
return {
success: true

View File

@ -16,10 +16,9 @@ import { TParams } from '../../definitions/ILivechatEditView';
import { store as reduxStore } from '../store/auxStore';
import { getDeviceToken } from '../notifications';
import { getBundleId, isIOS } from '../../utils/deviceInfo';
import { RoomTypes, roomTypeToApiType } from '../methods';
import { RoomTypes, roomTypeToApiType, unsubscribeRooms } from '../methods';
import sdk from './sdk';
import { compareServerVersion } from '../methods/helpers/compareServerVersion';
import RocketChat from '../rocketchat';
export const createChannel = ({
name,
@ -254,7 +253,7 @@ export const markAsUnread = ({ messageId }: { messageId: string }) =>
// RC 0.65.0
sdk.post('subscriptions.unread', { firstUnreadMessage: { _id: messageId } });
export const toggleStarMessage = (messageId: string, starred: boolean) => {
export const toggleStarMessage = (messageId: string, starred?: boolean) => {
if (starred) {
// RC 0.59.0
return sdk.post('chat.unStarMessage', { messageId });
@ -263,7 +262,7 @@ export const toggleStarMessage = (messageId: string, starred: boolean) => {
return sdk.post('chat.starMessage', { messageId });
};
export const togglePinMessage = (messageId: string, pinned: boolean) => {
export const togglePinMessage = (messageId: string, pinned?: boolean) => {
if (pinned) {
// RC 0.59.0
return sdk.post('chat.unPinMessage', { messageId });
@ -807,7 +806,7 @@ export const emitTyping = (room: IRoom, typing = true) => {
export function e2eResetOwnKey(): Promise<boolean | {}> {
// {} when TOTP is enabled
RocketChat.unsubscribeRooms();
unsubscribeRooms();
// RC 0.72.0
return sdk.methodCallWrapper('e2e.resetOwnE2EKey');

View File

@ -7,7 +7,6 @@ import { appStart } from '../actions/app';
import { selectServerRequest, serverFinishAdd } from '../actions/server';
import { loginFailure, loginSuccess, logout as logoutAction, setUser } from '../actions/login';
import { roomsRequest } from '../actions/rooms';
import RocketChat from '../lib/rocketchat';
import log, { events, logEvent } from '../utils/log';
import I18n, { setLanguage } from '../i18n';
import database from '../lib/database';
@ -30,14 +29,16 @@ import {
getSlashCommands,
getUserPresence,
isOmnichannelModuleAvailable,
subscribeSettings
logout,
subscribeSettings,
subscribeUsersPresence
} from '../lib/methods';
import { Services } from '../lib/services';
const getServer = state => state.server.server;
const loginWithPasswordCall = args => Services.loginWithPassword(args);
const loginCall = (credentials, isFromWebView) => Services.login(credentials, isFromWebView);
const logoutCall = args => RocketChat.logout(args);
const logoutCall = args => logout(args);
const handleLoginRequest = function* handleLoginRequest({ credentials, logoutOnError = false, isFromWebView = false }) {
logEvent(events.LOGIN_DEFAULT_LOGIN);
@ -114,7 +115,7 @@ const registerPushTokenFork = function* registerPushTokenFork() {
};
const fetchUsersPresenceFork = function* fetchUsersPresenceFork() {
RocketChat.subscribeUsersPresence();
subscribeUsersPresence();
};
const fetchEnterpriseModulesFork = function* fetchEnterpriseModulesFork({ user }) {

View File

@ -7,9 +7,8 @@ import { roomsFailure, roomsRefresh, roomsSuccess } from '../actions/rooms';
import database from '../lib/database';
import log from '../utils/log';
import mergeSubscriptionsRooms from '../lib/methods/helpers/mergeSubscriptionsRooms';
import RocketChat from '../lib/rocketchat';
import buildMessage from '../lib/methods/helpers/buildMessage';
import { getRooms } from '../lib/methods';
import { getRooms, subscribeRooms } from '../lib/methods';
const updateRooms = function* updateRooms({ server, newRoomsUpdatedAt }) {
const serversDB = database.servers;
@ -30,7 +29,7 @@ const updateRooms = function* updateRooms({ server, newRoomsUpdatedAt }) {
const handleRoomsRequest = function* handleRoomsRequest({ params }) {
try {
const serversDB = database.servers;
RocketChat.subscribeRooms();
subscribeRooms();
const newRoomsUpdatedAt = new Date();
let roomsUpdatedAt;
const server = yield select(state => state.server.server);

View File

@ -11,7 +11,6 @@ import { selectServerFailure, selectServerRequest, selectServerSuccess, serverFa
import { clearSettings } from '../actions/settings';
import { clearUser, setUser } from '../actions/login';
import { clearActiveUsers } from '../actions/activeUsers';
import RocketChat from '../lib/rocketchat';
import database from '../lib/database';
import log, { logServerVersion } from '../utils/log';
import I18n from '../i18n';
@ -25,6 +24,7 @@ import { RootEnum } from '../definitions';
import { CERTIFICATE_KEY, CURRENT_SERVER, TOKEN_KEY } from '../lib/constants';
import { getLoginSettings, setCustomEmojis, setEnterpriseModules, setPermissions, setRoles, setSettings } from '../lib/methods';
import { Services } from '../lib/services';
import { connect } from '../lib/services/connect';
const getServerInfo = function* getServerInfo({ server, raiseError = true }) {
try {
@ -115,11 +115,11 @@ const handleSelectServer = function* handleSelectServer({ server, version, fetch
if (user) {
yield put(clearSettings());
yield put(setUser(user));
yield RocketChat.connect({ server, logoutOnError: true });
yield connect({ server, logoutOnError: true });
yield put(appStart({ root: RootEnum.ROOT_INSIDE }));
} else {
yield put(clearUser());
yield RocketChat.connect({ server });
yield connect({ server });
yield put(appStart({ root: RootEnum.ROOT_OUTSIDE }));
}

View File

@ -1,11 +1,11 @@
import { select, takeLatest } from 'redux-saga/effects';
import Push from '../lib/notifications/push';
import log from '../utils/log';
import { localAuthenticate, saveLastLocalAuthenticationSession } from '../utils/localAuthentication';
import { APP_STATE } from '../actions/actionsTypes';
import { RootEnum } from '../definitions';
import { Services } from '../lib/services';
import { setBadgeCount } from '../lib/notifications';
const appHasComeBackToForeground = function* appHasComeBackToForeground() {
const appRoot = yield select(state => state.app.root);
@ -20,7 +20,7 @@ const appHasComeBackToForeground = function* appHasComeBackToForeground() {
try {
yield localAuthenticate(server.server);
Services.checkAndReopen();
Push.setBadgeCount();
setBadgeCount();
return yield Services.setUserPresenceOnline();
} catch (e) {
log(e);

View File

@ -101,7 +101,7 @@ const ChatsStackNavigator = () => {
options={SearchMessagesView.navigationOptions}
/>
<ChatsStack.Screen name='SelectedUsersView' component={SelectedUsersView} />
<ChatsStack.Screen name='InviteUsersView' component={InviteUsersView} options={InviteUsersView.navigationOptions} />
<ChatsStack.Screen name='InviteUsersView' component={InviteUsersView} />
<ChatsStack.Screen
name='InviteUsersEditView'
component={InviteUsersEditView}
@ -131,7 +131,7 @@ const ChatsStackNavigator = () => {
component={AddExistingChannelView}
options={AddExistingChannelView.navigationOptions}
/>
<ChatsStack.Screen name='MarkdownTableView' component={MarkdownTableView} options={MarkdownTableView.navigationOptions} />
<ChatsStack.Screen name='MarkdownTableView' component={MarkdownTableView} />
<ChatsStack.Screen name='ReadReceiptsView' component={ReadReceiptsView} options={ReadReceiptsView.navigationOptions} />
<ChatsStack.Screen name='QueueListView' component={QueueListView} options={QueueListView.navigationOptions} />
<ChatsStack.Screen name='CannedResponsesListView' component={CannedResponsesListView} />
@ -198,7 +198,7 @@ const AdminPanelStackNavigator = () => {
return (
<AdminPanelStack.Navigator
screenOptions={{ ...defaultHeader, ...themedHeader(theme), ...StackAnimation } as StackNavigationOptions}>
<AdminPanelStack.Screen name='AdminPanelView' component={AdminPanelView} options={AdminPanelView.navigationOptions} />
<AdminPanelStack.Screen name='AdminPanelView' component={AdminPanelView} />
</AdminPanelStack.Navigator>
);
};

View File

@ -129,7 +129,7 @@ const ModalStackNavigator = React.memo(({ navigation }: INavigation) => {
options={SearchMessagesView.navigationOptions}
/>
<ModalStack.Screen name='SelectedUsersView' component={SelectedUsersView} />
<ModalStack.Screen name='InviteUsersView' component={InviteUsersView} options={InviteUsersView.navigationOptions} />
<ModalStack.Screen name='InviteUsersView' component={InviteUsersView} />
<ModalStack.Screen name='AddChannelTeamView' component={AddChannelTeamView} />
<ModalStack.Screen
name='AddExistingChannelView'
@ -166,7 +166,7 @@ const ModalStackNavigator = React.memo(({ navigation }: INavigation) => {
<ModalStack.Screen name='ThreadMessagesView' component={ThreadMessagesView} />
<ModalStack.Screen name='DiscussionsView' component={DiscussionsView} />
<ModalStack.Screen name='TeamChannelsView' component={TeamChannelsView} options={TeamChannelsView.navigationOptions} />
<ModalStack.Screen name='MarkdownTableView' component={MarkdownTableView} options={MarkdownTableView.navigationOptions} />
<ModalStack.Screen name='MarkdownTableView' component={MarkdownTableView} />
<ModalStack.Screen
name='ReadReceiptsView'
component={ReadReceiptsView}
@ -196,11 +196,7 @@ const ModalStackNavigator = React.memo(({ navigation }: INavigation) => {
options={props => ProfileView.navigationOptions!({ ...props, isMasterDetail: true })}
/>
<ModalStack.Screen name='DisplayPrefsView' component={DisplayPrefsView} />
<ModalStack.Screen
name='AdminPanelView'
component={AdminPanelView}
options={props => AdminPanelView.navigationOptions!({ ...props, isMasterDetail: true })}
/>
<ModalStack.Screen name='AdminPanelView' component={AdminPanelView} />
<ModalStack.Screen name='NewMessageView' component={NewMessageView} options={NewMessageView.navigationOptions} />
<ModalStack.Screen name='SelectedUsersViewCreateChannel' component={SelectedUsersView} />
<ModalStack.Screen name='CreateChannelView' component={CreateChannelView} options={CreateChannelView.navigationOptions} />

View File

@ -28,7 +28,7 @@ const _OutsideStack = () => {
<Outside.Screen name='ForgotPasswordView' component={ForgotPasswordView} options={ForgotPasswordView.navigationOptions} />
<Outside.Screen name='SendEmailConfirmationView' component={SendEmailConfirmationView} />
<Outside.Screen name='RegisterView' component={RegisterView} options={RegisterView.navigationOptions} />
<Outside.Screen name='LegalView' component={LegalView} options={LegalView.navigationOptions} />
<Outside.Screen name='LegalView' component={LegalView} />
</Outside.Navigator>
);
};

View File

@ -10,6 +10,7 @@ import { ISubscription, SubscriptionType, TSubscriptionModel } from '../definiti
import { ICannedResponse } from '../definitions/ICannedResponse';
import { TDataSelect } from '../definitions/IDataSelect';
import { ModalStackParamList } from './MasterDetailStack/types';
import { TThreadModel } from '../definitions';
export type ChatsStackParamList = {
ModalStackNavigator: NavigatorScreenParams<ModalStackParamList>;
@ -246,7 +247,7 @@ export type InsideStackParamList = {
serverInfo: IServer;
text: string;
room: TSubscriptionModel;
thread: any; // TODO: Change
thread: TThreadModel;
};
ModalBlockView: {
data: any; // TODO: Change;

View File

@ -2,8 +2,6 @@ import { Platform } from 'react-native';
import DeviceInfo from 'react-native-device-info';
import { settings as RocketChatSettings } from '@rocket.chat/sdk';
import RocketChat from '../lib/rocketchat';
export type TMethods = 'POST' | 'GET' | 'DELETE' | 'PUT' | 'post' | 'get' | 'delete' | 'put';
interface CustomHeaders {
@ -46,13 +44,11 @@ export default (url: string, options: IOptions = {}): Promise<Response> => {
if (options && options.headers) {
customOptions = { ...customOptions, headers: { ...options.headers, ...customOptions.headers } };
}
// TODO: Refactor when migrate rocketchat.js
// @ts-ignore
// WHAT?
if (RocketChat.controller) {
// @ts-ignore
const { signal } = RocketChat.controller;
customOptions = { ...customOptions, signal };
}
// TODO: Check if this really works and if anyone else has complained about this problem.
// if (RocketChat.controller) {
// // @ts-ignore
// const { signal } = RocketChat.controller;
// customOptions = { ...customOptions, signal };
// }
return fetch(url, customOptions);
};

View File

@ -142,13 +142,13 @@ export default {
SE_CONTACT_US: 'se_contact_us',
SE_CONTACT_US_F: 'se_contact_us_f',
SE_GO_LANGUAGE: 'se_go_language',
SE_REVIEW_THIS_APP: 'se_review_this_app',
SE_REVIEW_THIS_APP_F: 'se_review_this_app_f',
SE_SHARE_THIS_APP: 'se_share_this_app',
SE_GO_DEFAULTBROWSER: 'se_go_default_browser',
SE_GO_THEME: 'se_go_theme',
SE_GO_PROFILE: 'se_go_profile',
SE_GO_SECURITYPRIVACY: 'se_go_securityprivacy',
SE_REVIEW_THIS_APP: 'se_review_this_app',
SE_REVIEW_THIS_APP_F: 'se_review_this_app_f',
SE_SHARE_THIS_APP: 'se_share_this_app',
SE_READ_LICENSE: 'se_read_license',
SE_COPY_APP_VERSION: 'se_copy_app_version',
SE_COPY_SERVER_VERSION: 'se_copy_server_version',

View File

@ -1,15 +1,20 @@
import { IAttachment } from '../views/ShareView/interfaces';
import { IShareAttachment } from '../definitions';
export const canUploadFile = (
file: IAttachment,
allowList: string,
maxFileSize: number,
permissionToUploadFile: boolean
): { success: boolean; error?: string } => {
export const canUploadFile = ({
file,
allowList,
maxFileSize,
permissionToUploadFile
}: {
file: IShareAttachment;
allowList?: string;
maxFileSize?: number;
permissionToUploadFile: boolean;
}): { success: boolean; error?: string } => {
if (!(file && file.path)) {
return { success: true };
}
if (maxFileSize > -1 && file.size > maxFileSize) {
if (maxFileSize && maxFileSize > -1 && file.size > maxFileSize) {
return { success: false, error: 'error-file-too-large' };
}
if (!permissionToUploadFile) {

View File

@ -29,8 +29,7 @@ const RCSSLPinning = Platform.select({
pickCertificate: () =>
new Promise(async (resolve, reject) => {
try {
const res = await DocumentPicker.pick({
// @ts-ignore
const res = await DocumentPicker.pickSingle({
type: ['com.rsa.pkcs-12']
});
const { uri, name } = res;

View File

@ -1,7 +1,7 @@
import React, { useEffect } from 'react';
import { StackNavigationOptions, StackNavigationProp } from '@react-navigation/stack';
import { RouteProp } from '@react-navigation/native';
import { connect } from 'react-redux';
import { RouteProp, useNavigation, useRoute } from '@react-navigation/native';
import { useSelector } from 'react-redux';
import { CompositeNavigationProp } from '@react-navigation/core';
import * as List from '../containers/List';
@ -10,15 +10,14 @@ import * as HeaderButton from '../containers/HeaderButton';
import SafeAreaView from '../containers/SafeAreaView';
import I18n from '../i18n';
import { ChatsStackParamList, DrawerParamList, NewMessageStackParamList } from '../stacks/types';
import { IApplicationState } from '../definitions';
interface IAddChannelTeamView {
navigation: CompositeNavigationProp<
StackNavigationProp<ChatsStackParamList, 'AddChannelTeamView'>,
CompositeNavigationProp<StackNavigationProp<NewMessageStackParamList>, StackNavigationProp<DrawerParamList>>
>;
route: RouteProp<ChatsStackParamList, 'AddChannelTeamView'>;
isMasterDetail: boolean;
}
type TRoute = RouteProp<ChatsStackParamList, 'AddChannelTeamView'>;
type TNavigation = CompositeNavigationProp<
StackNavigationProp<ChatsStackParamList, 'AddChannelTeamView'>,
CompositeNavigationProp<StackNavigationProp<NewMessageStackParamList>, StackNavigationProp<DrawerParamList>>
>;
const setHeader = ({
navigation,
@ -38,12 +37,14 @@ const setHeader = ({
navigation.setOptions(options);
};
const AddChannelTeamView = ({ navigation, route, isMasterDetail }: IAddChannelTeamView) => {
const { teamId, teamChannels } = route.params;
const AddChannelTeamView = () => {
const navigation = useNavigation<TNavigation>();
const isMasterDetail = useSelector((state: IApplicationState) => state.app.isMasterDetail);
const { teamChannels, teamId } = useRoute<TRoute>().params;
useEffect(() => {
setHeader({ navigation, isMasterDetail });
}, []);
}, [isMasterDetail, navigation]);
return (
<SafeAreaView testID='add-channel-team-view'>
@ -80,8 +81,4 @@ const AddChannelTeamView = ({ navigation, route, isMasterDetail }: IAddChannelTe
);
};
const mapStateToProps = (state: any) => ({
isMasterDetail: state.app.isMasterDetail
});
export default connect(mapStateToProps)(AddChannelTeamView);
export default AddChannelTeamView;

View File

@ -21,7 +21,7 @@ import { goRoom } from '../utils/goRoom';
import { showErrorAlert } from '../utils/info';
import debounce from '../utils/debounce';
import { ChatsStackParamList } from '../stacks/types';
import { TSubscriptionModel, SubscriptionType } from '../definitions';
import { TSubscriptionModel, SubscriptionType, IApplicationState } from '../definitions';
import { getRoomTitle, hasPermission } from '../lib/methods';
import { Services } from '../lib/services';
@ -37,7 +37,7 @@ interface IAddExistingChannelViewProps {
route: RouteProp<ChatsStackParamList, 'AddExistingChannelView'>;
theme: TSupportedThemes;
isMasterDetail: boolean;
addTeamChannelPermission: string[];
addTeamChannelPermission?: string[];
}
const QUERY_SIZE = 50;
@ -222,7 +222,7 @@ class AddExistingChannelView extends React.Component<IAddExistingChannelViewProp
}
}
const mapStateToProps = (state: any) => ({
const mapStateToProps = (state: IApplicationState) => ({
isMasterDetail: state.app.isMasterDetail,
addTeamChannelPermission: state.permissions['add-team-channel']
});

View File

@ -1,55 +1,45 @@
import React from 'react';
import React, { useEffect } from 'react';
import { useNavigation } from '@react-navigation/native';
import { WebView } from 'react-native-webview';
import { connect } from 'react-redux';
import { DrawerScreenProps } from '@react-navigation/drawer';
import { StackNavigationOptions } from '@react-navigation/stack';
import { useSelector } from 'react-redux';
import { StackNavigationProp } from '@react-navigation/stack';
import I18n from '../../i18n';
import StatusBar from '../../containers/StatusBar';
import * as HeaderButton from '../../containers/HeaderButton';
import { withTheme } from '../../theme';
import { getUserSelector } from '../../selectors/login';
import SafeAreaView from '../../containers/SafeAreaView';
import { AdminPanelStackParamList } from '../../stacks/types';
import { IApplicationState } from '../../definitions';
interface IAdminPanelViewProps {
baseUrl: string;
token: string;
}
const AdminPanelView = () => {
const navigation = useNavigation<StackNavigationProp<AdminPanelStackParamList, 'AdminPanelView'>>();
const baseUrl = useSelector((state: IApplicationState) => state.server.server);
const token = useSelector((state: IApplicationState) => getUserSelector(state).token);
const isMasterDetail = useSelector((state: IApplicationState) => state.app.isMasterDetail);
interface INavigationOptions {
navigation: DrawerScreenProps<AdminPanelStackParamList, 'AdminPanelView'>;
isMasterDetail: boolean;
}
useEffect(() => {
navigation.setOptions({
headerLeft: isMasterDetail ? undefined : () => <HeaderButton.Drawer navigation={navigation} />,
title: I18n.t('Admin_Panel')
});
}, [isMasterDetail, navigation]);
class AdminPanelView extends React.Component<IAdminPanelViewProps, any> {
static navigationOptions = ({ navigation, isMasterDetail }: INavigationOptions): StackNavigationOptions => ({
headerLeft: isMasterDetail ? undefined : () => <HeaderButton.Drawer navigation={navigation} />,
title: I18n.t('Admin_Panel')
});
render() {
const { baseUrl, token } = this.props;
if (!baseUrl) {
return null;
}
return (
<SafeAreaView>
<StatusBar />
<WebView
// https://github.com/react-native-community/react-native-webview/issues/1311
onMessage={() => {}}
source={{ uri: `${baseUrl}/admin/info?layout=embedded` }}
injectedJavaScript={`Meteor.loginWithToken('${token}', function() { })`}
/>
</SafeAreaView>
);
if (!baseUrl) {
return null;
}
}
const mapStateToProps = (state: any) => ({
baseUrl: state.server.server,
token: getUserSelector(state).token
});
return (
<SafeAreaView>
<StatusBar />
<WebView
// https://github.com/react-native-community/react-native-webview/issues/1311
onMessage={() => {}}
source={{ uri: `${baseUrl}/admin/info?layout=embedded` }}
injectedJavaScript={`Meteor.loginWithToken('${token}', function() { })`}
/>
</SafeAreaView>
);
};
export default connect(mapStateToProps)(withTheme(AdminPanelView));
export default AdminPanelView;

View File

@ -26,6 +26,7 @@ import StatusBar from '../containers/StatusBar';
import { InsideStackParamList } from '../stacks/types';
import { IAttachment } from '../definitions/IAttachment';
import { formatAttachmentUrl } from '../lib/methods/helpers/formatAttachmentUrl';
import { IApplicationState, IUser } from '../definitions';
const styles = StyleSheet.create({
container: {
@ -46,10 +47,7 @@ interface IAttachmentViewProps {
width: number;
height: number;
insets: { left: number; bottom: number; right: number; top: number };
user: {
id: string;
token: string;
};
user: IUser;
Allow_Save_Media_to_Gallery: boolean;
}
@ -200,10 +198,10 @@ class AttachmentView extends React.Component<IAttachmentViewProps, IAttachmentVi
}
}
const mapStateToProps = (state: any) => ({
const mapStateToProps = (state: IApplicationState) => ({
baseUrl: state.server.server,
user: getUserSelector(state),
Allow_Save_Media_to_Gallery: state.settings.Allow_Save_Media_to_Gallery ?? true
Allow_Save_Media_to_Gallery: (state.settings.Allow_Save_Media_to_Gallery as boolean) ?? true
});
export default connect(mapStateToProps)(withTheme(withDimensions(withSafeAreaInsets(AttachmentView))));

View File

@ -1,12 +1,11 @@
import React from 'react';
import { ActivityIndicator, StyleSheet, Text, View } from 'react-native';
import { useSelector } from 'react-redux';
import { IApplicationState } from '../definitions';
import I18n from '../i18n';
import StatusBar from '../containers/StatusBar';
import { useTheme } from '../theme';
import sharedStyles from './Styles';
import { useAppSelector } from '../lib/hooks';
const styles = StyleSheet.create({
container: {
@ -23,7 +22,7 @@ const styles = StyleSheet.create({
});
const AuthLoadingView = React.memo((): React.ReactElement => {
const text = useSelector((state: IApplicationState) => state.app.text);
const text = useAppSelector(state => state.app.text);
const { colors } = useTheme();
return (
<View style={[styles.container, { backgroundColor: colors.backgroundColor }]}>

View File

@ -14,6 +14,7 @@ import { TSupportedThemes, withTheme } from '../theme';
import debounce from '../utils/debounce';
import * as HeaderButton from '../containers/HeaderButton';
import { Services } from '../lib/services';
import { IApplicationState, ICredentials } from '../definitions';
const userAgent = isIOS
? 'Mozilla/5.0 (iPhone; CPU iPhone OS 10_3_1 like Mac OS X) AppleWebKit/603.1.30 (KHTML, like Gecko) Version/10.0 Mobile/14E304 Safari/602.1'
@ -93,7 +94,7 @@ class AuthenticationWebView extends React.PureComponent<IAuthenticationWebView,
navigation.pop();
};
login = (params: any) => {
login = (params: ICredentials) => {
const { logging } = this.state;
if (logging) {
return;
@ -111,7 +112,7 @@ class AuthenticationWebView extends React.PureComponent<IAuthenticationWebView,
};
// Force 3s delay so the server has time to evaluate the token
debouncedLogin = debounce((params: any) => this.login(params), 3000);
debouncedLogin = debounce((params: ICredentials) => this.login(params), 3000);
tryLogin = debounce(
async () => {
@ -135,7 +136,7 @@ class AuthenticationWebView extends React.PureComponent<IAuthenticationWebView,
const parsedUrl = parse(url, true);
// ticket -> cas / validate & saml_idp_credentialToken -> saml
if (parsedUrl.pathname?.includes('validate') || parsedUrl.query?.ticket || parsedUrl.query?.saml_idp_credentialToken) {
let payload;
let payload: ICredentials;
if (authType === 'saml') {
const token = parsedUrl.query?.saml_idp_credentialToken || ssoToken;
const credentialToken = { credentialToken: token };
@ -202,10 +203,10 @@ class AuthenticationWebView extends React.PureComponent<IAuthenticationWebView,
}
}
const mapStateToProps = (state: any) => ({
const mapStateToProps = (state: IApplicationState) => ({
server: state.server.server,
Accounts_Iframe_api_url: state.settings.Accounts_Iframe_api_url,
Accounts_Iframe_api_method: state.settings.Accounts_Iframe_api_method
Accounts_Iframe_api_url: state.settings.Accounts_Iframe_api_url as string,
Accounts_Iframe_api_method: state.settings.Accounts_Iframe_api_method as string
});
export default connect(mapStateToProps)(withTheme(AuthenticationWebView));

View File

@ -2,7 +2,6 @@ import React, { useEffect } from 'react';
import { StackNavigationProp } from '@react-navigation/stack';
import { RouteProp } from '@react-navigation/native';
import { StyleSheet, Text, View, ScrollView } from 'react-native';
import { useSelector } from 'react-redux';
import I18n from '../i18n';
import SafeAreaView from '../containers/SafeAreaView';
@ -17,6 +16,7 @@ import { ICannedResponse } from '../definitions/ICannedResponse';
import { ChatsStackParamList } from '../stacks/types';
import sharedStyles from './Styles';
import { getRoomTitle, getUidDirectMessage } from '../lib/methods';
import { useAppSelector } from '../lib/hooks';
const styles = StyleSheet.create({
scroll: {
@ -96,8 +96,8 @@ interface ICannedResponseDetailProps {
const CannedResponseDetail = ({ navigation, route }: ICannedResponseDetailProps): JSX.Element => {
const { cannedResponse } = route?.params;
const { theme } = useTheme();
const { isMasterDetail } = useSelector((state: any) => state.app);
const { rooms } = useSelector((state: any) => state.room);
const { isMasterDetail } = useAppSelector(state => state.app);
const { rooms } = useAppSelector(state => state.room);
useEffect(() => {
navigation.setOptions({
@ -159,13 +159,7 @@ const CannedResponseDetail = ({ navigation, route }: ICannedResponseDetailProps)
</View>
</View>
</View>
<Button
title={I18n.t('Use')}
theme={theme}
style={styles.button}
type='primary'
onPress={() => navigateToRoom(cannedResponse)}
/>
<Button title={I18n.t('Use')} style={styles.button} type='primary' onPress={() => navigateToRoom(cannedResponse)} />
</ScrollView>
</SafeAreaView>
);

View File

@ -40,7 +40,6 @@ const CannedResponseItem = ({
fontSize={12}
color={themes[theme].titleText}
style={[styles.cannedUseButton, { backgroundColor: themes[theme].chatComponentBackground }]}
theme={theme}
onPress={onPressUse}
/>
</View>

File diff suppressed because one or more lines are too long

View File

@ -1,6 +1,5 @@
import React, { useEffect, useState, useCallback } from 'react';
import { FlatList } from 'react-native';
import { useSelector } from 'react-redux';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { RouteProp } from '@react-navigation/native';
import { HeaderBackButton, StackNavigationOptions, StackNavigationProp } from '@react-navigation/stack';
@ -30,8 +29,8 @@ import { ChatsStackParamList } from '../../stacks/types';
import { ISubscription } from '../../definitions/ISubscription';
import { getRoomTitle, getUidDirectMessage } from '../../lib/methods';
import { Services } from '../../lib/services';
import { IApplicationState } from '../../definitions';
import { ILivechatDepartment } from '../../definitions/ILivechatDepartment';
import { useAppSelector } from '../../lib/hooks';
const COUNT = 25;
@ -76,8 +75,8 @@ const CannedResponsesListView = ({ navigation, route }: ICannedResponsesListView
const insets = useSafeAreaInsets();
const { theme } = useTheme();
const isMasterDetail = useSelector((state: IApplicationState) => state.app.isMasterDetail);
const rooms = useSelector((state: IApplicationState) => state.room.rooms);
const isMasterDetail = useAppSelector(state => state.app.isMasterDetail);
const rooms = useAppSelector(state => state.room.rooms);
const getRoomFromDb = async () => {
const { rid } = route.params;

View File

@ -4,7 +4,7 @@ import { FlatList, ScrollView, StyleSheet, Switch, Text, View, SwitchProps } fro
import { dequal } from 'dequal';
import * as List from '../containers/List';
import TextInput from '../presentation/TextInput';
import TextInput from '../containers/TextInput';
import Loading from '../containers/Loading';
import { createChannelRequest } from '../actions/createChannel';
import { removeUser } from '../actions/selectedUsers';

View File

@ -11,7 +11,7 @@ import * as HeaderButton from '../../containers/HeaderButton';
import StatusBar from '../../containers/StatusBar';
import { withTheme } from '../../theme';
import { getUserSelector } from '../../selectors/login';
import TextInput from '../../containers/TextInput';
import FormTextInput from '../../containers/TextInput/FormTextInput';
import Navigation from '../../lib/navigation/appNavigation';
import { createDiscussionRequest, ICreateDiscussionRequestData } from '../../actions/createDiscussion';
import { showErrorAlert } from '../../utils/info';
@ -167,7 +167,7 @@ class CreateChannelView extends React.Component<ICreateChannelViewProps, ICreate
serverVersion={serverVersion}
theme={theme}
/>
<TextInput
<FormTextInput
label={I18n.t('Discussion_name')}
testID='multi-select-discussion-name'
placeholder={I18n.t('A_meaningful_name_for_the_discussion_room')}

View File

@ -1,11 +1,10 @@
import React, { useEffect, useLayoutEffect, useState } from 'react';
import { FlatList, StyleSheet } from 'react-native';
import { useSelector } from 'react-redux';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { HeaderBackButton, StackNavigationOptions, StackNavigationProp } from '@react-navigation/stack';
import { RouteProp } from '@react-navigation/core';
import { IApplicationState, IMessageFromServer } from '../../definitions';
import { IMessageFromServer } from '../../definitions';
import { ChatsStackParamList } from '../../stacks/types';
import ActivityIndicator from '../../containers/ActivityIndicator';
import I18n from '../../i18n';
@ -23,6 +22,7 @@ import SearchHeader from '../../containers/SearchHeader';
import { TThreadModel } from '../../definitions/IThread';
import Item from './Item';
import { Services } from '../../lib/services';
import { useAppSelector } from '../../lib/hooks';
const API_FETCH_COUNT = 50;
@ -42,8 +42,8 @@ const DiscussionsView = ({ navigation, route }: IDiscussionsViewProps): React.Re
const rid = route.params?.rid;
const t = route.params?.t;
const baseUrl = useSelector((state: IApplicationState) => state.server?.server);
const isMasterDetail = useSelector((state: IApplicationState) => state.app?.isMasterDetail);
const baseUrl = useAppSelector(state => state.server?.server);
const isMasterDetail = useAppSelector(state => state.app?.isMasterDetail);
const [loading, setLoading] = useState(false);
const [discussions, setDiscussions] = useState<IMessageFromServer[]>([]);

View File

@ -3,7 +3,7 @@ import { StackNavigationProp } from '@react-navigation/stack';
import { useNavigation } from '@react-navigation/native';
import { Switch } from 'react-native';
import { RadioButton } from 'react-native-ui-lib';
import { useDispatch, useSelector } from 'react-redux';
import { useDispatch } from 'react-redux';
import { setPreference } from '../actions/sortPreferences';
import { DisplayMode, SortBy } from '../lib/constants';
@ -12,21 +12,22 @@ import * as List from '../containers/List';
import { ICON_SIZE } from '../containers/List/constants';
import SafeAreaView from '../containers/SafeAreaView';
import StatusBar from '../containers/StatusBar';
import { IApplicationState, IPreferences } from '../definitions';
import { IPreferences } from '../definitions';
import I18n from '../i18n';
import { SettingsStackParamList } from '../stacks/types';
import { useTheme } from '../theme';
import { events, logEvent } from '../utils/log';
import { saveSortPreference } from '../lib/methods';
import { useAppSelector } from '../lib/hooks';
const DisplayPrefsView = (): React.ReactElement => {
const navigation = useNavigation<StackNavigationProp<SettingsStackParamList, 'DisplayPrefsView'>>();
const { colors } = useTheme();
const { sortBy, groupByType, showFavorites, showUnread, showAvatar, displayMode } = useSelector(
(state: IApplicationState) => state.sortPreferences
const { sortBy, groupByType, showFavorites, showUnread, showAvatar, displayMode } = useAppSelector(
state => state.sortPreferences
);
const { isMasterDetail } = useSelector((state: IApplicationState) => state.app);
const { isMasterDetail } = useAppSelector(state => state.app);
const dispatch = useDispatch();
useEffect(() => {

View File

@ -1,29 +1,29 @@
import React from 'react';
import { StyleSheet, Text, View, TextInput as TextInputComp } from 'react-native';
import { StyleSheet, Text, View, TextInput as RNTextInput } from 'react-native';
import { StackNavigationOptions } from '@react-navigation/stack';
import { connect } from 'react-redux';
import { Dispatch } from 'redux';
import StatusBar from '../containers/StatusBar';
import * as List from '../containers/List';
import I18n from '../i18n';
import log, { events, logEvent } from '../utils/log';
import { TSupportedThemes, withTheme } from '../theme';
import { withTheme } from '../theme';
import SafeAreaView from '../containers/SafeAreaView';
import TextInput from '../containers/TextInput';
import FormTextInput from '../containers/TextInput/FormTextInput';
import Button from '../containers/Button';
import { getUserSelector } from '../selectors/login';
import { PADDING_HORIZONTAL } from '../containers/List/constants';
import { themes } from '../lib/constants';
import { Encryption } from '../lib/encryption';
import { logout as logoutAction } from '../actions/login';
import { logout } from '../actions/login';
import { showConfirmationAlert, showErrorAlert } from '../utils/info';
import EventEmitter from '../utils/events';
import { LISTENER } from '../containers/Toast';
import debounce from '../utils/debounce';
import sharedStyles from './Styles';
import { IUser } from '../definitions';
import { IApplicationState, IBaseScreen, IUser } from '../definitions';
import { Services } from '../lib/services';
import { SettingsStackParamList } from '../stacks/types';
const styles = StyleSheet.create({
container: {
@ -47,12 +47,10 @@ interface IE2EEncryptionSecurityViewState {
newPassword: string;
}
interface IE2EEncryptionSecurityViewProps {
theme?: TSupportedThemes;
interface IE2EEncryptionSecurityViewProps extends IBaseScreen<SettingsStackParamList, 'E2EEncryptionSecurityView'> {
user: IUser;
server: string;
encryptionEnabled: boolean;
logout(): void;
}
class E2EEncryptionSecurityView extends React.Component<IE2EEncryptionSecurityViewProps, IE2EEncryptionSecurityViewState> {
@ -66,7 +64,7 @@ class E2EEncryptionSecurityView extends React.Component<IE2EEncryptionSecurityVi
onChangePasswordText = debounce((text: string) => this.setState({ newPassword: text }), 300);
setNewPasswordRef = (ref: TextInputComp) => (this.newPasswordInputRef = ref);
setNewPasswordRef = (ref: RNTextInput) => (this.newPasswordInputRef = ref);
changePassword = () => {
const { newPassword } = this.state;
@ -107,8 +105,8 @@ class E2EEncryptionSecurityView extends React.Component<IE2EEncryptionSecurityVi
* that's why we're using strict equality to boolean
*/
if (res === true) {
const { logout } = this.props;
logout();
const { dispatch } = this.props;
dispatch(logout());
}
} catch (e) {
log(e);
@ -127,13 +125,13 @@ class E2EEncryptionSecurityView extends React.Component<IE2EEncryptionSecurityVi
return (
<>
<List.Section>
<Text style={[styles.title, { color: themes[theme!].headerTitleColor }]}>
<Text style={[styles.title, { color: themes[theme].headerTitleColor }]}>
{I18n.t('E2E_encryption_change_password_title')}
</Text>
<Text style={[styles.description, { color: themes[theme!].bodyText }]}>
<Text style={[styles.description, { color: themes[theme].bodyText }]}>
{I18n.t('E2E_encryption_change_password_description')}
</Text>
<TextInput
<FormTextInput
inputRef={this.setNewPasswordRef}
placeholder={I18n.t('New_Password')}
returnKeyType='send'
@ -146,7 +144,6 @@ class E2EEncryptionSecurityView extends React.Component<IE2EEncryptionSecurityVi
<Button
onPress={this.changePassword}
title={I18n.t('Save_Changes')}
theme={theme}
disabled={!newPassword.trim()}
style={styles.changePasswordButton}
testID='e2e-encryption-security-view-change-password'
@ -161,25 +158,24 @@ class E2EEncryptionSecurityView extends React.Component<IE2EEncryptionSecurityVi
render() {
const { theme } = this.props;
return (
<SafeAreaView testID='e2e-encryption-security-view' style={{ backgroundColor: themes[theme!].backgroundColor }}>
<SafeAreaView testID='e2e-encryption-security-view' style={{ backgroundColor: themes[theme].backgroundColor }}>
<StatusBar />
<List.Container>
<View style={styles.container}>
{this.renderChangePassword()}
<List.Section>
<Text style={[styles.title, { color: themes[theme!].headerTitleColor }]}>
<Text style={[styles.title, { color: themes[theme].headerTitleColor }]}>
{I18n.t('E2E_encryption_reset_title')}
</Text>
<Text style={[styles.description, { color: themes[theme!].bodyText }]}>
<Text style={[styles.description, { color: themes[theme].bodyText }]}>
{I18n.t('E2E_encryption_reset_description')}
</Text>
<Button
onPress={this.resetOwnKey}
title={I18n.t('E2E_encryption_reset_button')}
theme={theme}
type='secondary'
backgroundColor={themes[theme!].chatComponentBackground}
backgroundColor={themes[theme].chatComponentBackground}
testID='e2e-encryption-security-view-reset-key'
/>
</List.Section>
@ -190,14 +186,10 @@ class E2EEncryptionSecurityView extends React.Component<IE2EEncryptionSecurityVi
}
}
const mapStateToProps = (state: any) => ({
const mapStateToProps = (state: IApplicationState) => ({
server: state.server.server,
user: getUserSelector(state),
encryptionEnabled: state.encryption.enabled
});
const mapDispatchToProps = (dispatch: Dispatch) => ({
logout: () => dispatch(logoutAction(true))
});
export default connect(mapStateToProps, mapDispatchToProps)(withTheme(E2EEncryptionSecurityView));
export default connect(mapStateToProps)(withTheme(E2EEncryptionSecurityView));

View File

@ -9,7 +9,7 @@ import Button from '../containers/Button';
import * as HeaderButton from '../containers/HeaderButton';
import SafeAreaView from '../containers/SafeAreaView';
import StatusBar from '../containers/StatusBar';
import TextInput from '../containers/TextInput';
import FormTextInput from '../containers/TextInput/FormTextInput';
import { IBaseScreen } from '../definitions';
import I18n from '../i18n';
import KeyboardView from '../containers/KeyboardView';
@ -75,7 +75,7 @@ class E2EEnterYourPasswordView extends React.Component<TE2EEnterYourPasswordView
<SafeAreaView
style={[styles.container, { backgroundColor: themes[theme].backgroundColor }]}
testID='e2e-enter-your-password-view'>
<TextInput
<FormTextInput
inputRef={(e: RNTextInput) => {
this.passwordInput = e;
}}
@ -93,7 +93,6 @@ class E2EEnterYourPasswordView extends React.Component<TE2EEnterYourPasswordView
onPress={this.submit}
title={I18n.t('Confirm')}
disabled={!password}
theme={theme}
testID='e2e-enter-your-password-view-confirm'
/>
<Text style={[styles.info, { color: themes[theme].bodyText }]}>{I18n.t('Enter_Your_Encryption_Password_desc1')}</Text>

View File

@ -1,15 +1,14 @@
import React from 'react';
import { StackNavigationProp } from '@react-navigation/stack';
import { RouteProp } from '@react-navigation/native';
import { StyleSheet } from 'react-native';
import SafeAreaView from '../containers/SafeAreaView';
import { themes } from '../lib/constants';
import * as HeaderButton from '../containers/HeaderButton';
import Markdown from '../containers/markdown';
import { TSupportedThemes, withTheme } from '../theme';
import { withTheme } from '../theme';
import I18n from '../i18n';
import { E2ESaveYourPasswordStackParamList } from '../stacks/types';
import { IBaseScreen } from '../definitions';
const styles = StyleSheet.create({
container: {
@ -23,17 +22,10 @@ const styles = StyleSheet.create({
}
});
interface INavigation {
navigation: StackNavigationProp<E2ESaveYourPasswordStackParamList, 'E2EHowItWorksView'>;
route: RouteProp<E2ESaveYourPasswordStackParamList, 'E2EHowItWorksView'>;
}
type TE2EHowItWorksViewProps = IBaseScreen<E2ESaveYourPasswordStackParamList, 'E2EHowItWorksView'>;
interface IE2EHowItWorksViewProps extends INavigation {
theme: TSupportedThemes;
}
class E2EHowItWorksView extends React.Component<IE2EHowItWorksViewProps, any> {
static navigationOptions = ({ route, navigation }: INavigation) => {
class E2EHowItWorksView extends React.Component<TE2EHowItWorksViewProps, any> {
static navigationOptions = ({ route, navigation }: Pick<TE2EHowItWorksViewProps, 'navigation' | 'route'>) => {
const showCloseModal = route.params?.showCloseModal;
return {
title: I18n.t('How_It_Works'),

View File

@ -55,7 +55,7 @@ const styles = StyleSheet.create({
});
interface IE2ESaveYourPasswordViewState {
password: string;
password: string | null;
}
interface IE2ESaveYourPasswordViewProps extends IBaseScreen<E2ESaveYourPasswordStackParamList, 'E2ESaveYourPasswordView'> {
@ -87,7 +87,7 @@ class E2ESaveYourPasswordView extends React.Component<IE2ESaveYourPasswordViewPr
// Set stored password on local state
const password = UserPreferences.getString(`${server}-${E2E_RANDOM_PASSWORD_KEY}`);
if (this.mounted) {
this.setState({ password: password! });
this.setState({ password });
} else {
// @ts-ignore
this.state.password = password;
@ -110,8 +110,10 @@ class E2ESaveYourPasswordView extends React.Component<IE2ESaveYourPasswordViewPr
onCopy = () => {
logEvent(events.E2E_SAVE_PW_COPY);
const { password } = this.state;
Clipboard.setString(password);
EventEmitter.emit(LISTENER, { message: I18n.t('Copied_to_clipboard') });
if (password) {
Clipboard.setString(password);
EventEmitter.emit(LISTENER, { message: I18n.t('Copied_to_clipboard') });
}
};
onHowItWorks = () => {
@ -144,7 +146,6 @@ class E2ESaveYourPasswordView extends React.Component<IE2ESaveYourPasswordViewPr
title={I18n.t('Copy')}
type='secondary'
fontSize={12}
theme={theme}
/>
</View>
<Text style={[styles.info, { color: themes[theme].bodyText }]}>{I18n.t('Save_Your_Encryption_Password_info')}</Text>
@ -153,13 +154,11 @@ class E2ESaveYourPasswordView extends React.Component<IE2ESaveYourPasswordViewPr
style={{ backgroundColor: themes[theme].auxiliaryBackground }}
title={I18n.t('How_It_Works')}
type='secondary'
theme={theme}
testID='e2e-save-password-view-how-it-works'
/>
<Button
onPress={this.onSaved}
title={I18n.t('I_Saved_My_E2E_Password')}
theme={theme}
testID='e2e-save-password-view-saved-password'
/>
</View>

View File

@ -3,7 +3,7 @@ import { Text } from 'react-native';
import Button from '../containers/Button';
import FormContainer, { FormContainerInner } from '../containers/FormContainer';
import TextInput from '../containers/TextInput';
import FormTextInput from '../containers/TextInput/FormTextInput';
import I18n from '../i18n';
import { themes } from '../lib/constants';
import { Services } from '../lib/services';
@ -92,7 +92,7 @@ class ForgotPasswordView extends React.Component<IForgotPasswordViewProps, IForg
<Text style={[sharedStyles.loginTitle, sharedStyles.textBold, { color: themes[theme].titleText }]}>
{I18n.t('Forgot_password')}
</Text>
<TextInput
<FormTextInput
autoFocus
placeholder={I18n.t('Email')}
keyboardType='email-address'
@ -110,7 +110,6 @@ class ForgotPasswordView extends React.Component<IForgotPasswordViewProps, IForg
testID='forgot-password-view-submit'
loading={isFetching}
disabled={invalidEmail}
theme={theme}
/>
</FormContainerInner>
</FormContainer>

View File

@ -121,7 +121,6 @@ class InviteUsersEditView extends React.Component<IInviteUsersEditViewProps, any
};
render() {
const { theme } = this.props;
return (
<SafeAreaView>
<List.Container>
@ -134,7 +133,7 @@ class InviteUsersEditView extends React.Component<IInviteUsersEditViewProps, any
<List.Separator />
</List.Section>
<View style={styles.innerContainer}>
<Button title={I18n.t('Generate_New_Link')} type='primary' onPress={this.createInviteLink} theme={theme} />
<Button title={I18n.t('Generate_New_Link')} type='primary' onPress={this.createInviteLink} />
</View>
</List.Container>
</SafeAreaView>

View File

@ -1,69 +1,58 @@
import { StackNavigationOptions } from '@react-navigation/stack';
import React, { useEffect } from 'react';
import moment from 'moment';
import React from 'react';
import { ScrollView, Share, View } from 'react-native';
import { connect } from 'react-redux';
import { useDispatch, useSelector } from 'react-redux';
import { inviteLinksClear, inviteLinksCreate } from '../../actions/inviteLinks';
import { themes } from '../../lib/constants';
import Button from '../../containers/Button';
import Markdown from '../../containers/markdown';
import SafeAreaView from '../../containers/SafeAreaView';
import StatusBar from '../../containers/StatusBar';
import RCTextInput from '../../containers/TextInput';
import FormTextInput from '../../containers/TextInput/FormTextInput';
import { IApplicationState, IBaseScreen } from '../../definitions';
import I18n from '../../i18n';
import { TInvite } from '../../reducers/inviteLinks';
import { ChatsStackParamList } from '../../stacks/types';
import { withTheme } from '../../theme';
import { useTheme } from '../../theme';
import { events, logEvent } from '../../utils/log';
import scrollPersistTaps from '../../utils/scrollPersistTaps';
import styles from './styles';
interface IInviteUsersViewProps extends IBaseScreen<ChatsStackParamList, 'InviteUsersView'> {
timeDateFormat: string;
invite: TInvite;
}
class InviteUsersView extends React.Component<IInviteUsersViewProps, any> {
static navigationOptions = (): StackNavigationOptions => ({
title: I18n.t('Invite_users')
});
type IInviteUsersViewProps = IBaseScreen<ChatsStackParamList, 'InviteUsersView'>;
private rid: string;
const InviteUsersView = ({ route, navigation }: IInviteUsersViewProps): React.ReactElement => {
const rid = route.params?.rid;
const timeDateFormat = useSelector((state: IApplicationState) => state.settings.Message_TimeAndDateFormat as string);
const invite = useSelector((state: IApplicationState) => state.inviteLinks.invite);
const { colors, theme } = useTheme();
const dispatch = useDispatch();
constructor(props: IInviteUsersViewProps) {
super(props);
this.rid = props.route.params?.rid;
}
useEffect(() => {
navigation.setOptions({
title: I18n.t('Invite_users')
});
}, []);
componentDidMount() {
const { dispatch } = this.props;
dispatch(inviteLinksCreate(this.rid));
}
useEffect(() => {
dispatch(inviteLinksCreate(rid));
return () => {
dispatch(inviteLinksClear());
};
}, []);
componentWillUnmount() {
const { dispatch } = this.props;
dispatch(inviteLinksClear());
}
share = () => {
const share = () => {
logEvent(events.IU_SHARE);
const { invite } = this.props;
if (!invite || !invite.url) {
return;
}
Share.share({ message: invite.url });
};
edit = () => {
const edit = () => {
logEvent(events.IU_GO_IU_EDIT);
const { navigation } = this.props;
navigation.navigate('InviteUsersEditView', { rid: this.rid });
navigation.navigate('InviteUsersEditView', { rid });
};
linkExpirationText = () => {
const { timeDateFormat, invite } = this.props;
const linkExpirationText = () => {
if (!invite || !invite.url) {
return null;
}
@ -90,38 +79,28 @@ class InviteUsersView extends React.Component<IInviteUsersViewProps, any> {
return I18n.t('Your_invite_link_will_never_expire');
};
renderExpiration = () => {
const { theme } = this.props;
const expirationMessage = this.linkExpirationText();
const renderExpiration = () => {
const expirationMessage = linkExpirationText();
return <Markdown msg={expirationMessage} theme={theme} />;
};
render() {
const { theme, invite } = this.props;
return (
<SafeAreaView style={{ backgroundColor: themes[theme].backgroundColor }}>
{/* @ts-ignore*/}
<ScrollView
{...scrollPersistTaps}
style={{ backgroundColor: themes[theme].auxiliaryBackground }}
showsVerticalScrollIndicator={false}>
<StatusBar />
<View style={styles.innerContainer}>
<RCTextInput label={I18n.t('Invite_Link')} theme={theme} value={invite && invite.url} editable={false} />
{this.renderExpiration()}
<View style={[styles.divider, { backgroundColor: themes[theme].separatorColor }]} />
<Button title={I18n.t('Share_Link')} type='primary' onPress={this.share} theme={theme} />
<Button title={I18n.t('Edit_Invite')} type='secondary' onPress={this.edit} theme={theme} />
</View>
</ScrollView>
</SafeAreaView>
);
}
}
return (
<SafeAreaView style={{ backgroundColor: colors.backgroundColor }}>
<ScrollView
{...scrollPersistTaps}
style={{ backgroundColor: colors.auxiliaryBackground }}
showsVerticalScrollIndicator={false}>
<StatusBar />
<View style={styles.innerContainer}>
<FormTextInput label={I18n.t('Invite_Link')} theme={theme} value={invite && invite.url} editable={false} />
{renderExpiration()}
<View style={[styles.divider, { backgroundColor: colors.separatorColor }]} />
<Button title={I18n.t('Share_Link')} type='primary' onPress={share} />
<Button title={I18n.t('Edit_Invite')} type='secondary' onPress={edit} />
</View>
</ScrollView>
</SafeAreaView>
);
};
const mapStateToProps = (state: IApplicationState) => ({
timeDateFormat: state.settings.Message_TimeAndDateFormat as string,
invite: state.inviteLinks.invite
});
export default connect(mapStateToProps)(withTheme(InviteUsersView));
export default InviteUsersView;

View File

@ -1,62 +1,60 @@
import React from 'react';
import { connect } from 'react-redux';
import { StackNavigationOptions } from '@react-navigation/stack';
import React, { useEffect } from 'react';
import { useSelector } from 'react-redux';
import I18n from '../i18n';
import StatusBar from '../containers/StatusBar';
import openLink from '../utils/openLink';
import { TSupportedThemes, withTheme } from '../theme';
import { useTheme } from '../theme';
import SafeAreaView from '../containers/SafeAreaView';
import * as List from '../containers/List';
import { OutsideParamList } from '../stacks/types';
import { IBaseScreen, IApplicationState } from '../definitions';
interface ILegalView {
interface ILegalViewProps extends IBaseScreen<OutsideParamList, 'LegalView'> {
server: string;
theme: TSupportedThemes;
}
class LegalView extends React.Component<ILegalView, any> {
static navigationOptions = (): StackNavigationOptions => ({
title: I18n.t('Legal')
});
const LegalView = ({ navigation }: ILegalViewProps): React.ReactElement => {
const server = useSelector((state: IApplicationState) => state.server.server);
const { theme } = useTheme();
onPressItem = ({ route }: { route: string }) => {
const { server, theme } = this.props;
useEffect(() => {
navigation.setOptions({
title: I18n.t('Legal')
});
}, []);
const onPressItem = ({ route }: { route: string }) => {
if (!server) {
return;
}
openLink(`${server}/${route}`, theme);
};
render() {
return (
<SafeAreaView testID='legal-view'>
<StatusBar />
<List.Container>
<List.Section>
<List.Separator />
<List.Item
title='Terms_of_Service'
onPress={() => this.onPressItem({ route: 'terms-of-service' })}
testID='legal-terms-button'
showActionIndicator
/>
<List.Separator />
<List.Item
title='Privacy_Policy'
onPress={() => this.onPressItem({ route: 'privacy-policy' })}
testID='legal-privacy-button'
showActionIndicator
/>
<List.Separator />
</List.Section>
</List.Container>
</SafeAreaView>
);
}
}
return (
<SafeAreaView testID='legal-view'>
<StatusBar />
<List.Container>
<List.Section>
<List.Separator />
<List.Item
title='Terms_of_Service'
onPress={() => onPressItem({ route: 'terms-of-service' })}
testID='legal-terms-button'
showActionIndicator
/>
<List.Separator />
<List.Item
title='Privacy_Policy'
onPress={() => onPressItem({ route: 'privacy-policy' })}
testID='legal-privacy-button'
showActionIndicator
/>
<List.Separator />
</List.Section>
</List.Container>
</SafeAreaView>
);
};
const mapStateToProps = (state: any) => ({
server: state.server.server
});
export default connect(mapStateToProps)(withTheme(LegalView));
export default LegalView;

View File

@ -7,7 +7,7 @@ import { BLOCK_CONTEXT } from '@rocket.chat/ui-kit';
import { TSupportedThemes, withTheme } from '../theme';
import { themes } from '../lib/constants';
import TextInput from '../containers/TextInput';
import FormTextInput from '../containers/TextInput/FormTextInput';
import KeyboardView from '../containers/KeyboardView';
import I18n from '../i18n';
import { LISTENER } from '../containers/Toast';
@ -193,7 +193,7 @@ const LivechatEditView = ({
<ScrollView {...scrollPersistTaps} style={styles.container}>
<SafeAreaView>
<Title title={visitor?.username} theme={theme} />
<TextInput
<FormTextInput
label={I18n.t('Name')}
defaultValue={visitor?.name}
onChangeText={text => onChangeText('name', text)}
@ -203,7 +203,7 @@ const LivechatEditView = ({
theme={theme}
editable={!!permissions[0]}
/>
<TextInput
<FormTextInput
label={I18n.t('Email')}
inputRef={e => {
inputs.name = e;
@ -216,7 +216,7 @@ const LivechatEditView = ({
theme={theme}
editable={!!permissions[0]}
/>
<TextInput
<FormTextInput
label={I18n.t('Phone')}
inputRef={e => {
inputs.phone = e;
@ -236,7 +236,7 @@ const LivechatEditView = ({
editable={!!permissions[0]}
/>
{Object.entries(customFields?.visitor || {}).map(([key, value], index, array) => (
<TextInput
<FormTextInput
label={key}
defaultValue={value}
inputRef={e => {
@ -254,7 +254,7 @@ const LivechatEditView = ({
/>
))}
<Title title={I18n.t('Conversation')} theme={theme} />
<TextInput
<FormTextInput
label={I18n.t('Topic')}
inputRef={e => {
inputs.topic = e;
@ -280,7 +280,7 @@ const LivechatEditView = ({
/>
{Object.entries(customFields?.livechat || {}).map(([key, value], index, array: any) => (
<TextInput
<FormTextInput
label={key}
defaultValue={value}
inputRef={e => {
@ -298,7 +298,7 @@ const LivechatEditView = ({
/>
))}
<Button title={I18n.t('Save')} onPress={submit} theme={theme} />
<Button title={I18n.t('Save')} onPress={submit} />
</SafeAreaView>
</ScrollView>
</KeyboardView>

View File

@ -9,7 +9,7 @@ import Button from '../containers/Button';
import FormContainer, { FormContainerInner } from '../containers/FormContainer';
import * as HeaderButton from '../containers/HeaderButton';
import LoginServices from '../containers/LoginServices';
import TextInput from '../containers/TextInput';
import FormTextInput from '../containers/TextInput/FormTextInput';
import { IApplicationState, IBaseScreen } from '../definitions';
import I18n from '../i18n';
import { OutsideParamList } from '../stacks/types';
@ -157,7 +157,7 @@ class LoginView extends React.Component<ILoginViewProps, ILoginViewState> {
return (
<>
<Text style={[styles.title, sharedStyles.textBold, { color: themes[theme].titleText }]}>{I18n.t('Login')}</Text>
<TextInput
<FormTextInput
label={I18n.t('Username_or_email')}
containerStyle={styles.inputContainer}
placeholder={Accounts_EmailOrUsernamePlaceholder || I18n.t('Username_or_email')}
@ -173,7 +173,7 @@ class LoginView extends React.Component<ILoginViewProps, ILoginViewState> {
theme={theme}
value={user}
/>
<TextInput
<FormTextInput
label={I18n.t('Password')}
containerStyle={styles.inputContainer}
inputRef={e => {
@ -196,7 +196,6 @@ class LoginView extends React.Component<ILoginViewProps, ILoginViewState> {
testID='login-view-submit'
loading={isFetching}
disabled={!this.valid()}
theme={theme}
style={styles.loginButton}
/>
{Accounts_PasswordReset && (
@ -205,7 +204,6 @@ class LoginView extends React.Component<ILoginViewProps, ILoginViewState> {
type='secondary'
onPress={this.forgotPassword}
testID='login-view-forgot-password'
theme={theme}
color={themes[theme].auxiliaryText}
fontSize={14}
/>

View File

@ -1,43 +1,38 @@
import React from 'react';
import React, { useEffect } from 'react';
import { ScrollView } from 'react-native';
import { StackNavigationOptions } from '@react-navigation/stack';
import { RouteProp } from '@react-navigation/native';
import I18n from '../i18n';
import { isIOS } from '../utils/deviceInfo';
import { themes } from '../lib/constants';
import { TSupportedThemes, withTheme } from '../theme';
import { useTheme } from '../theme';
import { ChatsStackParamList } from '../stacks/types';
import { IBaseScreen } from '../definitions';
interface IMarkdownTableViewProps {
route: RouteProp<ChatsStackParamList, 'MarkdownTableView'>;
theme: TSupportedThemes;
}
type IMarkdownTableViewProps = IBaseScreen<ChatsStackParamList, 'MarkdownTableView'>;
class MarkdownTableView extends React.Component<IMarkdownTableViewProps> {
static navigationOptions = (): StackNavigationOptions => ({
title: I18n.t('Table')
});
const MarkdownTableView = ({ navigation, route }: IMarkdownTableViewProps): React.ReactElement => {
const renderRows = route.params?.renderRows;
const tableWidth = route.params?.tableWidth;
const { colors } = useTheme();
render() {
const { route, theme } = this.props;
const renderRows = route.params?.renderRows;
const tableWidth = route.params?.tableWidth;
if (isIOS) {
return (
<ScrollView style={{ backgroundColor: themes[theme].backgroundColor }} contentContainerStyle={{ width: tableWidth }}>
{renderRows()}
</ScrollView>
);
}
useEffect(() => {
navigation.setOptions({
title: I18n.t('Table')
});
}, []);
if (isIOS) {
return (
<ScrollView style={{ backgroundColor: themes[theme].backgroundColor }}>
<ScrollView horizontal>{renderRows()}</ScrollView>
<ScrollView style={{ backgroundColor: colors.backgroundColor }} contentContainerStyle={{ width: tableWidth }}>
{renderRows()}
</ScrollView>
);
}
}
export default withTheme(MarkdownTableView);
return (
<ScrollView style={{ backgroundColor: colors.backgroundColor }}>
<ScrollView horizontal>{renderRows()}</ScrollView>
</ScrollView>
);
};
export default MarkdownTableView;

View File

@ -20,7 +20,17 @@ import getThreadName from '../../lib/methods/getThreadName';
import styles from './styles';
import { ChatsStackParamList } from '../../stacks/types';
import { IRoomInfoParam } from '../SearchMessagesView';
import { TMessageModel, IEmoji, ISubscription, SubscriptionType, IUrl } from '../../definitions';
import {
IApplicationState,
TMessageModel,
IEmoji,
ISubscription,
SubscriptionType,
IAttachment,
IMessage,
TAnyMessageModel,
IUrl
} from '../../definitions';
import { Services } from '../../lib/services';
interface IMessagesViewProps {
@ -37,36 +47,19 @@ interface IMessagesViewProps {
route: RouteProp<ChatsStackParamList, 'MessagesView'>;
customEmojis: { [key: string]: IEmoji };
theme: TSupportedThemes;
showActionSheet: Function;
showActionSheet: (params: { options: string[]; hasCancel: boolean }) => void;
useRealName: boolean;
isMasterDetail: boolean;
}
interface IMessagesViewState {
loading: boolean;
messages: [];
messages: IMessage[];
message?: IMessage;
fileLoading: boolean;
total: number;
}
interface IMessageItem {
u?: string;
user?: string;
editedAt?: Date;
attachments?: any;
_id: string;
tmid?: string;
ts?: Date;
uploadedAt?: Date;
name?: string;
description?: string;
msg?: string;
starred: boolean;
pinned: boolean;
type: string;
url: string;
}
interface IParams {
rid: string;
t: SubscriptionType;
@ -75,24 +68,25 @@ interface IParams {
name?: string;
fname?: string;
prid?: string;
room: ISubscription;
room?: ISubscription;
jumpToMessageId?: string;
jumpToThreadId?: string;
roomUserId?: string;
}
class MessagesView extends React.Component<IMessagesViewProps, any> {
class MessagesView extends React.Component<IMessagesViewProps, IMessagesViewState> {
private rid: string;
private t: SubscriptionType;
private content: any;
private room: any;
private room?: ISubscription;
constructor(props: IMessagesViewProps) {
super(props);
this.state = {
loading: false,
messages: [],
fileLoading: true
fileLoading: true,
total: 0
};
this.setHeader();
this.rid = props.route.params?.rid;
@ -104,7 +98,7 @@ class MessagesView extends React.Component<IMessagesViewProps, any> {
this.load();
}
shouldComponentUpdate(nextProps: IMessagesViewProps, nextState: any) {
shouldComponentUpdate(nextProps: IMessagesViewProps, nextState: IMessagesViewState) {
const { loading, messages, fileLoading } = this.state;
const { theme } = this.props;
if (nextProps.theme !== theme) {
@ -137,7 +131,7 @@ class MessagesView extends React.Component<IMessagesViewProps, any> {
navigation.navigate('RoomInfoView', navParam);
};
jumpToMessage = async ({ item }: { item: IMessageItem }) => {
jumpToMessage = async ({ item }: { item: IMessage }) => {
const { navigation, isMasterDetail } = this.props;
let params: IParams = {
rid: this.rid,
@ -165,7 +159,7 @@ class MessagesView extends React.Component<IMessagesViewProps, any> {
defineMessagesViewContent = (name: string) => {
const { user, baseUrl, theme, useRealName } = this.props;
const renderItemCommonProps = (item: IMessageItem) => ({
const renderItemCommonProps = (item: TAnyMessageModel) => ({
item,
baseUrl,
user,
@ -224,8 +218,7 @@ class MessagesView extends React.Component<IMessagesViewProps, any> {
},
noDataMsg: I18n.t('No_mentioned_messages'),
testID: 'mentioned-messages-view',
// @ts-ignore TODO: unify IMessage
renderItem: (item: IMessageItem) => <Message {...renderItemCommonProps(item)} msg={item.msg} theme={theme} />
renderItem: (item: TAnyMessageModel) => <Message {...renderItemCommonProps(item)} msg={item.msg} theme={theme} />
},
// Starred Messages Screen
Starred: {
@ -236,16 +229,15 @@ class MessagesView extends React.Component<IMessagesViewProps, any> {
},
noDataMsg: I18n.t('No_starred_messages'),
testID: 'starred-messages-view',
renderItem: (item: IMessageItem) => (
// @ts-ignore TODO: unify IMessage
renderItem: (item: TAnyMessageModel) => (
<Message {...renderItemCommonProps(item)} msg={item.msg} onLongPress={() => this.onLongPress(item)} theme={theme} />
),
action: (message: IMessageItem) => ({
action: (message: IMessage) => ({
title: I18n.t('Unstar'),
icon: message.starred ? 'star-filled' : 'star',
onPress: this.handleActionPress
}),
handleActionPress: (message: IMessageItem) => Services.toggleStarMessage(message._id, message.starred)
handleActionPress: (message: IMessage) => Services.toggleStarMessage(message._id, message.starred)
},
// Pinned Messages Screen
Pinned: {
@ -256,14 +248,12 @@ class MessagesView extends React.Component<IMessagesViewProps, any> {
},
noDataMsg: I18n.t('No_pinned_messages'),
testID: 'pinned-messages-view',
renderItem: (item: IMessageItem) => (
// @ts-ignore TODO: unify IMessage
renderItem: (item: TAnyMessageModel) => (
<Message {...renderItemCommonProps(item)} msg={item.msg} onLongPress={() => this.onLongPress(item)} theme={theme} />
),
action: () => ({ title: I18n.t('Unpin'), icon: 'pin', onPress: this.handleActionPress }),
handleActionPress: (message: IMessageItem) => Services.togglePinMessage(message._id, message.pinned)
handleActionPress: (message: IMessage) => Services.togglePinMessage(message._id, message.pinned)
}
// @ts-ignore
}[name];
};
@ -316,12 +306,12 @@ class MessagesView extends React.Component<IMessagesViewProps, any> {
return null;
};
showAttachment = (attachment: any) => {
showAttachment = (attachment: IAttachment) => {
const { navigation } = this.props;
navigation.navigate('AttachmentView', { attachment });
};
onLongPress = (message: IMessageItem) => {
onLongPress = (message: IMessage) => {
this.setState({ message }, this.showActionSheet);
};
@ -338,7 +328,7 @@ class MessagesView extends React.Component<IMessagesViewProps, any> {
const result = await this.content.handleActionPress(message);
if (result.success) {
this.setState((prevState: IMessagesViewState) => ({
messages: prevState.messages.filter((item: IMessageItem) => item._id !== message._id),
messages: prevState.messages.filter((item: IMessage) => item._id !== message?._id),
total: prevState.total - 1
}));
}
@ -360,7 +350,7 @@ class MessagesView extends React.Component<IMessagesViewProps, any> {
);
};
renderItem = ({ item }: { item: IMessageItem }) => this.content.renderItem(item);
renderItem = ({ item }: { item: IMessage }) => this.content.renderItem(item);
render() {
const { messages, loading } = this.state;
@ -386,7 +376,7 @@ class MessagesView extends React.Component<IMessagesViewProps, any> {
}
}
const mapStateToProps = (state: any) => ({
const mapStateToProps = (state: IApplicationState) => ({
baseUrl: state.server.server,
user: getUserSelector(state),
customEmojis: state.customEmojis,

View File

@ -171,16 +171,11 @@ class ModalBlockView extends React.Component<IModalBlockViewProps, IModalBlockVi
}
};
cancel = async ({ closeModal }: { closeModal?: () => void }) => {
cancel = async () => {
const { data } = this.state;
const { appId, viewId, view } = data;
// handle tablet case
if (closeModal) {
closeModal();
} else {
Navigation.back();
}
Navigation.back();
try {
await triggerCancel({

View File

@ -1,7 +1,7 @@
import React, { useState } from 'react';
import { FlatList, StyleSheet, TextInputProps, View } from 'react-native';
import TextInput from '../../../containers/TextInput';
import FormTextInput from '../../../containers/TextInput/FormTextInput';
import * as List from '../../../containers/List';
import { themes } from '../../../lib/constants';
import I18n from '../../../i18n';
@ -51,7 +51,7 @@ const ServerInput = ({
const [focused, setFocused] = useState(false);
return (
<View style={styles.container}>
<TextInput
<FormTextInput
label={I18n.t('Enter_workspace_URL')}
placeholder={I18n.t('Workspace_URL_Example')}
containerStyle={styles.inputContainer}

Some files were not shown because too many files have changed in this diff Show More