Merge 4.26.1 into single-server (#3981)

This commit is contained in:
Diego Mello 2022-03-29 15:53:27 -03:00 committed by GitHub
parent fc9e9a4f2a
commit 9d2175485c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
400 changed files with 7918 additions and 5349 deletions

1
.gitignore vendored
View File

@ -64,5 +64,6 @@ artifacts
.vscode/ .vscode/
e2e/docker/rc_test_env/docker-compose.yml e2e/docker/rc_test_env/docker-compose.yml
e2e/docker/data/db e2e/docker/data/db
e2e/e2e_account.js
*.p8 *.p8

View File

@ -20,6 +20,26 @@ jest.mock('react-native-file-viewer', () => ({
jest.mock('../app/lib/database', () => jest.fn(() => null)); jest.mock('../app/lib/database', () => jest.fn(() => null));
global.Date.now = jest.fn(() => new Date('2019-10-10').getTime()); global.Date.now = jest.fn(() => new Date('2019-10-10').getTime());
jest.mock('react-native-mmkv-storage', () => {
return {
Loader: jest.fn().mockImplementation(() => {
return {
setProcessingMode: jest.fn().mockImplementation(() => {
return {
withEncryption: jest.fn().mockImplementation(() => {
return {
initialize: jest.fn()
};
})
};
})
};
}),
create: jest.fn(),
MODES: { MULTI_PROCESS: '' }
};
});
const converter = new Stories2SnapsConverter(); const converter = new Stories2SnapsConverter();
initStoryshots({ initStoryshots({

View File

@ -144,7 +144,7 @@ android {
minSdkVersion rootProject.ext.minSdkVersion minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion
versionCode VERSIONCODE as Integer versionCode VERSIONCODE as Integer
versionName "4.25.0" versionName "4.26.1"
vectorDrawables.useSupportLibrary = true vectorDrawables.useSupportLibrary = true
if (!isFoss) { if (!isFoss) {
manifestPlaceholders = [BugsnagAPIKey: BugsnagAPIKey as String] manifestPlaceholders = [BugsnagAPIKey: BugsnagAPIKey as String]

View File

@ -12,7 +12,6 @@ import com.facebook.soloader.SoLoader;
import com.reactnativecommunity.viewpager.RNCViewPagerPackage; import com.reactnativecommunity.viewpager.RNCViewPagerPackage;
import com.facebook.react.bridge.JSIModulePackage; import com.facebook.react.bridge.JSIModulePackage;
import com.swmansion.reanimated.ReanimatedJSIModulePackage; import com.swmansion.reanimated.ReanimatedJSIModulePackage;
import org.unimodules.adapters.react.ModuleRegistryAdapter; import org.unimodules.adapters.react.ModuleRegistryAdapter;
import org.unimodules.adapters.react.ReactModuleRegistryProvider; import org.unimodules.adapters.react.ReactModuleRegistryProvider;
@ -54,7 +53,7 @@ public class MainApplication extends Application implements ReactApplication {
@Override @Override
protected JSIModulePackage getJSIModulePackage() { protected JSIModulePackage getJSIModulePackage() {
return new ReanimatedJSIModulePackage(); // <- add return new ReanimatedJSIModulePackage();
} }
@Override @Override

View File

@ -53,17 +53,9 @@ public class Ejson {
String alias = Utils.toHex("com.MMKV.default"); String alias = Utils.toHex("com.MMKV.default");
// Retrieve container password // Retrieve container password
secureKeystore.getSecureKey(alias, new RNCallback() { String password = secureKeystore.getSecureKey(alias);
@Override
public void invoke(Object... args) {
String error = (String) args[0];
if (error == null) {
String password = (String) args[1];
mmkv = MMKV.mmkvWithID("default", MMKV.SINGLE_PROCESS_MODE, password); mmkv = MMKV.mmkvWithID("default", MMKV.SINGLE_PROCESS_MODE, password);
} }
}
});
}
public String getAvatarUri() { public String getAvatarUri() {
if (type == null) { if (type == null) {

View File

@ -11,7 +11,7 @@ function createRequestTypes(base = {}, types = defaultTypes): Record<string, str
// Login events // Login events
export const LOGIN = createRequestTypes('LOGIN', [...defaultTypes, 'SET_SERVICES', 'SET_PREFERENCE', 'SET_LOCAL_AUTHENTICATED']); export const LOGIN = createRequestTypes('LOGIN', [...defaultTypes, 'SET_SERVICES', 'SET_PREFERENCE', 'SET_LOCAL_AUTHENTICATED']);
export const SHARE = createRequestTypes('SHARE', ['SELECT_SERVER', 'SET_USER', 'SET_SETTINGS', 'SET_SERVER_INFO']); export const SHARE = createRequestTypes('SHARE', ['SELECT_SERVER', 'SET_USER', 'SET_SETTINGS', 'SET_SERVER_INFO']);
export const USER = createRequestTypes('USER', ['SET']); export const USER = createRequestTypes('USER', ['SET', 'CLEAR']);
export const ROOMS = createRequestTypes('ROOMS', [ export const ROOMS = createRequestTypes('ROOMS', [
...defaultTypes, ...defaultTypes,
'REFRESH', 'REFRESH',

View File

@ -93,6 +93,12 @@ export function setUser(user: Partial<IUser>): ISetUser {
}; };
} }
export function clearUser(): Action {
return {
type: types.USER.CLEAR
};
}
export function setLoginServices(data: Record<string, any>): ISetServices { export function setLoginServices(data: Record<string, any>): ISetServices {
return { return {
type: types.LOGIN.SET_SERVICES, type: types.LOGIN.SET_SERVICES,

View File

@ -4,7 +4,7 @@ import { ERoomType } from '../definitions/ERoomType';
import { ROOM } from './actionsTypes'; import { ROOM } from './actionsTypes';
// TYPE RETURN RELATED // TYPE RETURN RELATED
type ISelected = Record<string, string>; type ISelected = string[];
export interface ITransferData { export interface ITransferData {
roomId: string; roomId: string;

View File

@ -1,26 +1,26 @@
import { Action } from 'redux'; import { Action } from 'redux';
import { ISettings, TSettings } from '../reducers/settings'; import { TSettingsState, TSupportedSettings, TSettingsValues } from '../reducers/settings';
import { SETTINGS } from './actionsTypes'; import { SETTINGS } from './actionsTypes';
interface IAddSettings extends Action { interface IAddSettings extends Action {
payload: ISettings; payload: TSettingsState;
} }
interface IUpdateSettings extends Action { interface IUpdateSettings extends Action {
payload: { id: string; value: TSettings }; payload: { id: TSupportedSettings; value: TSettingsValues };
} }
export type IActionSettings = IAddSettings & IUpdateSettings; export type IActionSettings = IAddSettings & IUpdateSettings;
export function addSettings(settings: ISettings): IAddSettings { export function addSettings(settings: TSettingsState): IAddSettings {
return { return {
type: SETTINGS.ADD, type: SETTINGS.ADD,
payload: settings payload: settings
}; };
} }
export function updateSettings(id: string, value: TSettings): IUpdateSettings { export function updateSettings(id: TSupportedSettings, value: TSettingsValues): IUpdateSettings {
return { return {
type: SETTINGS.UPDATE, type: SETTINGS.UPDATE,
payload: { id, value } payload: { id, value }

View File

@ -143,8 +143,8 @@ export const deleteKeyCommands = (): void => KeyCommands.deleteKeyCommands(keyCo
export const KEY_COMMAND = 'KEY_COMMAND'; export const KEY_COMMAND = 'KEY_COMMAND';
interface IKeyCommandEvent extends NativeSyntheticEvent<typeof KeyCommand> { export interface IKeyCommandEvent extends NativeSyntheticEvent<typeof KeyCommand> {
input: string; input: number & string;
modifierFlags: string | number; modifierFlags: string | number;
} }

View File

@ -37,6 +37,7 @@ export const themes: any = {
infoText: '#6d6d72', infoText: '#6d6d72',
tintColor: '#1d74f5', tintColor: '#1d74f5',
tintActive: '#549df9', tintActive: '#549df9',
tintDisabled: '#88B4F5',
auxiliaryTintColor: '#6C727A', auxiliaryTintColor: '#6C727A',
actionTintColor: '#1d74f5', actionTintColor: '#1d74f5',
separatorColor: '#cbcbcc', separatorColor: '#cbcbcc',
@ -66,6 +67,8 @@ export const themes: any = {
previewTintColor: '#ffffff', previewTintColor: '#ffffff',
backdropOpacity: 0.3, backdropOpacity: 0.3,
attachmentLoadingOpacity: 0.7, attachmentLoadingOpacity: 0.7,
collapsibleQuoteBorder: '#CBCED1',
collapsibleChevron: '#6C727A',
...mentions ...mentions
}, },
dark: { dark: {
@ -85,6 +88,7 @@ export const themes: any = {
infoText: '#6D6D72', infoText: '#6D6D72',
tintColor: '#1d74f5', tintColor: '#1d74f5',
tintActive: '#549df9', tintActive: '#549df9',
tintDisabled: '#88B4F5',
auxiliaryTintColor: '#f9f9f9', auxiliaryTintColor: '#f9f9f9',
actionTintColor: '#1d74f5', actionTintColor: '#1d74f5',
separatorColor: '#2b2b2d', separatorColor: '#2b2b2d',
@ -114,6 +118,8 @@ export const themes: any = {
previewTintColor: '#ffffff', previewTintColor: '#ffffff',
backdropOpacity: 0.9, backdropOpacity: 0.9,
attachmentLoadingOpacity: 0.3, attachmentLoadingOpacity: 0.3,
collapsibleQuoteBorder: '#CBCED1',
collapsibleChevron: '#6C727A',
...mentions ...mentions
}, },
black: { black: {
@ -133,6 +139,7 @@ export const themes: any = {
infoText: '#6d6d72', infoText: '#6d6d72',
tintColor: '#1e9bfe', tintColor: '#1e9bfe',
tintActive: '#76b7fc', tintActive: '#76b7fc',
tintDisabled: '#88B4F5', // TODO: Evaluate this with design team
auxiliaryTintColor: '#f9f9f9', auxiliaryTintColor: '#f9f9f9',
actionTintColor: '#1e9bfe', actionTintColor: '#1e9bfe',
separatorColor: '#272728', separatorColor: '#272728',
@ -162,6 +169,8 @@ export const themes: any = {
previewTintColor: '#ffffff', previewTintColor: '#ffffff',
backdropOpacity: 0.9, backdropOpacity: 0.9,
attachmentLoadingOpacity: 0.3, attachmentLoadingOpacity: 0.3,
collapsibleQuoteBorder: '#CBCED1',
collapsibleChevron: '#6C727A',
...mentions ...mentions
} }
}; };

View File

@ -1,5 +1,7 @@
export const MESSAGE_TYPE_LOAD_MORE = 'load_more'; export enum MessageTypeLoad {
export const MESSAGE_TYPE_LOAD_PREVIOUS_CHUNK = 'load_previous_chunk'; MORE = 'load_more',
export const MESSAGE_TYPE_LOAD_NEXT_CHUNK = 'load_next_chunk'; PREVIOUS_CHUNK = 'load_previous_chunk',
NEXT_CHUNK = 'load_next_chunk'
}
export const MESSAGE_TYPE_ANY_LOAD = [MESSAGE_TYPE_LOAD_MORE, MESSAGE_TYPE_LOAD_PREVIOUS_CHUNK, MESSAGE_TYPE_LOAD_NEXT_CHUNK]; export const MESSAGE_TYPE_ANY_LOAD = [MessageTypeLoad.MORE, MessageTypeLoad.PREVIOUS_CHUNK, MessageTypeLoad.NEXT_CHUNK];

View File

@ -206,4 +206,4 @@ export default {
Canned_Responses_Enable: { Canned_Responses_Enable: {
type: 'valueAsBoolean' type: 'valueAsBoolean'
} }
}; } as const;

View File

@ -1,30 +1,29 @@
import { useBackHandler } from '@react-native-community/hooks';
import * as Haptics from 'expo-haptics';
import React, { forwardRef, isValidElement, useEffect, useImperativeHandle, useRef, useState } from 'react'; import React, { forwardRef, isValidElement, useEffect, useImperativeHandle, useRef, useState } from 'react';
import { Keyboard, Text } from 'react-native'; 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 { useSafeAreaInsets } from 'react-native-safe-area-context'; import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { State, TapGestureHandler } from 'react-native-gesture-handler';
import ScrollBottomSheet from 'react-native-scroll-bottom-sheet'; import ScrollBottomSheet from 'react-native-scroll-bottom-sheet';
import Animated, { Easing, Extrapolate, Value, interpolateNode } from 'react-native-reanimated';
import * as Haptics from 'expo-haptics';
import { useBackHandler } from '@react-native-community/hooks';
import { Item } from './Item';
import { Handle } from './Handle';
import { Button } from './Button';
import { themes } from '../../constants/colors'; import { themes } from '../../constants/colors';
import styles, { ITEM_HEIGHT } from './styles'; import { useDimensions, useOrientation } from '../../dimensions';
import I18n from '../../i18n';
import { useTheme } from '../../theme';
import { isIOS, isTablet } from '../../utils/deviceInfo'; import { isIOS, isTablet } from '../../utils/deviceInfo';
import * as List from '../List'; import * as List from '../List';
import I18n from '../../i18n'; import { Button } from './Button';
import { IDimensionsContextProps, useDimensions, useOrientation } from '../../dimensions'; import { Handle } from './Handle';
import { IActionSheetItem, Item } from './Item';
import { TActionSheetOptions, TActionSheetOptionsItem } from './Provider';
import styles, { ITEM_HEIGHT } from './styles';
interface IActionSheetData { const getItemLayout = (data: TActionSheetOptionsItem[] | null | undefined, index: number) => ({
options: any; length: ITEM_HEIGHT,
headerHeight?: number; offset: ITEM_HEIGHT * index,
hasCancel?: boolean; index
customHeader: any; });
}
const getItemLayout = (data: any, index: number) => ({ length: ITEM_HEIGHT, offset: ITEM_HEIGHT * index, index });
const HANDLE_HEIGHT = isIOS ? 40 : 56; const HANDLE_HEIGHT = isIOS ? 40 : 56;
const MAX_SNAP_HEIGHT = 16; const MAX_SNAP_HEIGHT = 16;
@ -39,16 +38,17 @@ const ANIMATION_CONFIG = {
}; };
const ActionSheet = React.memo( const ActionSheet = React.memo(
forwardRef(({ children, theme }: { children: JSX.Element; theme: string }, ref) => { forwardRef(({ children }: { children: React.ReactElement }, ref) => {
const bottomSheetRef: any = useRef(); const { theme } = useTheme();
const [data, setData] = useState<IActionSheetData>({} as IActionSheetData); const bottomSheetRef = useRef<ScrollBottomSheet<TActionSheetOptionsItem>>(null);
const [data, setData] = useState<TActionSheetOptions>({} as TActionSheetOptions);
const [isVisible, setVisible] = useState(false); const [isVisible, setVisible] = useState(false);
const { height }: Partial<IDimensionsContextProps> = useDimensions(); const { height } = useDimensions();
const { isLandscape } = useOrientation(); const { isLandscape } = useOrientation();
const insets = useSafeAreaInsets(); const insets = useSafeAreaInsets();
const maxSnap = Math.max( const maxSnap = Math.max(
height! - height -
// Items height // Items height
ITEM_HEIGHT * (data?.options?.length || 0) - ITEM_HEIGHT * (data?.options?.length || 0) -
// Handle height // Handle height
@ -69,7 +69,7 @@ const ActionSheet = React.memo(
* we'll provide more one snap * we'll provide more one snap
* that point 50% of the whole screen * that point 50% of the whole screen
*/ */
const snaps: any = height! - maxSnap > height! * 0.6 && !isLandscape ? [maxSnap, height! * 0.5, height] : [maxSnap, height]; const snaps = height - maxSnap > height * 0.6 && !isLandscape ? [maxSnap, height * 0.5, height] : [maxSnap, height];
const openedSnapIndex = snaps.length > 2 ? 1 : 0; const openedSnapIndex = snaps.length > 2 ? 1 : 0;
const closedSnapIndex = snaps.length - 1; const closedSnapIndex = snaps.length - 1;
@ -79,12 +79,12 @@ const ActionSheet = React.memo(
bottomSheetRef.current?.snapTo(closedSnapIndex); bottomSheetRef.current?.snapTo(closedSnapIndex);
}; };
const show = (options: any) => { const show = (options: TActionSheetOptions) => {
setData(options); setData(options);
toggleVisible(); toggleVisible();
}; };
const onBackdropPressed = ({ nativeEvent }: any) => { const onBackdropPressed = ({ nativeEvent }: { nativeEvent: HandlerStateChangeEventPayload }) => {
if (nativeEvent.oldState === State.ACTIVE) { if (nativeEvent.oldState === State.ACTIVE) {
hide(); hide();
} }
@ -117,7 +117,7 @@ const ActionSheet = React.memo(
const renderHandle = () => ( const renderHandle = () => (
<> <>
<Handle theme={theme} /> <Handle />
{isValidElement(data?.customHeader) ? data.customHeader : null} {isValidElement(data?.customHeader) ? data.customHeader : null}
</> </>
); );
@ -127,21 +127,23 @@ const ActionSheet = React.memo(
<Button <Button
onPress={hide} onPress={hide}
style={[styles.button, { backgroundColor: themes[theme].auxiliaryBackground }]} style={[styles.button, { backgroundColor: themes[theme].auxiliaryBackground }]}
// TODO: Remove when migrate Touch
theme={theme} theme={theme}
accessibilityLabel={I18n.t('Cancel')}> accessibilityLabel={I18n.t('Cancel')}>
<Text style={[styles.text, { color: themes[theme].bodyText }]}>{I18n.t('Cancel')}</Text> <Text style={[styles.text, { color: themes[theme].bodyText }]}>{I18n.t('Cancel')}</Text>
</Button> </Button>
) : null; ) : null;
const renderItem = ({ item }: any) => <Item item={item} hide={hide} theme={theme} />; const renderItem = ({ item }: { item: IActionSheetItem['item'] }) => <Item item={item} hide={hide} />;
const animatedPosition = React.useRef(new Value(0)); const animatedPosition = React.useRef(new Value(0));
// TODO: Similar to https://github.com/wcandillon/react-native-redash/issues/307#issuecomment-827442320
const opacity = interpolateNode(animatedPosition.current, { const opacity = interpolateNode(animatedPosition.current, {
inputRange: [0, 1], inputRange: [0, 1],
outputRange: [0, themes[theme].backdropOpacity], outputRange: [0, themes[theme].backdropOpacity],
extrapolate: Extrapolate.CLAMP extrapolate: Extrapolate.CLAMP
}) as any; }) 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 bottomSheet = isLandscape || isTablet ? styles.bottomSheet : {};
return ( return (
<> <>
@ -160,7 +162,7 @@ const ActionSheet = React.memo(
]} ]}
/> />
</TapGestureHandler> </TapGestureHandler>
<ScrollBottomSheet <ScrollBottomSheet<TActionSheetOptionsItem>
testID='action-sheet' testID='action-sheet'
ref={bottomSheetRef} ref={bottomSheetRef}
componentType='FlatList' componentType='FlatList'
@ -169,18 +171,11 @@ const ActionSheet = React.memo(
renderHandle={renderHandle} renderHandle={renderHandle}
onSettle={index => index === closedSnapIndex && toggleVisible()} onSettle={index => index === closedSnapIndex && toggleVisible()}
animatedPosition={animatedPosition.current} animatedPosition={animatedPosition.current}
containerStyle={ containerStyle={{ ...styles.container, ...bottomSheet, backgroundColor: themes[theme].focusedBackground }}
[
styles.container,
{ backgroundColor: themes[theme].focusedBackground },
(isLandscape || isTablet) && styles.bottomSheet
] as any
}
animationConfig={ANIMATION_CONFIG} animationConfig={ANIMATION_CONFIG}
// FlatList props data={data.options}
data={data?.options}
renderItem={renderItem} renderItem={renderItem}
keyExtractor={(item: any) => item.title} keyExtractor={item => item.title}
style={{ backgroundColor: themes[theme].focusedBackground }} style={{ backgroundColor: themes[theme].focusedBackground }}
contentContainerStyle={styles.content} contentContainerStyle={styles.content}
ItemSeparatorComponent={List.Separator} ItemSeparatorComponent={List.Separator}

View File

@ -3,9 +3,13 @@ import { View } from 'react-native';
import styles from './styles'; import styles from './styles';
import { themes } from '../../constants/colors'; import { themes } from '../../constants/colors';
import { useTheme } from '../../theme';
export const Handle = React.memo(({ theme }: { theme: string }) => ( export const Handle = React.memo(() => {
const { theme } = useTheme();
return (
<View style={[styles.handle, { backgroundColor: themes[theme].focusedBackground }]} testID='action-sheet-handle'> <View style={[styles.handle, { backgroundColor: themes[theme].focusedBackground }]} testID='action-sheet-handle'>
<View style={[styles.handleIndicator, { backgroundColor: themes[theme].auxiliaryText }]} /> <View style={[styles.handleIndicator, { backgroundColor: themes[theme].auxiliaryText }]} />
</View> </View>
)); );
});

View File

@ -3,23 +3,24 @@ import { Text, View } from 'react-native';
import { themes } from '../../constants/colors'; import { themes } from '../../constants/colors';
import { CustomIcon } from '../../lib/Icons'; import { CustomIcon } from '../../lib/Icons';
import { useTheme } from '../../theme';
import { Button } from './Button'; import { Button } from './Button';
import styles from './styles'; import styles from './styles';
interface IActionSheetItem { export interface IActionSheetItem {
item: { item: {
title: string; title: string;
icon: string; icon: string;
danger: boolean; danger?: boolean;
testID: string; testID?: string;
onPress(): void; onPress: () => void;
right: Function; right?: Function;
}; };
theme: string;
hide(): void; hide(): void;
} }
export const Item = React.memo(({ item, hide, theme }: IActionSheetItem) => { export const Item = React.memo(({ item, hide }: IActionSheetItem) => {
const { theme } = useTheme();
const onPress = () => { const onPress = () => {
hide(); hide();
item?.onPress(); item?.onPress();

View File

@ -1,14 +1,21 @@
import React, { ForwardedRef, forwardRef, useContext, useRef } from 'react'; import React, { ForwardedRef, forwardRef, useContext, useRef } from 'react';
import ActionSheet from './ActionSheet'; import ActionSheet from './ActionSheet';
import { useTheme } from '../../theme';
export type TActionSheetOptionsItem = { title: string; icon: string; onPress: () => void };
export type TActionSheetOptions = {
options: TActionSheetOptionsItem[];
headerHeight: number;
customHeader: React.ReactElement | null;
hasCancel?: boolean;
};
interface IActionSheetProvider { interface IActionSheetProvider {
Provider: any; showActionSheet: (item: TActionSheetOptions) => void;
Consumer: any; hideActionSheet: () => void;
} }
const context: IActionSheetProvider = React.createContext({ const context = React.createContext<IActionSheetProvider>({
showActionSheet: () => {}, showActionSheet: () => {},
hideActionSheet: () => {} hideActionSheet: () => {}
}); });
@ -17,17 +24,16 @@ export const useActionSheet = () => useContext(context);
const { Provider, Consumer } = context; const { Provider, Consumer } = context;
export const withActionSheet = (Component: any): any => export const withActionSheet = (Component: React.ComponentType<any>): typeof Component =>
forwardRef((props: any, ref: ForwardedRef<any>) => ( forwardRef((props: typeof React.Component, ref: ForwardedRef<IActionSheetProvider>) => (
<Consumer>{(contexts: any) => <Component {...props} {...contexts} ref={ref} />}</Consumer> <Consumer>{(contexts: IActionSheetProvider) => <Component {...props} {...contexts} ref={ref} />}</Consumer>
)); ));
export const ActionSheetProvider = React.memo(({ children }: { children: JSX.Element | JSX.Element[] }) => { export const ActionSheetProvider = React.memo(({ children }: { children: React.ReactElement | React.ReactElement[] }) => {
const ref: ForwardedRef<any> = useRef(); const ref: ForwardedRef<IActionSheetProvider> = useRef(null);
const { theme }: any = useTheme();
const getContext = () => ({ const getContext = () => ({
showActionSheet: (options: any) => { showActionSheet: (options: TActionSheetOptions) => {
ref.current?.showActionSheet(options); ref.current?.showActionSheet(options);
}, },
hideActionSheet: () => { hideActionSheet: () => {
@ -37,7 +43,7 @@ export const ActionSheetProvider = React.memo(({ children }: { children: JSX.Ele
return ( return (
<Provider value={getContext()}> <Provider value={getContext()}>
<ActionSheet ref={ref} theme={theme}> <ActionSheet ref={ref}>
<>{children}</> <>{children}</>
</ActionSheet> </ActionSheet>
</Provider> </Provider>

View File

@ -1,14 +1,11 @@
import React from 'react'; import React from 'react';
import { ActivityIndicator, ActivityIndicatorProps, StyleSheet } from 'react-native'; import { ActivityIndicator, ActivityIndicatorProps, StyleSheet } from 'react-native';
import { useTheme } from '../theme';
import { themes } from '../constants/colors'; import { themes } from '../constants/colors';
type TTheme = 'light' | 'dark' | 'black' | string;
interface IActivityIndicator extends ActivityIndicatorProps { interface IActivityIndicator extends ActivityIndicatorProps {
theme?: TTheme;
absolute?: boolean; absolute?: boolean;
props?: object;
} }
const styles = StyleSheet.create({ const styles = StyleSheet.create({
@ -27,8 +24,11 @@ const styles = StyleSheet.create({
} }
}); });
const RCActivityIndicator = ({ theme = 'light', absolute, ...props }: IActivityIndicator) => ( const RCActivityIndicator = ({ absolute, ...props }: IActivityIndicator): React.ReactElement => {
const { theme } = useTheme();
return (
<ActivityIndicator style={[styles.indicator, absolute && styles.absolute]} color={themes[theme].auxiliaryText} {...props} /> <ActivityIndicator style={[styles.indicator, absolute && styles.absolute]} color={themes[theme].auxiliaryText} {...props} />
); );
};
export default RCActivityIndicator; export default RCActivityIndicator;

View File

@ -37,6 +37,21 @@ class AvatarContainer extends React.Component<IAvatar, any> {
} }
} }
shouldComponentUpdate(nextProps: IAvatar, nextState: { avatarETag: string }) {
const { avatarETag } = this.state;
const { text, type } = this.props;
if (nextState.avatarETag !== avatarETag) {
return true;
}
if (nextProps.text !== text) {
return true;
}
if (nextProps.type !== type) {
return true;
}
return false;
}
componentWillUnmount() { componentWillUnmount() {
if (this.subscription?.unsubscribe) { if (this.subscription?.unsubscribe) {
this.subscription.unsubscribe(); this.subscription.unsubscribe();

View File

@ -1,3 +1,5 @@
import React from 'react';
import { TGetCustomEmoji } from '../../definitions/IEmoji'; import { TGetCustomEmoji } from '../../definitions/IEmoji';
export interface IAvatar { export interface IAvatar {
@ -9,7 +11,7 @@ export interface IAvatar {
size?: number; size?: number;
borderRadius?: number; borderRadius?: number;
type?: string; type?: string;
children?: JSX.Element; children?: React.ReactElement | null;
user?: { user?: {
id?: string; id?: string;
token?: string; token?: string;

View File

@ -1,13 +1,12 @@
import React from 'react'; import React from 'react';
import { ActivityIndicator, ImageBackground, StyleSheet, Text, View } from 'react-native'; import { ActivityIndicator, ImageBackground, StyleSheet, Text, View } from 'react-native';
import { withTheme } from '../../theme'; import { useTheme } from '../../theme';
import sharedStyles from '../../views/Styles'; import sharedStyles from '../../views/Styles';
import { themes } from '../../constants/colors'; import { themes } from '../../constants/colors';
interface IBackgroundContainer { interface IBackgroundContainer {
text?: string; text?: string;
theme?: string;
loading?: boolean; loading?: boolean;
} }
@ -32,12 +31,15 @@ const styles = StyleSheet.create({
} }
}); });
const BackgroundContainer = ({ theme, text, loading }: IBackgroundContainer) => ( const BackgroundContainer = ({ text, loading }: IBackgroundContainer): React.ReactElement => {
const { theme } = useTheme();
return (
<View style={styles.container}> <View style={styles.container}>
<ImageBackground source={{ uri: `message_empty_${theme}` }} style={styles.image} /> <ImageBackground source={{ uri: `message_empty_${theme}` }} style={styles.image} />
{text && !loading ? <Text style={[styles.text, { color: themes[theme!].auxiliaryTintColor }]}>{text}</Text> : null} {text && !loading ? <Text style={[styles.text, { color: themes[theme].auxiliaryTintColor }]}>{text}</Text> : null}
{loading ? <ActivityIndicator style={styles.text} color={themes[theme!].auxiliaryTintColor} /> : null} {loading ? <ActivityIndicator style={styles.text} color={themes[theme].auxiliaryTintColor} /> : null}
</View> </View>
); );
};
export default withTheme(BackgroundContainer); export default BackgroundContainer;

View File

@ -3,11 +3,8 @@ import { StyleSheet } from 'react-native';
import { CustomIcon } from '../lib/Icons'; import { CustomIcon } from '../lib/Icons';
import { themes } from '../constants/colors'; import { themes } from '../constants/colors';
import { useTheme } from '../theme';
interface ICheck {
style?: object;
theme: string;
}
const styles = StyleSheet.create({ const styles = StyleSheet.create({
icon: { icon: {
width: 22, width: 22,
@ -16,8 +13,9 @@ const styles = StyleSheet.create({
} }
}); });
const Check = React.memo(({ theme, style }: ICheck) => ( const Check = React.memo(() => {
<CustomIcon style={[styles.icon, style]} color={themes[theme].tintColor} size={22} name='check' /> const { theme } = useTheme();
)); return <CustomIcon style={styles.icon} color={themes[theme].tintColor} size={22} name='check' />;
});
export default Check; export default Check;

View File

@ -5,15 +5,15 @@ import { themes } from '../constants/colors';
import sharedStyles from '../views/Styles'; import sharedStyles from '../views/Styles';
import scrollPersistTaps from '../utils/scrollPersistTaps'; import scrollPersistTaps from '../utils/scrollPersistTaps';
import KeyboardView from '../presentation/KeyboardView'; import KeyboardView from '../presentation/KeyboardView';
import { useTheme } from '../theme';
import StatusBar from './StatusBar'; import StatusBar from './StatusBar';
import AppVersion from './AppVersion'; import AppVersion from './AppVersion';
import { isTablet } from '../utils/deviceInfo'; import { isTablet } from '../utils/deviceInfo';
import SafeAreaView from './SafeAreaView'; import SafeAreaView from './SafeAreaView';
interface IFormContainer extends ScrollViewProps { interface IFormContainer extends ScrollViewProps {
theme: string;
testID: string; testID: string;
children: React.ReactNode; children: React.ReactElement | React.ReactElement[] | null;
} }
const styles = StyleSheet.create({ const styles = StyleSheet.create({
@ -22,11 +22,14 @@ const styles = StyleSheet.create({
} }
}); });
export const FormContainerInner = ({ children }: { children: React.ReactNode }): JSX.Element => ( export const FormContainerInner = ({ children }: { children: (React.ReactElement | null)[] }) => (
<View style={[sharedStyles.container, isTablet && sharedStyles.tabletScreenContent]}>{children}</View> <View style={[sharedStyles.container, isTablet && sharedStyles.tabletScreenContent]}>{children}</View>
); );
const FormContainer = ({ children, theme, testID, ...props }: IFormContainer): JSX.Element => ( const FormContainer = ({ children, testID, ...props }: IFormContainer) => {
const { theme } = useTheme();
return (
<KeyboardView <KeyboardView
style={{ backgroundColor: themes[theme].backgroundColor }} style={{ backgroundColor: themes[theme].backgroundColor }}
contentContainerStyle={sharedStyles.container} contentContainerStyle={sharedStyles.container}
@ -44,5 +47,6 @@ const FormContainer = ({ children, theme, testID, ...props }: IFormContainer): J
</ScrollView> </ScrollView>
</KeyboardView> </KeyboardView>
); );
};
export default FormContainer; export default FormContainer;

View File

@ -5,12 +5,11 @@ import { StyleSheet, View } from 'react-native';
import { themes } from '../../constants/colors'; import { themes } from '../../constants/colors';
import { themedHeader } from '../../utils/navigation'; import { themedHeader } from '../../utils/navigation';
import { isIOS, isTablet } from '../../utils/deviceInfo'; import { isIOS, isTablet } from '../../utils/deviceInfo';
import { withTheme } from '../../theme'; import { useTheme } from '../../theme';
// Get from https://github.com/react-navigation/react-navigation/blob/master/packages/stack/src/views/Header/HeaderSegment.tsx#L69
export const headerHeight = isIOS ? 44 : 56; export const headerHeight = isIOS ? 44 : 56;
export const getHeaderHeight = (isLandscape: boolean) => { export const getHeaderHeight = (isLandscape: boolean): number => {
if (isIOS) { if (isIOS) {
if (isLandscape && !isTablet) { if (isLandscape && !isTablet) {
return 32; return 32;
@ -28,7 +27,13 @@ interface IHeaderTitlePosition {
numIconsRight: number; numIconsRight: number;
} }
export const getHeaderTitlePosition = ({ insets, numIconsRight }: IHeaderTitlePosition) => ({ export const getHeaderTitlePosition = ({
insets,
numIconsRight
}: IHeaderTitlePosition): {
left: number;
right: number;
} => ({
left: insets.left + 60, left: insets.left + 60,
right: insets.right + Math.max(45 * numIconsRight, 15) right: insets.right + Math.max(45 * numIconsRight, 15)
}); });
@ -43,13 +48,14 @@ const styles = StyleSheet.create({
}); });
interface IHeader { interface IHeader {
theme: string; headerLeft: () => React.ReactElement | null;
headerLeft(): void; headerTitle: () => React.ReactElement;
headerTitle(): void; headerRight: () => React.ReactElement | null;
headerRight(): void;
} }
const Header = ({ theme, headerLeft, headerTitle, headerRight }: IHeader) => ( const Header = ({ headerLeft, headerTitle, headerRight }: IHeader): React.ReactElement => {
const { theme } = useTheme();
return (
<SafeAreaView style={{ backgroundColor: themes[theme].headerBackground }} edges={['top', 'left', 'right']}> <SafeAreaView style={{ backgroundColor: themes[theme].headerBackground }} edges={['top', 'left', 'right']}>
<View style={[styles.container, { ...themedHeader(theme).headerStyle }]}> <View style={[styles.container, { ...themedHeader(theme).headerStyle }]}>
{headerLeft ? headerLeft() : null} {headerLeft ? headerLeft() : null}
@ -58,5 +64,6 @@ const Header = ({ theme, headerLeft, headerTitle, headerRight }: IHeader) => (
</View> </View>
</SafeAreaView> </SafeAreaView>
); );
};
export default withTheme(Header); export default Header;

View File

@ -6,20 +6,22 @@ import Container from './HeaderButtonContainer';
import Item from './HeaderButtonItem'; import Item from './HeaderButtonItem';
interface IHeaderButtonCommon { interface IHeaderButtonCommon {
navigation: any; navigation?: any; // TODO: Evaluate proper type
onPress?(): void; onPress?: () => void;
testID?: string; testID?: string;
} }
// Left // Left
export const Drawer = React.memo(({ navigation, testID, ...props }: Partial<IHeaderButtonCommon>) => ( export const Drawer = React.memo(
({ navigation, testID, onPress = () => navigation?.toggleDrawer(), ...props }: IHeaderButtonCommon) => (
<Container left> <Container left>
<Item iconName='hamburguer' onPress={() => navigation.toggleDrawer()} testID={testID} {...props} /> <Item iconName='hamburguer' onPress={onPress} testID={testID} {...props} />
</Container> </Container>
)); )
);
export const CloseModal = React.memo( export const CloseModal = React.memo(
({ navigation, testID, onPress = () => navigation.pop(), ...props }: IHeaderButtonCommon) => ( ({ navigation, testID, onPress = () => navigation?.pop(), ...props }: IHeaderButtonCommon) => (
<Container left> <Container left>
<Item iconName='close' onPress={onPress} testID={testID} {...props} /> <Item iconName='close' onPress={onPress} testID={testID} {...props} />
</Container> </Container>
@ -29,9 +31,9 @@ export const CloseModal = React.memo(
export const CancelModal = React.memo(({ onPress, testID }: Partial<IHeaderButtonCommon>) => ( export const CancelModal = React.memo(({ onPress, testID }: Partial<IHeaderButtonCommon>) => (
<Container left> <Container left>
{isIOS ? ( {isIOS ? (
<Item title={I18n.t('Cancel')} onPress={onPress!} testID={testID} /> <Item title={I18n.t('Cancel')} onPress={onPress} testID={testID} />
) : ( ) : (
<Item iconName='close' onPress={onPress!} testID={testID} /> <Item iconName='close' onPress={onPress} testID={testID} />
)} )}
</Container> </Container>
)); ));
@ -39,22 +41,24 @@ export const CancelModal = React.memo(({ onPress, testID }: Partial<IHeaderButto
// Right // Right
export const More = React.memo(({ onPress, testID }: Partial<IHeaderButtonCommon>) => ( export const More = React.memo(({ onPress, testID }: Partial<IHeaderButtonCommon>) => (
<Container> <Container>
<Item iconName='kebab' onPress={onPress!} testID={testID} /> <Item iconName='kebab' onPress={onPress} testID={testID} />
</Container> </Container>
)); ));
export const Download = React.memo(({ onPress, testID, ...props }: Partial<IHeaderButtonCommon>) => ( export const Download = React.memo(({ onPress, testID, ...props }: IHeaderButtonCommon) => (
<Container> <Container>
<Item iconName='download' onPress={onPress!} testID={testID} {...props} /> <Item iconName='download' onPress={onPress} testID={testID} {...props} />
</Container> </Container>
)); ));
export const Preferences = React.memo(({ onPress, testID, ...props }: Partial<IHeaderButtonCommon>) => ( export const Preferences = React.memo(({ onPress, testID, ...props }: IHeaderButtonCommon) => (
<Container> <Container>
<Item iconName='settings' onPress={onPress!} testID={testID} {...props} /> <Item iconName='settings' onPress={onPress} testID={testID} {...props} />
</Container> </Container>
)); ));
export const Legal = React.memo(({ navigation, testID }: Partial<IHeaderButtonCommon>) => ( export const Legal = React.memo(
<More onPress={() => navigation.navigate('LegalView')} testID={testID} /> ({ navigation, testID, onPress = () => navigation?.navigate('LegalView') }: IHeaderButtonCommon) => (
)); <More onPress={onPress} testID={testID} />
)
);

View File

@ -2,7 +2,7 @@ import React from 'react';
import { StyleSheet, View } from 'react-native'; import { StyleSheet, View } from 'react-native';
interface IHeaderButtonContainer { interface IHeaderButtonContainer {
children: React.ReactNode; children?: React.ReactElement | (React.ReactElement | null)[] | null;
left?: boolean; left?: boolean;
} }
@ -20,7 +20,7 @@ const styles = StyleSheet.create({
} }
}); });
const Container = ({ children, left = false }: IHeaderButtonContainer) => ( const Container = ({ children, left = false }: IHeaderButtonContainer): React.ReactElement => (
<View style={[styles.container, left ? styles.left : styles.right]}>{children}</View> <View style={[styles.container, left ? styles.left : styles.right]}>{children}</View>
); );

View File

@ -3,16 +3,15 @@ import { Platform, StyleSheet, Text } from 'react-native';
import Touchable from 'react-native-platform-touchable'; import Touchable from 'react-native-platform-touchable';
import { CustomIcon } from '../../lib/Icons'; import { CustomIcon } from '../../lib/Icons';
import { withTheme } from '../../theme'; import { useTheme } from '../../theme';
import { themes } from '../../constants/colors'; import { themes } from '../../constants/colors';
import sharedStyles from '../../views/Styles'; import sharedStyles from '../../views/Styles';
interface IHeaderButtonItem { interface IHeaderButtonItem {
title?: string; title?: string;
iconName?: string; iconName?: string;
onPress: <T>(arg: T) => void; onPress?: <T>(arg: T) => void;
testID?: string; testID?: string;
theme?: string;
badge?(): void; badge?(): void;
} }
@ -40,19 +39,22 @@ const styles = StyleSheet.create({
} }
}); });
const Item = ({ title, iconName, onPress, testID, theme, badge }: IHeaderButtonItem) => ( const Item = ({ title, iconName, onPress, testID, badge }: IHeaderButtonItem): React.ReactElement => {
const { theme } = useTheme();
return (
<Touchable onPress={onPress} testID={testID} hitSlop={BUTTON_HIT_SLOP} style={styles.container}> <Touchable onPress={onPress} testID={testID} hitSlop={BUTTON_HIT_SLOP} style={styles.container}>
<> <>
{iconName ? ( {iconName ? (
<CustomIcon name={iconName} size={24} color={themes[theme!].headerTintColor} /> <CustomIcon name={iconName} size={24} color={themes[theme].headerTintColor} />
) : ( ) : (
<Text style={[styles.title, { color: themes[theme!].headerTintColor }]}>{title}</Text> <Text style={[styles.title, { color: themes[theme].headerTintColor }]}>{title}</Text>
)} )}
{badge ? badge() : null} {badge ? badge() : null}
</> </>
</Touchable> </Touchable>
); );
};
Item.displayName = 'HeaderButton.Item'; Item.displayName = 'HeaderButton.Item';
export default withTheme(Item); export default Item;

View File

@ -15,6 +15,6 @@ const styles = StyleSheet.create({
} }
}); });
export const Badge = ({ ...props }) => <UnreadBadge {...props} style={styles.badgeContainer} small />; export const Badge = ({ ...props }): React.ReactElement => <UnreadBadge {...props} style={styles.badgeContainer} small />;
export default Badge; export default Badge;

View File

@ -14,9 +14,18 @@ import { ROW_HEIGHT } from '../../presentation/RoomItem';
import { goRoom } from '../../utils/goRoom'; import { goRoom } from '../../utils/goRoom';
import Navigation from '../../lib/Navigation'; import Navigation from '../../lib/Navigation';
import { useOrientation } from '../../dimensions'; import { useOrientation } from '../../dimensions';
import { IApplicationState, ISubscription, SubscriptionType } from '../../definitions';
interface INotifierComponent { export interface INotifierComponent {
notification: object; notification: {
text: string;
payload: {
sender: { username: string };
type: SubscriptionType;
} & Pick<ISubscription, '_id' | 'name' | 'rid' | 'prid'>;
title: string;
avatar: string;
};
isMasterDetail: boolean; isMasterDetail: boolean;
} }
@ -67,15 +76,15 @@ const styles = StyleSheet.create({
const hideNotification = () => Notifier.hideNotification(); const hideNotification = () => Notifier.hideNotification();
const NotifierComponent = React.memo(({ notification, isMasterDetail }: INotifierComponent) => { const NotifierComponent = React.memo(({ notification, isMasterDetail }: INotifierComponent) => {
const { theme }: any = useTheme(); const { theme } = useTheme();
const insets = useSafeAreaInsets(); const insets = useSafeAreaInsets();
const { isLandscape } = useOrientation(); const { isLandscape } = useOrientation();
const { text, payload }: any = notification; const { text, payload } = notification;
const { type, rid } = payload; const { type, rid } = payload;
const name = type === 'd' ? payload.sender.username : payload.name; const name = type === 'd' ? payload.sender.username : payload.name;
// if sub is not on local database, title and avatar will be null, so we use payload from notification // if sub is not on local database, title and avatar will be null, so we use payload from notification
const { title = name, avatar = name }: any = notification; const { title = name, avatar = name } = notification;
const onPress = () => { const onPress = () => {
const { prid, _id } = payload; const { prid, _id } = payload;
@ -133,7 +142,7 @@ const NotifierComponent = React.memo(({ notification, isMasterDetail }: INotifie
); );
}); });
const mapStateToProps = (state: any) => ({ const mapStateToProps = (state: IApplicationState) => ({
isMasterDetail: state.app.isMasterDetail isMasterDetail: state.app.isMasterDetail
}); });

View File

@ -3,16 +3,18 @@ import { Easing, Notifier, NotifierRoot } from 'react-native-notifier';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { dequal } from 'dequal'; import { dequal } from 'dequal';
import NotifierComponent from './NotifierComponent'; import NotifierComponent, { INotifierComponent } from './NotifierComponent';
import EventEmitter from '../../utils/events'; import EventEmitter from '../../utils/events';
import Navigation from '../../lib/Navigation'; import Navigation from '../../lib/Navigation';
import { getActiveRoute } from '../../utils/navigation'; import { getActiveRoute } from '../../utils/navigation';
import { IApplicationState } from '../../definitions';
import { IRoom } from '../../reducers/room';
export const INAPP_NOTIFICATION_EMITTER = 'NotificationInApp'; export const INAPP_NOTIFICATION_EMITTER = 'NotificationInApp';
const InAppNotification = memo( const InAppNotification = memo(
({ rooms, appState }: { rooms: any; appState: string }) => { ({ rooms, appState }: { rooms: IRoom['rooms']; appState: string }) => {
const show = (notification: any) => { const show = (notification: INotifierComponent['notification']) => {
if (appState !== 'foreground') { if (appState !== 'foreground') {
return; return;
} }
@ -46,7 +48,7 @@ const InAppNotification = memo(
(prevProps, nextProps) => dequal(prevProps.rooms, nextProps.rooms) (prevProps, nextProps) => dequal(prevProps.rooms, nextProps.rooms)
); );
const mapStateToProps = (state: any) => ({ const mapStateToProps = (state: IApplicationState) => ({
rooms: state.room.rooms, rooms: state.room.rooms,
appState: state.app.ready && state.app.foreground ? 'foreground' : 'background' appState: state.app.ready && state.app.foreground ? 'foreground' : 'background'
}); });

View File

@ -11,7 +11,7 @@ const styles = StyleSheet.create({
}); });
interface IListContainer { interface IListContainer {
children: React.ReactNode; children: (React.ReactElement | null)[] | React.ReactElement | null;
testID?: string; testID?: string;
} }
const ListContainer = React.memo(({ children, ...props }: IListContainer) => ( const ListContainer = React.memo(({ children, ...props }: IListContainer) => (

View File

@ -4,7 +4,7 @@ import { StyleSheet, Text, View } from 'react-native';
import sharedStyles from '../../views/Styles'; import sharedStyles from '../../views/Styles';
import { themes } from '../../constants/colors'; import { themes } from '../../constants/colors';
import I18n from '../../i18n'; import I18n from '../../i18n';
import { withTheme } from '../../theme'; import { useTheme } from '../../theme';
import { PADDING_HORIZONTAL } from './constants'; import { PADDING_HORIZONTAL } from './constants';
const styles = StyleSheet.create({ const styles = StyleSheet.create({
@ -20,18 +20,21 @@ const styles = StyleSheet.create({
interface IListHeader { interface IListHeader {
title: string; title: string;
theme?: string;
translateTitle?: boolean; translateTitle?: boolean;
} }
const ListHeader = React.memo(({ title, theme, translateTitle = true }: IListHeader) => ( const ListHeader = React.memo(({ title, translateTitle = true }: IListHeader) => {
const { theme } = useTheme();
return (
<View style={styles.container}> <View style={styles.container}>
<Text style={[styles.title, { color: themes[theme!].infoText }]} numberOfLines={1}> <Text style={[styles.title, { color: themes[theme].infoText }]} numberOfLines={1}>
{translateTitle ? I18n.t(title) : title} {translateTitle ? I18n.t(title) : title}
</Text> </Text>
</View> </View>
)); );
});
ListHeader.displayName = 'List.Header'; ListHeader.displayName = 'List.Header';
export default withTheme(ListHeader); export default ListHeader;

View File

@ -3,11 +3,10 @@ import { StyleProp, StyleSheet, View, ViewStyle } from 'react-native';
import { themes } from '../../constants/colors'; import { themes } from '../../constants/colors';
import { CustomIcon } from '../../lib/Icons'; import { CustomIcon } from '../../lib/Icons';
import { withTheme } from '../../theme'; import { useTheme } from '../../theme';
import { ICON_SIZE } from './constants'; import { ICON_SIZE } from './constants';
interface IListIcon { interface IListIcon {
theme?: string;
name: string; name: string;
color?: string; color?: string;
style?: StyleProp<ViewStyle>; style?: StyleProp<ViewStyle>;
@ -21,12 +20,16 @@ const styles = StyleSheet.create({
} }
}); });
const ListIcon = React.memo(({ theme, name, color, style, testID }: IListIcon) => ( const ListIcon = React.memo(({ name, color, style, testID }: IListIcon) => {
const { theme } = useTheme();
return (
<View style={[styles.icon, style]}> <View style={[styles.icon, style]}>
<CustomIcon name={name} color={color ?? themes[theme!].auxiliaryText} size={ICON_SIZE} testID={testID} /> <CustomIcon name={name} color={color ?? themes[theme].auxiliaryText} size={ICON_SIZE} testID={testID} />
</View> </View>
)); );
});
ListIcon.displayName = 'List.Icon'; ListIcon.displayName = 'List.Icon';
export default withTheme(ListIcon); export default ListIcon;

View File

@ -3,7 +3,7 @@ import { StyleSheet, Text, View } from 'react-native';
import sharedStyles from '../../views/Styles'; import sharedStyles from '../../views/Styles';
import { themes } from '../../constants/colors'; import { themes } from '../../constants/colors';
import { withTheme } from '../../theme'; import { useTheme } from '../../theme';
import { PADDING_HORIZONTAL } from './constants'; import { PADDING_HORIZONTAL } from './constants';
import I18n from '../../i18n'; import I18n from '../../i18n';
@ -18,18 +18,20 @@ const styles = StyleSheet.create({
} }
}); });
interface IListHeader { interface IListInfo {
info: string; info: string;
theme?: string;
translateInfo?: boolean; translateInfo?: boolean;
} }
const ListInfo = React.memo(({ info, theme, translateInfo = true }: IListHeader) => ( const ListInfo = React.memo(({ info, translateInfo = true }: IListInfo) => {
const { theme } = useTheme();
return (
<View style={styles.container}> <View style={styles.container}>
<Text style={[styles.text, { color: themes[theme!].infoText }]}>{translateInfo ? I18n.t(info) : info}</Text> <Text style={[styles.text, { color: themes[theme].infoText }]}>{translateInfo ? I18n.t(info) : info}</Text>
</View> </View>
)); );
});
ListInfo.displayName = 'List.Info'; ListInfo.displayName = 'List.Info';
export default withTheme(ListInfo); export default ListInfo;

View File

@ -4,11 +4,11 @@ import { I18nManager, StyleSheet, Text, View } from 'react-native';
import Touch from '../../utils/touch'; import Touch from '../../utils/touch';
import { themes } from '../../constants/colors'; import { themes } from '../../constants/colors';
import sharedStyles from '../../views/Styles'; import sharedStyles from '../../views/Styles';
import { withTheme } from '../../theme'; import { useTheme } from '../../theme';
import I18n from '../../i18n'; import I18n from '../../i18n';
import { Icon } from '.'; import { Icon } from '.';
import { BASE_HEIGHT, ICON_SIZE, PADDING_HORIZONTAL } from './constants'; import { BASE_HEIGHT, ICON_SIZE, PADDING_HORIZONTAL } from './constants';
import { withDimensions } from '../../dimensions'; import { useDimensions } from '../../dimensions';
import { CustomIcon } from '../../lib/Icons'; import { CustomIcon } from '../../lib/Icons';
const styles = StyleSheet.create({ const styles = StyleSheet.create({
@ -59,13 +59,12 @@ interface IListItemContent {
left?: () => JSX.Element | null; left?: () => JSX.Element | null;
right?: () => JSX.Element | null; right?: () => JSX.Element | null;
disabled?: boolean; disabled?: boolean;
theme: string;
testID?: string; testID?: string;
theme?: string;
color?: string; color?: string;
translateTitle?: boolean; translateTitle?: boolean;
translateSubtitle?: boolean; translateSubtitle?: boolean;
showActionIndicator?: boolean; showActionIndicator?: boolean;
fontScale?: number;
alert?: boolean; alert?: boolean;
} }
@ -78,26 +77,28 @@ const Content = React.memo(
left, left,
right, right,
color, color,
theme,
fontScale,
alert, alert,
translateTitle = true, translateTitle = true,
translateSubtitle = true, translateSubtitle = true,
showActionIndicator = false showActionIndicator = false,
}: IListItemContent) => ( theme
<View style={[styles.container, disabled && styles.disabled, { height: BASE_HEIGHT * fontScale! }]} testID={testID}> }: IListItemContent) => {
const { fontScale } = useDimensions();
return (
<View style={[styles.container, disabled && styles.disabled, { height: BASE_HEIGHT * fontScale }]} testID={testID}>
{left ? <View style={styles.leftContainer}>{left()}</View> : null} {left ? <View style={styles.leftContainer}>{left()}</View> : null}
<View style={styles.textContainer}> <View style={styles.textContainer}>
<View style={styles.textAlertContainer}> <View style={styles.textAlertContainer}>
<Text style={[styles.title, { color: color || themes[theme!].titleText }]} numberOfLines={1}> <Text style={[styles.title, { color: color || themes[theme].titleText }]} numberOfLines={1}>
{translateTitle ? I18n.t(title) : title} {translateTitle ? I18n.t(title) : title}
</Text> </Text>
{alert ? ( {alert ? (
<CustomIcon style={[styles.alertIcon, { color: themes[theme!].dangerColor }]} size={ICON_SIZE} name='info' /> <CustomIcon style={[styles.alertIcon, { color: themes[theme].dangerColor }]} size={ICON_SIZE} name='info' />
) : null} ) : null}
</View> </View>
{subtitle ? ( {subtitle ? (
<Text style={[styles.subtitle, { color: themes[theme!].auxiliaryText }]} numberOfLines={1}> <Text style={[styles.subtitle, { color: themes[theme].auxiliaryText }]} numberOfLines={1}>
{translateSubtitle ? I18n.t(subtitle) : subtitle} {translateSubtitle ? I18n.t(subtitle) : subtitle}
</Text> </Text>
) : null} ) : null}
@ -109,47 +110,52 @@ const Content = React.memo(
</View> </View>
) : null} ) : null}
</View> </View>
) );
}
); );
interface IListButtonPress { interface IListButtonPress extends IListItemButton {
onPress?: Function; onPress: Function;
} }
interface IListItemButton extends IListButtonPress { interface IListItemButton {
title?: string; title?: string;
disabled?: boolean; disabled?: boolean;
theme?: string; theme: string;
backgroundColor?: string; backgroundColor?: string;
underlayColor?: string; underlayColor?: string;
} }
const Button = React.memo<IListItemButton>(({ onPress, backgroundColor, underlayColor, ...props }: IListItemButton) => ( const Button = React.memo(({ onPress, backgroundColor, underlayColor, ...props }: IListButtonPress) => (
<Touch <Touch
onPress={() => onPress!(props.title)} onPress={() => onPress(props.title)}
style={{ backgroundColor: backgroundColor || themes[props.theme!].backgroundColor }} style={{ backgroundColor: backgroundColor || themes[props.theme].backgroundColor }}
underlayColor={underlayColor} underlayColor={underlayColor}
enabled={!props.disabled} enabled={!props.disabled}
theme={props.theme!}> theme={props.theme}>
<Content {...props} /> <Content {...props} />
</Touch> </Touch>
)); ));
interface IListItem extends IListItemContent, IListButtonPress { interface IListItem extends Omit<IListItemContent, 'theme'>, Omit<IListItemButton, 'theme'> {
backgroundColor?: string; backgroundColor?: string;
onPress?: Function;
} }
const ListItem = React.memo<IListItem>(({ ...props }: IListItem) => { const ListItem = React.memo(({ ...props }: IListItem) => {
const { theme } = useTheme();
if (props.onPress) { if (props.onPress) {
return <Button {...props} />; const { onPress } = props;
return <Button {...props} theme={theme} onPress={onPress} />;
} }
return ( return (
<View style={{ backgroundColor: props.backgroundColor || themes[props.theme!].backgroundColor }}> <View style={{ backgroundColor: props.backgroundColor || themes[theme].backgroundColor }}>
<Content {...props} /> <Content {...props} theme={theme} />
</View> </View>
); );
}); });
ListItem.displayName = 'List.Item'; ListItem.displayName = 'List.Item';
export default withTheme(withDimensions(ListItem)); export default ListItem;

View File

@ -1,7 +1,6 @@
import React from 'react'; import React from 'react';
import { StyleSheet, View } from 'react-native'; import { StyleSheet, View } from 'react-native';
import { withTheme } from '../../theme';
import { Header } from '.'; import { Header } from '.';
const styles = StyleSheet.create({ const styles = StyleSheet.create({
@ -11,7 +10,7 @@ const styles = StyleSheet.create({
}); });
interface IListSection { interface IListSection {
children: React.ReactNode; children: (React.ReactElement | null)[] | React.ReactElement | null;
title?: string; title?: string;
translateTitle?: boolean; translateTitle?: boolean;
} }
@ -25,4 +24,4 @@ const ListSection = React.memo(({ children, title, translateTitle }: IListSectio
ListSection.displayName = 'List.Section'; ListSection.displayName = 'List.Section';
export default withTheme(ListSection); export default ListSection;

View File

@ -2,7 +2,7 @@ import React from 'react';
import { StyleSheet, View, ViewStyle } from 'react-native'; import { StyleSheet, View, ViewStyle } from 'react-native';
import { themes } from '../../constants/colors'; import { themes } from '../../constants/colors';
import { withTheme } from '../../theme'; import { useTheme } from '../../theme';
const styles = StyleSheet.create({ const styles = StyleSheet.create({
separator: { separator: {
@ -12,13 +12,14 @@ const styles = StyleSheet.create({
interface IListSeparator { interface IListSeparator {
style?: ViewStyle; style?: ViewStyle;
theme?: string;
} }
const ListSeparator = React.memo(({ style, theme }: IListSeparator) => ( const ListSeparator = React.memo(({ style }: IListSeparator) => {
<View style={[styles.separator, style, { backgroundColor: themes[theme!].separatorColor }]} /> const { theme } = useTheme();
));
return <View style={[styles.separator, style, { backgroundColor: themes[theme].separatorColor }]} />;
});
ListSeparator.displayName = 'List.Separator'; ListSeparator.displayName = 'List.Separator';
export default withTheme(ListSeparator); export default ListSeparator;

View File

@ -22,15 +22,20 @@ interface ILoadingProps {
theme?: string; theme?: string;
} }
class Loading extends React.PureComponent<ILoadingProps, any> { interface ILoadingState {
scale: Animated.Value;
opacity: Animated.Value;
}
class Loading extends React.PureComponent<ILoadingProps, ILoadingState> {
state = { state = {
scale: new Animated.Value(1), scale: new Animated.Value(1),
opacity: new Animated.Value(0) opacity: new Animated.Value(0)
}; };
private opacityAnimation: any; private opacityAnimation?: Animated.CompositeAnimation;
private scaleAnimation: any; private scaleAnimation?: Animated.CompositeAnimation;
componentDidMount() { componentDidMount() {
const { opacity, scale } = this.state; const { opacity, scale } = this.state;
@ -61,7 +66,7 @@ class Loading extends React.PureComponent<ILoadingProps, any> {
} }
} }
componentDidUpdate(prevProps: any) { componentDidUpdate(prevProps: ILoadingProps) {
const { visible } = this.props; const { visible } = this.props;
if (visible && visible !== prevProps.visible) { if (visible && visible !== prevProps.visible) {
this.startAnimations(); this.startAnimations();
@ -107,8 +112,7 @@ class Loading extends React.PureComponent<ILoadingProps, any> {
<Animated.View <Animated.View
style={[ style={[
{ {
// @ts-ignore ...StyleSheet.absoluteFillObject,
...StyleSheet.absoluteFill,
backgroundColor: themes[theme!].backdropColor, backgroundColor: themes[theme!].backdropColor,
opacity: opacityAnimation opacity: opacityAnimation
} }

View File

@ -3,6 +3,7 @@ import { Animated, Easing, Linking, StyleSheet, Text, View } from 'react-native'
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { Base64 } from 'js-base64'; import { Base64 } from 'js-base64';
import * as AppleAuthentication from 'expo-apple-authentication'; import * as AppleAuthentication from 'expo-apple-authentication';
import { StackNavigationProp } from '@react-navigation/stack';
import { withTheme } from '../theme'; import { withTheme } from '../theme';
import sharedStyles from '../views/Styles'; import sharedStyles from '../views/Styles';
@ -15,6 +16,9 @@ import random from '../utils/random';
import { events, logEvent } from '../utils/log'; import { events, logEvent } from '../utils/log';
import RocketChat from '../lib/rocketchat'; import RocketChat from '../lib/rocketchat';
import { CustomIcon } from '../lib/Icons'; import { CustomIcon } from '../lib/Icons';
import { IServices } from '../selectors/login';
import { OutsideParamList } from '../stacks/types';
import { IApplicationState } from '../definitions';
const BUTTON_HEIGHT = 48; const BUTTON_HEIGHT = 48;
const SERVICE_HEIGHT = 58; const SERVICE_HEIGHT = 58;
@ -58,31 +62,40 @@ const styles = StyleSheet.create({
}); });
interface IOpenOAuth { interface IOpenOAuth {
url?: string; url: string;
ssoToken?: string; ssoToken?: string;
authType?: string; authType?: string;
} }
interface IService { interface IItemService {
name: string; name: string;
service: string; service: string;
authType: string; authType: string;
buttonColor: string; buttonColor: string;
buttonLabelColor: string; buttonLabelColor: string;
clientConfig: { provider: string };
serverURL: string;
authorizePath: string;
clientId: string;
scope: string;
}
interface IOauthProvider {
[key: string]: () => void;
facebook: () => void;
github: () => void;
gitlab: () => void;
google: () => void;
linkedin: () => void;
'meteor-developer': () => void;
twitter: () => void;
wordpress: () => void;
} }
interface ILoginServicesProps { interface ILoginServicesProps {
navigation: any; navigation: StackNavigationProp<OutsideParamList>;
server: string; server: string;
services: { services: IServices;
facebook: { clientId: string };
github: { clientId: string };
gitlab: { clientId: string };
google: { clientId: string };
linkedin: { clientId: string };
'meteor-developer': { clientId: string };
wordpress: { clientId: string; serverURL: string };
};
Gitlab_URL: string; Gitlab_URL: string;
CAS_enabled: boolean; CAS_enabled: boolean;
CAS_login_url: string; CAS_login_url: string;
@ -90,12 +103,13 @@ interface ILoginServicesProps {
theme: string; theme: string;
} }
class LoginServices extends React.PureComponent<ILoginServicesProps, any> { interface ILoginServicesState {
private _animation: any; collapsed: boolean;
servicesHeight: Animated.Value;
}
static defaultProps = { class LoginServices extends React.PureComponent<ILoginServicesProps, ILoginServicesState> {
separator: true private _animation?: Animated.CompositeAnimation | void;
};
state = { state = {
collapsed: true, collapsed: true,
@ -194,7 +208,7 @@ class LoginServices extends React.PureComponent<ILoginServicesProps, any> {
this.openOAuth({ url: `${endpoint}${params}` }); this.openOAuth({ url: `${endpoint}${params}` });
}; };
onPressCustomOAuth = (loginService: any) => { onPressCustomOAuth = (loginService: IItemService) => {
logEvent(events.ENTER_WITH_CUSTOM_OAUTH); logEvent(events.ENTER_WITH_CUSTOM_OAUTH);
const { server } = this.props; const { server } = this.props;
const { serverURL, authorizePath, clientId, scope, service } = loginService; const { serverURL, authorizePath, clientId, scope, service } = loginService;
@ -207,7 +221,7 @@ class LoginServices extends React.PureComponent<ILoginServicesProps, any> {
this.openOAuth({ url }); this.openOAuth({ url });
}; };
onPressSaml = (loginService: any) => { onPressSaml = (loginService: IItemService) => {
logEvent(events.ENTER_WITH_SAML); logEvent(events.ENTER_WITH_SAML);
const { server } = this.props; const { server } = this.props;
const { clientConfig } = loginService; const { clientConfig } = loginService;
@ -234,7 +248,6 @@ class LoginServices extends React.PureComponent<ILoginServicesProps, any> {
AppleAuthentication.AppleAuthenticationScope.EMAIL AppleAuthentication.AppleAuthenticationScope.EMAIL
] ]
}); });
await RocketChat.loginOAuthOrSso({ fullName, email, identityToken }); await RocketChat.loginOAuthOrSso({ fullName, email, identityToken });
} catch { } catch {
logEvent(events.ENTER_WITH_APPLE_F); logEvent(events.ENTER_WITH_APPLE_F);
@ -243,7 +256,12 @@ class LoginServices extends React.PureComponent<ILoginServicesProps, any> {
getOAuthState = (loginStyle = LOGIN_STYPE_POPUP) => { getOAuthState = (loginStyle = LOGIN_STYPE_POPUP) => {
const credentialToken = random(43); const credentialToken = random(43);
let obj: any = { loginStyle, credentialToken, isCordova: true }; let obj: {
loginStyle: string;
credentialToken: string;
isCordova: boolean;
redirectUrl?: string;
} = { loginStyle, credentialToken, isCordova: true };
if (loginStyle === LOGIN_STYPE_REDIRECT) { if (loginStyle === LOGIN_STYPE_REDIRECT) {
obj = { obj = {
...obj, ...obj,
@ -263,12 +281,11 @@ class LoginServices extends React.PureComponent<ILoginServicesProps, any> {
if (this._animation) { if (this._animation) {
this._animation.stop(); this._animation.stop();
} }
// @ts-ignore
this._animation = Animated.timing(servicesHeight, { this._animation = Animated.timing(servicesHeight, {
toValue: height, toValue: height,
duration: 300, duration: 300,
// @ts-ignore easing: Easing.inOut(Easing.quad),
easing: Easing.easeOutCubic useNativeDriver: true
}).start(); }).start();
}; };
@ -281,11 +298,11 @@ class LoginServices extends React.PureComponent<ILoginServicesProps, any> {
} else { } else {
this.transitionServicesTo(SERVICES_COLLAPSED_HEIGHT); this.transitionServicesTo(SERVICES_COLLAPSED_HEIGHT);
} }
this.setState((prevState: any) => ({ collapsed: !prevState.collapsed })); this.setState((prevState: ILoginServicesState) => ({ collapsed: !prevState.collapsed }));
}; };
getSocialOauthProvider = (name: string) => { getSocialOauthProvider = (name: string) => {
const oauthProviders: any = { const oauthProviders: IOauthProvider = {
facebook: this.onPressFacebook, facebook: this.onPressFacebook,
github: this.onPressGithub, github: this.onPressGithub,
gitlab: this.onPressGitlab, gitlab: this.onPressGitlab,
@ -324,7 +341,7 @@ class LoginServices extends React.PureComponent<ILoginServicesProps, any> {
return null; return null;
}; };
renderItem = (service: IService) => { renderItem = (service: IItemService) => {
const { CAS_enabled, theme } = this.props; const { CAS_enabled, theme } = this.props;
let { name } = service; let { name } = service;
name = name === 'meteor-developer' ? 'meteor' : name; name = name === 'meteor-developer' ? 'meteor' : name;
@ -401,26 +418,28 @@ class LoginServices extends React.PureComponent<ILoginServicesProps, any> {
if (length > 3 && separator) { if (length > 3 && separator) {
return ( return (
<> <>
<Animated.View style={style}>{Object.values(services).map((service: any) => this.renderItem(service))}</Animated.View> <Animated.View style={style}>
{Object.values(services).map((service: IItemService) => this.renderItem(service))}
</Animated.View>
{this.renderServicesSeparator()} {this.renderServicesSeparator()}
</> </>
); );
} }
return ( return (
<> <>
{Object.values(services).map((service: any) => this.renderItem(service))} {Object.values(services).map((service: IItemService) => this.renderItem(service))}
{this.renderServicesSeparator()} {this.renderServicesSeparator()}
</> </>
); );
} }
} }
const mapStateToProps = (state: any) => ({ const mapStateToProps = (state: IApplicationState) => ({
server: state.server.server, server: state.server.server,
Gitlab_URL: state.settings.API_Gitlab_URL, Gitlab_URL: state.settings.API_Gitlab_URL as string,
CAS_enabled: state.settings.CAS_enabled, CAS_enabled: state.settings.CAS_enabled as boolean,
CAS_login_url: state.settings.CAS_login_url, CAS_login_url: state.settings.CAS_login_url as string,
services: state.login.services services: state.login.services as IServices
}); });
export default connect(mapStateToProps)(withTheme(LoginServices)) as any; export default connect(mapStateToProps)(withTheme(LoginServices));

View File

@ -15,19 +15,12 @@ import { showConfirmationAlert } from '../../utils/info';
import { useActionSheet } from '../ActionSheet'; import { useActionSheet } from '../ActionSheet';
import Header, { HEADER_HEIGHT } from './Header'; import Header, { HEADER_HEIGHT } from './Header';
import events from '../../utils/log/events'; import events from '../../utils/log/events';
import { TMessageModel } from '../../definitions/IMessage'; import { ILoggedUser, TAnyMessageModel, TSubscriptionModel } from '../../definitions';
interface IMessageActions { export interface IMessageActions {
room: { room: TSubscriptionModel;
rid: string; tmid?: string;
autoTranslateLanguage: any; user: Pick<ILoggedUser, 'id'>;
autoTranslate: any;
reactWhenReadOnly: any;
};
tmid: string;
user: {
id: string | number;
};
editInit: Function; editInit: Function;
reactionInit: Function; reactionInit: Function;
onReactionPress: Function; onReactionPress: Function;
@ -270,8 +263,11 @@ const MessageActions = React.memo(
} }
}; };
const handleToggleTranslation = async (message: TMessageModel) => { const handleToggleTranslation = async (message: TAnyMessageModel) => {
try { try {
if (!room.autoTranslateLanguage) {
return;
}
const db = database.active; const db = database.active;
await db.write(async () => { await db.write(async () => {
await message.update(m => { await message.update(m => {
@ -321,7 +317,7 @@ const MessageActions = React.memo(
}); });
}; };
const getOptions = (message: TMessageModel) => { const getOptions = (message: TAnyMessageModel) => {
let options: any = []; let options: any = [];
// Reply // Reply
@ -447,7 +443,7 @@ const MessageActions = React.memo(
return options; return options;
}; };
const showMessageActions = async (message: TMessageModel) => { const showMessageActions = async (message: TAnyMessageModel) => {
logEvent(events.ROOM_SHOW_MSG_ACTIONS); logEvent(events.ROOM_SHOW_MSG_ACTIONS);
await getPermissions(); await getPermissions();
showActionSheet({ showActionSheet({

View File

@ -34,7 +34,7 @@ const Item = ({ item, theme }: IMessageBoxCommandsPreviewItem) => {
resizeMode={FastImage.resizeMode.cover} resizeMode={FastImage.resizeMode.cover}
onLoadStart={() => setLoading(true)} onLoadStart={() => setLoading(true)}
onLoad={() => setLoading(false)}> onLoad={() => setLoading(false)}>
{loading ? <ActivityIndicator theme={theme} /> : null} {loading ? <ActivityIndicator /> : null}
</FastImage> </FastImage>
) : ( ) : (
<CustomIcon name='attach' size={36} color={themes[theme!].actionTintColor} /> <CustomIcon name='attach' size={36} color={themes[theme!].actionTintColor} />

View File

@ -6,9 +6,10 @@ import Item from './Item';
import styles from '../styles'; import styles from '../styles';
import { themes } from '../../../constants/colors'; import { themes } from '../../../constants/colors';
import { withTheme } from '../../../theme'; import { withTheme } from '../../../theme';
import { IPreviewItem } from '../../../definitions';
interface IMessageBoxCommandsPreview { interface IMessageBoxCommandsPreview {
commandPreview: []; commandPreview: IPreviewItem[];
showCommandPreview: boolean; showCommandPreview: boolean;
theme?: string; theme?: string;
} }

View File

@ -0,0 +1,56 @@
import getMentionRegexp from './getMentionRegexp';
const regexp = getMentionRegexp();
describe('getMentionRegexpUser', function () {
test('removing query text on user suggestion autocomplete (latin)', () => {
const message = 'Hey @test123';
expect(message.replace(regexp, '')).toBe('Hey @');
});
test('removing query text on user suggestion autocomplete (arabic)', () => {
const message = 'Hey @اختبار123';
expect(message.replace(regexp, '')).toBe('Hey @');
});
test('removing query text on user suggestion autocomplete (russian)', () => {
const message = 'Hey @тест123';
expect(message.replace(regexp, '')).toBe('Hey @');
});
test('removing query text on user suggestion autocomplete (chinese trad)', () => {
const message = 'Hey @測試123';
expect(message.replace(regexp, '')).toBe('Hey @');
});
test('removing query text on user suggestion autocomplete (japanese)', () => {
const message = 'Hey @テスト123';
expect(message.replace(regexp, '')).toBe('Hey @');
});
test('removing query text on user suggestion autocomplete (special characters in query)', () => {
const message = "Hey @'=test123";
expect(message.replace(regexp, '')).toBe('Hey @');
});
});
describe('getMentionRegexpEmoji', function () {
test('removing query text on emoji suggestion autocomplete ', () => {
const message = 'Hey :smiley';
expect(message.replace(regexp, '')).toBe('Hey :');
});
});
describe('getMentionRegexpCommand', function () {
test('removing query text on emoji suggestion autocomplete ', () => {
const message = '/archive';
expect(message.replace(regexp, '')).toBe('/');
});
});
describe('getMentionRegexpRoom', function () {
test('removing query text on emoji suggestion autocomplete ', () => {
const message = 'Check #general';
expect(message.replace(regexp, '')).toBe('Check #');
});
});

View File

@ -0,0 +1,4 @@
// Match query string from the message to replace it with the suggestion
const getMentionRegexp = (): any => /[^@:#/!]*$/;
export default getMentionRegexp;

View File

@ -9,7 +9,7 @@ import { Q } from '@nozbe/watermelondb';
import { TouchableWithoutFeedback } from 'react-native-gesture-handler'; import { TouchableWithoutFeedback } from 'react-native-gesture-handler';
import { generateTriggerId } from '../../lib/methods/actions'; import { generateTriggerId } from '../../lib/methods/actions';
import TextInput from '../../presentation/TextInput'; import TextInput, { IThemedTextInput } from '../../presentation/TextInput';
import { userTyping as userTypingAction } from '../../actions/room'; import { userTyping as userTypingAction } from '../../actions/room';
import RocketChat from '../../lib/rocketchat'; import RocketChat from '../../lib/rocketchat';
import styles from './styles'; import styles from './styles';
@ -31,6 +31,7 @@ import { isAndroid, isTablet } from '../../utils/deviceInfo';
import { canUploadFile } from '../../utils/media'; import { canUploadFile } from '../../utils/media';
import EventEmiter from '../../utils/events'; import EventEmiter from '../../utils/events';
import { KEY_COMMAND, handleCommandShowUpload, handleCommandSubmit, handleCommandTyping } from '../../commands'; import { KEY_COMMAND, handleCommandShowUpload, handleCommandSubmit, handleCommandTyping } from '../../commands';
import getMentionRegexp from './getMentionRegexp';
import Mentions from './Mentions'; import Mentions from './Mentions';
import MessageboxContext from './Context'; import MessageboxContext from './Context';
import { import {
@ -49,6 +50,7 @@ import { sanitizeLikeString } from '../../lib/database/utils';
import { CustomIcon } from '../../lib/Icons'; import { CustomIcon } from '../../lib/Icons';
import { IMessage } from '../../definitions/IMessage'; import { IMessage } from '../../definitions/IMessage';
import { forceJpgExtension } from './forceJpgExtension'; import { forceJpgExtension } from './forceJpgExtension';
import { IPreviewItem, IUser } from '../../definitions';
if (isAndroid) { if (isAndroid) {
require('./EmojiKeyboard'); require('./EmojiKeyboard');
@ -72,7 +74,7 @@ const videoPickerConfig = {
mediaType: 'video' mediaType: 'video'
}; };
interface IMessageBoxProps { export interface IMessageBoxProps {
rid: string; rid: string;
baseUrl: string; baseUrl: string;
message: IMessage; message: IMessage;
@ -80,12 +82,7 @@ interface IMessageBoxProps {
editing: boolean; editing: boolean;
threadsEnabled: boolean; threadsEnabled: boolean;
isFocused(): boolean; isFocused(): boolean;
user: { user: IUser;
id: string;
_id: string;
username: string;
token: string;
};
roomType: string; roomType: string;
tmid: string; tmid: string;
replyWithMention: boolean; replyWithMention: boolean;
@ -118,7 +115,7 @@ interface IMessageBoxState {
showSend: any; showSend: any;
recording: boolean; recording: boolean;
trackingType: string; trackingType: string;
commandPreview: []; commandPreview: IPreviewItem[];
showCommandPreview: boolean; showCommandPreview: boolean;
command: { command: {
appId?: any; appId?: any;
@ -493,7 +490,7 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
const msg = this.text; const msg = this.text;
const { start, end } = this.selection; const { start, end } = this.selection;
const cursor = Math.max(start, end); const cursor = Math.max(start, end);
const regexp = /([a-z0-9._-]+)$/im; const regexp = getMentionRegexp();
let result = msg.substr(0, cursor).replace(regexp, ''); let result = msg.substr(0, cursor).replace(regexp, '');
// Remove the ! after select the canned response // Remove the ! after select the canned response
if (trackingType === MENTIONS_TRACKING_TYPE_CANNED) { if (trackingType === MENTIONS_TRACKING_TYPE_CANNED) {
@ -609,7 +606,7 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
getCannedResponses = debounce(async (text?: string) => { getCannedResponses = debounce(async (text?: string) => {
const res = await RocketChat.getListCannedResponse({ text }); const res = await RocketChat.getListCannedResponse({ text });
this.setState({ mentions: res?.cannedResponses || [], mentionLoading: false }); this.setState({ mentions: res.success ? res.cannedResponses : [], mentionLoading: false });
}, 500); }, 500);
focus = () => { focus = () => {
@ -642,12 +639,12 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
}, 1000); }, 1000);
}; };
setCommandPreview = async (command: any, name: string, params: any) => { setCommandPreview = async (command: any, name: string, params: string) => {
const { rid } = this.props; const { rid } = this.props;
try { try {
const { success, preview } = await RocketChat.getCommandPreview(name, rid, params); const response = await RocketChat.getCommandPreview(name, rid, params);
if (success) { if (response.success) {
return this.setState({ commandPreview: preview?.items, showCommandPreview: true, command }); return this.setState({ commandPreview: response.preview?.items || [], showCommandPreview: true, command });
} }
} catch (e) { } catch (e) {
log(e); log(e);
@ -891,7 +888,7 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
const messageWithoutCommand = message.replace(/([^\s]+)/, '').trim(); const messageWithoutCommand = message.replace(/([^\s]+)/, '').trim();
const [{ appId }] = slashCommand; const [{ appId }] = slashCommand;
const triggerId = generateTriggerId(appId); const triggerId = generateTriggerId(appId);
RocketChat.runSlashCommand(command, roomId, messageWithoutCommand, triggerId, tmid || messageTmid); await RocketChat.runSlashCommand(command, roomId, messageWithoutCommand, triggerId, tmid || messageTmid);
replyCancel(); replyCancel();
} catch (e) { } catch (e) {
logEvent(events.COMMAND_RUN_F); logEvent(events.COMMAND_RUN_F);
@ -926,8 +923,8 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
let msg = `[ ](${permalink}) `; let msg = `[ ](${permalink}) `;
// if original message wasn't sent by current user and neither from a direct room // if original message wasn't sent by current user and neither from a direct room
if (user.username !== replyingMessage.u.username && roomType !== 'd' && replyWithMention) { if (user.username !== replyingMessage?.u?.username && roomType !== 'd' && replyWithMention) {
msg += `@${replyingMessage.u.username} `; msg += `@${replyingMessage?.u?.username} `;
} }
msg = `${msg} ${message}`; msg = `${msg} ${message}`;
@ -1041,7 +1038,7 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
tmid tmid
} = this.props; } = this.props;
const isAndroidTablet = const isAndroidTablet: Partial<IThemedTextInput> =
isTablet && isAndroid isTablet && isAndroid
? { ? {
multiline: false, multiline: false,
@ -1093,7 +1090,6 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
<TextInput <TextInput
ref={component => (this.component = component)} ref={component => (this.component = component)}
style={[styles.textBoxInput, { color: themes[theme].bodyText }]} style={[styles.textBoxInput, { color: themes[theme].bodyText }]}
// @ts-ignore
returnKeyType='default' returnKeyType='default'
keyboardType='twitter' keyboardType='twitter'
blurOnSubmit={false} blurOnSubmit={false}

View File

@ -1,4 +1,5 @@
import { forwardRef, useImperativeHandle } from 'react'; import { forwardRef, useImperativeHandle } from 'react';
import Model from '@nozbe/watermelondb/Model';
import RocketChat from '../lib/rocketchat'; import RocketChat from '../lib/rocketchat';
import database from '../lib/database'; import database from '../lib/database';
@ -6,18 +7,20 @@ import protectedFunction from '../lib/methods/helpers/protectedFunction';
import { useActionSheet } from './ActionSheet'; import { useActionSheet } from './ActionSheet';
import I18n from '../i18n'; import I18n from '../i18n';
import log from '../utils/log'; import log from '../utils/log';
import { TMessageModel } from '../definitions';
const MessageErrorActions = forwardRef(({ tmid }: any, ref): any => { const MessageErrorActions = forwardRef(({ tmid }: { tmid: string }, ref) => {
// TODO - remove this any after merge ActionSheet evaluate
const { showActionSheet }: any = useActionSheet(); const { showActionSheet }: any = useActionSheet();
const handleResend = protectedFunction(async (message: any) => { const handleResend = protectedFunction(async (message: TMessageModel) => {
await RocketChat.resendMessage(message, tmid); await RocketChat.resendMessage(message, tmid);
}); });
const handleDelete = async (message: any) => { const handleDelete = async (message: TMessageModel) => {
try { try {
const db = database.active; const db = database.active;
const deleteBatch: any = []; const deleteBatch: Model[] = [];
const msgCollection = db.get('messages'); const msgCollection = db.get('messages');
const threadCollection = db.get('threads'); const threadCollection = db.get('threads');
@ -38,7 +41,7 @@ const MessageErrorActions = forwardRef(({ tmid }: any, ref): any => {
const msg = await msgCollection.find(tmid); const msg = await msgCollection.find(tmid);
if (msg?.tcount && msg.tcount <= 1) { if (msg?.tcount && msg.tcount <= 1) {
deleteBatch.push( deleteBatch.push(
msg.prepareUpdate((m: any) => { msg.prepareUpdate(m => {
m.tcount = null; m.tcount = null;
m.tlm = null; m.tlm = null;
}) })
@ -53,8 +56,10 @@ const MessageErrorActions = forwardRef(({ tmid }: any, ref): any => {
} }
} else { } else {
deleteBatch.push( deleteBatch.push(
msg.prepareUpdate((m: any) => { msg.prepareUpdate(m => {
if (m.tcount) {
m.tcount -= 1; m.tcount -= 1;
}
}) })
); );
} }
@ -70,7 +75,7 @@ const MessageErrorActions = forwardRef(({ tmid }: any, ref): any => {
} }
}; };
const showMessageErrorActions = (message: any) => { const showMessageErrorActions = (message: TMessageModel) => {
showActionSheet({ showActionSheet({
options: [ options: [
{ {

View File

@ -5,16 +5,18 @@ import styles from './styles';
import { themes } from '../../../constants/colors'; import { themes } from '../../../constants/colors';
import Touch from '../../../utils/touch'; import Touch from '../../../utils/touch';
import { CustomIcon } from '../../../lib/Icons'; import { CustomIcon } from '../../../lib/Icons';
import { useTheme } from '../../../theme';
interface IPasscodeButton { interface IPasscodeButton {
text?: string; text?: string;
icon?: string; icon?: string;
theme: string;
disabled?: boolean; disabled?: boolean;
onPress?: Function; onPress?: Function;
} }
const Button = React.memo(({ text, disabled, theme, onPress, icon }: IPasscodeButton) => { const Button = React.memo(({ text, disabled, onPress, icon }: IPasscodeButton) => {
const { theme } = useTheme();
const press = () => onPress && onPress(text); const press = () => onPress && onPress(text);
return ( return (

View File

@ -4,17 +4,20 @@ import range from 'lodash/range';
import styles from './styles'; import styles from './styles';
import { themes } from '../../../constants/colors'; import { themes } from '../../../constants/colors';
import { useTheme } from '../../../theme';
const SIZE_EMPTY = 12; const SIZE_EMPTY = 12;
const SIZE_FULL = 16; const SIZE_FULL = 16;
interface IPasscodeDots { interface IPasscodeDots {
passcode: string; passcode: string;
theme: string;
length: number; length: number;
} }
const Dots = React.memo(({ passcode, theme, length }: IPasscodeDots) => ( const Dots = React.memo(({ passcode, length }: IPasscodeDots) => {
const { theme } = useTheme();
return (
<View style={styles.dotsContainer}> <View style={styles.dotsContainer}>
{range(length).map(val => { {range(length).map(val => {
const lengthSup = passcode.length >= val + 1; const lengthSup = passcode.length >= val + 1;
@ -45,6 +48,7 @@ const Dots = React.memo(({ passcode, theme, length }: IPasscodeDots) => (
); );
})} })}
</View> </View>
)); );
});
export default Dots; export default Dots;

View File

@ -5,13 +5,18 @@ import { Row } from 'react-native-easy-grid';
import styles from './styles'; import styles from './styles';
import { themes } from '../../../constants/colors'; import { themes } from '../../../constants/colors';
import { CustomIcon } from '../../../lib/Icons'; import { CustomIcon } from '../../../lib/Icons';
import { useTheme } from '../../../theme';
const LockIcon = React.memo(({ theme }: { theme: string }) => ( const LockIcon = React.memo(() => {
const { theme } = useTheme();
return (
<Row style={styles.row}> <Row style={styles.row}>
<View style={styles.iconView}> <View style={styles.iconView}>
<CustomIcon name='auth' size={40} color={themes[theme].passcodeLockIcon} /> <CustomIcon name='auth' size={40} color={themes[theme].passcodeLockIcon} />
</View> </View>
</Row> </Row>
)); );
});
export default LockIcon; export default LockIcon;

View File

@ -6,36 +6,35 @@ import { resetAttempts } from '../../../utils/localAuthentication';
import { TYPE } from '../constants'; import { TYPE } from '../constants';
import { getDiff, getLockedUntil } from '../utils'; import { getDiff, getLockedUntil } from '../utils';
import I18n from '../../../i18n'; import I18n from '../../../i18n';
import { useTheme } from '../../../theme';
import styles from './styles'; import styles from './styles';
import Title from './Title'; import Title from './Title';
import Subtitle from './Subtitle'; import Subtitle from './Subtitle';
import LockIcon from './LockIcon'; import LockIcon from './LockIcon';
interface IPasscodeTimer { interface IPasscodeTimer {
time: string; time: Date | null;
theme: string;
setStatus: Function; setStatus: Function;
} }
interface IPasscodeLocked { interface IPasscodeLocked {
theme: string;
setStatus: Function; setStatus: Function;
} }
const Timer = React.memo(({ time, theme, setStatus }: IPasscodeTimer) => { const Timer = React.memo(({ time, setStatus }: IPasscodeTimer) => {
const calcTimeLeft = () => { const calcTimeLeft = () => {
const diff = getDiff(time); const diff = getDiff(time || 0);
if (diff > 0) { if (diff > 0) {
return Math.floor((diff / 1000) % 60); return Math.floor((diff / 1000) % 60);
} }
}; };
const [timeLeft, setTimeLeft] = useState<any>(calcTimeLeft()); const [timeLeft, setTimeLeft] = useState(calcTimeLeft());
useEffect(() => { useEffect(() => {
setTimeout(() => { setTimeout(() => {
setTimeLeft(calcTimeLeft()); setTimeLeft(calcTimeLeft());
if (timeLeft <= 1) { if (timeLeft && timeLeft <= 1) {
resetAttempts(); resetAttempts();
setStatus(TYPE.ENTER); setStatus(TYPE.ENTER);
} }
@ -46,11 +45,12 @@ const Timer = React.memo(({ time, theme, setStatus }: IPasscodeTimer) => {
return null; return null;
} }
return <Subtitle text={I18n.t('Passcode_app_locked_subtitle', { timeLeft })} theme={theme} />; return <Subtitle text={I18n.t('Passcode_app_locked_subtitle', { timeLeft })} />;
}); });
const Locked = React.memo(({ theme, setStatus }: IPasscodeLocked) => { const Locked = React.memo(({ setStatus }: IPasscodeLocked) => {
const [lockedUntil, setLockedUntil] = useState<any>(null); const [lockedUntil, setLockedUntil] = useState<Date | null>(null);
const { theme } = useTheme();
const readItemFromStorage = async () => { const readItemFromStorage = async () => {
const l = await getLockedUntil(); const l = await getLockedUntil();
@ -63,9 +63,9 @@ const Locked = React.memo(({ theme, setStatus }: IPasscodeLocked) => {
return ( return (
<Grid style={[styles.grid, { backgroundColor: themes[theme].passcodeBackground }]}> <Grid style={[styles.grid, { backgroundColor: themes[theme].passcodeBackground }]}>
<LockIcon theme={theme} /> <LockIcon />
<Title text={I18n.t('Passcode_app_locked_title')} theme={theme} /> <Title text={I18n.t('Passcode_app_locked_title')} />
<Timer theme={theme} time={lockedUntil} setStatus={setStatus} /> <Timer time={lockedUntil} setStatus={setStatus} />
</Grid> </Grid>
); );
}); });

View File

@ -4,18 +4,22 @@ import { Row } from 'react-native-easy-grid';
import styles from './styles'; import styles from './styles';
import { themes } from '../../../constants/colors'; import { themes } from '../../../constants/colors';
import { useTheme } from '../../../theme';
interface IPasscodeSubtitle { interface IPasscodeSubtitle {
text: string; text: string;
theme: string;
} }
const Subtitle = React.memo(({ text, theme }: IPasscodeSubtitle) => ( const Subtitle = React.memo(({ text }: IPasscodeSubtitle) => {
const { theme } = useTheme();
return (
<Row style={styles.row}> <Row style={styles.row}>
<View style={styles.subtitleView}> <View style={styles.subtitleView}>
<Text style={[styles.textSubtitle, { color: themes[theme].passcodeSecondary }]}>{text}</Text> <Text style={[styles.textSubtitle, { color: themes[theme].passcodeSecondary }]}>{text}</Text>
</View> </View>
</Row> </Row>
)); );
});
export default Subtitle; export default Subtitle;

View File

@ -4,18 +4,22 @@ import { Row } from 'react-native-easy-grid';
import styles from './styles'; import styles from './styles';
import { themes } from '../../../constants/colors'; import { themes } from '../../../constants/colors';
import { useTheme } from '../../../theme';
interface IPasscodeTitle { interface IPasscodeTitle {
text: string; text: string;
theme: string;
} }
const Title = React.memo(({ text, theme }: IPasscodeTitle) => ( const Title = React.memo(({ text }: IPasscodeTitle) => {
const { theme } = useTheme();
return (
<Row style={styles.row}> <Row style={styles.row}>
<View style={styles.titleView}> <View style={styles.titleView}>
<Text style={[styles.textTitle, { color: themes[theme].passcodePrimary }]}>{text}</Text> <Text style={[styles.textTitle, { color: themes[theme].passcodePrimary }]}>{text}</Text>
</View> </View>
</Row> </Row>
)); );
});
export default Title; export default Title;

View File

@ -1,6 +1,7 @@
import React, { forwardRef, useImperativeHandle, useRef, useState } from 'react'; import React, { forwardRef, useImperativeHandle, useRef, useState } from 'react';
import { Col, Grid, Row } from 'react-native-easy-grid'; import { Col, Grid, Row } from 'react-native-easy-grid';
import range from 'lodash/range'; import range from 'lodash/range';
import { View } from 'react-native';
import * as Animatable from 'react-native-animatable'; import * as Animatable from 'react-native-animatable';
import * as Haptics from 'expo-haptics'; import * as Haptics from 'expo-haptics';
@ -10,12 +11,12 @@ import Dots from './Dots';
import { TYPE } from '../constants'; import { TYPE } from '../constants';
import { themes } from '../../../constants/colors'; import { themes } from '../../../constants/colors';
import { PASSCODE_LENGTH } from '../../../constants/localAuthentication'; import { PASSCODE_LENGTH } from '../../../constants/localAuthentication';
import { useTheme } from '../../../theme';
import LockIcon from './LockIcon'; import LockIcon from './LockIcon';
import Title from './Title'; import Title from './Title';
import Subtitle from './Subtitle'; import Subtitle from './Subtitle';
interface IPasscodeBase { interface IPasscodeBase {
theme: string;
type: string; type: string;
previousPasscode?: string; previousPasscode?: string;
title: string; title: string;
@ -26,25 +27,30 @@ interface IPasscodeBase {
onBiometryPress?(): void; onBiometryPress?(): void;
} }
const Base = forwardRef( export interface IBase {
( clearPasscode: () => void;
{ theme, type, onEndProcess, previousPasscode, title, subtitle, onError, showBiometry, onBiometryPress }: IPasscodeBase, wrongPasscode: () => void;
ref animate: (animation: Animatable.Animation, duration?: number) => void;
) => { }
const rootRef = useRef<any>();
const dotsRef = useRef<any>(); const Base = forwardRef<IBase, IPasscodeBase>(
({ type, onEndProcess, previousPasscode, title, subtitle, onError, showBiometry, onBiometryPress }, ref) => {
const { theme } = useTheme();
const rootRef = useRef<Animatable.View & View>(null);
const dotsRef = useRef<Animatable.View & View>(null);
const [passcode, setPasscode] = useState(''); const [passcode, setPasscode] = useState('');
const clearPasscode = () => setPasscode(''); const clearPasscode = () => setPasscode('');
const wrongPasscode = () => { const wrongPasscode = () => {
clearPasscode(); clearPasscode();
dotsRef?.current?.shake(500); dotsRef?.current?.shake?.(500);
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error); Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error);
}; };
const animate = (animation: string, duration = 500) => { const animate = (animation: Animatable.Animation, duration = 500) => {
rootRef?.current?.[animation](duration); rootRef?.current?.[animation]?.(duration);
}; };
const onPressNumber = (text: string) => const onPressNumber = (text: string) =>
@ -90,48 +96,48 @@ const Base = forwardRef(
return ( return (
<Animatable.View ref={rootRef} style={styles.container}> <Animatable.View ref={rootRef} style={styles.container}>
<Grid style={[styles.grid, { backgroundColor: themes[theme].passcodeBackground }]}> <Grid style={[styles.grid, { backgroundColor: themes[theme].passcodeBackground }]}>
<LockIcon theme={theme} /> <LockIcon />
<Title text={title} theme={theme} /> <Title text={title} />
<Subtitle text={subtitle!} theme={theme} /> {subtitle ? <Subtitle text={subtitle} /> : null}
<Row style={styles.row}> <Row style={styles.row}>
<Animatable.View ref={dotsRef}> <Animatable.View ref={dotsRef}>
<Dots passcode={passcode} theme={theme} length={PASSCODE_LENGTH} /> <Dots passcode={passcode} length={PASSCODE_LENGTH} />
</Animatable.View> </Animatable.View>
</Row> </Row>
<Row style={[styles.row, styles.buttonRow]}> <Row style={[styles.row, styles.buttonRow]}>
{range(1, 4).map((i: any) => ( {range(1, 4).map(i => (
<Col key={i} style={styles.colButton}> <Col key={i} style={styles.colButton}>
<Button text={i} theme={theme} onPress={onPressNumber} /> <Button text={i.toString()} onPress={onPressNumber} />
</Col> </Col>
))} ))}
</Row> </Row>
<Row style={[styles.row, styles.buttonRow]}> <Row style={[styles.row, styles.buttonRow]}>
{range(4, 7).map((i: any) => ( {range(4, 7).map(i => (
<Col key={i} style={styles.colButton}> <Col key={i} style={styles.colButton}>
<Button text={i} theme={theme} onPress={onPressNumber} /> <Button text={i.toString()} onPress={onPressNumber} />
</Col> </Col>
))} ))}
</Row> </Row>
<Row style={[styles.row, styles.buttonRow]}> <Row style={[styles.row, styles.buttonRow]}>
{range(7, 10).map((i: any) => ( {range(7, 10).map(i => (
<Col key={i} style={styles.colButton}> <Col key={i} style={styles.colButton}>
<Button text={i} theme={theme} onPress={onPressNumber} /> <Button text={i.toString()} onPress={onPressNumber} />
</Col> </Col>
))} ))}
</Row> </Row>
<Row style={[styles.row, styles.buttonRow]}> <Row style={[styles.row, styles.buttonRow]}>
{showBiometry ? ( {showBiometry ? (
<Col style={styles.colButton}> <Col style={styles.colButton}>
<Button icon='fingerprint' theme={theme} onPress={onBiometryPress} /> <Button icon='fingerprint' onPress={onBiometryPress} />
</Col> </Col>
) : ( ) : (
<Col style={styles.colButton} /> <Col style={styles.colButton} />
)} )}
<Col style={styles.colButton}> <Col style={styles.colButton}>
<Button text='0' theme={theme} onPress={onPressNumber} /> <Button text='0' onPress={onPressNumber} />
</Col> </Col>
<Col style={styles.colButton}> <Col style={styles.colButton}>
<Button icon='backspace' theme={theme} onPress={onPressDelete} /> <Button icon='backspace' onPress={onPressDelete} />
</Col> </Col>
</Row> </Row>
</Grid> </Grid>

View File

@ -2,24 +2,23 @@ import React, { useRef, useState } from 'react';
import * as Haptics from 'expo-haptics'; import * as Haptics from 'expo-haptics';
import { gestureHandlerRootHOC } from 'react-native-gesture-handler'; import { gestureHandlerRootHOC } from 'react-native-gesture-handler';
import Base from './Base'; import Base, { IBase } from './Base';
import { TYPE } from './constants'; import { TYPE } from './constants';
import I18n from '../../i18n'; import I18n from '../../i18n';
interface IPasscodeChoose { interface IPasscodeChoose {
theme: string;
force?: boolean; force?: boolean;
finishProcess: Function; finishProcess: Function;
} }
const PasscodeChoose = ({ theme, finishProcess, force = false }: IPasscodeChoose) => { const PasscodeChoose = ({ finishProcess, force = false }: IPasscodeChoose) => {
const chooseRef = useRef<any>(null); const chooseRef = useRef<IBase>(null);
const confirmRef = useRef<any>(null); const confirmRef = useRef<IBase>(null);
const [subtitle, setSubtitle] = useState(null); const [subtitle, setSubtitle] = useState(null);
const [status, setStatus] = useState(TYPE.CHOOSE); const [status, setStatus] = useState(TYPE.CHOOSE);
const [previousPasscode, setPreviouPasscode] = useState<any>(null); const [previousPasscode, setPreviouPasscode] = useState('');
const firstStep = (p: any) => { const firstStep = (p: string) => {
setTimeout(() => { setTimeout(() => {
setStatus(TYPE.CONFIRM); setStatus(TYPE.CONFIRM);
setPreviouPasscode(p); setPreviouPasscode(p);
@ -43,7 +42,6 @@ const PasscodeChoose = ({ theme, finishProcess, force = false }: IPasscodeChoose
return ( return (
<Base <Base
ref={confirmRef} ref={confirmRef}
theme={theme}
type={TYPE.CONFIRM} type={TYPE.CONFIRM}
onEndProcess={changePasscode} onEndProcess={changePasscode}
previousPasscode={previousPasscode} previousPasscode={previousPasscode}
@ -56,7 +54,6 @@ const PasscodeChoose = ({ theme, finishProcess, force = false }: IPasscodeChoose
return ( return (
<Base <Base
ref={chooseRef} ref={chooseRef}
theme={theme}
type={TYPE.CHOOSE} type={TYPE.CHOOSE}
onEndProcess={firstStep} onEndProcess={firstStep}
title={I18n.t('Passcode_choose_title')} title={I18n.t('Passcode_choose_title')}

View File

@ -4,35 +4,29 @@ import { gestureHandlerRootHOC } from 'react-native-gesture-handler';
import * as Haptics from 'expo-haptics'; import * as Haptics from 'expo-haptics';
import { sha256 } from 'js-sha256'; import { sha256 } from 'js-sha256';
import Base from './Base'; import Base, { IBase } from './Base';
import Locked from './Base/Locked'; import Locked from './Base/Locked';
import { TYPE } from './constants'; import { TYPE } from './constants';
import { ATTEMPTS_KEY, LOCKED_OUT_TIMER_KEY, MAX_ATTEMPTS, PASSCODE_KEY } from '../../constants/localAuthentication'; import { ATTEMPTS_KEY, LOCKED_OUT_TIMER_KEY, MAX_ATTEMPTS, PASSCODE_KEY } from '../../constants/localAuthentication';
import { biometryAuth, resetAttempts } from '../../utils/localAuthentication'; import { biometryAuth, resetAttempts } from '../../utils/localAuthentication';
import { getDiff, getLockedUntil } from './utils'; import { getDiff, getLockedUntil } from './utils';
import UserPreferences from '../../lib/userPreferences'; import { useUserPreferences } from '../../lib/userPreferences';
import I18n from '../../i18n'; import I18n from '../../i18n';
interface IPasscodePasscodeEnter { interface IPasscodePasscodeEnter {
theme: string;
hasBiometry: boolean; hasBiometry: boolean;
finishProcess: Function; finishProcess: Function;
} }
const PasscodeEnter = ({ theme, hasBiometry, finishProcess }: IPasscodePasscodeEnter) => { const PasscodeEnter = ({ hasBiometry, finishProcess }: IPasscodePasscodeEnter) => {
const ref = useRef(null); const ref = useRef<IBase>(null);
let attempts: any = 0; let attempts = 0;
let lockedUntil: any = false; let lockedUntil: any = false;
const [passcode, setPasscode] = useState(null); const [passcode] = useUserPreferences(PASSCODE_KEY);
const [status, setStatus] = useState(null); const [status, setStatus] = useState<TYPE | null>(null);
const { getItem: getAttempts, setItem: setAttempts } = useAsyncStorage(ATTEMPTS_KEY); const { setItem: setAttempts } = useAsyncStorage(ATTEMPTS_KEY);
const { setItem: setLockedUntil } = useAsyncStorage(LOCKED_OUT_TIMER_KEY); const { setItem: setLockedUntil } = useAsyncStorage(LOCKED_OUT_TIMER_KEY);
const fetchPasscode = async () => {
const p: any = await UserPreferences.getStringAsync(PASSCODE_KEY);
setPasscode(p);
};
const biometry = async () => { const biometry = async () => {
if (hasBiometry && status === TYPE.ENTER) { if (hasBiometry && status === TYPE.ENTER) {
const result = await biometryAuth(); const result = await biometryAuth();
@ -50,13 +44,11 @@ const PasscodeEnter = ({ theme, hasBiometry, finishProcess }: IPasscodePasscodeE
await resetAttempts(); await resetAttempts();
setStatus(TYPE.ENTER); setStatus(TYPE.ENTER);
} else { } else {
attempts = await getAttempts();
setStatus(TYPE.LOCKED); setStatus(TYPE.LOCKED);
} }
} else { } else {
setStatus(TYPE.ENTER); setStatus(TYPE.ENTER);
} }
await fetchPasscode();
biometry(); biometry();
}; };
@ -64,7 +56,7 @@ const PasscodeEnter = ({ theme, hasBiometry, finishProcess }: IPasscodePasscodeE
readStorage(); readStorage();
}, [status]); }, [status]);
const onEndProcess = (p: any) => { const onEndProcess = (p: string) => {
setTimeout(() => { setTimeout(() => {
if (sha256(p) === passcode) { if (sha256(p) === passcode) {
finishProcess(); finishProcess();
@ -75,8 +67,7 @@ const PasscodeEnter = ({ theme, hasBiometry, finishProcess }: IPasscodePasscodeE
setLockedUntil(new Date().toISOString()); setLockedUntil(new Date().toISOString());
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error); Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error);
} else { } else {
// @ts-ignore ref?.current?.wrongPasscode();
ref.current.wrongPasscode();
setAttempts(attempts?.toString()); setAttempts(attempts?.toString());
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Warning); Haptics.notificationAsync(Haptics.NotificationFeedbackType.Warning);
} }
@ -85,13 +76,12 @@ const PasscodeEnter = ({ theme, hasBiometry, finishProcess }: IPasscodePasscodeE
}; };
if (status === TYPE.LOCKED) { if (status === TYPE.LOCKED) {
return <Locked theme={theme} setStatus={setStatus} />; return <Locked setStatus={setStatus} />;
} }
return ( return (
<Base <Base
ref={ref} ref={ref}
theme={theme}
type={TYPE.ENTER} type={TYPE.ENTER}
title={I18n.t('Passcode_enter_title')} title={I18n.t('Passcode_enter_title')}
showBiometry={hasBiometry} showBiometry={hasBiometry}

View File

@ -1,6 +1,6 @@
export const TYPE: any = { export enum TYPE {
CHOOSE: 'choose', CHOOSE = 'choose',
CONFIRM: 'confirm', CONFIRM = 'confirm',
ENTER: 'enter', ENTER = 'enter',
LOCKED: 'locked' LOCKED = 'locked'
}; }

View File

@ -4,11 +4,11 @@ import moment from 'moment';
import { LOCKED_OUT_TIMER_KEY, TIME_TO_LOCK } from '../../constants/localAuthentication'; import { LOCKED_OUT_TIMER_KEY, TIME_TO_LOCK } from '../../constants/localAuthentication';
export const getLockedUntil = async () => { export const getLockedUntil = async () => {
const t: any = await AsyncStorage.getItem(LOCKED_OUT_TIMER_KEY); const t = await AsyncStorage.getItem(LOCKED_OUT_TIMER_KEY);
if (t) { if (t) {
return moment(t).add(TIME_TO_LOCK); return moment(t).add(TIME_TO_LOCK).toDate();
} }
return null; return null;
}; };
// @ts-ignore
export const getDiff = t => new Date(t) - new Date(); export const getDiff = (t: string | number | Date) => new Date(t).getTime() - new Date().getTime();

View File

@ -10,6 +10,7 @@ import sharedStyles from '../views/Styles';
import { themes } from '../constants/colors'; import { themes } from '../constants/colors';
import { withTheme } from '../theme'; import { withTheme } from '../theme';
import { TGetCustomEmoji } from '../definitions/IEmoji'; import { TGetCustomEmoji } from '../definitions/IEmoji';
import { TMessageModel, ILoggedUser } from '../definitions';
import SafeAreaView from './SafeAreaView'; import SafeAreaView from './SafeAreaView';
const styles = StyleSheet.create({ const styles = StyleSheet.create({
@ -65,23 +66,25 @@ interface IItem {
usernames: any; usernames: any;
emoji: string; emoji: string;
}; };
user?: { username: any }; user?: Pick<ILoggedUser, 'username'>;
baseUrl?: string; baseUrl?: string;
getCustomEmoji?: TGetCustomEmoji; getCustomEmoji?: TGetCustomEmoji;
theme?: string; theme?: string;
} }
interface IModalContent { interface IModalContent {
message?: { message?: TMessageModel;
reactions: any;
};
onClose: Function; onClose: Function;
theme: string; theme: string;
} }
interface IReactionsModal { interface IReactionsModal {
message?: any;
user?: Pick<ILoggedUser, 'username'>;
isVisible: boolean; isVisible: boolean;
onClose(): void; onClose(): void;
baseUrl: string;
getCustomEmoji?: TGetCustomEmoji;
theme: string; theme: string;
} }

View File

@ -7,6 +7,7 @@ import { themes } from '../../constants/colors';
import { MarkdownPreview } from '../markdown'; import { MarkdownPreview } from '../markdown';
import RoomTypeIcon from '../RoomTypeIcon'; import RoomTypeIcon from '../RoomTypeIcon';
import { withTheme } from '../../theme'; import { withTheme } from '../../theme';
import { TUserStatus } from '../../definitions';
const HIT_SLOP = { const HIT_SLOP = {
top: 5, top: 5,
@ -67,7 +68,7 @@ interface IRoomHeader {
prid: string; prid: string;
tmid: string; tmid: string;
teamMain: boolean; teamMain: boolean;
status: string; status: TUserStatus;
theme?: string; theme?: string;
usersTyping: []; usersTyping: [];
isGroupChat: boolean; isGroupChat: boolean;

View File

@ -2,7 +2,7 @@ import { dequal } from 'dequal';
import React, { Component } from 'react'; import React, { Component } from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { IApplicationState } from '../../definitions'; import { IApplicationState, TUserStatus } from '../../definitions';
import { withDimensions } from '../../dimensions'; import { withDimensions } from '../../dimensions';
import I18n from '../../i18n'; import I18n from '../../i18n';
import RoomHeader from './RoomHeader'; import RoomHeader from './RoomHeader';
@ -15,7 +15,7 @@ interface IRoomHeaderContainerProps {
tmid: string; tmid: string;
teamMain: boolean; teamMain: boolean;
usersTyping: []; usersTyping: [];
status: string; status: TUserStatus;
statusText: string; statusText: string;
connecting: boolean; connecting: boolean;
connected: boolean; connected: boolean;
@ -140,7 +140,7 @@ const mapStateToProps = (state: IApplicationState, ownProps: any) => {
connecting: state.meteor.connecting || state.server.loading, connecting: state.meteor.connecting || state.server.loading,
connected: state.meteor.connected, connected: state.meteor.connected,
usersTyping: state.usersTyping, usersTyping: state.usersTyping,
status, status: status as TUserStatus,
statusText statusText
}; };
}; };

View File

@ -5,6 +5,7 @@ import { CustomIcon } from '../lib/Icons';
import { STATUS_COLORS, themes } from '../constants/colors'; import { STATUS_COLORS, themes } from '../constants/colors';
import Status from './Status/Status'; import Status from './Status/Status';
import { withTheme } from '../theme'; import { withTheme } from '../theme';
import { TUserStatus } from '../definitions';
const styles = StyleSheet.create({ const styles = StyleSheet.create({
icon: { icon: {
@ -17,7 +18,7 @@ interface IRoomTypeIcon {
type: string; type: string;
isGroupChat?: boolean; isGroupChat?: boolean;
teamMain?: boolean; teamMain?: boolean;
status?: string; status?: TUserStatus;
size?: number; size?: number;
style?: ViewStyle; style?: ViewStyle;
} }
@ -31,9 +32,10 @@ const RoomTypeIcon = React.memo(({ type, isGroupChat, status, style, theme, team
const iconStyle = [styles.icon, { color }, style]; const iconStyle = [styles.icon, { color }, style];
if (type === 'd' && !isGroupChat) { if (type === 'd' && !isGroupChat) {
return ( if (!status) {
<Status style={[iconStyle, { color: STATUS_COLORS[status!] ?? STATUS_COLORS.offline }]} size={size} status={status!} /> status = 'offline';
); }
return <Status style={[iconStyle, { color: STATUS_COLORS[status] }]} size={size} status={status} />;
} }
// TODO: move this to a separate function // TODO: move this to a separate function

View File

@ -1,22 +1,14 @@
import React from 'react'; import React from 'react';
import { import { StyleSheet, Text, TextInput as RNTextInput, TextInputProps, View } from 'react-native';
NativeSyntheticEvent,
StyleSheet,
TextInput as RNTextInput,
Text,
TextInputFocusEventData,
TextInputProps,
View
} from 'react-native';
import Touchable from 'react-native-platform-touchable'; import Touchable from 'react-native-platform-touchable';
import TextInput from '../presentation/TextInput'; import { themes } from '../constants/colors';
import I18n from '../i18n'; import I18n from '../i18n';
import { CustomIcon } from '../lib/Icons'; import { CustomIcon } from '../lib/Icons';
import sharedStyles from '../views/Styles'; import TextInput from '../presentation/TextInput';
import { withTheme } from '../theme'; import { useTheme } from '../theme';
import { themes } from '../constants/colors';
import { isIOS } from '../utils/deviceInfo'; import { isIOS } from '../utils/deviceInfo';
import sharedStyles from '../views/Styles';
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { container: {
@ -52,41 +44,32 @@ const styles = StyleSheet.create({
} }
}); });
interface ISearchBox { interface ISearchBox extends TextInputProps {
value?: string; value?: string;
onChangeText: TextInputProps['onChangeText'];
onSubmitEditing?: () => void;
hasCancel?: boolean; hasCancel?: boolean;
onCancelPress?: Function; onCancelPress?: Function;
theme?: string;
inputRef?: React.Ref<RNTextInput>; inputRef?: React.Ref<RNTextInput>;
testID?: string;
onFocus?: (e: NativeSyntheticEvent<TextInputFocusEventData>) => void;
} }
const CancelButton = (onCancelPress: Function, theme: string) => ( const CancelButton = ({ onCancelPress }: { onCancelPress?: Function }) => {
const { theme } = useTheme();
return (
<Touchable onPress={onCancelPress} style={styles.cancel}> <Touchable onPress={onCancelPress} style={styles.cancel}>
<Text style={[styles.cancelText, { color: themes[theme].headerTintColor }]}>{I18n.t('Cancel')}</Text> <Text style={[styles.cancelText, { color: themes[theme].headerTintColor }]}>{I18n.t('Cancel')}</Text>
</Touchable> </Touchable>
); );
};
const SearchBox = ({ const SearchBox = ({ hasCancel, onCancelPress, inputRef, ...props }: ISearchBox): React.ReactElement => {
onChangeText, const { theme } = useTheme();
onSubmitEditing, return (
testID,
hasCancel,
onCancelPress,
inputRef,
theme,
...props
}: ISearchBox) => (
<View <View
style={[ style={[
styles.container, styles.container,
{ backgroundColor: isIOS ? themes[theme!].headerBackground : themes[theme!].headerSecondaryBackground } { backgroundColor: isIOS ? themes[theme].headerBackground : themes[theme].headerSecondaryBackground }
]}> ]}>
<View style={[styles.searchBox, { backgroundColor: themes[theme!].searchboxBackground }]}> <View style={[styles.searchBox, { backgroundColor: themes[theme].searchboxBackground }]}>
<CustomIcon name='search' size={14} color={themes[theme!].auxiliaryText} /> <CustomIcon name='search' size={14} color={themes[theme].auxiliaryText} />
<TextInput <TextInput
ref={inputRef} ref={inputRef}
autoCapitalize='none' autoCapitalize='none'
@ -96,16 +79,14 @@ const SearchBox = ({
placeholder={I18n.t('Search')} placeholder={I18n.t('Search')}
returnKeyType='search' returnKeyType='search'
style={styles.input} style={styles.input}
testID={testID}
underlineColorAndroid='transparent' underlineColorAndroid='transparent'
onChangeText={onChangeText} theme={theme}
onSubmitEditing={onSubmitEditing}
theme={theme!}
{...props} {...props}
/> />
</View> </View>
{hasCancel ? CancelButton(onCancelPress!, theme!) : null} {hasCancel ? <CancelButton onCancelPress={onCancelPress} /> : null}
</View> </View>
); );
};
export default withTheme(SearchBox); export default SearchBox;

View File

@ -3,15 +3,9 @@ import { StyleProp, TextStyle } from 'react-native';
import { CustomIcon } from '../../lib/Icons'; import { CustomIcon } from '../../lib/Icons';
import { STATUS_COLORS } from '../../constants/colors'; import { STATUS_COLORS } from '../../constants/colors';
import { IStatus } from './definition';
interface IStatus { const Status = React.memo(({ style, status = 'offline', size = 32, ...props }: Omit<IStatus, 'id'>) => {
status: string;
size: number;
style?: StyleProp<TextStyle>;
testID?: string;
}
const Status = React.memo(({ style, status = 'offline', size = 32, ...props }: IStatus) => {
const name = `status-${status}`; const name = `status-${status}`;
const isNameValid = CustomIcon.hasIcon(name); const isNameValid = CustomIcon.hasIcon(name);
const iconName = isNameValid ? name : 'status-offline'; const iconName = isNameValid ? name : 'status-offline';

View File

@ -0,0 +1,9 @@
import { TextProps } from 'react-native';
import { TUserStatus } from '../../definitions';
export interface IStatus extends TextProps {
id: string;
size: number;
status: TUserStatus;
}

View File

@ -1,20 +1,15 @@
import React, { memo } from 'react'; import React from 'react';
import { connect } from 'react-redux'; import { useSelector } from 'react-redux';
import { IApplicationState, TUserStatus } from '../../definitions';
import Status from './Status'; import Status from './Status';
import { IStatus } from './definition';
interface IStatusContainer { const StatusContainer = ({ id, style, size = 32, ...props }: Omit<IStatus, 'status'>): React.ReactElement => {
style: any; const status = useSelector((state: IApplicationState) =>
size: number; state.meteor.connected ? state.activeUsers[id] && state.activeUsers[id].status : 'loading'
status: string; ) as TUserStatus;
} return <Status size={size} style={style} status={status} {...props} />;
};
const StatusContainer = memo(({ style, size = 32, status }: IStatusContainer) => ( export default StatusContainer;
<Status size={size} style={style} status={status} />
));
const mapStateToProps = (state: any, ownProps: any) => ({
status: state.meteor.connected ? state.activeUsers[ownProps.id] && state.activeUsers[ownProps.id].status : 'loading'
});
export default connect(mapStateToProps)(StatusContainer);

View File

@ -2,22 +2,27 @@ import React from 'react';
import { StatusBar as StatusBarRN } from 'react-native'; import { StatusBar as StatusBarRN } from 'react-native';
import { themes } from '../constants/colors'; import { themes } from '../constants/colors';
import { withTheme } from '../theme'; import { useTheme } from '../theme';
const supportedStyles = {
'light-content': 'light-content',
'dark-content': 'dark-content'
};
interface IStatusBar { interface IStatusBar {
theme?: string; barStyle?: keyof typeof supportedStyles;
barStyle?: any;
backgroundColor?: string; backgroundColor?: string;
} }
const StatusBar = React.memo(({ theme, barStyle, backgroundColor }: IStatusBar) => { const StatusBar = React.memo(({ barStyle, backgroundColor }: IStatusBar) => {
const { theme } = useTheme();
if (!barStyle) { if (!barStyle) {
barStyle = 'light-content'; barStyle = 'light-content';
if (theme === 'light') { if (theme === 'light') {
barStyle = 'dark-content'; barStyle = 'dark-content';
} }
} }
return <StatusBarRN backgroundColor={backgroundColor ?? themes[theme!].headerBackground} barStyle={barStyle} animated />; return <StatusBarRN backgroundColor={backgroundColor ?? themes[theme].headerBackground} barStyle={barStyle} animated />;
}); });
export default withTheme(StatusBar); export default StatusBar;

View File

@ -52,10 +52,7 @@ const styles = StyleSheet.create({
export interface IRCTextInputProps extends TextInputProps { export interface IRCTextInputProps extends TextInputProps {
label?: string; label?: string;
error?: { error?: any;
error: any;
reason: any;
};
loading?: boolean; loading?: boolean;
containerStyle?: StyleProp<ViewStyle>; containerStyle?: StyleProp<ViewStyle>;
inputStyle?: StyleProp<TextStyle>; inputStyle?: StyleProp<TextStyle>;
@ -68,7 +65,11 @@ export interface IRCTextInputProps extends TextInputProps {
theme: string; theme: string;
} }
export default class RCTextInput extends React.PureComponent<IRCTextInputProps, any> { interface IRCTextInputState {
showPassword: boolean;
}
export default class RCTextInput extends React.PureComponent<IRCTextInputProps, IRCTextInputState> {
static defaultProps = { static defaultProps = {
error: {}, error: {},
theme: 'light' theme: 'light'
@ -116,12 +117,11 @@ export default class RCTextInput extends React.PureComponent<IRCTextInputProps,
get loading() { get loading() {
const { theme } = this.props; const { theme } = this.props;
// @ts-ignore return <ActivityIndicator style={[styles.iconContainer, styles.iconRight]} color={themes[theme].bodyText} />;
return <ActivityIndicator style={[styles.iconContainer, styles.iconRight, { color: themes[theme].bodyText }]} />;
} }
tooglePassword = () => { tooglePassword = () => {
this.setState((prevState: any) => ({ showPassword: !prevState.showPassword })); this.setState(prevState => ({ showPassword: !prevState.showPassword }));
}; };
render() { render() {

View File

@ -41,7 +41,7 @@ const styles = StyleSheet.create({
}); });
interface IThreadDetails { interface IThreadDetails {
item: Partial<TThreadModel>; item: Pick<TThreadModel, 'tcount' | 'replies' | 'id'>;
user: { user: {
id: string; id: string;
}; };
@ -52,9 +52,9 @@ interface IThreadDetails {
const ThreadDetails = ({ item, user, badgeColor, toggleFollowThread, style }: IThreadDetails): JSX.Element => { const ThreadDetails = ({ item, user, badgeColor, toggleFollowThread, style }: IThreadDetails): JSX.Element => {
const { theme } = useTheme(); const { theme } = useTheme();
let { tcount } = item; let count: string | number | undefined | null = item.tcount;
if (tcount && tcount >= 1000) { if (count && count >= 1000) {
tcount = '+999'; count = '+999';
} }
let replies: number | string = item?.replies?.length ?? 0; let replies: number | string = item?.replies?.length ?? 0;
@ -62,21 +62,21 @@ const ThreadDetails = ({ item, user, badgeColor, toggleFollowThread, style }: IT
replies = '+999'; replies = '+999';
} }
const isFollowing = item.replies?.find((u: any) => u === user?.id); const isFollowing = item.replies?.find((u: string) => u === user?.id);
return ( return (
<View style={[styles.container, style]}> <View style={[styles.container, style]}>
<View style={styles.detailsContainer}> <View style={styles.detailsContainer}>
<View style={styles.detailContainer}> <View style={styles.detailContainer}>
<CustomIcon name='threads' size={24} color={themes[theme!].auxiliaryText} /> <CustomIcon name='threads' size={24} color={themes[theme].auxiliaryText} />
<Text style={[styles.detailText, { color: themes[theme!].auxiliaryText }]} numberOfLines={1}> <Text style={[styles.detailText, { color: themes[theme].auxiliaryText }]} numberOfLines={1}>
{tcount} {count}
</Text> </Text>
</View> </View>
<View style={styles.detailContainer}> <View style={styles.detailContainer}>
<CustomIcon name='user' size={24} color={themes[theme!].auxiliaryText} /> <CustomIcon name='user' size={24} color={themes[theme].auxiliaryText} />
<Text style={[styles.detailText, { color: themes[theme!].auxiliaryText }]} numberOfLines={1}> <Text style={[styles.detailText, { color: themes[theme].auxiliaryText }]} numberOfLines={1}>
{replies} {replies}
</Text> </Text>
</View> </View>
@ -87,7 +87,7 @@ const ThreadDetails = ({ item, user, badgeColor, toggleFollowThread, style }: IT
<CustomIcon <CustomIcon
size={24} size={24}
name={isFollowing ? 'notification' : 'notification-disabled'} name={isFollowing ? 'notification' : 'notification-disabled'}
color={themes[theme!].auxiliaryTintColor} color={themes[theme].auxiliaryTintColor}
/> />
</Touchable> </Touchable>
</View> </View>

View File

@ -26,9 +26,9 @@ interface IToastProps {
} }
class Toast extends React.Component<IToastProps, any> { class Toast extends React.Component<IToastProps, any> {
private listener: any; private listener?: Function;
private toast: any; private toast: EasyToast | null | undefined;
componentDidMount() { componentDidMount() {
this.listener = EventEmitter.addEventListener(LISTENER, this.showToast); this.listener = EventEmitter.addEventListener(LISTENER, this.showToast);
@ -43,12 +43,14 @@ class Toast extends React.Component<IToastProps, any> {
} }
componentWillUnmount() { componentWillUnmount() {
if (this.listener) {
EventEmitter.removeListener(LISTENER, this.listener); EventEmitter.removeListener(LISTENER, this.listener);
} }
}
getToastRef = (toast: any) => (this.toast = toast); getToastRef = (toast: EasyToast | null) => (this.toast = toast);
showToast = ({ message }: any) => { showToast = ({ message }: { message: string }) => {
if (this.toast && this.toast.show) { if (this.toast && this.toast.show) {
this.toast.show(message, 1000); this.toast.show(message, 1000);
} }

View File

@ -9,21 +9,36 @@ import { connect } from 'react-redux';
import TextInput from '../TextInput'; import TextInput from '../TextInput';
import I18n from '../../i18n'; import I18n from '../../i18n';
import EventEmitter from '../../utils/events'; import EventEmitter from '../../utils/events';
import { withTheme } from '../../theme'; import { useTheme } from '../../theme';
import { themes } from '../../constants/colors'; import { themes } from '../../constants/colors';
import Button from '../Button'; import Button from '../Button';
import sharedStyles from '../../views/Styles'; import sharedStyles from '../../views/Styles';
import RocketChat from '../../lib/rocketchat'; import RocketChat from '../../lib/rocketchat';
import styles from './styles'; import styles from './styles';
import { IApplicationState } from '../../definitions';
export const TWO_FACTOR = 'TWO_FACTOR'; export const TWO_FACTOR = 'TWO_FACTOR';
interface ITwoFactor { interface IMethodsProp {
theme?: string; text: string;
isMasterDetail: boolean; keyboardType: 'numeric' | 'default';
title?: string;
secureTextEntry?: boolean;
}
interface IMethods {
totp: IMethodsProp;
email: IMethodsProp;
password: IMethodsProp;
} }
const methods: any = { interface EventListenerMethod {
method?: keyof IMethods;
submit?: (param: string) => void;
cancel?: () => void;
invalid?: boolean;
}
const methods: IMethods = {
totp: { totp: {
text: 'Open_your_authentication_app_and_enter_the_code', text: 'Open_your_authentication_app_and_enter_the_code',
keyboardType: 'numeric' keyboardType: 'numeric'
@ -40,14 +55,14 @@ const methods: any = {
} }
}; };
const TwoFactor = React.memo(({ theme, isMasterDetail }: ITwoFactor) => { const TwoFactor = React.memo(({ isMasterDetail }: { isMasterDetail: boolean }) => {
const { theme } = useTheme();
const [visible, setVisible] = useState(false); const [visible, setVisible] = useState(false);
const [data, setData] = useState<any>({}); const [data, setData] = useState<EventListenerMethod>({});
const [code, setCode] = useState<any>(''); const [code, setCode] = useState<string>('');
const method = methods[data.method]; const method = data.method ? methods[data.method] : null;
const isEmail = data.method === 'email'; const isEmail = data.method === 'email';
const sendEmail = () => RocketChat.sendEmailCode(); const sendEmail = () => RocketChat.sendEmailCode();
useDeepCompareEffect(() => { useDeepCompareEffect(() => {
@ -59,7 +74,7 @@ const TwoFactor = React.memo(({ theme, isMasterDetail }: ITwoFactor) => {
} }
}, [data]); }, [data]);
const showTwoFactor = (args: any) => setData(args); const showTwoFactor = (args: EventListenerMethod) => setData(args);
useEffect(() => { useEffect(() => {
const listener = EventEmitter.addEventListener(TWO_FACTOR, showTwoFactor); const listener = EventEmitter.addEventListener(TWO_FACTOR, showTwoFactor);
@ -87,26 +102,19 @@ const TwoFactor = React.memo(({ theme, isMasterDetail }: ITwoFactor) => {
setData({}); setData({});
}; };
const color = themes[theme!].titleText; const color = themes[theme].titleText;
return ( return (
<Modal <Modal avoidKeyboard useNativeDriver isVisible={visible} hideModalContentWhileAnimating>
// @ts-ignore
transparent
avoidKeyboard
useNativeDriver
isVisible={visible}
hideModalContentWhileAnimating>
<View style={styles.container} testID='two-factor'> <View style={styles.container} testID='two-factor'>
<View <View
style={[ style={[
styles.content, styles.content,
isMasterDetail && [sharedStyles.modalFormSheet, styles.tablet], isMasterDetail && [sharedStyles.modalFormSheet, styles.tablet],
{ backgroundColor: themes[theme!].backgroundColor } { backgroundColor: themes[theme].backgroundColor }
]}> ]}>
<Text style={[styles.title, { color }]}>{I18n.t(method?.title || 'Two_Factor_Authentication')}</Text> <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} {method?.text ? <Text style={[styles.subtitle, { color }]}>{I18n.t(method.text)}</Text> : null}
<TextInput <TextInput
/* @ts-ignore*/
value={code} value={code}
theme={theme} theme={theme}
inputRef={(e: any) => InteractionManager.runAfterInteractions(() => e?.getNativeRef()?.focus())} inputRef={(e: any) => InteractionManager.runAfterInteractions(() => e?.getNativeRef()?.focus())}
@ -116,19 +124,19 @@ const TwoFactor = React.memo(({ theme, isMasterDetail }: ITwoFactor) => {
onSubmitEditing={onSubmit} onSubmitEditing={onSubmit}
keyboardType={method?.keyboardType} keyboardType={method?.keyboardType}
secureTextEntry={method?.secureTextEntry} secureTextEntry={method?.secureTextEntry}
error={data.invalid && { error: 'totp-invalid', reason: I18n.t('Code_or_password_invalid') }} error={data.invalid ? { error: 'totp-invalid', reason: I18n.t('Code_or_password_invalid') } : undefined}
testID='two-factor-input' testID='two-factor-input'
/> />
{isEmail && ( {isEmail ? (
<Text style={[styles.sendEmail, { color }]} onPress={sendEmail}> <Text style={[styles.sendEmail, { color }]} onPress={sendEmail}>
{I18n.t('Send_me_the_code_again')} {I18n.t('Send_me_the_code_again')}
</Text> </Text>
)} ) : null}
<View style={styles.buttonContainer}> <View style={styles.buttonContainer}>
<Button <Button
title={I18n.t('Cancel')} title={I18n.t('Cancel')}
type='secondary' type='secondary'
backgroundColor={themes[theme!].chatComponentBackground} backgroundColor={themes[theme].chatComponentBackground}
style={styles.button} style={styles.button}
onPress={onCancel} onPress={onCancel}
theme={theme} theme={theme}
@ -148,8 +156,8 @@ const TwoFactor = React.memo(({ theme, isMasterDetail }: ITwoFactor) => {
); );
}); });
const mapStateToProps = (state: any) => ({ const mapStateToProps = (state: IApplicationState) => ({
isMasterDetail: state.app.isMasterDetail isMasterDetail: state.app.isMasterDetail
}); });
export default connect(mapStateToProps)(withTheme(TwoFactor)); export default connect(mapStateToProps)(TwoFactor);

View File

@ -3,25 +3,18 @@ import { BLOCK_CONTEXT } from '@rocket.chat/ui-kit';
import Button from '../Button'; import Button from '../Button';
import I18n from '../../i18n'; import I18n from '../../i18n';
import { IActions } from './interfaces';
interface IActions {
blockId: string;
appId: string;
elements: any[];
parser: any;
theme: string;
}
export const Actions = ({ blockId, appId, elements, parser, theme }: IActions) => { export const Actions = ({ blockId, appId, elements, parser, theme }: IActions) => {
const [showMoreVisible, setShowMoreVisible] = useState(() => elements.length > 5); const [showMoreVisible, setShowMoreVisible] = useState(() => elements && elements.length > 5);
const renderedElements = showMoreVisible ? elements.slice(0, 5) : elements; const renderedElements = showMoreVisible ? elements?.slice(0, 5) : elements;
const Elements = () => const Elements = () => (
renderedElements.map((element: any) => parser.renderActions({ blockId, appId, ...element }, BLOCK_CONTEXT.ACTION, parser)); <>{renderedElements?.map(element => parser?.renderActions({ blockId, appId, ...element }, BLOCK_CONTEXT.ACTION, parser))}</>
);
return ( return (
<> <>
{/* @ts-ignore*/}
<Elements /> <Elements />
{showMoreVisible && <Button theme={theme} title={I18n.t('Show_more')} onPress={() => setShowMoreVisible(false)} />} {showMoreVisible && <Button theme={theme} title={I18n.t('Show_more')} onPress={() => setShowMoreVisible(false)} />}
</> </>

View File

@ -1,8 +1,9 @@
import React from 'react'; import React from 'react';
import { StyleSheet, View } from 'react-native'; import { StyleSheet, View } from 'react-native';
import PropTypes from 'prop-types';
import { BLOCK_CONTEXT } from '@rocket.chat/ui-kit'; import { BLOCK_CONTEXT } from '@rocket.chat/ui-kit';
import { IContext } from './interfaces';
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { container: {
minHeight: 36, minHeight: 36,
@ -11,13 +12,6 @@ const styles = StyleSheet.create({
} }
}); });
export const Context = ({ elements, parser }: any) => ( export const Context = ({ elements, parser }: IContext) => (
<View style={styles.container}> <View style={styles.container}>{elements?.map(element => parser?.renderContext(element, BLOCK_CONTEXT.CONTEXT, parser))}</View>
{elements.map((element: any) => parser.renderContext(element, BLOCK_CONTEXT.CONTEXT, parser))}
</View>
); );
Context.propTypes = {
elements: PropTypes.array,
parser: PropTypes.object
};

View File

@ -12,6 +12,7 @@ import sharedStyles from '../../views/Styles';
import { CustomIcon } from '../../lib/Icons'; import { CustomIcon } from '../../lib/Icons';
import { isAndroid } from '../../utils/deviceInfo'; import { isAndroid } from '../../utils/deviceInfo';
import ActivityIndicator from '../ActivityIndicator'; import ActivityIndicator from '../ActivityIndicator';
import { IDatePicker } from './interfaces';
const styles = StyleSheet.create({ const styles = StyleSheet.create({
input: { input: {
@ -35,23 +36,11 @@ const styles = StyleSheet.create({
} }
}); });
interface IDatePicker {
element: {
initial_date: any;
placeholder: string;
};
language: string;
action: Function;
context: number;
loading: boolean;
theme: string;
value: string;
error: string;
}
export const DatePicker = ({ element, language, action, context, theme, loading, value, error }: IDatePicker) => { export const DatePicker = ({ element, language, action, context, theme, loading, value, error }: IDatePicker) => {
const [show, onShow] = useState(false); const [show, onShow] = useState(false);
const { initial_date, placeholder } = element; const initial_date = element?.initial_date;
const placeholder = element?.placeholder;
const [currentDate, onChangeDate] = useState(new Date(initial_date || value)); const [currentDate, onChangeDate] = useState(new Date(initial_date || value));
const onChange = ({ nativeEvent: { timestamp } }: any, date: any) => { const onChange = ({ nativeEvent: { timestamp } }: any, date: any) => {

View File

@ -5,6 +5,8 @@ import { BLOCK_CONTEXT } from '@rocket.chat/ui-kit';
import ImageContainer from '../message/Image'; import ImageContainer from '../message/Image';
import Navigation from '../../lib/Navigation'; import Navigation from '../../lib/Navigation';
import { IThumb, IImage, IElement } from './interfaces';
import { TThemeMode } from '../../definitions/ITheme';
const styles = StyleSheet.create({ const styles = StyleSheet.create({
image: { image: {
@ -15,44 +17,25 @@ const styles = StyleSheet.create({
} }
}); });
interface IThumb { const ThumbContext = (args: IThumb) => (
element: {
imageUrl: string;
};
size?: number;
}
interface IMedia {
element: {
imageUrl: string;
};
theme: string;
}
interface IImage {
element: any;
context: any;
theme: string;
}
const ThumbContext = (args: any) => (
<View style={styles.mediaContext}> <View style={styles.mediaContext}>
<Thumb size={20} {...args} /> <Thumb size={20} {...args} />
</View> </View>
); );
export const Thumb = ({ element, size = 88 }: IThumb) => ( export const Thumb = ({ element, size = 88 }: IThumb) => (
<FastImage style={[{ width: size, height: size }, styles.image]} source={{ uri: element.imageUrl }} /> <FastImage style={[{ width: size, height: size }, styles.image]} source={{ uri: element?.imageUrl }} />
); );
export const Media = ({ element, theme }: IMedia) => { export const Media = ({ element, theme }: IImage) => {
const showAttachment = (attachment: any) => Navigation.navigate('AttachmentView', { attachment }); const showAttachment = (attachment: any) => Navigation.navigate('AttachmentView', { attachment });
const { imageUrl } = element; const imageUrl = element?.imageUrl ?? '';
// @ts-ignore // @ts-ignore
// TODO: delete ts-ignore after refactor Markdown and ImageContainer
return <ImageContainer file={{ image_url: imageUrl }} imageUrl={imageUrl} showAttachment={showAttachment} theme={theme} />; return <ImageContainer file={{ image_url: imageUrl }} imageUrl={imageUrl} showAttachment={showAttachment} theme={theme} />;
}; };
const genericImage = (element: any, context: any, theme: string) => { const genericImage = (theme: TThemeMode, element: IElement, context?: number) => {
switch (context) { switch (context) {
case BLOCK_CONTEXT.SECTION: case BLOCK_CONTEXT.SECTION:
return <Thumb element={element} />; return <Thumb element={element} />;
@ -63,4 +46,4 @@ const genericImage = (element: any, context: any, theme: string) => {
} }
}; };
export const Image = ({ element, context, theme }: IImage) => genericImage(element, context, theme); export const Image = ({ element, context, theme }: IImage) => genericImage(theme, element, context);

View File

@ -4,6 +4,7 @@ import { BLOCK_CONTEXT } from '@rocket.chat/ui-kit';
import sharedStyles from '../../views/Styles'; import sharedStyles from '../../views/Styles';
import { themes } from '../../constants/colors'; import { themes } from '../../constants/colors';
import { IInput } from './interfaces';
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { container: {
@ -31,16 +32,6 @@ const styles = StyleSheet.create({
} }
}); });
interface IInput {
element: object;
parser: any;
label: string;
description: string;
error: string;
hint: string;
theme: string;
}
export const Input = ({ element, parser, label, description, error, hint, theme }: IInput) => ( export const Input = ({ element, parser, label, description, error, hint, theme }: IInput) => (
<View style={styles.container}> <View style={styles.container}>
{label ? ( {label ? (

View File

@ -41,7 +41,7 @@ const Item = ({ item, selected, onSelect, theme }: IItem) => {
<> <>
{item.imageUrl ? <FastImage style={styles.itemImage} source={{ uri: item.imageUrl }} /> : null} {item.imageUrl ? <FastImage style={styles.itemImage} source={{ uri: item.imageUrl }} /> : null}
<Text style={{ color: themes[theme].titleText }}>{textParser([item.text])}</Text> <Text style={{ color: themes[theme].titleText }}>{textParser([item.text])}</Text>
{selected ? <Check theme={theme} /> : null} {selected ? <Check /> : null}
</> </>
</Touchable> </Touchable>
); );

View File

@ -24,7 +24,7 @@ interface IMultiSelect {
multiselect?: boolean; multiselect?: boolean;
onSearch?: () => void; onSearch?: () => void;
onClose?: () => void; onClose?: () => void;
inputStyle: object; inputStyle?: object;
value?: any[]; value?: any[];
disabled?: boolean | object; disabled?: boolean | object;
theme: string; theme: string;
@ -126,7 +126,6 @@ export const MultiSelect = React.memo(
<View style={[styles.content, { backgroundColor: themes[theme].backgroundColor }]}> <View style={[styles.content, { backgroundColor: themes[theme].backgroundColor }]}>
<TextInput <TextInput
testID='multi-select-search' testID='multi-select-search'
/* @ts-ignore*/
onChangeText={onSearch || onSearchChange} onChangeText={onSearch || onSearchChange}
placeholder={I18n.t('Search')} placeholder={I18n.t('Search')}
theme={theme} theme={theme}

View File

@ -8,32 +8,7 @@ import ActivityIndicator from '../ActivityIndicator';
import { themes } from '../../constants/colors'; import { themes } from '../../constants/colors';
import { BUTTON_HIT_SLOP } from '../message/utils'; import { BUTTON_HIT_SLOP } from '../message/utils';
import * as List from '../List'; import * as List from '../List';
import { IOption, IOptions, IOverflow } from './interfaces';
interface IOption {
option: {
text: string;
value: string;
};
onOptionPress: Function;
parser: any;
theme: string;
}
interface IOptions {
options: [];
onOptionPress: Function;
parser: object;
theme: string;
}
interface IOverflow {
element: any;
action: Function;
loading: boolean;
parser: object;
theme: string;
context: any;
}
const keyExtractor = (item: any) => item.value; const keyExtractor = (item: any) => item.value;
@ -68,10 +43,11 @@ const Options = ({ options, onOptionPress, parser, theme }: IOptions) => (
/> />
); );
const touchable = {}; const touchable: { [key: string]: any } = {};
export const Overflow = ({ element, loading, action, parser, theme }: IOverflow) => { export const Overflow = ({ element, loading, action, parser, theme }: IOverflow) => {
const { options, blockId } = element; const options = element?.options || [];
const blockId = element?.blockId || '';
const [show, onShow] = useState(false); const [show, onShow] = useState(false);
const onOptionPress = ({ value }: any) => { const onOptionPress = ({ value }: any) => {
@ -82,8 +58,7 @@ export const Overflow = ({ element, loading, action, parser, theme }: IOverflow)
return ( return (
<> <>
<Touchable <Touchable
/* @ts-ignore*/ ref={(ref: any) => (touchable[blockId] = ref)}
ref={ref => (touchable[blockId] = ref)}
background={Touchable.Ripple(themes[theme].bannerBackground)} background={Touchable.Ripple(themes[theme].bannerBackground)}
onPress={() => onShow(!show)} onPress={() => onShow(!show)}
hitSlop={BUTTON_HIT_SLOP} hitSlop={BUTTON_HIT_SLOP}
@ -91,7 +66,7 @@ export const Overflow = ({ element, loading, action, parser, theme }: IOverflow)
{!loading ? ( {!loading ? (
<CustomIcon size={18} name='kebab' color={themes[theme].bodyText} /> <CustomIcon size={18} name='kebab' color={themes[theme].bodyText} />
) : ( ) : (
<ActivityIndicator style={styles.loading} theme={theme} /> <ActivityIndicator style={styles.loading} />
)} )}
</Touchable> </Touchable>
<Popover <Popover

View File

@ -3,6 +3,7 @@ import { StyleSheet, Text, View } from 'react-native';
import { BLOCK_CONTEXT } from '@rocket.chat/ui-kit'; import { BLOCK_CONTEXT } from '@rocket.chat/ui-kit';
import { themes } from '../../constants/colors'; import { themes } from '../../constants/colors';
import { IAccessoryComponent, IFields, ISection } from './interfaces';
const styles = StyleSheet.create({ const styles = StyleSheet.create({
content: { content: {
@ -23,36 +24,16 @@ const styles = StyleSheet.create({
} }
}); });
interface IAccessory { const Accessory = ({ element, parser }: IAccessoryComponent) =>
blockId?: string; parser.renderAccessories({ ...element }, BLOCK_CONTEXT.SECTION, parser);
appId?: string;
element: any;
parser: any;
}
interface IFields { const Fields = ({ fields, parser, theme }: IFields) => (
fields: any; <>
parser: any; {fields.map(field => (
theme: string;
}
interface ISection {
blockId: string;
appId: string;
text: object;
fields: [];
accessory: any;
theme: string;
parser: any;
}
const Accessory = ({ blockId, appId, element, parser }: IAccessory) =>
parser.renderAccessories({ blockId, appId, ...element }, BLOCK_CONTEXT.SECTION, parser);
const Fields = ({ fields, parser, theme }: IFields) =>
fields.map((field: any) => (
<Text style={[styles.text, styles.field, { color: themes[theme].bodyText }]}>{parser.text(field)}</Text> <Text style={[styles.text, styles.field, { color: themes[theme].bodyText }]}>{parser.text(field)}</Text>
)); ))}
</>
);
const accessoriesRight = ['image', 'overflow']; const accessoriesRight = ['image', 'overflow'];

View File

@ -20,6 +20,7 @@ import { Input } from './Input';
import { DatePicker } from './DatePicker'; import { DatePicker } from './DatePicker';
import { Overflow } from './Overflow'; import { Overflow } from './Overflow';
import { ThemeContext } from '../../theme'; import { ThemeContext } from '../../theme';
import { BlockContext, IButton, IInputIndex, IParser, IText } from './interfaces';
const styles = StyleSheet.create({ const styles = StyleSheet.create({
input: { input: {
@ -42,8 +43,12 @@ const styles = StyleSheet.create({
const plainText = ({ text } = { text: '' }) => text; const plainText = ({ text } = { text: '' }) => text;
class MessageParser extends UiKitParserMessage { class MessageParser extends UiKitParserMessage {
text({ text, type }: any = { text: '' }, context: any) { get current() {
const { theme }: any = useContext(ThemeContext); return this as unknown as IParser;
}
text({ text, type }: Partial<IText> = { text: '' }, context: BlockContext) {
const { theme } = useContext(ThemeContext);
if (type !== 'mrkdwn') { if (type !== 'mrkdwn') {
return <Text style={[styles.text, { color: themes[theme].bodyText }]}>{text}</Text>; return <Text style={[styles.text, { color: themes[theme].bodyText }]}>{text}</Text>;
} }
@ -55,9 +60,9 @@ class MessageParser extends UiKitParserMessage {
return <Markdown msg={text} theme={theme} style={[isContext && { color: themes[theme].auxiliaryText }]} />; return <Markdown msg={text} theme={theme} style={[isContext && { color: themes[theme].auxiliaryText }]} />;
} }
button(element: any, context: any) { button(element: IButton, context: BlockContext) {
const { text, value, actionId, style } = element; const { text, value, actionId, style } = element;
const [{ loading }, action]: any = useBlockContext(element, context); const [{ loading }, action] = useBlockContext(element, context);
const { theme } = useContext(ThemeContext); const { theme } = useContext(ThemeContext);
return ( return (
<Button <Button
@ -73,7 +78,7 @@ class MessageParser extends UiKitParserMessage {
} }
divider() { divider() {
const { theme }: any = useContext(ThemeContext); const { theme } = useContext(ThemeContext);
// @ts-ignore // @ts-ignore
return <Divider theme={theme} />; return <Divider theme={theme} />;
} }
@ -91,7 +96,7 @@ class MessageParser extends UiKitParserMessage {
overflow(element: any, context: any) { overflow(element: any, context: any) {
const [{ loading }, action]: any = useBlockContext(element, context); const [{ loading }, action]: any = useBlockContext(element, context);
const { theme }: any = useContext(ThemeContext); const { theme }: any = useContext(ThemeContext);
return <Overflow element={element} context={context} loading={loading} action={action} theme={theme} parser={this} />; return <Overflow element={element} context={context} loading={loading} action={action} theme={theme} parser={this.current} />;
} }
datePicker(element: any, context: any) { datePicker(element: any, context: any) {
@ -150,12 +155,16 @@ class ModalParser extends UiKitParserModal {
}); });
} }
input({ element, blockId, appId, label, description, hint }: any, context: any) { get current() {
return this as unknown as IParser;
}
input({ element, blockId, appId, label, description, hint }: IInputIndex, context: number) {
const [{ error }]: any = useBlockContext({ ...element, appId, blockId }, context); const [{ error }]: any = useBlockContext({ ...element, appId, blockId }, context);
const { theme }: any = useContext(ThemeContext); const { theme }: any = useContext(ThemeContext);
return ( return (
<Input <Input
parser={this} parser={this.current}
element={{ ...element, appId, blockId }} element={{ ...element, appId, blockId }}
label={plainText(label)} label={plainText(label)}
description={plainText(description)} description={plainText(description)}
@ -178,16 +187,14 @@ class ModalParser extends UiKitParserModal {
return ( return (
// @ts-ignore // @ts-ignore
<TextInput <TextInput
id={actionId} key={actionId}
placeholder={plainText(placeholder)} placeholder={plainText(placeholder)}
onInput={action}
multiline={multiline} multiline={multiline}
loading={loading} loading={loading}
onChangeText={(text: any) => action({ value: text })} onChangeText={text => action({ value: text })}
inputStyle={multiline && styles.multiline} inputStyle={multiline && styles.multiline}
containerStyle={styles.input} containerStyle={styles.input}
value={value} value={value}
// @ts-ignore
error={{ error }} error={{ error }}
theme={theme} theme={theme}
/> />

View File

@ -0,0 +1,273 @@
import { TThemeMode } from '../../definitions/ITheme';
export enum ElementTypes {
IMAGE = 'image',
BUTTON = 'button',
STATIC_SELECT = 'static_select',
MULTI_STATIC_SELECT = 'multi_static_select',
CONVERSATION_SELECT = 'conversations_select',
CHANNEL_SELECT = 'channels_select',
USER_SELECT = 'users_select',
OVERFLOW = 'overflow',
DATEPICKER = 'datepicker',
PLAIN_TEXT_INPUT = 'plain_text_input',
SECTION = 'section',
DIVIDER = 'divider',
ACTIONS = 'actions',
CONTEXT = 'context',
FIELDS = 'fields',
INPUT = 'input',
PLAIN_TEXT = 'plain_text',
TEXT = 'text',
MARKDOWN = 'mrkdwn'
}
export enum BlockContext {
BLOCK,
SECTION,
ACTION,
FORM,
CONTEXT
}
export enum ActionTypes {
ACTION = 'blockAction',
SUBMIT = 'viewSubmit',
CLOSED = 'viewClosed'
}
export enum ContainerTypes {
VIEW = 'view',
MESSAGE = 'message'
}
export enum ModalActions {
MODAL = 'modal',
OPEN = 'modal.open',
CLOSE = 'modal.close',
UPDATE = 'modal.update',
ERRORS = 'errors'
}
export interface IStateView {
[key: string]: { [settings: string]: string | number };
}
export interface IView {
appId: string;
type: ModalActions;
id: string;
title: IText;
submit: IButton;
close: IButton;
blocks: Block[];
showIcon: boolean;
state?: IStateView;
}
export interface Block {
type: ElementTypes;
blockId: string;
element?: IElement;
label?: string;
appId: string;
optional?: boolean;
elements?: IElement[];
}
export interface IElement {
type: ElementTypes;
placeholder?: IText;
actionId: string;
initialValue?: string;
options?: Option[];
text?: IText;
value?: string;
initial_date?: any;
imageUrl?: string;
appId?: string;
blockId?: string;
}
export interface IText {
type: ElementTypes;
text: string;
emoji?: boolean;
}
export interface Option {
text: IText;
value: string;
}
export interface IButton {
type: ElementTypes;
text: IText;
actionId: string;
value?: any;
style?: any;
}
export interface IContainer {
type: ContainerTypes;
id: string;
}
// methods/actions
export interface IUserInteraction {
triggerId: string;
appId?: string;
viewId?: string;
view: IView;
}
export interface IEmitUserInteraction extends IUserInteraction {
type: ModalActions;
}
export interface ITriggerAction {
type: ActionTypes;
actionId?: string;
appId?: string;
container?: IContainer;
value?: number;
blockId?: string;
rid?: string;
mid?: string;
viewId?: string;
payload?: any;
view?: IView;
}
export interface ITriggerBlockAction {
container: IContainer;
actionId: string;
appId: string;
value: number;
blockId?: string;
mid?: string;
rid?: string;
}
export interface ITriggerSubmitView {
viewId: string;
appId: string;
payload: {
view: {
id: string;
state: IStateView;
};
};
}
export interface ITriggerCancel {
view: IView;
appId: string;
viewId: string;
isCleared: boolean;
}
// UiKit components
export interface IParser {
renderAccessories: (data: TElementAccessory, context: BlockContext, parser: IParser) => JSX.Element;
renderActions: (data: Block, context: BlockContext, parser: IParser) => JSX.Element;
renderContext: (data: IElement, context: BlockContext, parser: IParser) => JSX.Element;
renderInputs: (data: Partial<IElement>, context: BlockContext, parser: IParser) => JSX.Element;
text: (data: IText) => JSX.Element;
}
export interface IActions extends Block {
parser?: IParser;
theme: TThemeMode;
}
export interface IContext extends Block {
parser: IParser;
}
export interface IDatePicker extends Partial<Block> {
language: string;
action: Function;
context: number;
loading: boolean;
value: string;
error: string;
theme: TThemeMode;
}
export interface IInput extends Partial<Block> {
parser: IParser;
description: string;
error: string;
hint: string;
theme: TThemeMode;
}
export interface IInputIndex {
element: IElement;
blockId: string;
appId: string;
label: IText;
description: IText;
hint: IText;
}
export interface IThumb {
element: IElement;
size?: number;
}
export interface IImage {
element: IElement;
theme: TThemeMode;
context?: number;
}
// UiKit/Overflow
export interface IOverflow extends Partial<Block> {
action: Function;
loading: boolean;
parser: IParser;
theme: TThemeMode;
context: number;
}
interface PropsOption {
onOptionPress: Function;
parser: IParser;
theme: TThemeMode;
}
export interface IOptions extends PropsOption {
options: Option[];
}
export interface IOption extends PropsOption {
option: Option;
}
// UiKit/Section
interface IAccessory {
type: ElementTypes;
actionId: string;
value: number;
text: IText;
}
type TElementAccessory = IAccessory & { blockId: string; appId: string };
export interface IAccessoryComponent {
element: TElementAccessory;
parser: IParser;
}
export interface ISection {
blockId: string;
appId: string;
text?: IText;
accessory?: IAccessory;
parser: IParser;
theme: TThemeMode;
fields?: any[];
}
export interface IFields {
parser: IParser;
theme: TThemeMode;
fields: any[];
}

View File

@ -2,6 +2,8 @@
import React, { useContext, useState } from 'react'; import React, { useContext, useState } from 'react';
import { BLOCK_CONTEXT } from '@rocket.chat/ui-kit'; import { BLOCK_CONTEXT } from '@rocket.chat/ui-kit';
import { BlockContext } from './interfaces';
export const textParser = ([{ text }]: any) => text; export const textParser = ([{ text }]: any) => text;
export const defaultContext: any = { export const defaultContext: any = {
@ -13,7 +15,19 @@ export const defaultContext: any = {
export const KitContext = React.createContext(defaultContext); export const KitContext = React.createContext(defaultContext);
export const useBlockContext = ({ blockId, actionId, appId, initialValue }: any, context: any) => { type TObjectReturn = {
loading: boolean;
setLoading: React.Dispatch<React.SetStateAction<boolean>>;
error: any;
value: any;
language: any;
};
type TFunctionReturn = (value: any) => Promise<void>;
type TReturn = [TObjectReturn, TFunctionReturn];
export const useBlockContext = ({ blockId, actionId, appId, initialValue }: any, context: BlockContext): TReturn => {
const { action, appId: appIdFromContext, viewId, state, language, errors, values = {} } = useContext(KitContext); const { action, appId: appIdFromContext, viewId, state, language, errors, values = {} } = useContext(KitContext);
const { value = initialValue } = values[actionId] || {}; const { value = initialValue } = values[actionId] || {};
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);

View File

@ -1,18 +1,19 @@
import React from 'react'; import React from 'react';
import { Text } from 'react-native'; import { StyleProp, Text, TextStyle } from 'react-native';
import { useTheme } from '../../theme'; import { useTheme } from '../../theme';
import { themes } from '../../constants/colors'; import { themes } from '../../constants/colors';
import styles from './styles'; import styles from './styles';
import { events, logEvent } from '../../utils/log'; import { events, logEvent } from '../../utils/log';
import { IUserMention } from './interfaces';
interface IAtMention { interface IAtMention {
mention: string; mention: string;
username?: string; username?: string;
navToRoomInfo?: Function; navToRoomInfo?: Function;
style?: any; style?: StyleProp<TextStyle>[];
useRealName?: boolean; useRealName?: boolean;
mentions: any; mentions?: IUserMention[];
} }
const AtMention = React.memo(({ mention, mentions, username, navToRoomInfo, style = [], useRealName }: IAtMention) => { const AtMention = React.memo(({ mention, mentions, username, navToRoomInfo, style = [], useRealName }: IAtMention) => {
@ -23,7 +24,7 @@ const AtMention = React.memo(({ mention, mentions, username, navToRoomInfo, styl
style={[ style={[
styles.mention, styles.mention,
{ {
color: themes[theme!].mentionGroupColor color: themes[theme].mentionGroupColor
}, },
...style ...style
]}> ]}>
@ -35,11 +36,11 @@ const AtMention = React.memo(({ mention, mentions, username, navToRoomInfo, styl
let mentionStyle = {}; let mentionStyle = {};
if (mention === username) { if (mention === username) {
mentionStyle = { mentionStyle = {
color: themes[theme!].mentionMeColor color: themes[theme].mentionMeColor
}; };
} else { } else {
mentionStyle = { mentionStyle = {
color: themes[theme!].mentionOtherColor color: themes[theme].mentionOtherColor
}; };
} }
@ -64,7 +65,7 @@ const AtMention = React.memo(({ mention, mentions, username, navToRoomInfo, styl
); );
} }
return <Text style={[styles.text, { color: themes[theme!].bodyText }, ...style]}>{`@${mention}`}</Text>; return <Text style={[styles.text, { color: themes[theme].bodyText }, ...style]}>{`@${mention}`}</Text>;
}); });
export default AtMention; export default AtMention;

View File

@ -5,7 +5,7 @@ import { themes } from '../../constants/colors';
import styles from './styles'; import styles from './styles';
interface IBlockQuote { interface IBlockQuote {
children: JSX.Element; children: React.ReactElement | null;
theme: string; theme: string;
} }

View File

@ -1,5 +1,5 @@
import React from 'react'; import React from 'react';
import { Text, TextStyle, StyleProp } from 'react-native'; import { StyleProp, Text, TextStyle } from 'react-native';
import { themes } from '../../constants/colors'; import { themes } from '../../constants/colors';
import { useTheme } from '../../theme'; import { useTheme } from '../../theme';
@ -18,7 +18,7 @@ const Hashtag = React.memo(({ hashtag, channels, navToRoomInfo, style = [] }: IH
const handlePress = () => { const handlePress = () => {
const index = channels?.findIndex(channel => channel.name === hashtag); const index = channels?.findIndex(channel => channel.name === hashtag);
if (index && navToRoomInfo) { if (typeof index !== 'undefined' && navToRoomInfo) {
const navParam = { const navParam = {
t: 'c', t: 'c',
rid: channels?.[index]._id rid: channels?.[index]._id
@ -33,7 +33,7 @@ const Hashtag = React.memo(({ hashtag, channels, navToRoomInfo, style = [] }: IH
style={[ style={[
styles.mention, styles.mention,
{ {
color: themes[theme!].mentionOtherColor color: themes[theme].mentionOtherColor
}, },
...style ...style
]} ]}
@ -42,7 +42,7 @@ const Hashtag = React.memo(({ hashtag, channels, navToRoomInfo, style = [] }: IH
</Text> </Text>
); );
} }
return <Text style={[styles.text, { color: themes[theme!].bodyText }, ...style]}>{`#${hashtag}`}</Text>; return <Text style={[styles.text, { color: themes[theme].bodyText }, ...style]}>{`#${hashtag}`}</Text>;
}); });
export default Hashtag; export default Hashtag;

View File

@ -10,7 +10,7 @@ import openLink from '../../utils/openLink';
import { TOnLinkPress } from './interfaces'; import { TOnLinkPress } from './interfaces';
interface ILink { interface ILink {
children: JSX.Element; children: React.ReactElement | null;
link: string; link: string;
theme: string; theme: string;
onLinkPress?: TOnLinkPress; onLinkPress?: TOnLinkPress;

View File

@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
interface IList { interface IList {
children: JSX.Element; children: React.ReactElement[] | null;
ordered: boolean; ordered: boolean;
start: number; start: number;
tight: boolean; tight: boolean;
@ -11,9 +11,8 @@ interface IList {
const List = React.memo(({ children, ordered, tight, start = 1, numberOfLines = 0 }: IList) => { const List = React.memo(({ children, ordered, tight, start = 1, numberOfLines = 0 }: IList) => {
let bulletWidth = 15; let bulletWidth = 15;
if (ordered) { if (ordered && children) {
// @ts-ignore const lastNumber = start + children?.length - 1;
const lastNumber = start + children.length - 1;
bulletWidth = 9 * lastNumber.toString().length + 7; bulletWidth = 9 * lastNumber.toString().length + 7;
} }

View File

@ -18,7 +18,7 @@ const style = StyleSheet.create({
}); });
interface IListItem { interface IListItem {
children: JSX.Element; children: React.ReactElement | null;
bulletWidth: number; bulletWidth: number;
level: number; level: number;
ordered: boolean; ordered: boolean;

View File

@ -1,5 +1,5 @@
import React from 'react'; import React from 'react';
import { StyleProp, Text, TextStyle } from 'react-native'; import { Text, TextStyle } from 'react-native';
import removeMarkdown from 'remove-markdown'; import removeMarkdown from 'remove-markdown';
import shortnameToUnicode from '../../utils/shortnameToUnicode'; import shortnameToUnicode from '../../utils/shortnameToUnicode';
@ -13,10 +13,10 @@ interface IMarkdownPreview {
msg?: string; msg?: string;
numberOfLines?: number; numberOfLines?: number;
testID?: string; testID?: string;
style?: StyleProp<TextStyle>[]; style?: TextStyle[];
} }
const MarkdownPreview = ({ msg, numberOfLines = 1, testID, style = [] }: IMarkdownPreview): React.ReactElement | null => { const MarkdownPreview = ({ msg, numberOfLines = 1, testID, style = [] }: IMarkdownPreview) => {
if (!msg) { if (!msg) {
return null; return null;
} }

View File

@ -1,5 +1,5 @@
import React from 'react'; import React from 'react';
import { ScrollView, Text, TouchableOpacity, View } from 'react-native'; import { ScrollView, Text, TouchableOpacity, View, ViewStyle } from 'react-native';
import { CELL_WIDTH } from './TableCell'; import { CELL_WIDTH } from './TableCell';
import styles from './styles'; import styles from './styles';
@ -8,7 +8,7 @@ import I18n from '../../i18n';
import { themes } from '../../constants/colors'; import { themes } from '../../constants/colors';
interface ITable { interface ITable {
children: JSX.Element; children: React.ReactElement | null;
numColumns: number; numColumns: number;
theme: string; theme: string;
} }
@ -19,7 +19,7 @@ const Table = React.memo(({ children, numColumns, theme }: ITable) => {
const getTableWidth = () => numColumns * CELL_WIDTH; const getTableWidth = () => numColumns * CELL_WIDTH;
const renderRows = (drawExtraBorders = true) => { const renderRows = (drawExtraBorders = true) => {
const tableStyle = [styles.table, { borderColor: themes[theme].borderColor }]; const tableStyle: ViewStyle[] = [styles.table, { borderColor: themes[theme].borderColor }];
if (drawExtraBorders) { if (drawExtraBorders) {
tableStyle.push(styles.tableExtraBorders); tableStyle.push(styles.tableExtraBorders);
} }

View File

@ -1,12 +1,12 @@
import React from 'react'; import React from 'react';
import { Text, View } from 'react-native'; import { Text, View, ViewStyle } from 'react-native';
import { themes } from '../../constants/colors'; import { themes } from '../../constants/colors';
import styles from './styles'; import styles from './styles';
interface ITableCell { interface ITableCell {
align: '' | 'left' | 'center' | 'right'; align: '' | 'left' | 'center' | 'right';
children: JSX.Element; children: React.ReactElement | null;
isLastCell: boolean; isLastCell: boolean;
theme: string; theme: string;
} }
@ -14,7 +14,7 @@ interface ITableCell {
export const CELL_WIDTH = 100; export const CELL_WIDTH = 100;
const TableCell = React.memo(({ isLastCell, align, children, theme }: ITableCell) => { const TableCell = React.memo(({ isLastCell, align, children, theme }: ITableCell) => {
const cellStyle = [styles.cell, { borderColor: themes[theme].borderColor }]; const cellStyle: ViewStyle[] = [styles.cell, { borderColor: themes[theme].borderColor }];
if (!isLastCell) { if (!isLastCell) {
cellStyle.push(styles.cellRightBorder); cellStyle.push(styles.cellRightBorder);
} }

View File

@ -1,22 +1,22 @@
import React from 'react'; import React from 'react';
import { View } from 'react-native'; import { View, ViewStyle } from 'react-native';
import { themes } from '../../constants/colors'; import { themes } from '../../constants/colors';
import styles from './styles'; import styles from './styles';
interface ITableRow { interface ITableRow {
children: JSX.Element; children: React.ReactElement | null;
isLastRow: boolean; isLastRow: boolean;
theme: string; theme: string;
} }
const TableRow = React.memo(({ isLastRow, children: _children, theme }: ITableRow) => { const TableRow = React.memo(({ isLastRow, children: _children, theme }: ITableRow) => {
const rowStyle = [styles.row, { borderColor: themes[theme].borderColor }]; const rowStyle: ViewStyle[] = [styles.row, { borderColor: themes[theme].borderColor }];
if (!isLastRow) { if (!isLastRow) {
rowStyle.push(styles.rowBottomBorder); rowStyle.push(styles.rowBottomBorder);
} }
const children: any = React.Children.toArray(_children); const children = React.Children.toArray(_children) as React.ReactElement[];
children[children.length - 1] = React.cloneElement(children[children.length - 1], { children[children.length - 1] = React.cloneElement(children[children.length - 1], {
isLastCell: true isLastCell: true
}); });

View File

@ -280,6 +280,7 @@ class Markdown extends PureComponent<IMarkdownProps, any> {
renderHeading = ({ children, level }: any) => { renderHeading = ({ children, level }: any) => {
const { numberOfLines, theme } = this.props; const { numberOfLines, theme } = this.props;
// @ts-ignore
const textStyle = styles[`heading${level}Text`]; const textStyle = styles[`heading${level}Text`];
return ( return (
<Text numberOfLines={numberOfLines} style={[textStyle, { color: themes[theme].bodyText }]}> <Text numberOfLines={numberOfLines} style={[textStyle, { color: themes[theme].bodyText }]}>

View File

@ -2,6 +2,7 @@ export interface IUserMention {
_id: string; _id: string;
username: string; username: string;
name?: string; name?: string;
type?: string;
} }
export interface IUserChannel { export interface IUserChannel {

View File

@ -14,7 +14,7 @@ const styles = StyleSheet.create({
} }
}); });
const BigEmoji = ({ value }: IBigEmojiProps): JSX.Element => ( const BigEmoji = ({ value }: IBigEmojiProps) => (
<View style={styles.container}> <View style={styles.container}>
{value.map(block => ( {value.map(block => (
<Emoji value={block.value} isBigEmoji /> <Emoji value={block.value} isBigEmoji />

View File

@ -18,7 +18,7 @@ const styles = StyleSheet.create({
} }
}); });
const Bold = ({ value }: IBoldProps): JSX.Element => ( const Bold = ({ value }: IBoldProps) => (
<Text style={styles.text}> <Text style={styles.text}>
{value.map(block => { {value.map(block => {
switch (block.type) { switch (block.type) {

View File

@ -11,7 +11,7 @@ interface ICodeProps {
value: CodeProps['value']; value: CodeProps['value'];
} }
const Code = ({ value }: ICodeProps): JSX.Element => { const Code = ({ value }: ICodeProps) => {
const { theme } = useTheme(); const { theme } = useTheme();
return ( return (
@ -19,9 +19,9 @@ const Code = ({ value }: ICodeProps): JSX.Element => {
style={[ style={[
styles.codeBlock, styles.codeBlock,
{ {
color: themes[theme!].bodyText, color: themes[theme].bodyText,
backgroundColor: themes[theme!].bannerBackground, backgroundColor: themes[theme].bannerBackground,
borderColor: themes[theme!].borderColor borderColor: themes[theme].borderColor
} }
]}> ]}>
{value.map(block => { {value.map(block => {

View File

@ -6,7 +6,7 @@ interface ICodeLineProps {
value: CodeLineProps['value']; value: CodeLineProps['value'];
} }
const CodeLine = ({ value }: ICodeLineProps): JSX.Element | null => { const CodeLine = ({ value }: ICodeLineProps) => {
if (value.type !== 'PLAIN_TEXT') { if (value.type !== 'PLAIN_TEXT') {
return null; return null;
} }

View File

@ -14,7 +14,7 @@ interface IEmojiProps {
isBigEmoji?: boolean; isBigEmoji?: boolean;
} }
const Emoji = ({ value, isBigEmoji }: IEmojiProps): JSX.Element => { const Emoji = ({ value, isBigEmoji }: IEmojiProps) => {
const { theme } = useTheme(); const { theme } = useTheme();
const { baseUrl, getCustomEmoji } = useContext(MarkdownContext); const { baseUrl, getCustomEmoji } = useContext(MarkdownContext);
const emojiUnicode = shortnameToUnicode(`:${value.value}:`); const emojiUnicode = shortnameToUnicode(`:${value.value}:`);
@ -23,7 +23,7 @@ const Emoji = ({ value, isBigEmoji }: IEmojiProps): JSX.Element => {
if (emoji) { if (emoji) {
return <CustomEmoji baseUrl={baseUrl} style={[isBigEmoji ? styles.customEmojiBig : styles.customEmoji]} emoji={emoji} />; return <CustomEmoji baseUrl={baseUrl} style={[isBigEmoji ? styles.customEmojiBig : styles.customEmoji]} emoji={emoji} />;
} }
return <Text style={[{ color: themes[theme!].bodyText }, isBigEmoji ? styles.textBig : styles.text]}>{emojiUnicode}</Text>; return <Text style={[{ color: themes[theme].bodyText }, isBigEmoji ? styles.textBig : styles.text]}>{emojiUnicode}</Text>;
}; };
export default Emoji; export default Emoji;

View File

@ -11,12 +11,12 @@ interface IHeadingProps {
level: HeadingProps['level']; level: HeadingProps['level'];
} }
const Heading = ({ value, level }: IHeadingProps): JSX.Element => { const Heading = ({ value, level }: IHeadingProps) => {
const { theme } = useTheme(); const { theme } = useTheme();
const textStyle = styles[`heading${level}`]; const textStyle = styles[`heading${level}`];
return ( return (
<Text style={[textStyle, { color: themes[theme!].bodyText }]}> <Text style={[textStyle, { color: themes[theme].bodyText }]}>
{value.map(block => { {value.map(block => {
switch (block.type) { switch (block.type) {
case 'PLAIN_TEXT': case 'PLAIN_TEXT':

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