Merge 4.26.1 into single-server (#3981)
This commit is contained in:
parent
fc9e9a4f2a
commit
9d2175485c
|
@ -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
|
|
@ -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({
|
||||||
|
|
|
@ -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]
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -53,16 +53,8 @@ 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
|
mmkv = MMKV.mmkvWithID("default", MMKV.SINGLE_PROCESS_MODE, password);
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getAvatarUri() {
|
public String getAvatarUri() {
|
||||||
|
|
|
@ -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',
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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 }
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -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];
|
||||||
|
|
|
@ -206,4 +206,4 @@ export default {
|
||||||
Canned_Responses_Enable: {
|
Canned_Responses_Enable: {
|
||||||
type: 'valueAsBoolean'
|
type: 'valueAsBoolean'
|
||||||
}
|
}
|
||||||
};
|
} as const;
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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(() => {
|
||||||
<View style={[styles.handle, { backgroundColor: themes[theme].focusedBackground }]} testID='action-sheet-handle'>
|
const { theme } = useTheme();
|
||||||
<View style={[styles.handleIndicator, { backgroundColor: themes[theme].auxiliaryText }]} />
|
return (
|
||||||
</View>
|
<View style={[styles.handle, { backgroundColor: themes[theme].focusedBackground }]} testID='action-sheet-handle'>
|
||||||
));
|
<View style={[styles.handleIndicator, { backgroundColor: themes[theme].auxiliaryText }]} />
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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 => {
|
||||||
<ActivityIndicator style={[styles.indicator, absolute && styles.absolute]} color={themes[theme].auxiliaryText} {...props} />
|
const { theme } = useTheme();
|
||||||
);
|
return (
|
||||||
|
<ActivityIndicator style={[styles.indicator, absolute && styles.absolute]} color={themes[theme].auxiliaryText} {...props} />
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export default RCActivityIndicator;
|
export default RCActivityIndicator;
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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 => {
|
||||||
<View style={styles.container}>
|
const { theme } = useTheme();
|
||||||
<ImageBackground source={{ uri: `message_empty_${theme}` }} style={styles.image} />
|
return (
|
||||||
{text && !loading ? <Text style={[styles.text, { color: themes[theme!].auxiliaryTintColor }]}>{text}</Text> : null}
|
<View style={styles.container}>
|
||||||
{loading ? <ActivityIndicator style={styles.text} color={themes[theme!].auxiliaryTintColor} /> : null}
|
<ImageBackground source={{ uri: `message_empty_${theme}` }} style={styles.image} />
|
||||||
</View>
|
{text && !loading ? <Text style={[styles.text, { color: themes[theme].auxiliaryTintColor }]}>{text}</Text> : null}
|
||||||
);
|
{loading ? <ActivityIndicator style={styles.text} color={themes[theme].auxiliaryTintColor} /> : null}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export default withTheme(BackgroundContainer);
|
export default BackgroundContainer;
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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,27 +22,31 @@ 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) => {
|
||||||
<KeyboardView
|
const { theme } = useTheme();
|
||||||
style={{ backgroundColor: themes[theme].backgroundColor }}
|
|
||||||
contentContainerStyle={sharedStyles.container}
|
return (
|
||||||
keyboardVerticalOffset={128}>
|
<KeyboardView
|
||||||
<StatusBar />
|
style={{ backgroundColor: themes[theme].backgroundColor }}
|
||||||
<ScrollView
|
contentContainerStyle={sharedStyles.container}
|
||||||
style={sharedStyles.container}
|
keyboardVerticalOffset={128}>
|
||||||
contentContainerStyle={[sharedStyles.containerScrollView, styles.scrollView]}
|
<StatusBar />
|
||||||
{...scrollPersistTaps}
|
<ScrollView
|
||||||
{...props}>
|
style={sharedStyles.container}
|
||||||
<SafeAreaView testID={testID} style={{ backgroundColor: themes[theme].backgroundColor }}>
|
contentContainerStyle={[sharedStyles.containerScrollView, styles.scrollView]}
|
||||||
{children}
|
{...scrollPersistTaps}
|
||||||
<AppVersion theme={theme} />
|
{...props}>
|
||||||
</SafeAreaView>
|
<SafeAreaView testID={testID} style={{ backgroundColor: themes[theme].backgroundColor }}>
|
||||||
</ScrollView>
|
{children}
|
||||||
</KeyboardView>
|
<AppVersion theme={theme} />
|
||||||
);
|
</SafeAreaView>
|
||||||
|
</ScrollView>
|
||||||
|
</KeyboardView>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export default FormContainer;
|
export default FormContainer;
|
||||||
|
|
|
@ -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,20 +48,22 @@ 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 => {
|
||||||
<SafeAreaView style={{ backgroundColor: themes[theme].headerBackground }} edges={['top', 'left', 'right']}>
|
const { theme } = useTheme();
|
||||||
<View style={[styles.container, { ...themedHeader(theme).headerStyle }]}>
|
return (
|
||||||
{headerLeft ? headerLeft() : null}
|
<SafeAreaView style={{ backgroundColor: themes[theme].headerBackground }} edges={['top', 'left', 'right']}>
|
||||||
{headerTitle ? headerTitle() : null}
|
<View style={[styles.container, { ...themedHeader(theme).headerStyle }]}>
|
||||||
{headerRight ? headerRight() : null}
|
{headerLeft ? headerLeft() : null}
|
||||||
</View>
|
{headerTitle ? headerTitle() : null}
|
||||||
</SafeAreaView>
|
{headerRight ? headerRight() : null}
|
||||||
);
|
</View>
|
||||||
|
</SafeAreaView>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export default withTheme(Header);
|
export default Header;
|
||||||
|
|
|
@ -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(
|
||||||
<Container left>
|
({ navigation, testID, onPress = () => navigation?.toggleDrawer(), ...props }: IHeaderButtonCommon) => (
|
||||||
<Item iconName='hamburguer' onPress={() => navigation.toggleDrawer()} testID={testID} {...props} />
|
<Container left>
|
||||||
</Container>
|
<Item iconName='hamburguer' onPress={onPress} testID={testID} {...props} />
|
||||||
));
|
</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} />
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -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 => {
|
||||||
<Touchable onPress={onPress} testID={testID} hitSlop={BUTTON_HIT_SLOP} style={styles.container}>
|
const { theme } = useTheme();
|
||||||
<>
|
return (
|
||||||
{iconName ? (
|
<Touchable onPress={onPress} testID={testID} hitSlop={BUTTON_HIT_SLOP} style={styles.container}>
|
||||||
<CustomIcon name={iconName} size={24} color={themes[theme!].headerTintColor} />
|
<>
|
||||||
) : (
|
{iconName ? (
|
||||||
<Text style={[styles.title, { color: themes[theme!].headerTintColor }]}>{title}</Text>
|
<CustomIcon name={iconName} size={24} color={themes[theme].headerTintColor} />
|
||||||
)}
|
) : (
|
||||||
{badge ? badge() : null}
|
<Text style={[styles.title, { color: themes[theme].headerTintColor }]}>{title}</Text>
|
||||||
</>
|
)}
|
||||||
</Touchable>
|
{badge ? badge() : null}
|
||||||
);
|
</>
|
||||||
|
</Touchable>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
Item.displayName = 'HeaderButton.Item';
|
Item.displayName = 'HeaderButton.Item';
|
||||||
|
|
||||||
export default withTheme(Item);
|
export default Item;
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -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'
|
||||||
});
|
});
|
||||||
|
|
|
@ -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) => (
|
||||||
|
|
|
@ -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) => {
|
||||||
<View style={styles.container}>
|
const { theme } = useTheme();
|
||||||
<Text style={[styles.title, { color: themes[theme!].infoText }]} numberOfLines={1}>
|
|
||||||
{translateTitle ? I18n.t(title) : title}
|
return (
|
||||||
</Text>
|
<View style={styles.container}>
|
||||||
</View>
|
<Text style={[styles.title, { color: themes[theme].infoText }]} numberOfLines={1}>
|
||||||
));
|
{translateTitle ? I18n.t(title) : title}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
ListHeader.displayName = 'List.Header';
|
ListHeader.displayName = 'List.Header';
|
||||||
|
|
||||||
export default withTheme(ListHeader);
|
export default ListHeader;
|
||||||
|
|
|
@ -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) => {
|
||||||
<View style={[styles.icon, style]}>
|
const { theme } = useTheme();
|
||||||
<CustomIcon name={name} color={color ?? themes[theme!].auxiliaryText} size={ICON_SIZE} testID={testID} />
|
|
||||||
</View>
|
return (
|
||||||
));
|
<View style={[styles.icon, style]}>
|
||||||
|
<CustomIcon name={name} color={color ?? themes[theme].auxiliaryText} size={ICON_SIZE} testID={testID} />
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
ListIcon.displayName = 'List.Icon';
|
ListIcon.displayName = 'List.Icon';
|
||||||
|
|
||||||
export default withTheme(ListIcon);
|
export default ListIcon;
|
||||||
|
|
|
@ -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) => {
|
||||||
<View style={styles.container}>
|
const { theme } = useTheme();
|
||||||
<Text style={[styles.text, { color: themes[theme!].infoText }]}>{translateInfo ? I18n.t(info) : info}</Text>
|
return (
|
||||||
</View>
|
<View style={styles.container}>
|
||||||
));
|
<Text style={[styles.text, { color: themes[theme].infoText }]}>{translateInfo ? I18n.t(info) : info}</Text>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
ListInfo.displayName = 'List.Info';
|
ListInfo.displayName = 'List.Info';
|
||||||
|
|
||||||
export default withTheme(ListInfo);
|
export default ListInfo;
|
||||||
|
|
|
@ -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,78 +77,85 @@ 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) => {
|
||||||
{left ? <View style={styles.leftContainer}>{left()}</View> : null}
|
const { fontScale } = useDimensions();
|
||||||
<View style={styles.textContainer}>
|
|
||||||
<View style={styles.textAlertContainer}>
|
return (
|
||||||
<Text style={[styles.title, { color: color || themes[theme!].titleText }]} numberOfLines={1}>
|
<View style={[styles.container, disabled && styles.disabled, { height: BASE_HEIGHT * fontScale }]} testID={testID}>
|
||||||
{translateTitle ? I18n.t(title) : title}
|
{left ? <View style={styles.leftContainer}>{left()}</View> : null}
|
||||||
</Text>
|
<View style={styles.textContainer}>
|
||||||
{alert ? (
|
<View style={styles.textAlertContainer}>
|
||||||
<CustomIcon style={[styles.alertIcon, { color: themes[theme!].dangerColor }]} size={ICON_SIZE} name='info' />
|
<Text style={[styles.title, { color: color || themes[theme].titleText }]} numberOfLines={1}>
|
||||||
|
{translateTitle ? I18n.t(title) : title}
|
||||||
|
</Text>
|
||||||
|
{alert ? (
|
||||||
|
<CustomIcon style={[styles.alertIcon, { color: themes[theme].dangerColor }]} size={ICON_SIZE} name='info' />
|
||||||
|
) : null}
|
||||||
|
</View>
|
||||||
|
{subtitle ? (
|
||||||
|
<Text style={[styles.subtitle, { color: themes[theme].auxiliaryText }]} numberOfLines={1}>
|
||||||
|
{translateSubtitle ? I18n.t(subtitle) : subtitle}
|
||||||
|
</Text>
|
||||||
) : null}
|
) : null}
|
||||||
</View>
|
</View>
|
||||||
{subtitle ? (
|
{right || showActionIndicator ? (
|
||||||
<Text style={[styles.subtitle, { color: themes[theme!].auxiliaryText }]} numberOfLines={1}>
|
<View style={styles.rightContainer}>
|
||||||
{translateSubtitle ? I18n.t(subtitle) : subtitle}
|
{right ? right() : null}
|
||||||
</Text>
|
{showActionIndicator ? <Icon name='chevron-right' style={styles.actionIndicator} /> : null}
|
||||||
|
</View>
|
||||||
) : null}
|
) : null}
|
||||||
</View>
|
</View>
|
||||||
{right || showActionIndicator ? (
|
);
|
||||||
<View style={styles.rightContainer}>
|
}
|
||||||
{right ? right() : null}
|
|
||||||
{showActionIndicator ? <Icon name='chevron-right' style={styles.actionIndicator} /> : null}
|
|
||||||
</View>
|
|
||||||
) : null}
|
|
||||||
</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;
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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));
|
||||||
|
|
|
@ -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({
|
||||||
|
|
|
@ -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} />
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 #');
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,4 @@
|
||||||
|
// Match query string from the message to replace it with the suggestion
|
||||||
|
const getMentionRegexp = (): any => /[^@:#/!]*$/;
|
||||||
|
|
||||||
|
export default getMentionRegexp;
|
|
@ -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}
|
||||||
|
|
|
@ -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 => {
|
||||||
m.tcount -= 1;
|
if (m.tcount) {
|
||||||
|
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: [
|
||||||
{
|
{
|
||||||
|
|
|
@ -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 (
|
||||||
|
|
|
@ -4,47 +4,51 @@ 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) => {
|
||||||
<View style={styles.dotsContainer}>
|
const { theme } = useTheme();
|
||||||
{range(length).map(val => {
|
|
||||||
const lengthSup = passcode.length >= val + 1;
|
return (
|
||||||
const height = lengthSup ? SIZE_FULL : SIZE_EMPTY;
|
<View style={styles.dotsContainer}>
|
||||||
const width = lengthSup ? SIZE_FULL : SIZE_EMPTY;
|
{range(length).map(val => {
|
||||||
let backgroundColor = '';
|
const lengthSup = passcode.length >= val + 1;
|
||||||
if (lengthSup && passcode.length > 0) {
|
const height = lengthSup ? SIZE_FULL : SIZE_EMPTY;
|
||||||
backgroundColor = themes[theme].passcodeDotFull;
|
const width = lengthSup ? SIZE_FULL : SIZE_EMPTY;
|
||||||
} else {
|
let backgroundColor = '';
|
||||||
backgroundColor = themes[theme].passcodeDotEmpty;
|
if (lengthSup && passcode.length > 0) {
|
||||||
}
|
backgroundColor = themes[theme].passcodeDotFull;
|
||||||
const borderRadius = lengthSup ? SIZE_FULL / 2 : SIZE_EMPTY / 2;
|
} else {
|
||||||
const marginRight = lengthSup ? 10 - (SIZE_FULL - SIZE_EMPTY) / 2 : 10;
|
backgroundColor = themes[theme].passcodeDotEmpty;
|
||||||
const marginLeft = lengthSup ? 10 - (SIZE_FULL - SIZE_EMPTY) / 2 : 10;
|
}
|
||||||
return (
|
const borderRadius = lengthSup ? SIZE_FULL / 2 : SIZE_EMPTY / 2;
|
||||||
<View style={styles.dotsView}>
|
const marginRight = lengthSup ? 10 - (SIZE_FULL - SIZE_EMPTY) / 2 : 10;
|
||||||
<View
|
const marginLeft = lengthSup ? 10 - (SIZE_FULL - SIZE_EMPTY) / 2 : 10;
|
||||||
style={{
|
return (
|
||||||
height,
|
<View style={styles.dotsView}>
|
||||||
width,
|
<View
|
||||||
borderRadius,
|
style={{
|
||||||
backgroundColor,
|
height,
|
||||||
marginRight,
|
width,
|
||||||
marginLeft
|
borderRadius,
|
||||||
}}
|
backgroundColor,
|
||||||
/>
|
marginRight,
|
||||||
</View>
|
marginLeft
|
||||||
);
|
}}
|
||||||
})}
|
/>
|
||||||
</View>
|
</View>
|
||||||
));
|
);
|
||||||
|
})}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
export default Dots;
|
export default Dots;
|
||||||
|
|
|
@ -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(() => {
|
||||||
<Row style={styles.row}>
|
const { theme } = useTheme();
|
||||||
<View style={styles.iconView}>
|
|
||||||
<CustomIcon name='auth' size={40} color={themes[theme].passcodeLockIcon} />
|
return (
|
||||||
</View>
|
<Row style={styles.row}>
|
||||||
</Row>
|
<View style={styles.iconView}>
|
||||||
));
|
<CustomIcon name='auth' size={40} color={themes[theme].passcodeLockIcon} />
|
||||||
|
</View>
|
||||||
|
</Row>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
export default LockIcon;
|
export default LockIcon;
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
@ -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) => {
|
||||||
<Row style={styles.row}>
|
const { theme } = useTheme();
|
||||||
<View style={styles.subtitleView}>
|
|
||||||
<Text style={[styles.textSubtitle, { color: themes[theme].passcodeSecondary }]}>{text}</Text>
|
return (
|
||||||
</View>
|
<Row style={styles.row}>
|
||||||
</Row>
|
<View style={styles.subtitleView}>
|
||||||
));
|
<Text style={[styles.textSubtitle, { color: themes[theme].passcodeSecondary }]}>{text}</Text>
|
||||||
|
</View>
|
||||||
|
</Row>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
export default Subtitle;
|
export default Subtitle;
|
||||||
|
|
|
@ -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) => {
|
||||||
<Row style={styles.row}>
|
const { theme } = useTheme();
|
||||||
<View style={styles.titleView}>
|
|
||||||
<Text style={[styles.textTitle, { color: themes[theme].passcodePrimary }]}>{text}</Text>
|
return (
|
||||||
</View>
|
<Row style={styles.row}>
|
||||||
</Row>
|
<View style={styles.titleView}>
|
||||||
));
|
<Text style={[styles.textTitle, { color: themes[theme].passcodePrimary }]}>{text}</Text>
|
||||||
|
</View>
|
||||||
|
</Row>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
export default Title;
|
export default Title;
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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')}
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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'
|
||||||
};
|
}
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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,60 +44,49 @@ 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 }) => {
|
||||||
<Touchable onPress={onCancelPress} style={styles.cancel}>
|
const { theme } = useTheme();
|
||||||
<Text style={[styles.cancelText, { color: themes[theme].headerTintColor }]}>{I18n.t('Cancel')}</Text>
|
return (
|
||||||
</Touchable>
|
<Touchable onPress={onCancelPress} style={styles.cancel}>
|
||||||
);
|
<Text style={[styles.cancelText, { color: themes[theme].headerTintColor }]}>{I18n.t('Cancel')}</Text>
|
||||||
|
</Touchable>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const SearchBox = ({
|
const SearchBox = ({ hasCancel, onCancelPress, inputRef, ...props }: ISearchBox): React.ReactElement => {
|
||||||
onChangeText,
|
const { theme } = useTheme();
|
||||||
onSubmitEditing,
|
return (
|
||||||
testID,
|
<View
|
||||||
hasCancel,
|
style={[
|
||||||
onCancelPress,
|
styles.container,
|
||||||
inputRef,
|
{ backgroundColor: isIOS ? themes[theme].headerBackground : themes[theme].headerSecondaryBackground }
|
||||||
theme,
|
]}>
|
||||||
...props
|
<View style={[styles.searchBox, { backgroundColor: themes[theme].searchboxBackground }]}>
|
||||||
}: ISearchBox) => (
|
<CustomIcon name='search' size={14} color={themes[theme].auxiliaryText} />
|
||||||
<View
|
<TextInput
|
||||||
style={[
|
ref={inputRef}
|
||||||
styles.container,
|
autoCapitalize='none'
|
||||||
{ backgroundColor: isIOS ? themes[theme!].headerBackground : themes[theme!].headerSecondaryBackground }
|
autoCorrect={false}
|
||||||
]}>
|
blurOnSubmit
|
||||||
<View style={[styles.searchBox, { backgroundColor: themes[theme!].searchboxBackground }]}>
|
clearButtonMode='while-editing'
|
||||||
<CustomIcon name='search' size={14} color={themes[theme!].auxiliaryText} />
|
placeholder={I18n.t('Search')}
|
||||||
<TextInput
|
returnKeyType='search'
|
||||||
ref={inputRef}
|
style={styles.input}
|
||||||
autoCapitalize='none'
|
underlineColorAndroid='transparent'
|
||||||
autoCorrect={false}
|
theme={theme}
|
||||||
blurOnSubmit
|
{...props}
|
||||||
clearButtonMode='while-editing'
|
/>
|
||||||
placeholder={I18n.t('Search')}
|
</View>
|
||||||
returnKeyType='search'
|
{hasCancel ? <CancelButton onCancelPress={onCancelPress} /> : null}
|
||||||
style={styles.input}
|
|
||||||
testID={testID}
|
|
||||||
underlineColorAndroid='transparent'
|
|
||||||
onChangeText={onChangeText}
|
|
||||||
onSubmitEditing={onSubmitEditing}
|
|
||||||
theme={theme!}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
</View>
|
</View>
|
||||||
{hasCancel ? CancelButton(onCancelPress!, theme!) : null}
|
);
|
||||||
</View>
|
};
|
||||||
);
|
|
||||||
|
|
||||||
export default withTheme(SearchBox);
|
export default SearchBox;
|
||||||
|
|
|
@ -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';
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
import { TextProps } from 'react-native';
|
||||||
|
|
||||||
|
import { TUserStatus } from '../../definitions';
|
||||||
|
|
||||||
|
export interface IStatus extends TextProps {
|
||||||
|
id: string;
|
||||||
|
size: number;
|
||||||
|
status: TUserStatus;
|
||||||
|
}
|
|
@ -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);
|
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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() {
|
||||||
EventEmitter.removeListener(LISTENER, this.listener);
|
if (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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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)} />}
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -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
|
|
||||||
};
|
|
||||||
|
|
|
@ -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) => {
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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 ? (
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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;
|
<Text style={[styles.text, styles.field, { color: themes[theme].bodyText }]}>{parser.text(field)}</Text>
|
||||||
}
|
))}
|
||||||
|
</>
|
||||||
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>
|
|
||||||
));
|
|
||||||
|
|
||||||
const accessoriesRight = ['image', 'overflow'];
|
const accessoriesRight = ['image', 'overflow'];
|
||||||
|
|
||||||
|
|
|
@ -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}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -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[];
|
||||||
|
}
|
|
@ -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);
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
});
|
});
|
||||||
|
|
|
@ -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 }]}>
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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 />
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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 => {
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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
Loading…
Reference in New Issue