Merge 4.29.0 into single-server

This commit is contained in:
Diego Mello 2022-07-18 15:05:16 -03:00 committed by GitHub
commit 12c1112399
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
404 changed files with 6316 additions and 9539 deletions

View File

@ -1,7 +1,3 @@
export default {
getModel: () => '',
getReadableVersion: () => '',
getBundleId: () => '',
isTablet: () => false,
hasNotch: () => false
};
import mockDeviceInfo from 'react-native-device-info/jest/react-native-device-info-mock';
export default mockDeviceInfo;

View File

@ -1,78 +1,102 @@
GEM
remote: https://rubygems.org/
specs:
CFPropertyList (3.0.2)
addressable (2.7.0)
CFPropertyList (3.0.5)
rexml
addressable (2.8.0)
public_suffix (>= 2.0.2, < 5.0)
artifactory (3.0.15)
atomos (0.1.3)
aws-eventstream (1.0.3)
aws-partitions (1.294.0)
aws-sdk-core (3.92.0)
aws-eventstream (~> 1.0, >= 1.0.2)
aws-partitions (~> 1, >= 1.239.0)
aws-eventstream (1.2.0)
aws-partitions (1.600.0)
aws-sdk-core (3.131.2)
aws-eventstream (~> 1, >= 1.0.2)
aws-partitions (~> 1, >= 1.525.0)
aws-sigv4 (~> 1.1)
jmespath (~> 1.0)
aws-sdk-kms (1.30.0)
aws-sdk-core (~> 3, >= 3.71.0)
jmespath (~> 1, >= 1.6.1)
aws-sdk-kms (1.57.0)
aws-sdk-core (~> 3, >= 3.127.0)
aws-sigv4 (~> 1.1)
aws-sdk-s3 (1.61.2)
aws-sdk-core (~> 3, >= 3.83.0)
aws-sdk-s3 (1.114.0)
aws-sdk-core (~> 3, >= 3.127.0)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.1)
aws-sigv4 (1.1.1)
aws-eventstream (~> 1.0, >= 1.0.2)
babosa (1.0.3)
claide (1.0.3)
aws-sigv4 (~> 1.4)
aws-sigv4 (1.5.0)
aws-eventstream (~> 1, >= 1.0.2)
babosa (1.0.4)
claide (1.1.0)
colored (1.2)
colored2 (3.1.2)
commander-fastlane (4.4.6)
highline (~> 1.7.2)
declarative (0.0.10)
declarative-option (0.1.0)
digest-crc (0.5.1)
commander (4.6.0)
highline (~> 2.0.0)
declarative (0.0.20)
digest-crc (0.6.4)
rake (>= 12.0.0, < 14.0.0)
domain_name (0.5.20190701)
unf (>= 0.0.5, < 1.0.0)
dotenv (2.7.5)
emoji_regex (1.0.1)
excon (0.73.0)
faraday (0.17.3)
multipart-post (>= 1.2, < 3)
faraday-cookie_jar (0.0.6)
faraday (>= 0.7.4)
dotenv (2.7.6)
emoji_regex (3.2.3)
excon (0.92.3)
faraday (1.10.0)
faraday-em_http (~> 1.0)
faraday-em_synchrony (~> 1.0)
faraday-excon (~> 1.1)
faraday-httpclient (~> 1.0)
faraday-multipart (~> 1.0)
faraday-net_http (~> 1.0)
faraday-net_http_persistent (~> 1.0)
faraday-patron (~> 1.0)
faraday-rack (~> 1.0)
faraday-retry (~> 1.0)
ruby2_keywords (>= 0.0.4)
faraday-cookie_jar (0.0.7)
faraday (>= 0.8.0)
http-cookie (~> 1.0.0)
faraday_middleware (0.13.1)
faraday (>= 0.7.4, < 1.0)
fastimage (2.1.7)
fastlane (2.145.0)
faraday-em_http (1.0.0)
faraday-em_synchrony (1.0.0)
faraday-excon (1.1.0)
faraday-httpclient (1.0.1)
faraday-multipart (1.0.4)
multipart-post (~> 2)
faraday-net_http (1.0.1)
faraday-net_http_persistent (1.2.0)
faraday-patron (1.0.0)
faraday-rack (1.0.0)
faraday-retry (1.0.3)
faraday_middleware (1.2.0)
faraday (~> 1.0)
fastimage (2.2.6)
fastlane (2.206.2)
CFPropertyList (>= 2.3, < 4.0.0)
addressable (>= 2.3, < 3.0.0)
addressable (>= 2.8, < 3.0.0)
artifactory (~> 3.0)
aws-sdk-s3 (~> 1.0)
babosa (>= 1.0.2, < 2.0.0)
babosa (>= 1.0.3, < 2.0.0)
bundler (>= 1.12.0, < 3.0.0)
colored
commander-fastlane (>= 4.4.6, < 5.0.0)
commander (~> 4.6)
dotenv (>= 2.1.1, < 3.0.0)
emoji_regex (>= 0.1, < 2.0)
emoji_regex (>= 0.1, < 4.0)
excon (>= 0.71.0, < 1.0.0)
faraday (~> 0.17)
faraday (~> 1.0)
faraday-cookie_jar (~> 0.0.6)
faraday_middleware (~> 0.13.1)
faraday_middleware (~> 1.0)
fastimage (>= 2.1.0, < 3.0.0)
gh_inspector (>= 1.1.2, < 2.0.0)
google-api-client (>= 0.29.2, < 0.37.0)
google-cloud-storage (>= 1.15.0, < 2.0.0)
highline (>= 1.7.2, < 2.0.0)
google-apis-androidpublisher_v3 (~> 0.3)
google-apis-playcustomapp_v1 (~> 0.1)
google-cloud-storage (~> 1.31)
highline (~> 2.0)
json (< 3.0.0)
jwt (~> 2.1.0)
jwt (>= 2.1.0, < 3)
mini_magick (>= 4.9.4, < 5.0.0)
multi_xml (~> 0.5)
multipart-post (~> 2.0.0)
naturally (~> 2.2)
optparse (~> 0.1.1)
plist (>= 3.1.0, < 4.0.0)
public_suffix (~> 2.0.0)
rubyzip (>= 1.3.0, < 2.0.0)
rubyzip (>= 2.0.0, < 3.0.0)
security (= 0.1.3)
simctl (~> 1.6.3)
slack-notifier (>= 2.0.0, < 3.0.0)
terminal-notifier (>= 2.0.0, < 3.0.0)
terminal-table (>= 1.4.5, < 2.0.0)
tty-screen (>= 0.6.3, < 1.0.0)
@ -82,92 +106,106 @@ GEM
xcpretty (~> 0.3.0)
xcpretty-travis-formatter (>= 0.0.3)
gh_inspector (1.1.3)
google-api-client (0.36.4)
google-apis-androidpublisher_v3 (0.22.0)
google-apis-core (>= 0.5, < 2.a)
google-apis-core (0.6.0)
addressable (~> 2.5, >= 2.5.1)
googleauth (~> 0.9)
httpclient (>= 2.8.1, < 3.0)
googleauth (>= 0.16.2, < 2.a)
httpclient (>= 2.8.1, < 3.a)
mini_mime (~> 1.0)
representable (~> 3.0)
retriable (>= 2.0, < 4.0)
signet (~> 0.12)
google-cloud-core (1.5.0)
retriable (>= 2.0, < 4.a)
rexml
webrick
google-apis-iamcredentials_v1 (0.12.0)
google-apis-core (>= 0.6, < 2.a)
google-apis-playcustomapp_v1 (0.9.0)
google-apis-core (>= 0.6, < 2.a)
google-apis-storage_v1 (0.15.0)
google-apis-core (>= 0.5, < 2.a)
google-cloud-core (1.6.0)
google-cloud-env (~> 1.0)
google-cloud-errors (~> 1.0)
google-cloud-env (1.3.1)
faraday (>= 0.17.3, < 2.0)
google-cloud-errors (1.0.0)
google-cloud-storage (1.25.1)
addressable (~> 2.5)
google-cloud-env (1.6.0)
faraday (>= 0.17.3, < 3.0)
google-cloud-errors (1.2.0)
google-cloud-storage (1.36.2)
addressable (~> 2.8)
digest-crc (~> 0.4)
google-api-client (~> 0.33)
google-cloud-core (~> 1.2)
googleauth (~> 0.9)
google-apis-iamcredentials_v1 (~> 0.1)
google-apis-storage_v1 (~> 0.1)
google-cloud-core (~> 1.6)
googleauth (>= 0.16.2, < 2.a)
mini_mime (~> 1.0)
googleauth (0.11.0)
faraday (>= 0.17.3, < 2.0)
googleauth (1.2.0)
faraday (>= 0.17.3, < 3.a)
jwt (>= 1.4, < 3.0)
memoist (~> 0.16)
multi_json (~> 1.11)
os (>= 0.9, < 2.0)
signet (~> 0.12)
highline (1.7.10)
http-cookie (1.0.3)
signet (>= 0.16, < 2.a)
highline (2.0.3)
http-cookie (1.0.5)
domain_name (~> 0.5)
httpclient (2.8.3)
jmespath (1.4.0)
json (2.3.0)
jwt (2.1.0)
jmespath (1.6.1)
json (2.6.2)
jwt (2.4.1)
memoist (0.16.2)
mini_magick (4.10.1)
mini_mime (1.0.2)
multi_json (1.14.1)
multi_xml (0.6.0)
mini_magick (4.11.0)
mini_mime (1.1.2)
multi_json (1.15.0)
multipart-post (2.0.0)
nanaimo (0.2.6)
naturally (2.2.0)
os (1.1.0)
plist (3.5.0)
public_suffix (2.0.5)
representable (3.0.4)
nanaimo (0.3.0)
naturally (2.2.1)
optparse (0.1.1)
os (1.1.4)
plist (3.6.0)
public_suffix (4.0.7)
rake (13.0.6)
representable (3.2.0)
declarative (< 0.1.0)
declarative-option (< 0.2.0)
trailblazer-option (>= 0.1.1, < 0.2.0)
uber (< 0.2.0)
retriable (3.1.2)
rexml (3.2.5)
rouge (2.0.7)
rubyzip (1.3.0)
ruby2_keywords (0.0.5)
rubyzip (2.3.2)
security (0.1.3)
signet (0.14.0)
addressable (~> 2.3)
faraday (>= 0.17.3, < 2.0)
signet (0.17.0)
addressable (~> 2.8)
faraday (>= 0.17.5, < 3.a)
jwt (>= 1.5, < 3.0)
multi_json (~> 1.10)
simctl (1.6.8)
CFPropertyList
naturally
slack-notifier (2.3.2)
terminal-notifier (2.0.0)
terminal-table (1.8.0)
unicode-display_width (~> 1.1, >= 1.1.1)
trailblazer-option (0.1.2)
tty-cursor (0.7.1)
tty-screen (0.7.1)
tty-screen (0.8.1)
tty-spinner (0.9.3)
tty-cursor (~> 0.7)
uber (0.1.0)
unf (0.1.4)
unf_ext
unf_ext (0.0.7.7)
unf_ext (0.0.7.7-x64-mingw32)
unicode-display_width (1.7.0)
unf_ext (0.0.8.2)
unicode-display_width (1.8.0)
webrick (1.7.0)
word_wrap (1.0.0)
xcodeproj (1.15.0)
xcodeproj (1.22.0)
CFPropertyList (>= 2.3.3, < 4.0)
atomos (~> 0.1.3)
claide (>= 1.0.2, < 2.0)
colored2 (~> 3.1)
nanaimo (~> 0.2.6)
nanaimo (~> 0.3.0)
rexml (~> 3.2.4)
xcpretty (0.3.0)
rouge (~> 2.0.7)
xcpretty-travis-formatter (1.0.0)
xcpretty-travis-formatter (1.0.1)
xcpretty (~> 0.2, >= 0.0.7)
PLATFORMS
@ -178,4 +216,4 @@ DEPENDENCIES
fastlane
BUNDLED WITH
2.0.2
2.3.11

View File

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

View File

@ -12,7 +12,6 @@ import com.facebook.react.ReactRootView;
import com.facebook.react.ReactActivityDelegate;
import com.facebook.react.ReactFragmentActivity;
import com.swmansion.gesturehandler.react.RNGestureHandlerEnabledRootView;
import com.zoontek.rnbootsplash.RNBootSplash;
import com.google.gson.Gson;
@ -51,16 +50,6 @@ public class MainActivity extends ReactFragmentActivity {
return "RocketChatRN";
}
@Override
protected ReactActivityDelegate createReactActivityDelegate() {
return new ReactActivityDelegate(this, getMainComponentName()) {
@Override
protected ReactRootView createRootView() {
return new RNGestureHandlerEnabledRootView(MainActivity.this);
}
};
}
// from react-native-orientation
@Override
public void onConfigurationChanged(Configuration newConfig) {

View File

@ -3,21 +3,10 @@ package chat.rocket.reactnative.share;
import com.facebook.react.ReactActivity;
import com.facebook.react.ReactActivityDelegate;
import com.facebook.react.ReactRootView;
import com.swmansion.gesturehandler.react.RNGestureHandlerEnabledRootView;
public class ShareActivity extends ReactActivity {
@Override
protected String getMainComponentName() {
return "ShareRocketChatRN";
}
@Override
protected ReactActivityDelegate createReactActivityDelegate() {
return new ReactActivityDelegate(this, getMainComponentName()) {
@Override
protected ReactRootView createRootView() {
return new RNGestureHandlerEnabledRootView(ShareActivity.this);
}
};
}
}

View File

@ -14,7 +14,7 @@ buildscript {
targetSdkVersion = 30
ndkVersion = "20.1.5948944"
glideVersion = "4.11.0"
kotlin_version = "1.3.50"
kotlin_version = "1.6.10"
supportLibVersion = "28.0.0"
libre_build = !(isPlay.toBoolean())
jitsi_url = isPlay ? "https://github.com/RocketChat/jitsi-maven-repository/raw/master/releases" : "https://github.com/RocketChat/jitsi-maven-repository/raw/libre/releases"

View File

@ -37,6 +37,3 @@ KEYSTORE=my-upload-key.keystore
KEY_ALIAS=my-key-alias
KEYSTORE_PASSWORD=
KEY_PASSWORD=
# Version of flipper SDK to use with React Native
FLIPPER_VERSION=0.75.1

View File

@ -5,7 +5,7 @@ import { connect } from 'react-redux';
import { SetUsernameStackParamList, StackParamList } from './definitions/navigationTypes';
import Navigation from './lib/navigation/appNavigation';
import { defaultHeader, getActiveRouteName, navigationTheme } from './utils/navigation';
import { defaultHeader, getActiveRouteName, navigationTheme } from './lib/methods/helpers/navigation';
import { RootEnum } from './definitions';
// Stacks
import AuthLoadingView from './views/AuthLoadingView';
@ -15,7 +15,7 @@ import OutsideStack from './stacks/OutsideStack';
import InsideStack from './stacks/InsideStack';
import MasterDetailStack from './stacks/MasterDetailStack';
import { ThemeContext } from './theme';
import { setCurrentScreen } from './utils/log';
import { setCurrentScreen } from './lib/methods/helpers/log';
// SetUsernameStack
const SetUsername = createStackNavigator<SetUsernameStackParamList>();

View File

@ -27,7 +27,6 @@ export const ROOM = createRequestTypes('ROOM', [
'LEAVE',
'DELETE',
'REMOVED',
'CLOSE',
'FORWARD',
'USER_TYPING'
]);
@ -54,6 +53,7 @@ export const SERVER = createRequestTypes('SERVER', [
]);
export const METEOR = createRequestTypes('METEOR_CONNECT', [...defaultTypes, 'DISCONNECT']);
export const LOGOUT = 'LOGOUT'; // logout is always success
export const DELETE_ACCOUNT = 'DELETE_ACCOUNT';
export const SNIPPETED_MESSAGES = createRequestTypes('SNIPPETED_MESSAGES', ['OPEN', 'READY', 'CLOSE', 'MESSAGES_RECEIVED']);
export const DEEP_LINKING = createRequestTypes('DEEP_LINKING', ['OPEN']);
export const SORT_PREFERENCES = createRequestTypes('SORT_PREFERENCES', ['SET_ALL', 'SET']);

View File

@ -121,3 +121,9 @@ export function setLocalAuthenticated(isLocalAuthenticated: boolean): ISetLocalA
isLocalAuthenticated
};
}
export function deleteAccount(): Action {
return {
type: types.DELETE_ACCOUNT
};
}

View File

@ -19,7 +19,6 @@ interface IBaseReturn extends Action {
type TSubscribeRoom = IBaseReturn;
type TUnsubscribeRoom = IBaseReturn;
type TCloseRoom = IBaseReturn;
type TRoom = Record<string, any>;
@ -45,7 +44,7 @@ interface IUserTyping extends Action {
status: boolean;
}
export type TActionsRoom = TSubscribeRoom & TUnsubscribeRoom & TCloseRoom & ILeaveRoom & IDeleteRoom & IForwardRoom & IUserTyping;
export type TActionsRoom = TSubscribeRoom & TUnsubscribeRoom & ILeaveRoom & IDeleteRoom & IForwardRoom & IUserTyping;
export function subscribeRoom(rid: string): TSubscribeRoom {
return {
@ -79,13 +78,6 @@ export function deleteRoom(roomType: ERoomType, room: TRoom, selected?: ISelecte
};
}
export function closeRoom(rid: string): TCloseRoom {
return {
type: ROOM.CLOSE,
rid
};
}
export function forwardRoom(rid: string, transferData: ITransferData): IForwardRoom {
return {
type: ROOM.FORWARD,

View File

@ -8,7 +8,7 @@ import BottomSheet, { BottomSheetBackdrop } from '@gorhom/bottom-sheet';
import { useDimensions, useOrientation } from '../../dimensions';
import { useTheme } from '../../theme';
import { isIOS, isTablet } from '../../utils/deviceInfo';
import { isIOS, isTablet } from '../../lib/methods/helpers';
import { Handle } from './Handle';
import { TActionSheetOptions } from './Provider';
import BottomSheetContent from './BottomSheetContent';
@ -101,6 +101,11 @@ const ActionSheet = React.memo(
</>
);
const onClose = () => {
toggleVisible();
data?.onClose && data?.onClose();
};
const renderBackdrop = useCallback(
props => (
<BottomSheetBackdrop
@ -116,6 +121,10 @@ const ActionSheet = React.memo(
const bottomSheet = isLandscape || isTablet ? styles.bottomSheet : {};
// Must need this prop to avoid keyboard dismiss
// when is android tablet and the input text is focused
const androidTablet: any = isTablet && isLandscape && !isIOS ? { android_keyboardInputMode: 'adjustResize' } : {};
return (
<>
{children}
@ -130,7 +139,8 @@ const ActionSheet = React.memo(
enablePanDownToClose
style={{ ...styles.container, ...bottomSheet }}
backgroundStyle={{ backgroundColor: colors.focusedBackground }}
onChange={index => index === -1 && toggleVisible()}>
onChange={index => index === -1 && onClose()}
{...androidTablet}>
<BottomSheetContent options={data?.options} hide={hide} children={data?.children} hasCancel={data?.hasCancel} />
</BottomSheet>
)}

View File

@ -0,0 +1,141 @@
import React, { useState } from 'react';
import { StyleSheet, Text, View } from 'react-native';
import { CustomIcon, TIconsName } from '../../CustomIcon';
import i18n from '../../../i18n';
import { isIOS } from '../../../lib/methods/helpers';
import { useTheme } from '../../../theme';
import sharedStyles from '../../../views/Styles';
import Button from '../../Button';
import { FormTextInput } from '../../TextInput/FormTextInput';
import { useActionSheet } from '../Provider';
const styles = StyleSheet.create({
subtitleText: {
fontSize: 14,
...sharedStyles.textRegular,
marginBottom: 10
},
buttonSeparator: {
marginRight: 8
},
footerButtonsContainer: {
flexDirection: 'row',
paddingTop: 16
},
titleContainerText: {
fontSize: 16,
...sharedStyles.textSemibold
},
titleContainer: {
paddingRight: 80,
marginBottom: 16,
flexDirection: 'row',
alignItems: 'center'
}
});
const FooterButtons = ({
cancelAction = () => {},
confirmAction = () => {},
cancelTitle = '',
confirmTitle = '',
disabled = false,
cancelBackgroundColor = '',
confirmBackgroundColor = ''
}): React.ReactElement => {
const { colors } = useTheme();
return (
<View style={styles.footerButtonsContainer}>
<Button
style={[styles.buttonSeparator, { flex: 1, backgroundColor: cancelBackgroundColor || colors.cancelButton }]}
color={colors.backdropColor}
title={cancelTitle}
onPress={cancelAction}
/>
<Button
style={{ flex: 1, backgroundColor: confirmBackgroundColor || colors.dangerColor }}
title={confirmTitle}
onPress={confirmAction}
disabled={disabled}
/>
</View>
);
};
const ActionSheetContentWithInputAndSubmit = ({
onSubmit = () => {},
onCancel,
title = '',
description = '',
testID = '',
secureTextEntry = true,
placeholder = '',
confirmTitle,
iconName,
iconColor,
customText,
confirmBackgroundColor,
showInput = true
}: {
onSubmit: (inputValue: string) => void;
onCancel?: () => void;
title: string;
description: string;
testID: string;
secureTextEntry?: boolean;
placeholder: string;
confirmTitle?: string;
iconName?: TIconsName;
iconColor?: string;
customText?: React.ReactElement;
confirmBackgroundColor?: string;
showInput?: boolean;
}): React.ReactElement => {
const { colors } = useTheme();
const [inputValue, setInputValue] = useState('');
const { hideActionSheet } = useActionSheet();
return (
<View style={sharedStyles.containerScrollView} testID='action-sheet-content-with-input-and-submit'>
<>
<View style={styles.titleContainer}>
{iconName ? <CustomIcon name={iconName} size={32} color={iconColor || colors.dangerColor} /> : null}
<Text style={[styles.titleContainerText, { color: colors.passcodePrimary, paddingLeft: iconName ? 16 : 0 }]}>
{title}
</Text>
</View>
<Text style={[styles.subtitleText, { color: colors.titleText }]}>{description}</Text>
{customText}
</>
{showInput ? (
<FormTextInput
value={inputValue}
placeholder={placeholder}
onChangeText={value => setInputValue(value)}
onSubmitEditing={() => {
// fix android animation
setTimeout(() => {
hideActionSheet();
}, 100);
if (inputValue) onSubmit(inputValue);
}}
testID={testID}
secureTextEntry={secureTextEntry}
inputStyle={{ borderWidth: 2 }}
bottomSheet={isIOS}
/>
) : null}
<FooterButtons
confirmBackgroundColor={confirmBackgroundColor || colors.actionTintColor}
cancelAction={onCancel || hideActionSheet}
confirmAction={() => onSubmit(inputValue)}
cancelTitle={i18n.t('Cancel')}
confirmTitle={confirmTitle || i18n.t('Save')}
disabled={!showInput ? false : !inputValue}
/>
</View>
);
};
export default ActionSheetContentWithInputAndSubmit;

View File

@ -53,7 +53,7 @@ const BottomSheetContent = React.memo(({ options, hasCancel, hide, children }: I
/>
);
}
return <BottomSheetView>{children}</BottomSheetView>;
return <BottomSheetView style={styles.contentContainer}>{children}</BottomSheetView>;
});
export default BottomSheetContent;

View File

@ -1,8 +1,8 @@
import React from 'react';
import { TouchableOpacity } from 'react-native';
import { isAndroid } from '../../utils/deviceInfo';
import Touch from '../../utils/touch';
import { isAndroid } from '../../lib/methods/helpers';
import Touch from '../../lib/methods/helpers/touch';
// Taken from https://github.com/rgommezz/react-native-scroll-bottom-sheet#touchables
export const Button: typeof React.Component = isAndroid ? Touch : TouchableOpacity;

View File

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

View File

@ -1,7 +1,8 @@
import hoistNonReactStatics from 'hoist-non-react-statics';
import React, { ForwardedRef, forwardRef, useContext, useRef } from 'react';
import ActionSheet from './ActionSheet';
import { TIconsName } from '../CustomIcon';
import ActionSheet from './ActionSheet';
export type TActionSheetOptionsItem = {
title: string;
@ -19,9 +20,10 @@ export type TActionSheetOptions = {
hasCancel?: boolean;
type?: string;
children?: React.ReactElement | null;
snaps?: string[] | number[];
snaps?: (string | number)[];
onClose?: () => void;
};
interface IActionSheetProvider {
export interface IActionSheetProvider {
showActionSheet: (item: TActionSheetOptions) => void;
hideActionSheet: () => void;
}
@ -35,11 +37,15 @@ export const useActionSheet = () => useContext(context);
const { Provider, Consumer } = context;
export const withActionSheet = (Component: React.ComponentType<any>): typeof Component =>
forwardRef((props: typeof React.Component, ref: ForwardedRef<IActionSheetProvider>) => (
export const withActionSheet = (Component: React.ComponentType<any>): typeof Component => {
const WithActionSheetComponent = forwardRef((props: typeof React.Component, ref: ForwardedRef<IActionSheetProvider>) => (
<Consumer>{(contexts: IActionSheetProvider) => <Component {...props} {...contexts} ref={ref} />}</Consumer>
));
hoistNonReactStatics(WithActionSheetComponent, Component);
return WithActionSheetComponent;
};
export const ActionSheetProvider = React.memo(({ children }: { children: React.ReactElement | React.ReactElement[] }) => {
const ref: ForwardedRef<IActionSheetProvider> = useRef(null);

View File

@ -63,5 +63,15 @@ export default StyleSheet.create({
},
rightContainer: {
paddingLeft: 12
},
footerButtonsContainer: {
flexDirection: 'row',
paddingTop: 16
},
buttonSeparator: {
marginRight: 8
},
contentContainer: {
flex: 1
}
});

View File

@ -3,7 +3,7 @@ import { StyleSheet, Text, View } from 'react-native';
import { themes } from '../lib/constants';
import sharedStyles from '../views/Styles';
import { getReadableVersion } from '../utils/deviceInfo';
import { getReadableVersion } from '../lib/methods/helpers';
import I18n from '../i18n';
import { TSupportedThemes } from '../theme';

View File

@ -1,14 +1,13 @@
import React from 'react';
import { View } from 'react-native';
import FastImage from '@rocket.chat/react-native-fast-image';
import FastImage from 'react-native-fast-image';
import Touchable from 'react-native-platform-touchable';
import { settings as RocketChatSettings } from '@rocket.chat/sdk';
import { avatarURL } from '../../utils/avatar';
import { SubscriptionType } from '../../definitions/ISubscription';
import { getAvatarURL } from '../../lib/methods/helpers/getAvatarUrl';
import { SubscriptionType } from '../../definitions';
import Emoji from '../markdown/Emoji';
import { IAvatar } from './interfaces';
import { useTheme } from '../../theme';
const Avatar = React.memo(
({
@ -16,7 +15,8 @@ const Avatar = React.memo(
style,
avatar,
children,
user,
userId,
token,
onPress,
emoji,
getCustomEmoji,
@ -31,8 +31,6 @@ const Avatar = React.memo(
type = SubscriptionType.DIRECT,
externalProviderUrl
}: IAvatar) => {
const { theme } = useTheme();
if ((!text && !avatar && !emoji && !rid) || !server) {
return null;
}
@ -46,23 +44,17 @@ const Avatar = React.memo(
let image;
if (emoji) {
image = (
<Emoji
theme={theme}
baseUrl={server}
getCustomEmoji={getCustomEmoji}
isMessageContainsOnlyEmoji
literal={emoji}
style={avatarStyle}
/>
<Emoji baseUrl={server} getCustomEmoji={getCustomEmoji} isMessageContainsOnlyEmoji literal={emoji} style={avatarStyle} />
);
} else {
let uri = avatar;
if (!isStatic) {
uri = avatarURL({
uri = getAvatarURL({
type,
text,
size,
user,
userId,
token,
avatar,
server,
avatarETag,

View File

@ -1,84 +1,65 @@
import React from 'react';
import { connect } from 'react-redux';
import { Q } from '@nozbe/watermelondb';
import React, { useEffect, useRef, useState } from 'react';
import { shallowEqual, useSelector } from 'react-redux';
import { Observable, Subscription } from 'rxjs';
import { IApplicationState, TSubscriptionModel, TUserModel } from '../../definitions';
import database from '../../lib/database';
import { getUserSelector } from '../../selectors/login';
import { IApplicationState, TSubscriptionModel, TUserModel } from '../../definitions';
import Avatar from './Avatar';
import { IAvatar } from './interfaces';
class AvatarContainer extends React.Component<IAvatar, any> {
private subscription?: Subscription;
const AvatarContainer = ({
style,
text = '',
avatar,
emoji,
size,
borderRadius,
type,
children,
onPress,
getCustomEmoji,
isStatic,
rid
}: IAvatar): React.ReactElement => {
const subscription = useRef<Subscription>();
const [avatarETag, setAvatarETag] = useState<string | undefined>('');
static defaultProps = {
text: '',
type: 'd'
};
const isDirect = () => type === 'd';
constructor(props: IAvatar) {
super(props);
this.state = { avatarETag: '' };
this.init();
}
const server = useSelector((state: IApplicationState) => state.share.server.server || state.server.server);
const serverVersion = useSelector((state: IApplicationState) => state.share.server.version || state.server.version);
const { id, token } = useSelector(
(state: IApplicationState) => ({
id: getUserSelector(state).id,
token: getUserSelector(state).token
}),
shallowEqual
);
componentDidUpdate(prevProps: IAvatar) {
const { text, type } = this.props;
if (prevProps.text !== text || prevProps.type !== type) {
this.init();
}
}
const externalProviderUrl = useSelector(
(state: IApplicationState) => state.settings.Accounts_AvatarExternalProviderUrl as string
);
const blockUnauthenticatedAccess = useSelector(
(state: IApplicationState) =>
(state.share.settings?.Accounts_AvatarBlockUnauthenticatedAccess as boolean) ??
state.settings.Accounts_AvatarBlockUnauthenticatedAccess ??
true
);
shouldComponentUpdate(nextProps: IAvatar, nextState: { avatarETag: string }) {
const { avatarETag } = this.state;
const { text, type, size, externalProviderUrl } = this.props;
if (nextProps.externalProviderUrl !== externalProviderUrl) {
return true;
}
if (nextState.avatarETag !== avatarETag) {
return true;
}
if (nextProps.text !== text) {
return true;
}
if (nextProps.type !== type) {
return true;
}
if (nextProps.size !== size) {
return true;
}
return false;
}
componentWillUnmount() {
if (this.subscription?.unsubscribe) {
this.subscription.unsubscribe();
}
}
get isDirect() {
const { type } = this.props;
return type === 'd';
}
init = async () => {
const init = async () => {
const db = database.active;
const usersCollection = db.get('users');
const subsCollection = db.get('subscriptions');
let record;
try {
if (this.isDirect) {
const { text } = this.props;
if (isDirect()) {
const [user] = await usersCollection.query(Q.where('username', text)).fetch();
record = user;
} else {
const { rid } = this.props;
if (rid) {
record = await subsCollection.find(rid);
}
} else if (rid) {
record = await subsCollection.find(rid);
}
} catch {
// Record not found
@ -86,28 +67,46 @@ class AvatarContainer extends React.Component<IAvatar, any> {
if (record) {
const observable = record.observe() as Observable<TSubscriptionModel | TUserModel>;
this.subscription = observable.subscribe(r => {
const { avatarETag } = r;
this.setState({ avatarETag });
subscription.current = observable.subscribe(r => {
setAvatarETag(r.avatarETag);
});
}
};
render() {
const { avatarETag } = this.state;
const { serverVersion } = this.props;
return <Avatar {...this.props} avatarETag={avatarETag} serverVersion={serverVersion} />;
}
}
useEffect(() => {
if (!avatarETag) {
init();
}
return () => {
if (subscription?.current?.unsubscribe) {
subscription.current.unsubscribe();
}
};
}, [text, type, size, avatarETag, externalProviderUrl]);
const mapStateToProps = (state: IApplicationState) => ({
user: getUserSelector(state),
server: state.share.server.server || state.server.server,
serverVersion: state.share.server.version || state.server.version,
blockUnauthenticatedAccess:
(state.share.settings?.Accounts_AvatarBlockUnauthenticatedAccess as boolean) ??
state.settings.Accounts_AvatarBlockUnauthenticatedAccess ??
true,
externalProviderUrl: state.settings.Accounts_AvatarExternalProviderUrl as string
});
export default connect(mapStateToProps)(AvatarContainer);
return (
<Avatar
server={server}
style={style}
text={text}
avatar={avatar}
emoji={emoji}
size={size}
borderRadius={borderRadius}
type={type}
children={children}
userId={id}
token={token}
onPress={onPress}
getCustomEmoji={getCustomEmoji}
isStatic={isStatic}
rid={rid}
blockUnauthenticatedAccess={blockUnauthenticatedAccess}
externalProviderUrl={externalProviderUrl}
avatarETag={avatarETag}
serverVersion={serverVersion}
/>
);
};
export default AvatarContainer;

View File

@ -5,23 +5,21 @@ import { TGetCustomEmoji } from '../../definitions/IEmoji';
export interface IAvatar {
server?: string;
style?: any;
text: string;
text?: string;
avatar?: string;
emoji?: string;
size?: number;
borderRadius?: number;
type?: string;
children?: React.ReactElement | null;
user?: {
id?: string;
token?: string;
};
userId?: string;
token?: string;
onPress?: () => void;
getCustomEmoji?: TGetCustomEmoji;
avatarETag?: string;
isStatic?: boolean | string;
rid?: string;
blockUnauthenticatedAccess?: boolean;
serverVersion: string | null;
serverVersion?: string | null;
externalProviderUrl?: string;
}

View File

@ -10,7 +10,7 @@ export const IconSet = createIconSetFromIcoMoon(icoMoonConfig, 'custom', 'custom
export type TIconsName = keyof typeof mappedIcons;
interface ICustomIcon extends TextProps {
export interface ICustomIcon extends TextProps {
name: TIconsName;
size: number;
color: string;

View File

@ -1,6 +1,18 @@
export const mappedIcons = {
attach: 59676,
link: 59752,
'lamp-bulb': 59812,
'basketball': 59776,
'percentage': 59777,
'burger': 59813,
'leaf': 59814,
'airplane': 59815,
'rocket': 59816,
'directory': 59648,
'directory-disabled': 59649,
'directory-error': 59650,
'federation-disabled': 59651,
'federation': 59652,
'attach': 59676,
'link': 59752,
'status-away': 59741,
'status-busy': 59742,
'status-loading': 59743,
@ -10,193 +22,191 @@ export const mappedIcons = {
'channel-auto-join': 59746,
'channel-move-to-team': 59747,
'lock-filled': 59748,
locker: 59749,
teams: 59751,
shield: 59661,
ignore: 59740,
'checkbox-unchecked': 59648,
'checkbox-checked': 59649,
'github-monochromatic': 59650,
'gitlab-monochromatic': 59651,
'google-monochromatic': 59652,
'linkedin-monochromatic': 59653,
'meteor-monochromatic': 59654,
'twitter-monochromatic': 59655,
administration: 59657,
'adobe-reader-monochromatic': 59658,
'all-contacts-in-channels': 59659,
'all-contacts-in-queue': 59660,
'apple-monochromatic': 59662,
apps: 59663,
'arrow-back': 59664,
'arrow-collapse': 59665,
'arrow-decrease': 59666,
'arrow-down-box': 59667,
'arrow-down-circle': 59668,
'arrow-down': 59669,
'arrow-expand': 59670,
'arrow-increase': 59671,
'arrow-looping': 59672,
'arrow-return': 59673,
'arrow-up-box': 59674,
'arrow-up': 59675,
'audio-disabled': 59677,
'audio-unavailable': 59678,
audio: 59679,
auditing: 59680,
auth: 59681,
avatar: 59682,
backspace: 59683,
bold: 59684,
book: 59685,
business: 59686,
calendar: 59687,
'camera-disabled': 59688,
'camera-filled': 59689,
'camera-photo': 59690,
'camera-unavailable': 59691,
camera: 59692,
'canned-response': 59693,
card: 59694,
'channel-private': 59695,
'channel-public': 59696,
'chat-close': 59697,
'chat-forward': 59698,
check: 59699,
'chevron-down': 59700,
'chevron-left-big': 59701,
'chevron-left': 59702,
'chevron-right': 59703,
'chevron-up': 59704,
'circle-check': 59705,
clipboard: 59706,
clock: 59707,
close: 59708,
'cloud-connectivity': 59709,
code: 59710,
contacts: 59711,
copy: 59712,
create: 59713,
dashboard: 59714,
delete: 59715,
desktop: 59716,
dialpad: 59717,
'directory-disabled': 59718,
directory: 59719,
discussions: 59720,
document: 59721,
donner: 59722,
download: 59723,
edit: 59724,
'emoji-bad-mood': 59725,
'emoji-neutral-mood': 59726,
emoji: 59727,
encrypted: 59728,
'engagement-dashboard': 59729,
'enterprise-feature': 59730,
'facebook-monochromatic': 59731,
'file-document': 59732,
'file-sheet': 59733,
filter: 59734,
fingerprint: 59735,
flag: 59736,
folder: 59737,
game: 59738,
'giphy-monochromatic': 59739,
'locker': 59749,
'teams': 59751,
'shield': 59661,
'ignore': 59740,
'checkbox-unchecked': 59653,
'checkbox-checked': 59654,
'github-monochromatic': 59655,
'gitlab-monochromatic': 59656,
'google-monochromatic': 59657,
'linkedin-monochromatic': 59658,
'meteor-monochromatic': 59659,
'twitter-monochromatic': 59660,
'administration': 59662,
'adobe-reader-monochromatic': 59663,
'all-contacts-in-channels': 59664,
'all-contacts-in-queue': 59665,
'apple-monochromatic': 59666,
'apps': 59667,
'arrow-back': 59668,
'arrow-collapse': 59669,
'arrow-decrease': 59670,
'arrow-down-box': 59671,
'arrow-down-circle': 59672,
'arrow-down': 59673,
'arrow-expand': 59674,
'arrow-increase': 59675,
'arrow-looping': 59677,
'arrow-return': 59678,
'arrow-up-box': 59679,
'arrow-up': 59680,
'audio-disabled': 59681,
'audio-unavailable': 59682,
'audio': 59683,
'auditing': 59684,
'auth': 59685,
'avatar': 59686,
'backspace': 59687,
'bold': 59688,
'book': 59689,
'business': 59690,
'calendar': 59691,
'camera-disabled': 59692,
'camera-filled': 59693,
'camera-photo': 59694,
'camera-unavailable': 59695,
'camera': 59696,
'canned-response': 59697,
'card': 59698,
'channel-private': 59699,
'channel-public': 59700,
'chat-close': 59701,
'chat-forward': 59702,
'check': 59703,
'chevron-down': 59704,
'chevron-left-big': 59705,
'chevron-left': 59706,
'chevron-right': 59707,
'chevron-up': 59708,
'circle-check': 59709,
'clipboard': 59710,
'clock': 59711,
'close': 59712,
'cloud-connectivity': 59713,
'code': 59714,
'contacts': 59715,
'copy': 59716,
'create': 59717,
'dashboard': 59718,
'delete': 59719,
'desktop': 59720,
'dialpad': 59721,
'discussions': 59722,
'document': 59723,
'donner': 59724,
'download': 59725,
'edit': 59726,
'emoji-bad-mood': 59727,
'emoji-neutral-mood': 59728,
'emoji': 59729,
'encrypted': 59730,
'engagement-dashboard': 59731,
'enterprise-feature': 59732,
'facebook-monochromatic': 59733,
'file-document': 59734,
'file-sheet': 59735,
'filter': 59736,
'fingerprint': 59737,
'flag': 59738,
'folder': 59739,
'game': 59753,
'giphy-monochromatic': 59754,
'google-drive-monochromatic': 59756,
'group-by-type': 59757,
hamburguer: 59758,
history: 59759,
home: 59760,
image: 59761,
info: 59762,
'hamburguer': 59758,
'history': 59759,
'home': 59760,
'image': 59761,
'info': 59762,
'input-clear': 59763,
instance: 59764,
italic: 59765,
'instance': 59764,
'italic': 59765,
'jump-backward': 59766,
'jump-forward': 59767,
'jump-to-message': 59768,
kebab: 59769,
keyboard: 59770,
language: 59771,
'kebab': 59769,
'keyboard': 59770,
'language': 59771,
'live-streaming': 59773,
live: 59774,
'live': 59774,
'livechat-monochromatic': 59775,
'log-view': 59778,
login: 59779,
logout: 59780,
mail: 59781,
marketplace: 59782,
meatballs: 59783,
mention: 59784,
'login': 59779,
'logout': 59780,
'mail': 59781,
'marketplace': 59782,
'meatballs': 59783,
'mention': 59784,
'message-disabled': 59785,
message: 59786,
'message': 59786,
'microphone-disabled': 59787,
microphone: 59788,
mobile: 59789,
moon: 59790,
'microphone': 59788,
'mobile': 59789,
'moon': 59790,
'move-to-the-queue': 59791,
'musical-note': 59792,
'new-window': 59793,
'notification-disabled': 59794,
notification: 59795,
omnichannel: 59796,
order: 59797,
'notification': 59795,
'omnichannel': 59796,
'order': 59797,
'ordering-ascending': 59798,
'ordering-descending': 59800,
'pause-filled': 59802,
pause: 59803,
'pause': 59803,
'phone-disabled': 59804,
'phone-end': 59805,
phone: 59806,
'phone': 59806,
'pin-map': 59807,
pin: 59808,
Pipe: 59809,
'pin': 59808,
'Pipe': 59809,
'play-filled': 59810,
play: 59811,
prune: 59817,
queue: 59818,
quote: 59819,
'play': 59811,
'prune': 59817,
'queue': 59818,
'quote': 59819,
'reaction-add': 59820,
record: 59821,
refresh: 59822,
search: 59823,
'record': 59821,
'refresh': 59822,
'search': 59823,
'send-filled': 59824,
send: 59825,
settings: 59826,
share: 59827,
'send': 59825,
'settings': 59826,
'share': 59827,
'shield-check': 59828,
'shield-alt': 59829,
signal: 59830,
'signal': 59830,
'sort-az': 59831,
sort: 59832,
'sort': 59832,
'star-filled': 59833,
star: 59834,
strike: 59846,
sun: 59847,
support: 59848,
team: 59849,
threads: 59850,
total: 59851,
transcript: 59852,
underline: 59853,
undo: 59854,
Unlimited: 59855,
'star': 59834,
'strike': 59846,
'sun': 59847,
'support': 59848,
'team': 59849,
'threads': 59850,
'total': 59851,
'transcript': 59852,
'underline': 59853,
'undo': 59854,
'Unlimited': 59855,
'unread-on-top-disabled': 59856,
'unread-on-top': 59857,
upload: 59858,
'upload': 59858,
'user-add': 59859,
'user-forward': 59860,
user: 59861,
'user': 59861,
'view-condensed': 59862,
'view-extended': 59863,
'view-medium': 59864,
'waiting-on-me': 59865,
warning: 59866,
'warning': 59866,
'whatsapp-monochromatic': 59868,
'wordpress-monochromatic': 59656,
workspaces: 59870,
zip: 59871,
add: 59872,
sms: 59753
'wordpress-monochromatic': 59755,
'workspaces': 59870,
'zip': 59871,
'add': 59872,
'sms': 59772
};

File diff suppressed because one or more lines are too long

View File

@ -1,7 +1,7 @@
import React from 'react';
import { Text, View, ViewStyle } from 'react-native';
import Touch from '../../utils/touch';
import Touch from '../../lib/methods/helpers/touch';
import Avatar from '../Avatar';
import RoomTypeIcon from '../RoomTypeIcon';
import styles, { ROW_HEIGHT } from './styles';
@ -54,7 +54,7 @@ const DirectoryItem = ({
<Avatar text={avatar} size={30} type={type} rid={rid} style={styles.directoryItemAvatar} />
<View style={styles.directoryItemTextContainer}>
<View style={styles.directoryItemTextTitle}>
<RoomTypeIcon type={type} teamMain={teamMain} />
{type !== 'd' ? <RoomTypeIcon type={type} teamMain={teamMain} /> : null}
<Text style={[styles.directoryItemName, { color: themes[theme].titleText }]} numberOfLines={1}>
{title}
</Text>

View File

@ -1,5 +1,5 @@
import React from 'react';
import FastImage from '@rocket.chat/react-native-fast-image';
import FastImage from 'react-native-fast-image';
import { ICustomEmoji } from '../../definitions/IEmoji';

View File

@ -1,10 +1,10 @@
import React from 'react';
import { FlatList, Text, TouchableOpacity } from 'react-native';
import shortnameToUnicode from '../../utils/shortnameToUnicode';
import shortnameToUnicode from '../../lib/methods/helpers/shortnameToUnicode';
import styles from './styles';
import CustomEmoji from './CustomEmoji';
import scrollPersistTaps from '../../utils/scrollPersistTaps';
import scrollPersistTaps from '../../lib/methods/helpers/scrollPersistTaps';
import { IEmoji, IEmojiCategory } from '../../definitions/IEmoji';
const EMOJI_SIZE = 50;

View File

@ -5,7 +5,7 @@ import { dequal } from 'dequal';
import { connect } from 'react-redux';
import orderBy from 'lodash/orderBy';
import { sanitizedRaw } from '@nozbe/watermelondb/RawRecord';
import { ImageStyle } from '@rocket.chat/react-native-fast-image';
import { ImageStyle } from 'react-native-fast-image';
import TabBar from './TabBar';
import EmojiCategory from './EmojiCategory';
@ -14,8 +14,8 @@ import categories from './categories';
import database from '../../lib/database';
import { emojisByCategory } from './emojis';
import protectedFunction from '../../lib/methods/helpers/protectedFunction';
import shortnameToUnicode from '../../utils/shortnameToUnicode';
import log from '../../utils/log';
import shortnameToUnicode from '../../lib/methods/helpers/shortnameToUnicode';
import log from '../../lib/methods/helpers/log';
import { themes } from '../../lib/constants';
import { TSupportedThemes, withTheme } from '../../theme';
import { IEmoji, TGetCustomEmoji, IApplicationState, ICustomEmojis, TFrequentlyUsedEmojiModel } from '../../definitions';

View File

@ -3,12 +3,12 @@ import { ScrollView, ScrollViewProps, StyleSheet, View } from 'react-native';
import { themes } from '../lib/constants';
import sharedStyles from '../views/Styles';
import scrollPersistTaps from '../utils/scrollPersistTaps';
import scrollPersistTaps from '../lib/methods/helpers/scrollPersistTaps';
import KeyboardView from './KeyboardView';
import { useTheme } from '../theme';
import StatusBar from './StatusBar';
import AppVersion from './AppVersion';
import { isTablet } from '../utils/deviceInfo';
import { isTablet } from '../lib/methods/helpers';
import SafeAreaView from './SafeAreaView';
interface IFormContainer extends ScrollViewProps {

View File

@ -1,69 +0,0 @@
import React from 'react';
import { SafeAreaView } from 'react-native-safe-area-context';
import { StyleSheet, View } from 'react-native';
import { themes } from '../../lib/constants';
import { themedHeader } from '../../utils/navigation';
import { isIOS, isTablet } from '../../utils/deviceInfo';
import { useTheme } from '../../theme';
export const headerHeight = isIOS ? 44 : 56;
export const getHeaderHeight = (isLandscape: boolean): number => {
if (isIOS) {
if (isLandscape && !isTablet) {
return 32;
}
return 44;
}
return 56;
};
interface IHeaderTitlePosition {
insets: {
left: number;
right: number;
};
numIconsRight: number;
}
export const getHeaderTitlePosition = ({
insets,
numIconsRight
}: IHeaderTitlePosition): {
left: number;
right: number;
} => ({
left: insets.left + 60,
right: insets.right + Math.max(45 * numIconsRight, 15)
});
const styles = StyleSheet.create({
container: {
height: headerHeight,
flexDirection: 'row',
justifyContent: 'center',
elevation: 4
}
});
interface IHeader {
headerLeft: () => React.ReactElement | null;
headerTitle: () => React.ReactElement;
headerRight: () => React.ReactElement | null;
}
const Header = ({ headerLeft, headerTitle, headerRight }: IHeader): React.ReactElement => {
const { theme } = useTheme();
return (
<SafeAreaView style={{ backgroundColor: themes[theme].headerBackground }} edges={['top', 'left', 'right']}>
<View style={[styles.container, { ...themedHeader(theme).headerStyle }]}>
{headerLeft ? headerLeft() : null}
{headerTitle ? headerTitle() : null}
{headerRight ? headerRight() : null}
</View>
</SafeAreaView>
);
};
export default Header;

View File

@ -1,14 +1,12 @@
import React from 'react';
import { isIOS } from '../../utils/deviceInfo';
import { isIOS } from '../../lib/methods/helpers';
import I18n from '../../i18n';
import Container from './HeaderButtonContainer';
import Item from './HeaderButtonItem';
import Item, { IHeaderButtonItem } from './HeaderButtonItem';
interface IHeaderButtonCommon {
interface IHeaderButtonCommon extends IHeaderButtonItem {
navigation?: any; // TODO: Evaluate proper type
onPress?: () => void;
testID?: string;
}
// Left
@ -28,20 +26,20 @@ export const CloseModal = React.memo(
)
);
export const CancelModal = React.memo(({ onPress, testID }: Partial<IHeaderButtonCommon>) => (
export const CancelModal = React.memo(({ onPress, testID, ...props }: IHeaderButtonCommon) => (
<Container left>
{isIOS ? (
<Item title={I18n.t('Cancel')} onPress={onPress} testID={testID} />
<Item title={I18n.t('Cancel')} onPress={onPress} testID={testID} {...props} />
) : (
<Item iconName='close' onPress={onPress} testID={testID} />
<Item iconName='close' onPress={onPress} testID={testID} {...props} />
)}
</Container>
));
// Right
export const More = React.memo(({ onPress, testID }: Partial<IHeaderButtonCommon>) => (
export const More = React.memo(({ onPress, testID, ...props }: IHeaderButtonCommon) => (
<Container>
<Item iconName='kebab' onPress={onPress} testID={testID} />
<Item iconName='kebab' onPress={onPress} testID={testID} {...props} />
</Container>
));
@ -58,7 +56,7 @@ export const Preferences = React.memo(({ onPress, testID, ...props }: IHeaderBut
));
export const Legal = React.memo(
({ navigation, testID, onPress = () => navigation?.navigate('LegalView') }: IHeaderButtonCommon) => (
<More onPress={onPress} testID={testID} />
({ navigation, testID, onPress = () => navigation?.navigate('LegalView'), ...props }: IHeaderButtonCommon) => (
<More onPress={onPress} testID={testID} {...props} />
)
);

View File

@ -1,18 +1,18 @@
import React from 'react';
import { Platform, StyleSheet, Text } from 'react-native';
import Touchable from 'react-native-platform-touchable';
import { PlatformPressable } from '@react-navigation/elements';
import { CustomIcon, TIconsName } from '../CustomIcon';
import { CustomIcon, ICustomIcon, TIconsName } from '../CustomIcon';
import { useTheme } from '../../theme';
import { themes } from '../../lib/constants';
import sharedStyles from '../../views/Styles';
interface IHeaderButtonItem {
export interface IHeaderButtonItem extends Omit<ICustomIcon, 'name' | 'size' | 'color'> {
title?: string;
iconName?: TIconsName;
onPress?: <T>(arg: T) => void;
testID?: string;
badge?(): void;
color?: string;
}
export const BUTTON_HIT_SLOP = {
@ -24,7 +24,7 @@ export const BUTTON_HIT_SLOP = {
const styles = StyleSheet.create({
container: {
marginHorizontal: 6
padding: 6
},
title: {
...Platform.select({
@ -39,19 +39,21 @@ const styles = StyleSheet.create({
}
});
const Item = ({ title, iconName, onPress, testID, badge }: IHeaderButtonItem): React.ReactElement => {
const { theme } = useTheme();
const Item = ({ title, iconName, onPress, testID, badge, color, ...props }: IHeaderButtonItem): React.ReactElement => {
const { colors } = useTheme();
return (
<Touchable onPress={onPress} testID={testID} hitSlop={BUTTON_HIT_SLOP} style={styles.container}>
<PlatformPressable onPress={onPress} testID={testID} hitSlop={BUTTON_HIT_SLOP} style={styles.container}>
<>
{iconName ? (
<CustomIcon name={iconName} size={24} color={themes[theme].headerTintColor} />
<CustomIcon name={iconName} size={24} color={color || colors.headerTintColor} {...props} />
) : (
<Text style={[styles.title, { color: themes[theme].headerTintColor }]}>{title}</Text>
<Text style={[styles.title, { color: color || colors.headerTintColor }]} {...props}>
{title}
</Text>
)}
{badge ? badge() : null}
</>
</Touchable>
</PlatformPressable>
);
};

View File

@ -7,8 +7,8 @@ const styles = StyleSheet.create({
badgeContainer: {
padding: 2,
position: 'absolute',
right: -3,
top: -3,
right: 2,
top: 2,
borderRadius: 10,
alignItems: 'center',
justifyContent: 'center'

View File

@ -1,6 +1,6 @@
import React from 'react';
import { Image } from 'react-native';
import { FastImageProps } from '@rocket.chat/react-native-fast-image';
import { FastImageProps } from 'react-native-fast-image';
import { types } from './types';
@ -10,7 +10,7 @@ export const ImageComponent = (type?: string): React.ComponentType<Partial<Image
const { Image } = require('react-native');
Component = Image;
} else {
const FastImage = require('@rocket.chat/react-native-fast-image').default;
const FastImage = require('react-native-fast-image');
Component = FastImage;
}
return Component;

View File

@ -0,0 +1,125 @@
import React, { useState } from 'react';
import { LayoutChangeEvent, StyleSheet, StyleProp, ViewStyle, ImageStyle, View } from 'react-native';
import { Gesture, GestureDetector } from 'react-native-gesture-handler';
import Animated, { withTiming, useSharedValue, useAnimatedStyle, withSpring } from 'react-native-reanimated';
import { useTheme } from '../../theme';
import { ImageComponent } from './ImageComponent';
interface ImageViewerProps {
style?: StyleProp<ImageStyle>;
containerStyle?: StyleProp<ViewStyle>;
imageContainerStyle?: StyleProp<ViewStyle>;
uri: string;
imageComponentType?: string;
width: number;
height: number;
onLoadEnd?: () => void;
}
const styles = StyleSheet.create({
flex: {
flex: 1
},
image: {
flex: 1
}
});
export const ImageViewer = ({ uri = '', imageComponentType, width, height, ...props }: ImageViewerProps): React.ReactElement => {
const [centerX, setCenterX] = useState(0);
const [centerY, setCenterY] = useState(0);
const onLayout = ({
nativeEvent: {
layout: { x, y, width, height }
}
}: LayoutChangeEvent) => {
setCenterX(x + width / 2);
setCenterY(y + height / 2);
};
const translationX = useSharedValue<number>(0);
const translationY = useSharedValue<number>(0);
const offsetX = useSharedValue<number>(0);
const offsetY = useSharedValue<number>(0);
const scale = useSharedValue<number>(1);
const scaleOffset = useSharedValue<number>(1);
const style = useAnimatedStyle(() => ({
transform: [{ translateX: translationX.value }, { translateY: translationY.value }, { scale: scale.value }]
}));
const resetScaleAnimation = () => {
scaleOffset.value = 1;
offsetX.value = 0;
offsetY.value = 0;
scale.value = withSpring(1);
translationX.value = withSpring(0, { overshootClamping: true });
translationY.value = withSpring(0, { overshootClamping: true });
};
const clamp = (value: number, min: number, max: number) => Math.max(Math.min(value, max), min);
const pinchGesture = Gesture.Pinch()
.onUpdate(event => {
scale.value = clamp(scaleOffset.value * (event.scale > 0 ? event.scale : 1), 1, 4);
})
.onEnd(() => {
scaleOffset.value = scale.value > 0 ? scale.value : 1;
});
const panGesture = Gesture.Pan()
.maxPointers(2)
.onStart(() => {
translationX.value = offsetX.value;
translationY.value = offsetY.value;
})
.onUpdate(event => {
const scaleFactor = scale.value - 1;
translationX.value = clamp(event.translationX + offsetX.value, -scaleFactor * centerX, scaleFactor * centerX);
translationY.value = clamp(event.translationY + offsetY.value, -scaleFactor * centerY, scaleFactor * centerY);
})
.onEnd(() => {
offsetX.value = translationX.value;
offsetY.value = translationY.value;
if (scale.value === 1) resetScaleAnimation();
});
const doubleTapGesture = Gesture.Tap()
.numberOfTaps(2)
.maxDelay(120)
.maxDistance(70)
.onEnd(event => {
if (scaleOffset.value > 1) resetScaleAnimation();
else {
scale.value = withTiming(2, { duration: 200 });
translationX.value = withTiming(centerX - event.x, { duration: 200 });
offsetX.value = centerX - event.x;
scaleOffset.value = 2;
}
});
const gesture = Gesture.Simultaneous(pinchGesture, panGesture, doubleTapGesture);
const Component = ImageComponent(imageComponentType);
const { colors } = useTheme();
return (
<View style={[styles.flex, { width, height, backgroundColor: colors.previewBackground }]}>
<GestureDetector gesture={gesture}>
<Animated.View onLayout={onLayout} style={[styles.flex, style]}>
<Component
// @ts-ignore
style={styles.image}
resizeMode='contain'
source={{ uri }}
{...props}
/>
</Animated.View>
</GestureDetector>
</View>
);
};

View File

@ -11,7 +11,7 @@ import sharedStyles from '../../views/Styles';
import { themes } from '../../lib/constants';
import { useTheme } from '../../theme';
import { ROW_HEIGHT } from '../RoomItem';
import { goRoom } from '../../utils/goRoom';
import { goRoom } from '../../lib/methods/helpers/goRoom';
import Navigation from '../../lib/navigation/appNavigation';
import { useOrientation } from '../../dimensions';
import { IApplicationState, ISubscription, SubscriptionType } from '../../definitions';

View File

@ -4,9 +4,9 @@ import { connect } from 'react-redux';
import { dequal } from 'dequal';
import NotifierComponent, { INotifierComponent } from './NotifierComponent';
import EventEmitter from '../../utils/events';
import EventEmitter from '../../lib/methods/helpers/events';
import Navigation from '../../lib/navigation/appNavigation';
import { getActiveRoute } from '../../utils/navigation';
import { getActiveRoute } from '../../lib/methods/helpers/navigation';
import { IApplicationState } from '../../definitions';
import { IRoom } from '../../reducers/room';

View File

@ -1,7 +1,7 @@
import React from 'react';
import { KeyboardAwareScrollView, KeyboardAwareScrollViewProps } from '@codler/react-native-keyboard-aware-scroll-view';
import scrollPersistTaps from '../utils/scrollPersistTaps';
import scrollPersistTaps from '../lib/methods/helpers/scrollPersistTaps';
interface IKeyboardViewProps extends KeyboardAwareScrollViewProps {
keyboardVerticalOffset?: number;

View File

@ -2,7 +2,7 @@ import React from 'react';
import { ScrollView, StyleSheet } from 'react-native';
import { withTheme } from '../../theme';
import scrollPersistTaps from '../../utils/scrollPersistTaps';
import scrollPersistTaps from '../../lib/methods/helpers/scrollPersistTaps';
const styles = StyleSheet.create({
container: {

View File

@ -1,7 +1,7 @@
import React from 'react';
import { I18nManager, StyleProp, StyleSheet, Text, TextStyle, View } from 'react-native';
import Touch from '../../utils/touch';
import Touch from '../../lib/methods/helpers/touch';
import { themes } from '../../lib/constants';
import sharedStyles from '../../views/Styles';
import { TSupportedThemes, useTheme } from '../../theme';

View File

@ -1,444 +0,0 @@
import React from 'react';
import { Animated, Easing, Linking, StyleSheet, Text, View, ViewStyle } from 'react-native';
import { connect } from 'react-redux';
import { Base64 } from 'js-base64';
import * as AppleAuthentication from 'expo-apple-authentication';
import { StackNavigationProp } from '@react-navigation/stack';
import { TSupportedThemes, withTheme } from '../theme';
import sharedStyles from '../views/Styles';
import { themes } from '../lib/constants';
import Button from './Button';
import OrSeparator from './OrSeparator';
import Touch from '../utils/touch';
import I18n from '../i18n';
import random from '../utils/random';
import { events, logEvent } from '../utils/log';
import { CustomIcon, TIconsName } from './CustomIcon';
import { IServices } from '../selectors/login';
import { OutsideParamList } from '../stacks/types';
import { IApplicationState } from '../definitions';
import { Services } from '../lib/services';
const BUTTON_HEIGHT = 48;
const SERVICE_HEIGHT = 58;
const BORDER_RADIUS = 2;
const SERVICES_COLLAPSED_HEIGHT = 174;
const LOGIN_STYPE_POPUP = 'popup';
const LOGIN_STYPE_REDIRECT = 'redirect';
const styles = StyleSheet.create({
serviceButton: {
borderRadius: BORDER_RADIUS,
marginBottom: 10
},
serviceButtonContainer: {
borderRadius: BORDER_RADIUS,
width: '100%',
height: BUTTON_HEIGHT,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
paddingHorizontal: 15
},
serviceIcon: {
position: 'absolute',
left: 15,
top: 12,
width: 24,
height: 24
},
serviceText: {
...sharedStyles.textRegular,
fontSize: 16
},
serviceName: {
...sharedStyles.textSemibold
},
options: {
marginBottom: 0
}
});
interface IOpenOAuth {
url: string;
ssoToken?: string;
authType?: string;
}
interface IItemService {
name: string;
service: string;
authType: string;
buttonColor: 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 {
navigation: StackNavigationProp<OutsideParamList>;
server: string;
services: IServices;
Gitlab_URL: string;
CAS_enabled: boolean;
CAS_login_url: string;
separator: boolean;
theme: TSupportedThemes;
}
interface ILoginServicesState {
collapsed: boolean;
servicesHeight: Animated.Value;
}
class LoginServices extends React.PureComponent<ILoginServicesProps, ILoginServicesState> {
private _animation?: Animated.CompositeAnimation | void;
state = {
collapsed: true,
servicesHeight: new Animated.Value(SERVICES_COLLAPSED_HEIGHT)
};
onPressFacebook = () => {
logEvent(events.ENTER_WITH_FACEBOOK);
const { services, server } = this.props;
const { clientId } = services.facebook;
const endpoint = 'https://m.facebook.com/v2.9/dialog/oauth';
const redirect_uri = `${server}/_oauth/facebook?close`;
const scope = 'email';
const state = this.getOAuthState();
const params = `?client_id=${clientId}&redirect_uri=${redirect_uri}&scope=${scope}&state=${state}&display=touch`;
this.openOAuth({ url: `${endpoint}${params}` });
};
onPressGithub = () => {
logEvent(events.ENTER_WITH_GITHUB);
const { services, server } = this.props;
const { clientId } = services.github;
const endpoint = `https://github.com/login?client_id=${clientId}&return_to=${encodeURIComponent('/login/oauth/authorize')}`;
const redirect_uri = `${server}/_oauth/github?close`;
const scope = 'user:email';
const state = this.getOAuthState();
const params = `?client_id=${clientId}&redirect_uri=${redirect_uri}&scope=${scope}&state=${state}`;
this.openOAuth({ url: `${endpoint}${encodeURIComponent(params)}` });
};
onPressGitlab = () => {
logEvent(events.ENTER_WITH_GITLAB);
const { services, server, Gitlab_URL } = this.props;
const { clientId } = services.gitlab;
const baseURL = Gitlab_URL ? Gitlab_URL.trim().replace(/\/*$/, '') : 'https://gitlab.com';
const endpoint = `${baseURL}/oauth/authorize`;
const redirect_uri = `${server}/_oauth/gitlab?close`;
const scope = 'read_user';
const state = this.getOAuthState();
const params = `?client_id=${clientId}&redirect_uri=${redirect_uri}&scope=${scope}&state=${state}&response_type=code`;
this.openOAuth({ url: `${endpoint}${params}` });
};
onPressGoogle = () => {
logEvent(events.ENTER_WITH_GOOGLE);
const { services, server } = this.props;
const { clientId } = services.google;
const endpoint = 'https://accounts.google.com/o/oauth2/auth';
const redirect_uri = `${server}/_oauth/google?close`;
const scope = 'email';
const state = this.getOAuthState(LOGIN_STYPE_REDIRECT);
const params = `?client_id=${clientId}&redirect_uri=${redirect_uri}&scope=${scope}&state=${state}&response_type=code`;
Linking.openURL(`${endpoint}${params}`);
};
onPressLinkedin = () => {
logEvent(events.ENTER_WITH_LINKEDIN);
const { services, server } = this.props;
const { clientId } = services.linkedin;
const endpoint = 'https://www.linkedin.com/oauth/v2/authorization';
const redirect_uri = `${server}/_oauth/linkedin?close`;
const scope = 'r_liteprofile,r_emailaddress';
const state = this.getOAuthState();
const params = `?client_id=${clientId}&redirect_uri=${redirect_uri}&scope=${scope}&state=${state}&response_type=code`;
this.openOAuth({ url: `${endpoint}${params}` });
};
onPressMeteor = () => {
logEvent(events.ENTER_WITH_METEOR);
const { services, server } = this.props;
const { clientId } = services['meteor-developer'];
const endpoint = 'https://www.meteor.com/oauth2/authorize';
const redirect_uri = `${server}/_oauth/meteor-developer`;
const state = this.getOAuthState();
const params = `?client_id=${clientId}&redirect_uri=${redirect_uri}&state=${state}&response_type=code`;
this.openOAuth({ url: `${endpoint}${params}` });
};
onPressTwitter = () => {
logEvent(events.ENTER_WITH_TWITTER);
const { server } = this.props;
const state = this.getOAuthState();
const url = `${server}/_oauth/twitter/?requestTokenAndRedirect=true&state=${state}`;
this.openOAuth({ url });
};
onPressWordpress = () => {
logEvent(events.ENTER_WITH_WORDPRESS);
const { services, server } = this.props;
const { clientId, serverURL } = services.wordpress;
const endpoint = `${serverURL}/oauth/authorize`;
const redirect_uri = `${server}/_oauth/wordpress?close`;
const scope = 'openid';
const state = this.getOAuthState();
const params = `?client_id=${clientId}&redirect_uri=${redirect_uri}&scope=${scope}&state=${state}&response_type=code`;
this.openOAuth({ url: `${endpoint}${params}` });
};
onPressCustomOAuth = (loginService: IItemService) => {
logEvent(events.ENTER_WITH_CUSTOM_OAUTH);
const { server } = this.props;
const { serverURL, authorizePath, clientId, scope, service } = loginService;
const redirectUri = `${server}/_oauth/${service}`;
const state = this.getOAuthState();
const params = `?client_id=${clientId}&redirect_uri=${redirectUri}&response_type=code&state=${state}&scope=${scope}`;
const domain = `${serverURL}`;
const absolutePath = `${authorizePath}${params}`;
const url = absolutePath.includes(domain) ? absolutePath : domain + absolutePath;
this.openOAuth({ url });
};
onPressSaml = (loginService: IItemService) => {
logEvent(events.ENTER_WITH_SAML);
const { server } = this.props;
const { clientConfig } = loginService;
const { provider } = clientConfig;
const ssoToken = random(17);
const url = `${server}/_saml/authorize/${provider}/${ssoToken}`;
this.openOAuth({ url, ssoToken, authType: 'saml' });
};
onPressCas = () => {
logEvent(events.ENTER_WITH_CAS);
const { server, CAS_login_url } = this.props;
const ssoToken = random(17);
const url = `${CAS_login_url}?service=${server}/_cas/${ssoToken}`;
this.openOAuth({ url, ssoToken, authType: 'cas' });
};
onPressAppleLogin = async () => {
logEvent(events.ENTER_WITH_APPLE);
try {
const { fullName, email, identityToken } = await AppleAuthentication.signInAsync({
requestedScopes: [
AppleAuthentication.AppleAuthenticationScope.FULL_NAME,
AppleAuthentication.AppleAuthenticationScope.EMAIL
]
});
await Services.loginOAuthOrSso({ fullName, email, identityToken });
} catch {
logEvent(events.ENTER_WITH_APPLE_F);
}
};
getOAuthState = (loginStyle = LOGIN_STYPE_POPUP) => {
const credentialToken = random(43);
let obj: {
loginStyle: string;
credentialToken: string;
isCordova: boolean;
redirectUrl?: string;
} = { loginStyle, credentialToken, isCordova: true };
if (loginStyle === LOGIN_STYPE_REDIRECT) {
obj = {
...obj,
redirectUrl: 'rocketchat://auth'
};
}
return Base64.encodeURI(JSON.stringify(obj));
};
openOAuth = ({ url, ssoToken, authType = 'oauth' }: IOpenOAuth) => {
const { navigation } = this.props;
navigation.navigate('AuthenticationWebView', { url, authType, ssoToken });
};
transitionServicesTo = (height: number) => {
const { servicesHeight } = this.state;
if (this._animation) {
this._animation.stop();
}
this._animation = Animated.timing(servicesHeight, {
toValue: height,
duration: 300,
easing: Easing.inOut(Easing.quad),
useNativeDriver: false
}).start();
};
toggleServices = () => {
const { collapsed } = this.state;
const { services } = this.props;
const { length } = Object.values(services);
if (collapsed) {
this.transitionServicesTo(SERVICE_HEIGHT * length);
} else {
this.transitionServicesTo(SERVICES_COLLAPSED_HEIGHT);
}
this.setState((prevState: ILoginServicesState) => ({ collapsed: !prevState.collapsed }));
};
getSocialOauthProvider = (name: string) => {
const oauthProviders: IOauthProvider = {
facebook: this.onPressFacebook,
github: this.onPressGithub,
gitlab: this.onPressGitlab,
google: this.onPressGoogle,
linkedin: this.onPressLinkedin,
'meteor-developer': this.onPressMeteor,
twitter: this.onPressTwitter,
wordpress: this.onPressWordpress
};
return oauthProviders[name];
};
renderServicesSeparator = () => {
const { collapsed } = this.state;
const { services, separator, theme } = this.props;
const { length } = Object.values(services);
if (length > 3 && separator) {
return (
<>
<Button
title={collapsed ? I18n.t('Onboarding_more_options') : I18n.t('Onboarding_less_options')}
type='secondary'
onPress={this.toggleServices}
style={styles.options}
color={themes[theme].actionTintColor}
/>
<OrSeparator theme={theme} />
</>
);
}
if (length > 0 && separator) {
return <OrSeparator theme={theme} />;
}
return null;
};
renderItem = (service: IItemService) => {
const { CAS_enabled, theme } = this.props;
let { name } = service;
name = name === 'meteor-developer' ? 'meteor' : name;
const icon = `${name}-monochromatic` as TIconsName;
const isSaml = service.service === 'saml';
let onPress = () => {};
switch (service.authType) {
case 'oauth': {
onPress = this.getSocialOauthProvider(service.name);
break;
}
case 'oauth_custom': {
onPress = () => this.onPressCustomOAuth(service);
break;
}
case 'saml': {
onPress = () => this.onPressSaml(service);
break;
}
case 'cas': {
onPress = () => this.onPressCas();
break;
}
case 'apple': {
onPress = () => this.onPressAppleLogin();
break;
}
default:
break;
}
name = name.charAt(0).toUpperCase() + name.slice(1);
let buttonText;
if (isSaml || (service.service === 'cas' && CAS_enabled)) {
buttonText = <Text style={[styles.serviceName, isSaml && { color: service.buttonLabelColor }]}>{name}</Text>;
} else {
buttonText = (
<>
{I18n.t('Continue_with')} <Text style={styles.serviceName}>{name}</Text>
</>
);
}
const backgroundColor = isSaml && service.buttonColor ? service.buttonColor : themes[theme].chatComponentBackground;
return (
<Touch
key={service.name}
onPress={onPress}
style={[styles.serviceButton, { backgroundColor }]}
theme={theme}
activeOpacity={0.5}
underlayColor={themes[theme].buttonText}>
<View style={styles.serviceButtonContainer}>
{service.authType === 'oauth' || service.authType === 'apple' ? (
<CustomIcon name={icon} size={24} color={themes[theme].titleText} style={styles.serviceIcon} />
) : null}
<Text style={[styles.serviceText, { color: themes[theme].titleText }]}>{buttonText}</Text>
</View>
</Touch>
);
};
render() {
const { servicesHeight } = this.state;
const { services, separator } = this.props;
const { length } = Object.values(services);
const style: Animated.AnimatedProps<ViewStyle> = {
overflow: 'hidden',
height: servicesHeight
};
if (length > 3 && separator) {
return (
<>
<Animated.View style={style}>
{Object.values(services).map((service: IItemService) => this.renderItem(service))}
</Animated.View>
{this.renderServicesSeparator()}
</>
);
}
return (
<>
{Object.values(services).map((service: IItemService) => this.renderItem(service))}
{this.renderServicesSeparator()}
</>
);
}
}
const mapStateToProps = (state: IApplicationState) => ({
server: state.server.server,
Gitlab_URL: state.settings.API_Gitlab_URL as string,
CAS_enabled: state.settings.CAS_enabled as boolean,
CAS_login_url: state.settings.CAS_login_url as string,
services: state.login.services as IServices
});
export default connect(mapStateToProps)(withTheme(LoginServices));

View File

@ -0,0 +1,31 @@
import React from 'react';
import { Text, View } from 'react-native';
import { useTheme } from '../../theme';
import Touch from '../../lib/methods/helpers/touch';
import { CustomIcon } from '../CustomIcon';
import { IButtonService } from './interfaces';
import styles from './styles';
const ButtonService = ({ name, authType, onPress, backgroundColor, buttonText, icon }: IButtonService) => {
const { theme, colors } = useTheme();
return (
<Touch
key={name}
onPress={onPress}
style={[styles.serviceButton, { backgroundColor }]}
theme={theme}
activeOpacity={0.5}
underlayColor={colors.buttonText}>
<View style={styles.serviceButtonContainer}>
{authType === 'oauth' || authType === 'apple' ? (
<CustomIcon name={icon} size={24} color={colors.titleText} style={styles.serviceIcon} />
) : null}
<Text style={[styles.serviceText, { color: colors.titleText }]}>{buttonText}</Text>
</View>
</Touch>
);
};
export default ButtonService;

View File

@ -0,0 +1,99 @@
import React from 'react';
import { storiesOf } from '@storybook/react-native';
import { Provider } from 'react-redux';
import { StyleSheet, Text, ScrollView } from 'react-native';
import { store } from '../../../storybook/stories';
import { ThemeContext } from '../../theme';
import { colors } from '../../lib/constants';
import i18n from '../../i18n';
import sharedStyles from '../../views/Styles';
import ServicesSeparator from './ServicesSeparator';
import ButtonService from './ButtonService';
const styles = StyleSheet.create({
serviceName: {
...sharedStyles.textSemibold
}
});
const services = {
github: {
_id: 'github',
name: 'github',
clientId: 'github-123',
buttonLabelText: '',
buttonColor: '',
buttonLabelColor: '',
custom: false,
authType: 'oauth'
},
gitlab: {
_id: 'gitlab',
name: 'gitlab',
clientId: 'gitlab-123',
buttonLabelText: '',
buttonColor: '',
buttonLabelColor: '',
custom: false,
authType: 'oauth'
},
google: {
_id: 'google',
name: 'google',
clientId: 'google-123',
buttonLabelText: '',
buttonColor: '',
buttonLabelColor: '',
custom: false,
authType: 'oauth'
},
apple: {
_id: 'apple',
name: 'apple',
clientId: 'apple-123',
buttonLabelText: 'Sign in with Apple',
buttonColor: '#000',
buttonLabelColor: '#FFF',
custom: false,
authType: 'apple'
}
};
const theme = 'light';
const stories = storiesOf('Login Services', module)
.addDecorator(story => <Provider store={store}>{story()}</Provider>)
.addDecorator(story => <ThemeContext.Provider value={{ theme, colors: colors[theme] }}>{story()}</ThemeContext.Provider>)
.addDecorator(story => <ScrollView style={sharedStyles.containerScrollView}>{story()}</ScrollView>);
stories.add('ServicesSeparator', () => (
<>
<ServicesSeparator collapsed onPressButtonSeparator={() => {}} separator services={services} />
<ServicesSeparator collapsed={false} onPressButtonSeparator={() => {}} separator services={services} />
</>
));
stories.add('ServiceList', () => (
<>
{Object.values(services).map(service => {
const icon = `${service.name}-monochromatic`;
const buttonText = (
<>
{i18n.t('Continue_with')} <Text style={styles.serviceName}>{service.name}</Text>
</>
);
return (
<ButtonService
key={service._id}
onPress={() => {}}
backgroundColor={colors[theme].chatComponentBackground}
buttonText={buttonText}
icon={icon}
name={service.name}
authType={service.authType}
/>
);
})}
</>
));

View File

@ -0,0 +1,104 @@
import React, { useRef } from 'react';
import { Text } from 'react-native';
import { useTheme } from '../../theme';
import I18n from '../../i18n';
import { TIconsName } from '../CustomIcon';
import { IItemService, IOauthProvider } from './interfaces';
import styles from './styles';
import * as ServiceLogin from './serviceLogin';
import ButtonService from './ButtonService';
const Service = React.memo(
({
CAS_enabled,
CAS_login_url,
Gitlab_URL,
server,
service
}: {
service: IItemService;
server: string;
Gitlab_URL: string;
CAS_enabled: boolean;
CAS_login_url: string;
storiesTestOnPress?: () => void;
}) => {
const { colors } = useTheme();
const onPress = useRef<any>();
const buttonText = useRef<React.ReactElement>();
const modifiedName = useRef<string>();
const { name } = service;
modifiedName.current = name === 'meteor-developer' ? 'meteor' : name;
const icon = `${modifiedName.current}-monochromatic` as TIconsName;
const isSaml = service.service === 'saml';
const getSocialOauthProvider = (name: string) => {
const oauthProviders: IOauthProvider = {
facebook: () => ServiceLogin.onPressFacebook({ service, server }),
github: () => ServiceLogin.onPressGithub({ service, server }),
gitlab: () => ServiceLogin.onPressGitlab({ service, server, urlOption: Gitlab_URL }),
google: () => ServiceLogin.onPressGoogle({ service, server }),
linkedin: () => ServiceLogin.onPressLinkedin({ service, server }),
'meteor-developer': () => ServiceLogin.onPressMeteor({ service, server }),
twitter: () => ServiceLogin.onPressTwitter({ service, server }),
wordpress: () => ServiceLogin.onPressWordpress({ service, server })
};
return oauthProviders[name];
};
switch (service.authType) {
case 'oauth': {
onPress.current = getSocialOauthProvider(service.name);
break;
}
case 'oauth_custom': {
onPress.current = () => ServiceLogin.onPressCustomOAuth({ loginService: service, server });
break;
}
case 'saml': {
onPress.current = () => ServiceLogin.onPressSaml({ loginService: service, server });
break;
}
case 'cas': {
onPress.current = () => ServiceLogin.onPressCas({ casLoginUrl: CAS_login_url, server });
break;
}
case 'apple': {
onPress.current = () => ServiceLogin.onPressAppleLogin();
break;
}
default:
break;
}
modifiedName.current = modifiedName.current.charAt(0).toUpperCase() + modifiedName.current.slice(1);
if (isSaml || (service.service === 'cas' && CAS_enabled)) {
buttonText.current = (
<Text style={[styles.serviceName, isSaml && { color: service.buttonLabelColor }]}>{modifiedName.current}</Text>
);
} else {
buttonText.current = (
<>
{I18n.t('Continue_with')} <Text style={styles.serviceName}>{modifiedName.current}</Text>
</>
);
}
const backgroundColor = isSaml && service.buttonColor ? service.buttonColor : colors.chatComponentBackground;
return (
<ButtonService
onPress={onPress.current}
backgroundColor={backgroundColor}
buttonText={buttonText.current}
icon={icon}
name={service.name}
authType={service.authType}
/>
);
}
);
export default Service;

View File

@ -0,0 +1,35 @@
import React from 'react';
import Button from '../Button';
import OrSeparator from '../OrSeparator';
import { useTheme } from '../../theme';
import styles from './styles';
import I18n from '../../i18n';
import { IServicesSeparator } from './interfaces';
const ServicesSeparator = ({ services, separator, collapsed, onPress }: IServicesSeparator) => {
const { colors, theme } = useTheme();
const { length } = Object.values(services);
if (length > 3 && separator) {
return (
<>
<Button
title={collapsed ? I18n.t('Onboarding_more_options') : I18n.t('Onboarding_less_options')}
type='secondary'
onPress={onPress}
style={styles.options}
color={colors.actionTintColor}
/>
<OrSeparator theme={theme} />
</>
);
}
if (length > 0 && separator) {
return <OrSeparator theme={theme} />;
}
return null;
};
export default ServicesSeparator;

View File

@ -0,0 +1,5 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Storyshots Login Services ServiceList 1`] = `"{\\"type\\":\\"RCTScrollView\\",\\"props\\":{\\"style\\":{\\"padding\\":15,\\"paddingBottom\\":30}},\\"children\\":[{\\"type\\":\\"View\\",\\"props\\":{},\\"children\\":[{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":{\\"borderRadius\\":2,\\"width\\":\\"100%\\",\\"height\\":48,\\"flexDirection\\":\\"row\\",\\"alignItems\\":\\"center\\",\\"justifyContent\\":\\"center\\",\\"paddingHorizontal\\":15}},\\"children\\":[{\\"type\\":\\"Text\\",\\"props\\":{\\"selectable\\":false,\\"allowFontScaling\\":false,\\"style\\":[{\\"fontSize\\":24,\\"color\\":\\"#0d0e12\\"},{\\"position\\":\\"absolute\\",\\"left\\":15,\\"top\\":12,\\"width\\":24,\\"height\\":24},{\\"fontFamily\\":\\"custom\\",\\"fontWeight\\":\\"normal\\",\\"fontStyle\\":\\"normal\\"},{}]},\\"children\\":[\\"\\"]},{\\"type\\":\\"Text\\",\\"props\\":{\\"style\\":[{\\"textAlign\\":\\"left\\",\\"backgroundColor\\":\\"transparent\\",\\"fontFamily\\":\\"System\\",\\"fontWeight\\":\\"400\\",\\"fontSize\\":16},{\\"color\\":\\"#0d0e12\\"}]},\\"children\\":[\\"Continue with\\",\\" \\",{\\"type\\":\\"Text\\",\\"props\\":{\\"style\\":{\\"textAlign\\":\\"left\\",\\"backgroundColor\\":\\"transparent\\",\\"fontFamily\\":\\"System\\",\\"fontWeight\\":\\"600\\"}},\\"children\\":[\\"github\\"]}]}]},{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":{\\"borderRadius\\":2,\\"width\\":\\"100%\\",\\"height\\":48,\\"flexDirection\\":\\"row\\",\\"alignItems\\":\\"center\\",\\"justifyContent\\":\\"center\\",\\"paddingHorizontal\\":15}},\\"children\\":[{\\"type\\":\\"Text\\",\\"props\\":{\\"selectable\\":false,\\"allowFontScaling\\":false,\\"style\\":[{\\"fontSize\\":24,\\"color\\":\\"#0d0e12\\"},{\\"position\\":\\"absolute\\",\\"left\\":15,\\"top\\":12,\\"width\\":24,\\"height\\":24},{\\"fontFamily\\":\\"custom\\",\\"fontWeight\\":\\"normal\\",\\"fontStyle\\":\\"normal\\"},{}]},\\"children\\":[\\"\\"]},{\\"type\\":\\"Text\\",\\"props\\":{\\"style\\":[{\\"textAlign\\":\\"left\\",\\"backgroundColor\\":\\"transparent\\",\\"fontFamily\\":\\"System\\",\\"fontWeight\\":\\"400\\",\\"fontSize\\":16},{\\"color\\":\\"#0d0e12\\"}]},\\"children\\":[\\"Continue with\\",\\" \\",{\\"type\\":\\"Text\\",\\"props\\":{\\"style\\":{\\"textAlign\\":\\"left\\",\\"backgroundColor\\":\\"transparent\\",\\"fontFamily\\":\\"System\\",\\"fontWeight\\":\\"600\\"}},\\"children\\":[\\"gitlab\\"]}]}]},{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":{\\"borderRadius\\":2,\\"width\\":\\"100%\\",\\"height\\":48,\\"flexDirection\\":\\"row\\",\\"alignItems\\":\\"center\\",\\"justifyContent\\":\\"center\\",\\"paddingHorizontal\\":15}},\\"children\\":[{\\"type\\":\\"Text\\",\\"props\\":{\\"selectable\\":false,\\"allowFontScaling\\":false,\\"style\\":[{\\"fontSize\\":24,\\"color\\":\\"#0d0e12\\"},{\\"position\\":\\"absolute\\",\\"left\\":15,\\"top\\":12,\\"width\\":24,\\"height\\":24},{\\"fontFamily\\":\\"custom\\",\\"fontWeight\\":\\"normal\\",\\"fontStyle\\":\\"normal\\"},{}]},\\"children\\":[\\"\\"]},{\\"type\\":\\"Text\\",\\"props\\":{\\"style\\":[{\\"textAlign\\":\\"left\\",\\"backgroundColor\\":\\"transparent\\",\\"fontFamily\\":\\"System\\",\\"fontWeight\\":\\"400\\",\\"fontSize\\":16},{\\"color\\":\\"#0d0e12\\"}]},\\"children\\":[\\"Continue with\\",\\" \\",{\\"type\\":\\"Text\\",\\"props\\":{\\"style\\":{\\"textAlign\\":\\"left\\",\\"backgroundColor\\":\\"transparent\\",\\"fontFamily\\":\\"System\\",\\"fontWeight\\":\\"600\\"}},\\"children\\":[\\"google\\"]}]}]},{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":{\\"borderRadius\\":2,\\"width\\":\\"100%\\",\\"height\\":48,\\"flexDirection\\":\\"row\\",\\"alignItems\\":\\"center\\",\\"justifyContent\\":\\"center\\",\\"paddingHorizontal\\":15}},\\"children\\":[{\\"type\\":\\"Text\\",\\"props\\":{\\"selectable\\":false,\\"allowFontScaling\\":false,\\"style\\":[{\\"fontSize\\":24,\\"color\\":\\"#0d0e12\\"},{\\"position\\":\\"absolute\\",\\"left\\":15,\\"top\\":12,\\"width\\":24,\\"height\\":24},{\\"fontFamily\\":\\"custom\\",\\"fontWeight\\":\\"normal\\",\\"fontStyle\\":\\"normal\\"},{}]},\\"children\\":[\\"\\"]},{\\"type\\":\\"Text\\",\\"props\\":{\\"style\\":[{\\"textAlign\\":\\"left\\",\\"backgroundColor\\":\\"transparent\\",\\"fontFamily\\":\\"System\\",\\"fontWeight\\":\\"400\\",\\"fontSize\\":16},{\\"color\\":\\"#0d0e12\\"}]},\\"children\\":[\\"Continue with\\",\\" \\",{\\"type\\":\\"Text\\",\\"props\\":{\\"style\\":{\\"textAlign\\":\\"left\\",\\"backgroundColor\\":\\"transparent\\",\\"fontFamily\\":\\"System\\",\\"fontWeight\\":\\"600\\"}},\\"children\\":[\\"apple\\"]}]}]}]}]}"`;
exports[`Storyshots Login Services ServicesSeparator 1`] = `"{\\"type\\":\\"RCTScrollView\\",\\"props\\":{\\"style\\":{\\"padding\\":15,\\"paddingBottom\\":30}},\\"children\\":[{\\"type\\":\\"View\\",\\"props\\":{},\\"children\\":[{\\"type\\":\\"View\\",\\"props\\":{\\"accessible\\":true,\\"accessibilityLabel\\":\\"More options\\",\\"focusable\\":false,\\"style\\":{\\"paddingHorizontal\\":14,\\"justifyContent\\":\\"center\\",\\"height\\":48,\\"borderRadius\\":2,\\"marginBottom\\":0,\\"backgroundColor\\":\\"#ffffff\\",\\"opacity\\":1}},\\"children\\":[{\\"type\\":\\"Text\\",\\"props\\":{\\"style\\":[{\\"textAlign\\":\\"center\\",\\"backgroundColor\\":\\"transparent\\",\\"fontFamily\\":\\"System\\",\\"fontWeight\\":\\"500\\"},{\\"color\\":\\"#1d74f5\\",\\"fontSize\\":16},null],\\"accessibilityLabel\\":\\"More options\\"},\\"children\\":[\\"More options\\"]}]},{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":{\\"flexDirection\\":\\"row\\",\\"alignItems\\":\\"center\\",\\"marginVertical\\":24}},\\"children\\":[{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":[{\\"height\\":1,\\"flex\\":1},{\\"backgroundColor\\":\\"#e1e5e8\\"}]},\\"children\\":null},{\\"type\\":\\"Text\\",\\"props\\":{\\"style\\":[{\\"fontSize\\":14,\\"marginLeft\\":14,\\"marginRight\\":14,\\"textAlign\\":\\"left\\",\\"backgroundColor\\":\\"transparent\\",\\"fontFamily\\":\\"System\\",\\"fontWeight\\":\\"500\\"},{\\"color\\":\\"#9ca2a8\\"}]},\\"children\\":[\\"OR\\"]},{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":[{\\"height\\":1,\\"flex\\":1},{\\"backgroundColor\\":\\"#e1e5e8\\"}]},\\"children\\":null}]},{\\"type\\":\\"View\\",\\"props\\":{\\"accessible\\":true,\\"accessibilityLabel\\":\\"Less options\\",\\"focusable\\":false,\\"style\\":{\\"paddingHorizontal\\":14,\\"justifyContent\\":\\"center\\",\\"height\\":48,\\"borderRadius\\":2,\\"marginBottom\\":0,\\"backgroundColor\\":\\"#ffffff\\",\\"opacity\\":1}},\\"children\\":[{\\"type\\":\\"Text\\",\\"props\\":{\\"style\\":[{\\"textAlign\\":\\"center\\",\\"backgroundColor\\":\\"transparent\\",\\"fontFamily\\":\\"System\\",\\"fontWeight\\":\\"500\\"},{\\"color\\":\\"#1d74f5\\",\\"fontSize\\":16},null],\\"accessibilityLabel\\":\\"Less options\\"},\\"children\\":[\\"Less options\\"]}]},{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":{\\"flexDirection\\":\\"row\\",\\"alignItems\\":\\"center\\",\\"marginVertical\\":24}},\\"children\\":[{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":[{\\"height\\":1,\\"flex\\":1},{\\"backgroundColor\\":\\"#e1e5e8\\"}]},\\"children\\":null},{\\"type\\":\\"Text\\",\\"props\\":{\\"style\\":[{\\"fontSize\\":14,\\"marginLeft\\":14,\\"marginRight\\":14,\\"textAlign\\":\\"left\\",\\"backgroundColor\\":\\"transparent\\",\\"fontFamily\\":\\"System\\",\\"fontWeight\\":\\"500\\"},{\\"color\\":\\"#9ca2a8\\"}]},\\"children\\":[\\"OR\\"]},{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":[{\\"height\\":1,\\"flex\\":1},{\\"backgroundColor\\":\\"#e1e5e8\\"}]},\\"children\\":null}]}]}]}"`;

View File

@ -0,0 +1,84 @@
import React, { useState } from 'react';
import { shallowEqual } from 'react-redux';
import Animated, { Easing, useAnimatedStyle, useSharedValue, withTiming } from 'react-native-reanimated';
import { IServices } from '../../selectors/login';
import { useAppSelector } from '../../lib/hooks';
import { IItemService, IServiceList } from './interfaces';
import { SERVICES_COLLAPSED_HEIGHT, SERVICE_HEIGHT } from './styles';
import ServicesSeparator from './ServicesSeparator';
import Service from './Service';
const ServiceList = ({ services, CAS_enabled, CAS_login_url, Gitlab_URL, server }: IServiceList) => (
<>
{Object.values(services).map((service: IItemService) => (
<Service
key={service._id}
CAS_enabled={CAS_enabled}
CAS_login_url={CAS_login_url}
Gitlab_URL={Gitlab_URL}
server={server}
service={service}
/>
))}
</>
);
const LoginServices = ({ separator }: { separator: boolean }): React.ReactElement => {
const [collapsed, setCollapsed] = useState(true);
const { Gitlab_URL, CAS_enabled, CAS_login_url } = useAppSelector(
state => ({
Gitlab_URL: state.settings.API_Gitlab_URL as string,
CAS_enabled: state.settings.CAS_enabled as boolean,
CAS_login_url: state.settings.CAS_login_url as string
}),
shallowEqual
);
const server = useAppSelector(state => state.server.server);
const services = useAppSelector(state => state.login.services as IServices, shallowEqual);
const { length } = Object.values(services);
const heightButtons = useSharedValue(SERVICES_COLLAPSED_HEIGHT);
const animatedStyle = useAnimatedStyle(() => ({
overflow: 'hidden',
height: withTiming(heightButtons.value, { duration: 300, easing: Easing.inOut(Easing.quad) })
}));
const onPressButtonSeparator = () => {
heightButtons.value = collapsed ? SERVICE_HEIGHT * length : SERVICES_COLLAPSED_HEIGHT;
setCollapsed(prevState => !prevState);
};
if (length > 3 && separator) {
return (
<>
<Animated.View style={animatedStyle}>
<ServiceList
services={services}
CAS_enabled={CAS_enabled}
CAS_login_url={CAS_login_url}
Gitlab_URL={Gitlab_URL}
server={server}
/>
</Animated.View>
<ServicesSeparator services={services} separator={separator} collapsed={collapsed} onPress={onPressButtonSeparator} />
</>
);
}
return (
<>
<ServiceList
services={services}
CAS_enabled={CAS_enabled}
CAS_login_url={CAS_login_url}
Gitlab_URL={Gitlab_URL}
server={server}
/>
<ServicesSeparator services={services} separator={separator} collapsed={collapsed} onPress={onPressButtonSeparator} />
</>
);
};
export default LoginServices;

View File

@ -0,0 +1,69 @@
import { ReactElement } from 'react';
import { IServices } from '../../selectors/login';
import { TIconsName } from '../CustomIcon';
type TAuthType = 'oauth' | 'oauth_custom' | 'saml' | 'cas' | 'apple';
type TServiceName = 'facebook' | 'github' | 'gitlab' | 'google' | 'linkedin' | 'meteor-developer' | 'twitter' | 'wordpress';
export interface IOpenOAuth {
url: string;
ssoToken?: string;
authType?: TAuthType;
}
export interface IItemService {
_id: string;
name: TServiceName;
service: string;
authType: TAuthType;
buttonColor: string;
buttonLabelColor: string;
clientConfig: { provider: string };
serverURL: string;
authorizePath: string;
clientId: string;
scope: string;
}
export interface IServiceLogin {
service: IItemService;
server: string;
urlOption?: string;
}
export interface IOauthProvider {
[key: string]: ({ service, server }: IServiceLogin) => void;
facebook: ({ service, server }: IServiceLogin) => void;
github: ({ service, server }: IServiceLogin) => void;
gitlab: ({ service, server }: IServiceLogin) => void;
google: ({ service, server }: IServiceLogin) => void;
linkedin: ({ service, server }: IServiceLogin) => void;
'meteor-developer': ({ service, server }: IServiceLogin) => void;
twitter: ({ service, server }: IServiceLogin) => void;
wordpress: ({ service, server }: IServiceLogin) => void;
}
export interface IServiceList {
services: IServices;
CAS_enabled: boolean;
CAS_login_url: string;
Gitlab_URL: string;
server: string;
}
export interface IServicesSeparator {
services: IServices;
separator: boolean;
collapsed: boolean;
onPress(): void;
}
export interface IButtonService {
name: string;
authType: TAuthType;
onPress: () => void;
backgroundColor: string;
buttonText: ReactElement;
icon: TIconsName;
}

View File

@ -0,0 +1,159 @@
import * as AppleAuthentication from 'expo-apple-authentication';
import { Linking } from 'react-native';
import { Base64 } from 'js-base64';
import { Services } from '../../lib/services';
import Navigation from '../../lib/navigation/appNavigation';
import { IItemService, IOpenOAuth, IServiceLogin } from './interfaces';
import { random } from '../../lib/methods/helpers';
import { events, logEvent } from '../../lib/methods/helpers/log';
type TLoginStyle = 'popup' | 'redirect';
export const onPressFacebook = ({ service, server }: IServiceLogin) => {
logEvent(events.ENTER_WITH_FACEBOOK);
const { clientId } = service;
const endpoint = 'https://m.facebook.com/v2.9/dialog/oauth';
const redirect_uri = `${server}/_oauth/facebook?close`;
const scope = 'email';
const state = getOAuthState();
const params = `?client_id=${clientId}&redirect_uri=${redirect_uri}&scope=${scope}&state=${state}&display=touch`;
openOAuth({ url: `${endpoint}${params}` });
};
export const onPressGithub = ({ service, server }: IServiceLogin) => {
logEvent(events.ENTER_WITH_GITHUB);
const { clientId } = service;
const endpoint = `https://github.com/login?client_id=${clientId}&return_to=${encodeURIComponent('/login/oauth/authorize')}`;
const redirect_uri = `${server}/_oauth/github?close`;
const scope = 'user:email';
const state = getOAuthState();
const params = `?client_id=${clientId}&redirect_uri=${redirect_uri}&scope=${scope}&state=${state}`;
openOAuth({ url: `${endpoint}${encodeURIComponent(params)}` });
};
export const onPressGitlab = ({ service, server, urlOption }: IServiceLogin) => {
logEvent(events.ENTER_WITH_GITLAB);
const { clientId } = service;
const baseURL = urlOption ? urlOption.trim().replace(/\/*$/, '') : 'https://gitlab.com';
const endpoint = `${baseURL}/oauth/authorize`;
const redirect_uri = `${server}/_oauth/gitlab?close`;
const scope = 'read_user';
const state = getOAuthState();
const params = `?client_id=${clientId}&redirect_uri=${redirect_uri}&scope=${scope}&state=${state}&response_type=code`;
openOAuth({ url: `${endpoint}${params}` });
};
export const onPressGoogle = ({ service, server }: IServiceLogin) => {
logEvent(events.ENTER_WITH_GOOGLE);
const { clientId } = service;
const endpoint = 'https://accounts.google.com/o/oauth2/auth';
const redirect_uri = `${server}/_oauth/google?close`;
const scope = 'email';
const state = getOAuthState('redirect');
const params = `?client_id=${clientId}&redirect_uri=${redirect_uri}&scope=${scope}&state=${state}&response_type=code`;
Linking.openURL(`${endpoint}${params}`);
};
export const onPressLinkedin = ({ service, server }: IServiceLogin) => {
logEvent(events.ENTER_WITH_LINKEDIN);
const { clientId } = service;
const endpoint = 'https://www.linkedin.com/oauth/v2/authorization';
const redirect_uri = `${server}/_oauth/linkedin?close`;
const scope = 'r_liteprofile,r_emailaddress';
const state = getOAuthState();
const params = `?client_id=${clientId}&redirect_uri=${redirect_uri}&scope=${scope}&state=${state}&response_type=code`;
openOAuth({ url: `${endpoint}${params}` });
};
export const onPressMeteor = ({ service, server }: IServiceLogin) => {
logEvent(events.ENTER_WITH_METEOR);
const { clientId } = service;
const endpoint = 'https://www.meteor.com/oauth2/authorize';
const redirect_uri = `${server}/_oauth/meteor-developer`;
const state = getOAuthState();
const params = `?client_id=${clientId}&redirect_uri=${redirect_uri}&state=${state}&response_type=code`;
openOAuth({ url: `${endpoint}${params}` });
};
export const onPressTwitter = ({ server }: IServiceLogin) => {
logEvent(events.ENTER_WITH_TWITTER);
const state = getOAuthState();
const url = `${server}/_oauth/twitter/?requestTokenAndRedirect=true&state=${state}`;
openOAuth({ url });
};
export const onPressWordpress = ({ service, server }: IServiceLogin) => {
logEvent(events.ENTER_WITH_WORDPRESS);
const { clientId, serverURL } = service;
const endpoint = `${serverURL}/oauth/authorize`;
const redirect_uri = `${server}/_oauth/wordpress?close`;
const scope = 'openid';
const state = getOAuthState();
const params = `?client_id=${clientId}&redirect_uri=${redirect_uri}&scope=${scope}&state=${state}&response_type=code`;
openOAuth({ url: `${endpoint}${params}` });
};
export const onPressCustomOAuth = ({ loginService, server }: { loginService: IItemService; server: string }) => {
logEvent(events.ENTER_WITH_CUSTOM_OAUTH);
const { serverURL, authorizePath, clientId, scope, service } = loginService;
const redirectUri = `${server}/_oauth/${service}`;
const state = getOAuthState();
const params = `?client_id=${clientId}&redirect_uri=${redirectUri}&response_type=code&state=${state}&scope=${scope}`;
const domain = `${serverURL}`;
const absolutePath = `${authorizePath}${params}`;
const url = absolutePath.includes(domain) ? absolutePath : domain + absolutePath;
openOAuth({ url });
};
export const onPressSaml = ({ loginService, server }: { loginService: IItemService; server: string }) => {
logEvent(events.ENTER_WITH_SAML);
const { clientConfig } = loginService;
const { provider } = clientConfig;
const ssoToken = random(17);
const url = `${server}/_saml/authorize/${provider}/${ssoToken}`;
openOAuth({ url, ssoToken, authType: 'saml' });
};
export const onPressCas = ({ casLoginUrl, server }: { casLoginUrl: string; server: string }) => {
logEvent(events.ENTER_WITH_CAS);
const ssoToken = random(17);
const url = `${casLoginUrl}?service=${server}/_cas/${ssoToken}`;
openOAuth({ url, ssoToken, authType: 'cas' });
};
export const onPressAppleLogin = async () => {
logEvent(events.ENTER_WITH_APPLE);
try {
const { fullName, email, identityToken } = await AppleAuthentication.signInAsync({
requestedScopes: [
AppleAuthentication.AppleAuthenticationScope.FULL_NAME,
AppleAuthentication.AppleAuthenticationScope.EMAIL
]
});
await Services.loginOAuthOrSso({ fullName, email, identityToken });
} catch {
logEvent(events.ENTER_WITH_APPLE_F);
}
};
const getOAuthState = (loginStyle: TLoginStyle = 'popup') => {
const credentialToken = random(43);
let obj: {
loginStyle: string;
credentialToken: string;
isCordova: boolean;
redirectUrl?: string;
} = { loginStyle, credentialToken, isCordova: true };
if (loginStyle === 'redirect') {
obj = {
...obj,
redirectUrl: 'rocketchat://auth'
};
}
return Base64.encodeURI(JSON.stringify(obj));
};
const openOAuth = ({ url, ssoToken, authType = 'oauth' }: IOpenOAuth) => {
Navigation.navigate('AuthenticationWebView', { url, authType, ssoToken });
};

View File

@ -0,0 +1,41 @@
import { StyleSheet } from 'react-native';
import sharedStyles from '../../views/Styles';
export const BUTTON_HEIGHT = 48;
export const SERVICE_HEIGHT = 58;
export const BORDER_RADIUS = 2;
export const SERVICES_COLLAPSED_HEIGHT = 174;
export default StyleSheet.create({
serviceButton: {
borderRadius: BORDER_RADIUS,
marginBottom: 10
},
serviceButtonContainer: {
borderRadius: BORDER_RADIUS,
width: '100%',
height: BUTTON_HEIGHT,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
paddingHorizontal: 15
},
serviceIcon: {
position: 'absolute',
left: 15,
top: 12,
width: 24,
height: 24
},
serviceText: {
...sharedStyles.textRegular,
fontSize: 16
},
serviceName: {
...sharedStyles.textSemibold
},
options: {
marginBottom: 0
}
});

View File

@ -4,7 +4,7 @@ import { FlatList, StyleSheet, Text, View } from 'react-native';
import { TSupportedThemes, useTheme } from '../../theme';
import { themes } from '../../lib/constants';
import { CustomIcon } from '../CustomIcon';
import shortnameToUnicode from '../../utils/shortnameToUnicode';
import shortnameToUnicode from '../../lib/methods/helpers/shortnameToUnicode';
import CustomEmoji from '../EmojiPicker/CustomEmoji';
import database from '../../lib/database';
import { Button } from '../ActionSheet';

View File

@ -6,17 +6,18 @@ import moment from 'moment';
import database from '../../lib/database';
import I18n from '../../i18n';
import log, { logEvent } from '../../utils/log';
import log, { logEvent } from '../../lib/methods/helpers/log';
import Navigation from '../../lib/navigation/appNavigation';
import { getMessageTranslation } from '../message/utils';
import { LISTENER } from '../Toast';
import EventEmitter from '../../utils/events';
import { showConfirmationAlert } from '../../utils/info';
import EventEmitter from '../../lib/methods/helpers/events';
import { showConfirmationAlert } from '../../lib/methods/helpers/info';
import { TActionSheetOptionsItem, useActionSheet } from '../ActionSheet';
import Header, { HEADER_HEIGHT, IHeader } from './Header';
import events from '../../utils/log/events';
import events from '../../lib/methods/helpers/log/events';
import { IApplicationState, ILoggedUser, TAnyMessageModel, TSubscriptionModel } from '../../definitions';
import { getPermalinkMessage, hasPermission } from '../../lib/methods';
import { getPermalinkMessage } from '../../lib/methods';
import { hasPermission } from '../../lib/methods/helpers';
import { Services } from '../../lib/services';
export interface IMessageActionsProps {

View File

@ -1,4 +1,4 @@
import FastImage from '@rocket.chat/react-native-fast-image';
import FastImage from 'react-native-fast-image';
import React, { useContext, useState } from 'react';
import { TouchableOpacity } from 'react-native';

View File

@ -2,7 +2,7 @@ import React, { useContext } from 'react';
import { Text } from 'react-native';
import { IEmoji } from '../../../definitions/IEmoji';
import shortnameToUnicode from '../../../utils/shortnameToUnicode';
import shortnameToUnicode from '../../../lib/methods/helpers/shortnameToUnicode';
import CustomEmoji from '../../EmojiPicker/CustomEmoji';
import MessageboxContext from '../Context';
import styles from '../styles';

View File

@ -9,7 +9,7 @@ import styles from './styles';
import I18n from '../../i18n';
import { themes } from '../../lib/constants';
import { CustomIcon } from '../CustomIcon';
import { events, logEvent } from '../../utils/log';
import { events, logEvent } from '../../lib/methods/helpers/log';
import { TSupportedThemes } from '../../theme';
interface IMessageBoxRecordAudioProps {

View File

@ -1,6 +1,6 @@
import { ImageOrVideo } from 'react-native-image-crop-picker';
import { isIOS } from '../../utils/deviceInfo';
import { isIOS } from '../../lib/methods/helpers';
const regex = new RegExp(/\.[^/.]+$/); // Check from last '.' of the string

View File

@ -9,16 +9,15 @@ import { Q } from '@nozbe/watermelondb';
import { TouchableWithoutFeedback } from 'react-native-gesture-handler';
import { generateTriggerId } from '../../lib/methods/actions';
import TextInput, { IThemedTextInput } from '../TextInput';
import { TextInput, IThemedTextInput } from '../TextInput';
import { userTyping as userTypingAction } from '../../actions/room';
import styles from './styles';
import database from '../../lib/database';
import { emojis } from '../EmojiPicker/emojis';
import log, { events, logEvent } from '../../utils/log';
import log, { events, logEvent } from '../../lib/methods/helpers/log';
import RecordAudio from './RecordAudio';
import I18n from '../../i18n';
import ReplyPreview from './ReplyPreview';
import debounce from '../../utils/debounce';
import { themes } from '../../lib/constants';
// @ts-ignore
// eslint-disable-next-line import/extensions,import/no-unresolved
@ -26,9 +25,8 @@ import LeftButtons from './LeftButtons';
// @ts-ignore
// eslint-disable-next-line import/extensions,import/no-unresolved
import RightButtons from './RightButtons';
import { isAndroid, isTablet } from '../../utils/deviceInfo';
import { canUploadFile } from '../../utils/media';
import EventEmiter from '../../utils/events';
import { canUploadFile } from '../../lib/methods/helpers/media';
import EventEmiter from '../../lib/methods/helpers/events';
import { KEY_COMMAND, handleCommandShowUpload, handleCommandSubmit, handleCommandTyping } from '../../commands';
import getMentionRegexp from './getMentionRegexp';
import Mentions from './Mentions';
@ -47,13 +45,23 @@ import Navigation from '../../lib/navigation/appNavigation';
import { withActionSheet } from '../ActionSheet';
import { sanitizeLikeString } from '../../lib/database/utils';
import { CustomIcon } from '../CustomIcon';
import { IMessage } from '../../definitions/IMessage';
import { forceJpgExtension } from './forceJpgExtension';
import { IBaseScreen, IPreviewItem, IUser, TGetCustomEmoji, TSubscriptionModel, TThreadModel } from '../../definitions';
import {
IApplicationState,
IBaseScreen,
IPreviewItem,
IUser,
TGetCustomEmoji,
TSubscriptionModel,
TThreadModel,
IMessage
} from '../../definitions';
import { MasterDetailInsideStackParamList } from '../../stacks/MasterDetailStack/types';
import { getPermalinkMessage, hasPermission, search, sendFileMessage } from '../../lib/methods';
import { getPermalinkMessage, search, sendFileMessage } from '../../lib/methods';
import { hasPermission, debounce, isAndroid, isTablet } from '../../lib/methods/helpers';
import { Services } from '../../lib/services';
import { TSupportedThemes } from '../../theme';
import { ChatsStackParamList } from '../../stacks/types';
if (isAndroid) {
require('./EmojiKeyboard');
@ -77,7 +85,7 @@ const videoPickerConfig: Options = {
mediaType: 'video'
};
export interface IMessageBoxProps extends IBaseScreen<MasterDetailInsideStackParamList, any> {
export interface IMessageBoxProps extends IBaseScreen<ChatsStackParamList & MasterDetailInsideStackParamList, any> {
rid: string;
baseUrl: string;
message: IMessage;
@ -109,6 +117,7 @@ export interface IMessageBoxProps extends IBaseScreen<MasterDetailInsideStackPar
usedCannedResponse: string;
uploadFilePermission: string[];
serverVersion: string;
goToCannedResponses: () => void | null;
}
interface IMessageBoxState {
@ -307,7 +316,17 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
permissionToUpload
} = this.state;
const { roomType, replying, editing, isFocused, message, theme, usedCannedResponse, uploadFilePermission } = this.props;
const {
roomType,
replying,
editing,
isFocused,
message,
theme,
usedCannedResponse,
uploadFilePermission,
goToCannedResponses
} = this.props;
if (nextProps.theme !== theme) {
return true;
}
@ -359,12 +378,15 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
if (nextProps.usedCannedResponse !== usedCannedResponse) {
return true;
}
if (nextProps.goToCannedResponses !== goToCannedResponses) {
return true;
}
return false;
}
componentDidUpdate(prevProps: IMessageBoxProps) {
const { uploadFilePermission } = this.props;
if (!dequal(prevProps.uploadFilePermission, uploadFilePermission)) {
const { uploadFilePermission, goToCannedResponses } = this.props;
if (!dequal(prevProps.uploadFilePermission, uploadFilePermission) || prevProps.goToCannedResponses !== goToCannedResponses) {
this.setOptions();
}
}
@ -785,9 +807,16 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
showMessageBoxActions = () => {
logEvent(events.ROOM_SHOW_BOX_ACTIONS);
const { permissionToUpload } = this.state;
const { showActionSheet } = this.props;
const { showActionSheet, goToCannedResponses } = this.props;
const options = [];
if (goToCannedResponses) {
options.push({
title: I18n.t('Canned_Responses'),
icon: 'canned-response',
onPress: () => goToCannedResponses()
});
}
if (permissionToUpload) {
options.push(
{
@ -1106,7 +1135,6 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
defaultValue=''
multiline
testID={`messagebox-input${tmid ? '-thread' : ''}`}
theme={theme}
{...isAndroidTablet}
/>
<RightButtons
@ -1172,7 +1200,7 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
}
}
const mapStateToProps = (state: any) => ({
const mapStateToProps = (state: IApplicationState) => ({
isMasterDetail: state.app.isMasterDetail,
baseUrl: state.server.server,
threadsEnabled: state.settings.Threads_enabled,

View File

@ -1,6 +1,6 @@
import { StyleSheet } from 'react-native';
import { isIOS } from '../../utils/deviceInfo';
import { isIOS } from '../../lib/methods/helpers';
import sharedStyles from '../../views/Styles';
const MENTION_HEIGHT = 50;

View File

@ -5,7 +5,7 @@ import database from '../lib/database';
import protectedFunction from '../lib/methods/helpers/protectedFunction';
import { useActionSheet } from './ActionSheet';
import I18n from '../i18n';
import log from '../utils/log';
import log from '../lib/methods/helpers/log';
import { TMessageModel } from '../definitions';
import { resendMessage } from '../lib/methods';

View File

@ -3,7 +3,7 @@ import { Text } from 'react-native';
import styles from './styles';
import { themes } from '../../../lib/constants';
import Touch from '../../../utils/touch';
import Touch from '../../../lib/methods/helpers/touch';
import { CustomIcon, TIconsName } from '../../CustomIcon';
import { useTheme } from '../../../theme';

View File

@ -2,7 +2,7 @@ import React, { useEffect, useState } from 'react';
import { Grid } from 'react-native-easy-grid';
import { themes } from '../../../lib/constants';
import { resetAttempts } from '../../../utils/localAuthentication';
import { resetAttempts } from '../../../lib/methods/helpers/localAuthentication';
import { TYPE } from '../constants';
import { getDiff, getLockedUntil } from '../utils';
import I18n from '../../../i18n';

View File

@ -8,7 +8,7 @@ import Base, { IBase } from './Base';
import Locked from './Base/Locked';
import { TYPE } from './constants';
import { ATTEMPTS_KEY, LOCKED_OUT_TIMER_KEY, MAX_ATTEMPTS, PASSCODE_KEY } from '../../lib/constants';
import { biometryAuth, resetAttempts } from '../../utils/localAuthentication';
import { biometryAuth, resetAttempts } from '../../lib/methods/helpers/localAuthentication';
import { getDiff, getLockedUntil } from './utils';
import { useUserPreferences } from '../../lib/methods/userPreferences';
import I18n from '../../i18n';

View File

@ -1,24 +1,33 @@
/* eslint-disable import/no-extraneous-dependencies, import/no-unresolved, import/extensions, react/prop-types, react/destructuring-assignment */
import React from 'react';
import { Dimensions, View } from 'react-native';
import { Dimensions, SafeAreaView } from 'react-native';
import { storiesOf } from '@storybook/react-native';
import { Provider } from 'react-redux';
import { SafeAreaProvider } from 'react-native-safe-area-context';
import { Header, HeaderBackground } from '@react-navigation/elements';
import Header from '../Header';
import { longText } from '../../../storybook/utils';
import { ThemeContext } from '../../theme';
import { store } from '../../../storybook/stories';
import { colors, themes } from '../../lib/constants';
import RoomHeaderComponent from './RoomHeader';
const stories = storiesOf('RoomHeader', module).addDecorator(story => <Provider store={store}>{story()}</Provider>);
// TODO: refactor after react-navigation v6
const HeaderExample = ({ title }) => (
<Header headerTitle={() => <View style={{ flex: 1, paddingHorizontal: 12 }}>{title()}</View>} />
);
const stories = storiesOf('RoomHeader', module)
.addDecorator(story => <Provider store={store}>{story()}</Provider>)
.addDecorator(story => <SafeAreaProvider>{story()}</SafeAreaProvider>);
const { width, height } = Dimensions.get('window');
const HeaderExample = ({ title, theme = 'light' }) => (
<SafeAreaView>
<Header
title=''
headerTitle={title}
headerTitleAlign='left'
headerBackground={() => <HeaderBackground style={{ backgroundColor: themes[theme].headerBackground }} />}
/>
</SafeAreaView>
);
const RoomHeader = ({ ...props }) => (
<RoomHeaderComponent
width={width}
@ -27,6 +36,8 @@ const RoomHeader = ({ ...props }) => (
type='p'
testID={props.title}
onPress={() => alert('header pressed!')}
status={props.status}
usersTyping={props.usersTyping}
{...props}
/>
);
@ -82,8 +93,8 @@ stories.add('thread', () => (
));
const ThemeStory = ({ theme }) => (
<ThemeContext.Provider value={{ theme }}>
<HeaderExample title={() => <RoomHeader subtitle='subtitle' />} />
<ThemeContext.Provider value={{ theme, colors: colors[theme] }}>
<HeaderExample title={() => <RoomHeader subtitle='subtitle' />} theme={theme} />
</ThemeContext.Provider>
);

View File

@ -3,7 +3,6 @@ import { StyleSheet, Text, TouchableOpacity, View } from 'react-native';
import I18n from '../../i18n';
import sharedStyles from '../../views/Styles';
import { themes } from '../../lib/constants';
import { MarkdownPreview } from '../markdown';
import RoomTypeIcon from '../RoomTypeIcon';
import { TUserStatus, IOmnichannelSource } from '../../definitions';
@ -44,39 +43,39 @@ const styles = StyleSheet.create({
type TRoomHeaderSubTitle = {
usersTyping: [];
subtitle: string;
subtitle?: string;
renderFunc?: () => React.ReactElement;
scale: number;
};
type TRoomHeaderHeaderTitle = {
title: string;
tmid: string;
prid: string;
title?: string;
tmid?: string;
prid?: string;
scale: number;
testID: string;
testID?: string;
};
interface IRoomHeader {
title: string;
subtitle: string;
title?: string;
subtitle?: string;
type: string;
width: number;
height: number;
prid: string;
tmid: string;
teamMain: boolean;
prid?: string;
tmid?: string;
teamMain?: boolean;
status: TUserStatus;
usersTyping: [];
isGroupChat: boolean;
parentTitle: string;
onPress: () => void;
testID: string;
isGroupChat?: boolean;
parentTitle?: string;
onPress: Function;
testID?: string;
sourceType?: IOmnichannelSource;
}
const SubTitle = React.memo(({ usersTyping, subtitle, renderFunc, scale }: TRoomHeaderSubTitle) => {
const { theme } = useTheme();
const { colors } = useTheme();
const fontSize = getSubTitleSize(scale);
// typing
if (usersTyping.length) {
@ -87,7 +86,7 @@ const SubTitle = React.memo(({ usersTyping, subtitle, renderFunc, scale }: TRoom
usersText = usersTyping.join(', ');
}
return (
<Text style={[styles.subtitle, { fontSize, color: themes[theme].auxiliaryText }]} numberOfLines={1}>
<Text style={[styles.subtitle, { fontSize, color: colors.auxiliaryText }]} numberOfLines={1}>
<Text style={styles.typingUsers}>{usersText} </Text>
{usersTyping.length > 1 ? I18n.t('are_typing') : I18n.t('is_typing')}...
</Text>
@ -101,15 +100,15 @@ const SubTitle = React.memo(({ usersTyping, subtitle, renderFunc, scale }: TRoom
// subtitle
if (subtitle) {
return <MarkdownPreview msg={subtitle} style={[styles.subtitle, { fontSize, color: themes[theme].auxiliaryText }]} />;
return <MarkdownPreview msg={subtitle} style={[styles.subtitle, { fontSize, color: colors.auxiliaryText }]} />;
}
return null;
});
const HeaderTitle = React.memo(({ title, tmid, prid, scale, testID }: TRoomHeaderHeaderTitle) => {
const { theme } = useTheme();
const titleStyle = { fontSize: TITLE_SIZE * scale, color: themes[theme].headerTitleColor };
const { colors } = useTheme();
const titleStyle = { fontSize: TITLE_SIZE * scale, color: colors.headerTitleColor };
if (!tmid && !prid) {
return (
<Text style={[styles.title, titleStyle]} numberOfLines={1} testID={testID}>
@ -139,7 +138,7 @@ const Header = React.memo(
usersTyping = [],
sourceType
}: IRoomHeader) => {
const { theme } = useTheme();
const { colors } = useTheme();
const portrait = height > width;
let scale = 1;
@ -154,7 +153,7 @@ const Header = React.memo(
renderFunc = () => (
<View style={styles.titleContainer}>
<RoomTypeIcon type={prid ? 'discussion' : type} isGroupChat={isGroupChat} status={status} teamMain={teamMain} />
<Text style={[styles.subtitle, { color: themes[theme].auxiliaryText }]} numberOfLines={1}>
<Text style={[styles.subtitle, { color: colors.auxiliaryText }]} numberOfLines={1}>
{parentTitle}
</Text>
</View>

File diff suppressed because one or more lines are too long

View File

@ -1,117 +1,56 @@
import { dequal } from 'dequal';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import React from 'react';
import { shallowEqual, useSelector } from 'react-redux';
import { IApplicationState, TUserStatus, IOmnichannelSource } from '../../definitions';
import { withDimensions } from '../../dimensions';
import { IApplicationState, TUserStatus, IOmnichannelSource, IVisitor } from '../../definitions';
import { useDimensions } from '../../dimensions';
import I18n from '../../i18n';
import RoomHeader from './RoomHeader';
interface IRoomHeaderContainerProps {
title: string;
subtitle: string;
title?: string;
subtitle?: string;
type: string;
prid: string;
tmid: string;
teamMain: boolean;
usersTyping: [];
status: TUserStatus;
statusText: string;
connecting: boolean;
connected: boolean;
roomUserId: string;
widthOffset: number;
onPress(): void;
width: number;
height: number;
parentTitle: string;
isGroupChat: boolean;
testID: string;
prid?: string;
tmid?: string;
teamMain?: boolean;
roomUserId?: string | null;
onPress: Function;
parentTitle?: string;
isGroupChat?: boolean;
testID?: string;
sourceType?: IOmnichannelSource;
visitor?: IVisitor;
}
class RoomHeaderContainer extends Component<IRoomHeaderContainerProps, any> {
shouldComponentUpdate(nextProps: IRoomHeaderContainerProps) {
const {
type,
title,
subtitle,
status,
statusText,
connecting,
connected,
onPress,
usersTyping,
width,
height,
teamMain,
sourceType
} = this.props;
if (nextProps.type !== type) {
return true;
}
if (nextProps.title !== title) {
return true;
}
if (nextProps.subtitle !== subtitle) {
return true;
}
if (nextProps.status !== status) {
return true;
}
if (nextProps.statusText !== statusText) {
return true;
}
if (nextProps.connecting !== connecting) {
return true;
}
if (nextProps.connected !== connected) {
return true;
}
if (nextProps.width !== width) {
return true;
}
if (nextProps.height !== height) {
return true;
}
if (!dequal(nextProps.usersTyping, usersTyping)) {
return true;
}
if (!dequal(nextProps.sourceType, sourceType)) {
return true;
}
if (nextProps.onPress !== onPress) {
return true;
}
if (nextProps.teamMain !== teamMain) {
return true;
}
return false;
}
const RoomHeaderContainer = React.memo(
({
isGroupChat,
onPress,
parentTitle,
prid,
roomUserId,
subtitle: subtitleProp,
teamMain,
testID,
title,
tmid,
type,
sourceType,
visitor
}: IRoomHeaderContainerProps) => {
let subtitle: string | undefined;
let status: TUserStatus = 'offline';
let statusText: string | undefined;
const { width, height } = useDimensions();
render() {
const {
title,
subtitle: subtitleProp,
type,
teamMain,
prid,
tmid,
status = 'offline',
statusText,
connecting,
connected,
usersTyping,
onPress,
width,
height,
parentTitle,
isGroupChat,
testID,
sourceType
} = this.props;
const connecting = useSelector((state: IApplicationState) => state.meteor.connecting || state.server.loading);
const usersTyping = useSelector((state: IApplicationState) => state.usersTyping, shallowEqual);
const connected = useSelector((state: IApplicationState) => state.meteor.connected);
const activeUser = useSelector(
(state: IApplicationState) => (roomUserId ? state.activeUsers?.[roomUserId] : undefined),
shallowEqual
);
let subtitle;
if (connecting) {
subtitle = I18n.t('Connecting');
} else if (!connected) {
@ -120,6 +59,17 @@ class RoomHeaderContainer extends Component<IRoomHeaderContainerProps, any> {
subtitle = subtitleProp;
}
if (connected) {
if ((type === 'd' || (tmid && roomUserId)) && activeUser) {
const { status: statusActiveUser, statusText: statusTextActiveUser } = activeUser;
status = statusActiveUser;
statusText = statusTextActiveUser;
} else if (type === 'l' && visitor?.status) {
const { status: statusVisitor } = visitor;
status = statusVisitor;
}
}
return (
<RoomHeader
prid={prid}
@ -140,28 +90,6 @@ class RoomHeaderContainer extends Component<IRoomHeaderContainerProps, any> {
/>
);
}
}
);
const mapStateToProps = (state: IApplicationState, ownProps: any) => {
let statusText = '';
let status = 'offline';
const { roomUserId, type, visitor = {}, tmid } = ownProps;
if (state.meteor.connected) {
if ((type === 'd' || (tmid && roomUserId)) && state.activeUsers[roomUserId]) {
({ status, statusText } = state.activeUsers[roomUserId]);
} else if (type === 'l' && visitor?.status) {
({ status } = visitor);
}
}
return {
connecting: state.meteor.connecting || state.server.loading,
connected: state.meteor.connected,
usersTyping: state.usersTyping,
status: status as TUserStatus,
statusText
};
};
export default connect(mapStateToProps)(withDimensions(RoomHeaderContainer));
export default RoomHeaderContainer;

View File

@ -1,25 +1,32 @@
import React from 'react';
import { Animated, View } from 'react-native';
import { View } from 'react-native';
import Animated, {
useAnimatedStyle,
interpolate,
withSpring,
runOnJS,
useAnimatedReaction,
useSharedValue
} from 'react-native-reanimated';
import { RectButton } from 'react-native-gesture-handler';
import * as Haptics from 'expo-haptics';
import { isRTL } from '../../i18n';
import { CustomIcon } from '../CustomIcon';
import { DisplayMode, themes } from '../../lib/constants';
import { DisplayMode } from '../../lib/constants';
import styles, { ACTION_WIDTH, LONG_SWIPE, ROW_HEIGHT_CONDENSED } from './styles';
import { ILeftActionsProps, IRightActionsProps } from './interfaces';
import { useTheme } from '../../theme';
import I18n from '../../i18n';
const reverse = new Animated.Value(isRTL() ? -1 : 1);
const CONDENSED_ICON_SIZE = 24;
const EXPANDED_ICON_SIZE = 28;
export const LeftActions = React.memo(({ theme, transX, isRead, width, onToggleReadPress, displayMode }: ILeftActionsProps) => {
const translateX = Animated.multiply(
transX.interpolate({
inputRange: [0, ACTION_WIDTH],
outputRange: [-ACTION_WIDTH, 0]
}),
reverse
);
export const LeftActions = React.memo(({ transX, isRead, width, onToggleReadPress, displayMode }: ILeftActionsProps) => {
const { colors } = useTheme();
const animatedStyles = useAnimatedStyle(() => ({
transform: [{ translateX: transX.value }]
}));
const isCondensed = displayMode === DisplayMode.Condensed;
const viewHeight = isCondensed ? { height: ROW_HEIGHT_CONDENSED } : null;
@ -29,20 +36,16 @@ export const LeftActions = React.memo(({ theme, transX, isRead, width, onToggleR
<Animated.View
style={[
styles.actionLeftButtonContainer,
{
right: width - ACTION_WIDTH,
width,
transform: [{ translateX }],
backgroundColor: themes[theme].tintColor
},
viewHeight
{ width: width * 2, backgroundColor: colors.tintColor, right: '100%' },
viewHeight,
animatedStyles
]}>
<View style={[styles.actionLeftButtonContainer, viewHeight]}>
<RectButton style={styles.actionButton} onPress={onToggleReadPress}>
<CustomIcon
size={isCondensed ? CONDENSED_ICON_SIZE : EXPANDED_ICON_SIZE}
name={isRead ? 'flag' : 'check'}
color={themes[theme].buttonText}
color={colors.buttonText}
/>
</RectButton>
</View>
@ -51,64 +54,102 @@ export const LeftActions = React.memo(({ theme, transX, isRead, width, onToggleR
);
});
export const RightActions = React.memo(
({ transX, favorite, width, toggleFav, onHidePress, theme, displayMode }: IRightActionsProps) => {
const translateXFav = Animated.multiply(
transX.interpolate({
inputRange: [-width / 2, -ACTION_WIDTH * 2, 0],
outputRange: [width / 2, width - ACTION_WIDTH * 2, width]
}),
reverse
);
const translateXHide = Animated.multiply(
transX.interpolate({
inputRange: [-width, -LONG_SWIPE, -ACTION_WIDTH * 2, 0],
outputRange: [0, width - LONG_SWIPE, width - ACTION_WIDTH, width]
}),
reverse
);
export const RightActions = React.memo(({ transX, favorite, width, toggleFav, onHidePress, displayMode }: IRightActionsProps) => {
const { colors } = useTheme();
const isCondensed = displayMode === DisplayMode.Condensed;
const viewHeight = isCondensed ? { height: ROW_HEIGHT_CONDENSED } : null;
const animatedFavStyles = useAnimatedStyle(() => ({ transform: [{ translateX: transX.value }] }));
return (
<View style={[styles.actionsLeftContainer, viewHeight]} pointerEvents='box-none'>
<Animated.View
style={[
styles.actionRightButtonContainer,
{
width,
transform: [{ translateX: translateXFav }],
backgroundColor: themes[theme].hideBackground
},
viewHeight
]}>
<RectButton style={[styles.actionButton, { backgroundColor: themes[theme].favoriteBackground }]} onPress={toggleFav}>
<CustomIcon
size={isCondensed ? CONDENSED_ICON_SIZE : EXPANDED_ICON_SIZE}
name={favorite ? 'star-filled' : 'star'}
color={themes[theme].buttonText}
/>
</RectButton>
</Animated.View>
<Animated.View
style={[
styles.actionRightButtonContainer,
{
width,
transform: [{ translateX: translateXHide }]
},
isCondensed && { height: ROW_HEIGHT_CONDENSED }
]}>
<RectButton style={[styles.actionButton, { backgroundColor: themes[theme].hideBackground }]} onPress={onHidePress}>
<CustomIcon
size={isCondensed ? CONDENSED_ICON_SIZE : EXPANDED_ICON_SIZE}
name='unread-on-top-disabled'
color={themes[theme].buttonText}
/>
</RectButton>
</Animated.View>
</View>
);
}
);
const translateXHide = useSharedValue(0);
const triggerHideAnimation = (toValue: number) => {
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
translateXHide.value = withSpring(toValue, { overshootClamping: true, mass: 0.7 });
};
useAnimatedReaction(
() => transX.value,
(currentTransX, previousTransX) => {
// Triggers the animation and hapticFeedback if swipe reaches/unreaches the threshold.
if (I18n.isRTL) {
if (previousTransX && currentTransX > LONG_SWIPE && previousTransX <= LONG_SWIPE) {
runOnJS(triggerHideAnimation)(ACTION_WIDTH);
} else if (previousTransX && currentTransX <= LONG_SWIPE && previousTransX > LONG_SWIPE) {
runOnJS(triggerHideAnimation)(0);
}
} else if (previousTransX && currentTransX < -LONG_SWIPE && previousTransX >= -LONG_SWIPE) {
runOnJS(triggerHideAnimation)(-ACTION_WIDTH);
} else if (previousTransX && currentTransX >= -LONG_SWIPE && previousTransX < -LONG_SWIPE) {
runOnJS(triggerHideAnimation)(0);
}
}
);
const animatedHideStyles = useAnimatedStyle(() => {
if (I18n.isRTL) {
if (transX.value < LONG_SWIPE && transX.value >= 2 * ACTION_WIDTH) {
const parallaxSwipe = interpolate(
transX.value,
[2 * ACTION_WIDTH, LONG_SWIPE],
[ACTION_WIDTH, ACTION_WIDTH + 0.1 * transX.value]
);
return { transform: [{ translateX: parallaxSwipe + translateXHide.value }] };
}
return { transform: [{ translateX: transX.value - ACTION_WIDTH + translateXHide.value }] };
}
if (transX.value > -LONG_SWIPE && transX.value <= -2 * ACTION_WIDTH) {
const parallaxSwipe = interpolate(
transX.value,
[-2 * ACTION_WIDTH, -LONG_SWIPE],
[-ACTION_WIDTH, -ACTION_WIDTH + 0.1 * transX.value]
);
return { transform: [{ translateX: parallaxSwipe + translateXHide.value }] };
}
return { transform: [{ translateX: transX.value + ACTION_WIDTH + translateXHide.value }] };
});
const isCondensed = displayMode === DisplayMode.Condensed;
const viewHeight = isCondensed ? { height: ROW_HEIGHT_CONDENSED } : null;
return (
<View style={[styles.actionsLeftContainer, viewHeight]} pointerEvents='box-none'>
<Animated.View
style={[
styles.actionRightButtonContainer,
{
width,
backgroundColor: colors.favoriteBackground,
left: '100%'
},
viewHeight,
animatedFavStyles
]}>
<RectButton style={[styles.actionButton, { backgroundColor: colors.favoriteBackground }]} onPress={toggleFav}>
<CustomIcon
size={isCondensed ? CONDENSED_ICON_SIZE : EXPANDED_ICON_SIZE}
name={favorite ? 'star-filled' : 'star'}
color={colors.buttonText}
/>
</RectButton>
</Animated.View>
<Animated.View
style={[
styles.actionRightButtonContainer,
{
width: width * 2,
backgroundColor: colors.hideBackground,
left: '100%'
},
isCondensed && { height: ROW_HEIGHT_CONDENSED },
animatedHideStyles
]}>
<RectButton style={[styles.actionButton, { backgroundColor: colors.hideBackground }]} onPress={onHidePress}>
<CustomIcon
size={isCondensed ? CONDENSED_ICON_SIZE : EXPANDED_ICON_SIZE}
name='unread-on-top-disabled'
color={colors.buttonText}
/>
</RectButton>
</Animated.View>
</View>
);
});

View File

@ -1,11 +1,11 @@
import React from 'react';
import { View } from 'react-native';
import PropTypes from 'prop-types';
import Avatar from '../Avatar';
import { DisplayMode } from '../../lib/constants';
import TypeIcon from './TypeIcon';
import styles from './styles';
import { IIconOrAvatar } from './interfaces';
const IconOrAvatar = ({
avatar,
@ -17,10 +17,9 @@ const IconOrAvatar = ({
isGroupChat,
teamMain,
showLastMessage,
theme,
displayMode,
sourceType
}) => {
}: IIconOrAvatar): React.ReactElement | null => {
if (showAvatar) {
return (
<Avatar text={avatar} size={displayMode === DisplayMode.Condensed ? 36 : 48} type={type} style={styles.avatar} rid={rid} />
@ -35,7 +34,6 @@ const IconOrAvatar = ({
prid={prid}
status={status}
isGroupChat={isGroupChat}
theme={theme}
teamMain={teamMain}
size={24}
style={{ marginRight: 12 }}
@ -48,18 +46,4 @@ const IconOrAvatar = ({
return null;
};
IconOrAvatar.propTypes = {
avatar: PropTypes.string,
type: PropTypes.string,
theme: PropTypes.string,
rid: PropTypes.string,
showAvatar: PropTypes.bool,
displayMode: PropTypes.string,
prid: PropTypes.string,
status: PropTypes.string,
isGroupChat: PropTypes.bool,
teamMain: PropTypes.bool,
showLastMessage: PropTypes.bool
};
export default IconOrAvatar;

View File

@ -4,8 +4,9 @@ import { dequal } from 'dequal';
import I18n from '../../i18n';
import styles from './styles';
import { MarkdownPreview } from '../markdown';
import { E2E_MESSAGE_TYPE, E2E_STATUS, themes } from '../../lib/constants';
import { E2E_MESSAGE_TYPE, E2E_STATUS } from '../../lib/constants';
import { ILastMessageProps } from './interfaces';
import { useTheme } from '../../theme';
const formatMsg = ({ lastMessage, type, showLastMessage, username, useRealName }: Partial<ILastMessageProps>) => {
if (!showLastMessage) {
@ -46,8 +47,9 @@ const formatMsg = ({ lastMessage, type, showLastMessage, username, useRealName }
const arePropsEqual = (oldProps: any, newProps: any) => dequal(oldProps, newProps);
const LastMessage = React.memo(
({ lastMessage, type, showLastMessage, username, alert, useRealName, theme }: ILastMessageProps) => (
const LastMessage = React.memo(({ lastMessage, type, showLastMessage, username, alert, useRealName }: ILastMessageProps) => {
const { colors } = useTheme();
return (
<MarkdownPreview
msg={formatMsg({
lastMessage,
@ -56,12 +58,11 @@ const LastMessage = React.memo(
username,
useRealName
})}
style={[styles.markdownText, { color: alert ? themes[theme].bodyText : themes[theme].auxiliaryText }]}
style={[styles.markdownText, { color: alert ? colors.bodyText : colors.auxiliaryText }]}
numberOfLines={2}
testID='room-item-last-message'
/>
),
arePropsEqual
);
);
}, arePropsEqual);
export default LastMessage;

View File

@ -25,7 +25,6 @@ const RoomItem = ({
showLastMessage,
status = 'offline',
useRealName,
theme,
isFocused,
isGroupChat,
isRead,
@ -52,7 +51,8 @@ const RoomItem = ({
autoJoin,
showAvatar,
displayMode,
sourceType
sourceType,
hideMentionStatus
}: IRoomItemProps) => (
<Touchable
onPress={onPress}
@ -66,15 +66,13 @@ const RoomItem = ({
hideChannel={hideChannel}
testID={testID}
type={type}
theme={theme}
isFocused={isFocused}
isFocused={!!isFocused}
swipeEnabled={swipeEnabled}
displayMode={displayMode}>
<Wrapper
accessibilityLabel={accessibilityLabel}
avatar={avatar}
type={type}
theme={theme}
rid={rid}
prid={prid}
status={status}
@ -82,7 +80,7 @@ const RoomItem = ({
teamMain={teamMain}
displayMode={displayMode}
showAvatar={showAvatar}
showLastMessage={showLastMessage}
showLastMessage={!!showLastMessage}
sourceType={sourceType}>
{showLastMessage && displayMode === DisplayMode.Expanded ? (
<>
@ -97,19 +95,18 @@ const RoomItem = ({
sourceType={sourceType}
/>
) : null}
<Title name={name} theme={theme} hideUnreadStatus={hideUnreadStatus} alert={alert} />
<Title name={name} hideUnreadStatus={hideUnreadStatus} alert={alert} />
{autoJoin ? <Tag testID='auto-join-tag' name={I18n.t('Auto-join')} /> : null}
<UpdatedAt date={date} theme={theme} hideUnreadStatus={hideUnreadStatus} alert={alert} />
<UpdatedAt date={date} hideUnreadStatus={hideUnreadStatus} alert={alert} />
</View>
<View style={styles.row}>
<LastMessage
lastMessage={lastMessage}
type={type}
showLastMessage={showLastMessage}
username={username}
username={username || ''}
alert={alert && !hideUnreadStatus}
useRealName={useRealName}
theme={theme}
/>
<UnreadBadge
unread={unread}
@ -118,6 +115,8 @@ const RoomItem = ({
tunread={tunread}
tunreadUser={tunreadUser}
tunreadGroup={tunreadGroup}
hideMentionStatus={hideMentionStatus}
hideUnreadStatus={hideUnreadStatus}
/>
</View>
</>
@ -133,10 +132,10 @@ const RoomItem = ({
style={{ marginRight: 8 }}
sourceType={sourceType}
/>
<Title name={name} theme={theme} hideUnreadStatus={hideUnreadStatus} alert={alert} />
<Title name={name} hideUnreadStatus={hideUnreadStatus} alert={alert} />
{autoJoin ? <Tag name={I18n.t('Auto-join')} /> : null}
<View style={styles.wrapUpdatedAndBadge}>
<UpdatedAt date={date} theme={theme} hideUnreadStatus={hideUnreadStatus} alert={alert} />
<UpdatedAt date={date} hideUnreadStatus={hideUnreadStatus} alert={alert} />
<UnreadBadge
unread={unread}
userMentions={userMentions}
@ -144,6 +143,8 @@ const RoomItem = ({
tunread={tunread}
tunreadUser={tunreadUser}
tunreadGroup={tunreadGroup}
hideMentionStatus={hideMentionStatus}
hideUnreadStatus={hideUnreadStatus}
/>
</View>
</View>

View File

@ -2,16 +2,19 @@ import React from 'react';
import { Text } from 'react-native';
import styles from './styles';
import { themes } from '../../lib/constants';
import { ITitleProps } from './interfaces';
import { useTheme } from '../../theme';
const Title = React.memo(({ name, theme, hideUnreadStatus, alert }: ITitleProps) => (
<Text
style={[styles.title, alert && !hideUnreadStatus && styles.alert, { color: themes[theme].titleText }]}
ellipsizeMode='tail'
numberOfLines={1}>
{name}
</Text>
));
const Title = React.memo(({ name, hideUnreadStatus, alert }: ITitleProps) => {
const { colors } = useTheme();
return (
<Text
style={[styles.title, alert && !hideUnreadStatus && styles.alert, { color: colors.titleText }]}
ellipsizeMode='tail'
numberOfLines={1}>
{name}
</Text>
);
});
export default Title;

View File

@ -1,207 +1,98 @@
import React from 'react';
import { Animated } from 'react-native';
import Animated, {
useAnimatedGestureHandler,
useSharedValue,
useAnimatedStyle,
withSpring,
runOnJS
} from 'react-native-reanimated';
import {
GestureEvent,
HandlerStateChangeEventPayload,
LongPressGestureHandler,
PanGestureHandler,
PanGestureHandlerEventPayload,
State
State,
HandlerStateChangeEventPayload,
PanGestureHandlerEventPayload
} from 'react-native-gesture-handler';
import Touch from '../../utils/touch';
import Touch from '../../lib/methods/helpers/touch';
import { ACTION_WIDTH, LONG_SWIPE, SMALL_SWIPE } from './styles';
import { isRTL } from '../../i18n';
import { themes } from '../../lib/constants';
import { LeftActions, RightActions } from './Actions';
import { ITouchableProps } from './interfaces';
import { useTheme } from '../../theme';
import I18n from '../../i18n';
class Touchable extends React.Component<ITouchableProps, any> {
private dragX: Animated.Value;
private rowOffSet: Animated.Value;
private reverse: Animated.Value;
private transX: Animated.AnimatedAddition;
private transXReverse: Animated.AnimatedMultiplication;
private _onGestureEvent: (event: GestureEvent<PanGestureHandlerEventPayload>) => void;
private _value: number;
const Touchable = ({
children,
type,
onPress,
onLongPress,
testID,
width,
favorite,
isRead,
rid,
toggleFav,
toggleRead,
hideChannel,
isFocused,
swipeEnabled,
displayMode
}: ITouchableProps): React.ReactElement => {
const { theme, colors } = useTheme();
constructor(props: ITouchableProps) {
super(props);
this.dragX = new Animated.Value(0);
this.rowOffSet = new Animated.Value(0);
this.reverse = new Animated.Value(isRTL() ? -1 : 1);
this.transX = Animated.add(this.rowOffSet, this.dragX);
this.transXReverse = Animated.multiply(this.transX, this.reverse);
this.state = {
rowState: 0 // 0: closed, 1: right opened, -1: left opened
};
this._onGestureEvent = Animated.event([{ nativeEvent: { translationX: this.dragX } }], { useNativeDriver: true });
this._value = 0;
}
const rowOffSet = useSharedValue(0);
const transX = useSharedValue(0);
const rowState = useSharedValue(0); // 0: closed, 1: right opened, -1: left opened
let _value = 0;
_onHandlerStateChange = ({ nativeEvent }: { nativeEvent: HandlerStateChangeEventPayload & PanGestureHandlerEventPayload }) => {
if (nativeEvent.oldState === State.ACTIVE) {
this._handleRelease(nativeEvent);
}
const close = () => {
rowState.value = 0;
transX.value = withSpring(0, { overshootClamping: true });
rowOffSet.value = 0;
};
onLongPressHandlerStateChange = ({ nativeEvent }: { nativeEvent: HandlerStateChangeEventPayload }) => {
if (nativeEvent.state === State.ACTIVE) {
this.onLongPress();
}
};
_handleRelease = (nativeEvent: PanGestureHandlerEventPayload) => {
const { translationX } = nativeEvent;
const { rowState } = this.state;
this._value += translationX;
let toValue = 0;
if (rowState === 0) {
// if no option is opened
if (translationX > 0 && translationX < LONG_SWIPE) {
// open leading option if he swipe right but not enough to trigger action
if (isRTL()) {
toValue = 2 * ACTION_WIDTH;
} else {
toValue = ACTION_WIDTH;
}
this.setState({ rowState: -1 });
} else if (translationX >= LONG_SWIPE) {
toValue = 0;
if (isRTL()) {
this.hideChannel();
} else {
this.toggleRead();
}
} else if (translationX < 0 && translationX > -LONG_SWIPE) {
// open trailing option if he swipe left
if (isRTL()) {
toValue = -ACTION_WIDTH;
} else {
toValue = -2 * ACTION_WIDTH;
}
this.setState({ rowState: 1 });
} else if (translationX <= -LONG_SWIPE) {
toValue = 0;
this.setState({ rowState: 0 });
if (isRTL()) {
this.toggleRead();
} else {
this.hideChannel();
}
} else {
toValue = 0;
}
}
if (rowState === -1) {
// if left option is opened
if (this._value < SMALL_SWIPE) {
toValue = 0;
this.setState({ rowState: 0 });
} else if (this._value > LONG_SWIPE) {
toValue = 0;
this.setState({ rowState: 0 });
if (isRTL()) {
this.hideChannel();
} else {
this.toggleRead();
}
} else if (isRTL()) {
toValue = 2 * ACTION_WIDTH;
} else {
toValue = ACTION_WIDTH;
}
}
if (rowState === 1) {
// if right option is opened
if (this._value > -2 * SMALL_SWIPE) {
toValue = 0;
this.setState({ rowState: 0 });
} else if (this._value < -LONG_SWIPE) {
toValue = 0;
this.setState({ rowState: 0 });
if (isRTL()) {
this.toggleRead();
} else {
this.hideChannel();
}
} else if (isRTL()) {
toValue = -ACTION_WIDTH;
} else {
toValue = -2 * ACTION_WIDTH;
}
}
this._animateRow(toValue);
};
_animateRow = (toValue: number) => {
this.rowOffSet.setValue(this._value);
this._value = toValue;
this.dragX.setValue(0);
Animated.spring(this.rowOffSet, {
toValue,
bounciness: 0,
useNativeDriver: true
}).start();
};
close = () => {
this.setState({ rowState: 0 });
this._animateRow(0);
};
toggleFav = () => {
const { toggleFav, rid, favorite } = this.props;
const handleToggleFav = () => {
if (toggleFav) {
toggleFav(rid, favorite);
}
this.close();
close();
};
toggleRead = () => {
const { toggleRead, rid, isRead } = this.props;
const handleToggleRead = () => {
if (toggleRead) {
toggleRead(rid, isRead);
}
};
hideChannel = () => {
const { hideChannel, rid, type } = this.props;
const handleHideChannel = () => {
if (hideChannel) {
hideChannel(rid, type);
}
};
onToggleReadPress = () => {
this.toggleRead();
this.close();
const onToggleReadPress = () => {
handleToggleRead();
close();
};
onHidePress = () => {
this.hideChannel();
this.close();
const onHidePress = () => {
handleHideChannel();
close();
};
onPress = () => {
const { rowState } = this.state;
if (rowState !== 0) {
this.close();
const handlePress = () => {
if (rowState.value !== 0) {
close();
return;
}
const { onPress } = this.props;
if (onPress) {
onPress();
}
};
onLongPress = () => {
const { rowState } = this.state;
const { onLongPress } = this.props;
if (rowState !== 0) {
this.close();
const handleLongPress = () => {
if (rowState.value !== 0) {
close();
return;
}
@ -210,55 +101,139 @@ class Touchable extends React.Component<ITouchableProps, any> {
}
};
render() {
const { testID, isRead, width, favorite, children, theme, isFocused, swipeEnabled, displayMode } = this.props;
const onLongPressHandlerStateChange = ({ nativeEvent }: { nativeEvent: HandlerStateChangeEventPayload }) => {
if (nativeEvent.state === State.ACTIVE) {
handleLongPress();
}
};
return (
<LongPressGestureHandler onHandlerStateChange={this.onLongPressHandlerStateChange}>
<Animated.View>
<PanGestureHandler
minDeltaX={20}
onGestureEvent={this._onGestureEvent}
onHandlerStateChange={this._onHandlerStateChange}
enabled={swipeEnabled}>
<Animated.View>
<LeftActions
transX={this.transXReverse}
isRead={isRead}
width={width}
onToggleReadPress={this.onToggleReadPress}
const handleRelease = (event: PanGestureHandlerEventPayload) => {
const { translationX } = event;
_value += translationX;
let toValue = 0;
if (rowState.value === 0) {
// if no option is opened
if (translationX > 0 && translationX < LONG_SWIPE) {
if (I18n.isRTL) {
toValue = 2 * ACTION_WIDTH;
} else {
toValue = ACTION_WIDTH;
}
rowState.value = -1;
} else if (translationX >= LONG_SWIPE) {
toValue = 0;
if (I18n.isRTL) {
handleHideChannel();
} else {
handleToggleRead();
}
} else if (translationX < 0 && translationX > -LONG_SWIPE) {
// open trailing option if he swipe left
if (I18n.isRTL) {
toValue = -ACTION_WIDTH;
} else {
toValue = -2 * ACTION_WIDTH;
}
rowState.value = 1;
} else if (translationX <= -LONG_SWIPE) {
toValue = 0;
rowState.value = 1;
if (I18n.isRTL) {
handleToggleRead();
} else {
handleHideChannel();
}
} else {
toValue = 0;
}
} else if (rowState.value === -1) {
// if left option is opened
if (_value < SMALL_SWIPE) {
toValue = 0;
rowState.value = 0;
} else if (_value > LONG_SWIPE) {
toValue = 0;
rowState.value = 0;
if (I18n.isRTL) {
handleHideChannel();
} else {
handleToggleRead();
}
} else if (I18n.isRTL) {
toValue = 2 * ACTION_WIDTH;
} else {
toValue = ACTION_WIDTH;
}
} else if (rowState.value === 1) {
// if right option is opened
if (_value > -2 * SMALL_SWIPE) {
toValue = 0;
rowState.value = 0;
} else if (_value < -LONG_SWIPE) {
if (I18n.isRTL) {
handleToggleRead();
} else {
handleHideChannel();
}
} else if (I18n.isRTL) {
toValue = -ACTION_WIDTH;
} else {
toValue = -2 * ACTION_WIDTH;
}
}
transX.value = withSpring(toValue, { overshootClamping: true });
rowOffSet.value = toValue;
_value = toValue;
};
const onGestureEvent = useAnimatedGestureHandler({
onActive: event => {
transX.value = event.translationX + rowOffSet.value;
if (transX.value > 2 * width) transX.value = 2 * width;
},
onEnd: event => {
runOnJS(handleRelease)(event);
}
});
const animatedStyles = useAnimatedStyle(() => ({ transform: [{ translateX: transX.value }] }));
return (
<LongPressGestureHandler onHandlerStateChange={onLongPressHandlerStateChange}>
<Animated.View>
<PanGestureHandler activeOffsetX={[-20, 20]} onGestureEvent={onGestureEvent} enabled={swipeEnabled}>
<Animated.View>
<LeftActions
transX={transX}
isRead={isRead}
width={width}
onToggleReadPress={onToggleReadPress}
displayMode={displayMode}
/>
<RightActions
transX={transX}
favorite={favorite}
width={width}
toggleFav={handleToggleFav}
onHidePress={onHidePress}
displayMode={displayMode}
/>
<Animated.View style={animatedStyles}>
<Touch
onPress={handlePress}
theme={theme}
displayMode={displayMode}
/>
<RightActions
transX={this.transXReverse}
favorite={favorite}
width={width}
toggleFav={this.toggleFav}
onHidePress={this.onHidePress}
theme={theme}
displayMode={displayMode}
/>
<Animated.View
testID={testID}
style={{
transform: [{ translateX: this.transX }]
backgroundColor: isFocused ? colors.chatComponentBackground : colors.backgroundColor
}}>
<Touch
onPress={this.onPress}
theme={theme}
testID={testID}
style={{
backgroundColor: isFocused ? themes[theme].chatComponentBackground : themes[theme].backgroundColor
}}>
{children}
</Touch>
</Animated.View>
{children}
</Touch>
</Animated.View>
</PanGestureHandler>
</Animated.View>
</LongPressGestureHandler>
);
}
}
</Animated.View>
</PanGestureHandler>
</Animated.View>
</LongPressGestureHandler>
);
};
export default Touchable;

View File

@ -2,11 +2,13 @@ import React from 'react';
import { Text } from 'react-native';
import styles from './styles';
import { themes } from '../../lib/constants';
import { capitalize } from '../../utils/room';
import { capitalize } from '../../lib/methods/helpers/room';
import { IUpdatedAtProps } from './interfaces';
import { useTheme } from '../../theme';
const UpdatedAt = React.memo(({ date, hideUnreadStatus, alert }: IUpdatedAtProps) => {
const { colors } = useTheme();
const UpdatedAt = React.memo(({ date, theme, hideUnreadStatus, alert }: IUpdatedAtProps) => {
if (!date) {
return null;
}
@ -15,13 +17,13 @@ const UpdatedAt = React.memo(({ date, theme, hideUnreadStatus, alert }: IUpdated
style={[
styles.date,
{
color: themes[theme].auxiliaryText
color: colors.auxiliaryText
},
alert &&
!hideUnreadStatus && [
styles.updateAlert,
{
color: themes[theme].tintColor
color: colors.tintColor
}
]
]}

View File

@ -1,26 +1,30 @@
import React from 'react';
import { View } from 'react-native';
import { DisplayMode, themes } from '../../lib/constants';
import { DisplayMode } from '../../lib/constants';
import { useTheme } from '../../theme';
import IconOrAvatar from './IconOrAvatar';
import { IWrapperProps } from './interfaces';
import styles from './styles';
const Wrapper = ({ accessibilityLabel, theme, children, displayMode, ...props }: IWrapperProps): React.ReactElement => (
<View
style={[styles.container, displayMode === DisplayMode.Condensed && styles.containerCondensed]}
accessibilityLabel={accessibilityLabel}>
<IconOrAvatar theme={theme} displayMode={displayMode} {...props} />
const Wrapper = ({ accessibilityLabel, children, displayMode, ...props }: IWrapperProps): React.ReactElement => {
const { colors } = useTheme();
return (
<View
style={[
styles.centerContainer,
{
borderColor: themes[theme].separatorColor
}
]}>
{children}
style={[styles.container, displayMode === DisplayMode.Condensed && styles.containerCondensed]}
accessibilityLabel={accessibilityLabel}>
<IconOrAvatar displayMode={displayMode} {...props} />
<View
style={[
styles.centerContainer,
{
borderColor: colors.separatorColor
}
]}>
{children}
</View>
</View>
</View>
);
);
};
export default Wrapper;

View File

@ -1,172 +1,115 @@
import React from 'react';
import { connect } from 'react-redux';
import React, { useEffect, useReducer, useRef } from 'react';
import { Subscription } from 'rxjs';
import I18n from '../../i18n';
import { ROW_HEIGHT, ROW_HEIGHT_CONDENSED } from './styles';
import { formatDate } from '../../utils/room';
import RoomItem from './RoomItem';
import { ISubscription, TUserStatus } from '../../definitions';
import { useAppSelector } from '../../lib/hooks';
import { getUserPresence } from '../../lib/methods';
import { isGroupChat } from '../../lib/methods/helpers';
import { formatDate } from '../../lib/methods/helpers/room';
import { IRoomItemContainerProps } from './interfaces';
import RoomItem from './RoomItem';
import { ROW_HEIGHT, ROW_HEIGHT_CONDENSED } from './styles';
export { ROW_HEIGHT, ROW_HEIGHT_CONDENSED };
const attrs = [
'width',
'status',
'connected',
'theme',
'isFocused',
'forceUpdate',
'showLastMessage',
'autoJoin',
'showAvatar',
'displayMode'
];
const attrs = ['width', 'isFocused', 'showLastMessage', 'autoJoin', 'showAvatar', 'displayMode'];
class RoomItemContainer extends React.Component<IRoomItemContainerProps, any> {
private roomSubscription: ISubscription | undefined;
static defaultProps: Partial<IRoomItemContainerProps> = {
status: 'offline',
getUserPresence: () => {},
getRoomTitle: () => 'title',
getRoomAvatar: () => '',
getIsGroupChat: () => false,
getIsRead: () => false,
swipeEnabled: true
};
constructor(props: IRoomItemContainerProps) {
super(props);
this.init();
}
componentDidMount() {
const { connected, getUserPresence, id } = this.props;
if (connected && this.isDirect) {
getUserPresence(id);
}
}
shouldComponentUpdate(nextProps: IRoomItemContainerProps) {
const { props } = this;
return !attrs.every(key => props[key] === nextProps[key]);
}
componentDidUpdate(prevProps: IRoomItemContainerProps) {
const { connected, getUserPresence, id } = this.props;
if (prevProps.connected !== connected && connected && this.isDirect) {
getUserPresence(id);
}
}
componentWillUnmount() {
if (this.roomSubscription?.unsubscribe) {
this.roomSubscription.unsubscribe();
}
}
get isGroupChat() {
const { item, getIsGroupChat } = this.props;
return getIsGroupChat(item);
}
get isDirect() {
const {
item: { t },
id
} = this.props;
return t === 'd' && id && !this.isGroupChat;
}
init = () => {
const { item } = this.props;
if (item?.observe) {
const observable = item.observe();
this.roomSubscription = observable?.subscribe?.(() => {
this.forceUpdate();
});
}
};
onPress = () => {
const { item, onPress } = this.props;
return onPress(item);
};
onLongPress = () => {
const { item, onLongPress } = this.props;
if (onLongPress) {
return onLongPress(item);
}
};
render() {
const {
item,
getRoomTitle,
getRoomAvatar,
getIsRead,
width,
toggleFav,
toggleRead,
hideChannel,
theme,
isFocused,
status,
showLastMessage,
username,
useRealName,
swipeEnabled,
autoJoin,
showAvatar,
displayMode
} = this.props;
const RoomItemContainer = React.memo(
({
item,
id,
onPress,
onLongPress,
width,
toggleFav,
toggleRead,
hideChannel,
isFocused,
showLastMessage,
username,
useRealName,
autoJoin,
showAvatar,
displayMode,
getRoomTitle = () => 'title',
getRoomAvatar = () => '',
getIsRead = () => false,
swipeEnabled = true
}: IRoomItemContainerProps) => {
const name = getRoomTitle(item);
const testID = `rooms-list-view-item-${name}`;
const avatar = getRoomAvatar(item);
const isRead = getIsRead(item);
const date = item.roomUpdatedAt && formatDate(item.roomUpdatedAt);
const alert = item.alert || item.tunread?.length;
const connected = useAppSelector(state => state.meteor.connected);
const userStatus = useAppSelector(state => state.activeUsers[id || '']?.status);
const [_, forceUpdate] = useReducer(x => x + 1, 1);
const roomSubscription = useRef<Subscription | null>(null);
let accessibilityLabel = name;
useEffect(() => {
const init = () => {
if (item?.observe) {
const observable = item.observe();
roomSubscription.current = observable?.subscribe?.(() => {
if (_) forceUpdate();
});
}
};
init();
return () => roomSubscription.current?.unsubscribe();
}, []);
useEffect(() => {
const isDirect = !!(item.t === 'd' && id && !isGroupChat(item));
if (connected && isDirect) {
getUserPresence(id);
}
}, [connected]);
const handleOnPress = () => onPress(item);
const handleOnLongPress = () => onLongPress && onLongPress(item);
let accessibilityLabel = '';
if (item.unread === 1) {
accessibilityLabel += `, ${item.unread} ${I18n.t('alert')}`;
accessibilityLabel = `, ${item.unread} ${I18n.t('alert')}`;
} else if (item.unread > 1) {
accessibilityLabel += `, ${item.unread} ${I18n.t('alerts')}`;
accessibilityLabel = `, ${item.unread} ${I18n.t('alerts')}`;
}
if (item.userMentions > 0) {
accessibilityLabel += `, ${I18n.t('you_were_mentioned')}`;
accessibilityLabel = `, ${I18n.t('you_were_mentioned')}`;
}
if (date) {
accessibilityLabel = `, ${I18n.t('last_message')} ${date}`;
}
if (date) {
accessibilityLabel += `, ${I18n.t('last_message')} ${date}`;
}
const status = item.t === 'l' ? item.visitor?.status || item.v?.status : userStatus;
return (
<RoomItem
name={name}
avatar={avatar}
isGroupChat={this.isGroupChat}
isGroupChat={isGroupChat(item)}
isRead={isRead}
onPress={this.onPress}
onLongPress={this.onLongPress}
onPress={handleOnPress}
onLongPress={handleOnLongPress}
date={date}
accessibilityLabel={accessibilityLabel}
width={width}
favorite={item.f}
toggleFav={toggleFav}
rid={item.rid}
toggleFav={toggleFav}
toggleRead={toggleRead}
hideChannel={hideChannel}
testID={testID}
type={item.t}
theme={theme}
isFocused={isFocused}
prid={item.prid}
status={status}
hideUnreadStatus={item.hideUnreadStatus}
hideMentionStatus={item.hideMentionStatus}
alert={alert}
lastMessage={item.lastMessage}
showLastMessage={showLastMessage}
@ -186,23 +129,8 @@ class RoomItemContainer extends React.Component<IRoomItemContainerProps, any> {
sourceType={item.source}
/>
);
}
}
},
(props, nextProps) => attrs.every(key => props[key] === nextProps[key])
);
const mapStateToProps = (state: any, ownProps: any) => {
let status = 'loading';
const { id, type, visitor = {} } = ownProps;
if (state.meteor.connected) {
if (type === 'd') {
status = state.activeUsers[id]?.status || 'loading';
} else if (type === 'l' && visitor?.status) {
({ status } = visitor);
}
}
return {
connected: state.meteor.connected,
status: status as TUserStatus
};
};
export default connect(mapStateToProps)(RoomItemContainer);
export default RoomItemContainer;

View File

@ -1,12 +1,11 @@
import React from 'react';
import { Animated } from 'react-native';
import Animated from 'react-native-reanimated';
import { TSupportedThemes } from '../../theme';
import { TUserStatus, ILastMessage, SubscriptionType, IOmnichannelSource } from '../../definitions';
export interface ILeftActionsProps {
theme: TSupportedThemes;
transX: Animated.AnimatedAddition | Animated.AnimatedMultiplication;
transX: Animated.SharedValue<number>;
isRead: boolean;
width: number;
onToggleReadPress(): void;
@ -14,8 +13,7 @@ export interface ILeftActionsProps {
}
export interface IRightActionsProps {
theme: TSupportedThemes;
transX: Animated.AnimatedAddition | Animated.AnimatedMultiplication;
transX: Animated.SharedValue<number>;
favorite: boolean;
width: number;
toggleFav(): void;
@ -25,14 +23,12 @@ export interface IRightActionsProps {
export interface ITitleProps {
name: string;
theme: TSupportedThemes;
hideUnreadStatus: boolean;
alert: boolean;
}
export interface IUpdatedAtProps {
date: string;
theme: TSupportedThemes;
hideUnreadStatus: boolean;
alert: boolean;
}
@ -41,13 +37,12 @@ export interface IWrapperProps {
accessibilityLabel: string;
avatar: string;
type: string;
theme: TSupportedThemes;
rid: string;
children: React.ReactElement;
displayMode: string;
prid: string;
showLastMessage: boolean;
status: string;
status: TUserStatus;
isGroupChat: boolean;
teamMain: boolean;
showAvatar: boolean;
@ -66,48 +61,43 @@ export interface ITypeIconProps {
sourceType: IOmnichannelSource;
}
export interface IRoomItemContainerProps {
[key: string]: string | boolean | Function | number;
item: any;
showLastMessage: boolean;
id: string;
onPress: (item: any) => void;
onLongPress: (item: any) => Promise<void>;
username: string;
width: number;
status: TUserStatus;
toggleFav(): void;
toggleRead(): void;
hideChannel(): void;
useRealName: boolean;
getUserPresence: (uid: string) => void;
connected: boolean;
theme: TSupportedThemes;
isFocused: boolean;
getRoomTitle: (item: any) => string;
getRoomAvatar: (item: any) => string;
getIsGroupChat: (item: any) => boolean;
getIsRead: (item: any) => boolean;
swipeEnabled: boolean;
autoJoin: boolean;
showAvatar: boolean;
displayMode: string;
interface IRoomItemTouchables {
toggleFav?: (rid: string, favorite: boolean) => Promise<void>;
toggleRead?: (rid: string, tIsRead: boolean) => Promise<void>;
hideChannel?: (rid: string, type: SubscriptionType) => Promise<void>;
onPress: (item?: any) => void;
onLongPress?: (item?: any) => void;
}
export interface IRoomItemProps {
interface IBaseRoomItem extends IRoomItemTouchables {
[key: string]: any;
showLastMessage?: boolean;
useRealName: boolean;
isFocused?: boolean;
displayMode: string;
showAvatar: boolean;
swipeEnabled: boolean;
autoJoin?: boolean;
width: number;
username?: string;
}
export interface IRoomItemContainerProps extends IBaseRoomItem {
item: any;
id?: string;
getRoomTitle: (item: any) => string;
getRoomAvatar: (item: any) => string;
getIsRead?: (item: any) => boolean;
}
export interface IRoomItemProps extends IBaseRoomItem {
rid: string;
type: SubscriptionType;
prid: string;
name: string;
avatar: string;
showLastMessage: boolean;
username: string;
testID: string;
width: number;
status: TUserStatus;
useRealName: boolean;
theme: TSupportedThemes;
isFocused: boolean;
isGroupChat: boolean;
isRead: boolean;
teamMain: boolean;
@ -123,21 +113,12 @@ export interface IRoomItemProps {
tunread: [];
tunreadUser: [];
tunreadGroup: [];
swipeEnabled: boolean;
toggleFav(): void;
toggleRead(): void;
onPress(): void;
onLongPress(): void;
hideChannel(): void;
autoJoin: boolean;
size?: number;
showAvatar: boolean;
displayMode: string;
sourceType: IOmnichannelSource;
hideMentionStatus?: boolean;
}
export interface ILastMessageProps {
theme: TSupportedThemes;
lastMessage: ILastMessage;
type: SubscriptionType;
showLastMessage: boolean;
@ -146,21 +127,29 @@ export interface ILastMessageProps {
alert: boolean;
}
export interface ITouchableProps {
export interface ITouchableProps extends IRoomItemTouchables {
children: JSX.Element;
type: string;
onPress(): void;
onLongPress(): void;
type: SubscriptionType;
testID: string;
width: number;
favorite: boolean;
isRead: boolean;
rid: string;
toggleFav: Function;
toggleRead: Function;
hideChannel: Function;
theme: TSupportedThemes;
isFocused: boolean;
swipeEnabled: boolean;
displayMode: string;
}
export interface IIconOrAvatar {
avatar: string;
type: string;
rid: string;
showAvatar: boolean;
displayMode: string;
prid: string;
status: TUserStatus;
isGroupChat: boolean;
teamMain: boolean;
showLastMessage: boolean;
sourceType: IOmnichannelSource;
}

View File

@ -6,7 +6,7 @@ export const ROW_HEIGHT = 75 * PixelRatio.getFontScale();
export const ROW_HEIGHT_CONDENSED = 60 * PixelRatio.getFontScale();
export const ACTION_WIDTH = 80;
export const SMALL_SWIPE = ACTION_WIDTH / 2;
export const LONG_SWIPE = ACTION_WIDTH * 3;
export const LONG_SWIPE = ACTION_WIDTH * 2.5;
export default StyleSheet.create({
flex: {

View File

@ -0,0 +1,7 @@
import { storiesOf } from '@storybook/react-native';
import React from 'react';
import SearchBox from './index';
const stories = storiesOf('SearchBox', module);
stories.add('Item', () => <SearchBox />);

View File

@ -0,0 +1,45 @@
import React from 'react';
import { fireEvent, render } from '@testing-library/react-native';
import { TextInputProps } from 'react-native';
import SearchBox from '.';
const onChangeTextMock = jest.fn();
const testSearchInputs = {
onChangeText: onChangeTextMock,
testID: 'search-box-text-input'
};
const Render = ({ onChangeText, testID }: TextInputProps) => <SearchBox testID={testID} onChangeText={onChangeText} />;
describe('SearchBox', () => {
it('should render the searchbox component', () => {
const { findByTestId } = render(<Render onChangeText={testSearchInputs.onChangeText} testID={testSearchInputs.testID} />);
expect(findByTestId('searchbox')).toBeTruthy();
});
it('should not render clear-input icon', async () => {
const { queryByTestId } = render(<Render onChangeText={testSearchInputs.onChangeText} testID={testSearchInputs.testID} />);
const clearInput = await queryByTestId('clear-text-input');
expect(clearInput).toBeNull();
});
it('should input new value with onChangeText function', async () => {
const { findByTestId } = render(<Render onChangeText={onChangeTextMock} testID={testSearchInputs.testID} />);
const component = await findByTestId(testSearchInputs.testID);
fireEvent.changeText(component, 'new-input-value');
expect(onChangeTextMock).toHaveBeenCalledWith('new-input-value');
});
// we need skip this test for now, until discovery how handle with functions effect
// https://github.com/callstack/react-native-testing-library/issues/978
it.skip('should clear input when call onCancelSearch function', async () => {
const { findByTestId } = render(<Render testID={'input-with-value'} onChangeText={onChangeTextMock} />);
const component = await findByTestId('clear-text-input');
fireEvent.press(component, 'input-with-value');
expect(onChangeTextMock).toHaveBeenCalledWith('input-with-value');
});
});

View File

@ -0,0 +1,3 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Storyshots SearchBox Item 1`] = `"{\\"type\\":\\"View\\",\\"props\\":{\\"testID\\":\\"searchbox\\"},\\"children\\":[{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":[{\\"marginBottom\\":10},{\\"margin\\":16,\\"marginBottom\\":16}]},\\"children\\":[{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":{\\"position\\":\\"relative\\"}},\\"children\\":[{\\"type\\":\\"TextInput\\",\\"props\\":{\\"allowFontScaling\\":true,\\"rejectResponderTermination\\":true,\\"underlineColorAndroid\\":\\"transparent\\",\\"style\\":[{\\"color\\":\\"#0d0e12\\"},[{\\"textAlign\\":\\"left\\",\\"backgroundColor\\":\\"transparent\\",\\"fontFamily\\":\\"System\\",\\"fontWeight\\":\\"400\\",\\"height\\":48,\\"fontSize\\":16,\\"padding\\":14,\\"borderWidth\\":0.5,\\"borderRadius\\":2},null,{\\"paddingRight\\":45},{\\"backgroundColor\\":\\"#ffffff\\",\\"borderColor\\":\\"#cbcbcc\\",\\"color\\":\\"#0d0e12\\"},null,null],{\\"textAlign\\":\\"auto\\"}],\\"placeholderTextColor\\":\\"#9ca2a8\\",\\"keyboardAppearance\\":\\"light\\",\\"autoCorrect\\":false,\\"autoCapitalize\\":\\"none\\",\\"accessibilityLabel\\":\\"Search\\",\\"placeholder\\":\\"Search\\",\\"value\\":\\"\\",\\"blurOnSubmit\\":true,\\"returnKeyType\\":\\"search\\"},\\"children\\":null},{\\"type\\":\\"Text\\",\\"props\\":{\\"selectable\\":false,\\"allowFontScaling\\":false,\\"style\\":[{\\"fontSize\\":20,\\"color\\":\\"#2f343d\\"},[{\\"position\\":\\"absolute\\",\\"top\\":14},{\\"right\\":15}],{\\"fontFamily\\":\\"custom\\",\\"fontWeight\\":\\"normal\\",\\"fontStyle\\":\\"normal\\"},{}]},\\"children\\":[\\"\\"]}]}]}]}"`;

View File

@ -0,0 +1,43 @@
import React, { useCallback, useState } from 'react';
import { StyleSheet, TextInputProps, View } from 'react-native';
import I18n from '../../i18n';
import { FormTextInput } from '../TextInput';
const styles = StyleSheet.create({
inputContainer: {
margin: 16,
marginBottom: 16
}
});
const SearchBox = ({ onChangeText, onSubmitEditing, testID }: TextInputProps): JSX.Element => {
const [text, setText] = useState('');
const internalOnChangeText = useCallback(value => {
setText(value);
onChangeText?.(value);
}, []);
return (
<View testID='searchbox'>
<FormTextInput
autoCapitalize='none'
autoCorrect={false}
blurOnSubmit
placeholder={I18n.t('Search')}
returnKeyType='search'
underlineColorAndroid='transparent'
containerStyle={styles.inputContainer}
onChangeText={internalOnChangeText}
onSubmitEditing={onSubmitEditing}
value={text}
testID={testID}
onClearInput={() => internalOnChangeText('')}
iconRight={'search'}
/>
</View>
);
};
export default SearchBox;

View File

@ -5,8 +5,8 @@ import I18n from '../i18n';
import { useTheme } from '../theme';
import sharedStyles from '../views/Styles';
import { themes } from '../lib/constants';
import TextInput from './TextInput';
import { isIOS, isTablet } from '../utils/deviceInfo';
import { TextInput } from './TextInput';
import { isIOS, isTablet } from '../lib/methods/helpers';
import { useOrientation } from '../dimensions';
const styles = StyleSheet.create({
@ -39,7 +39,6 @@ const SearchHeader = ({ onSearchChangeText, testID }: ISearchHeaderProps): JSX.E
style={[styles.title, isLight && { color: themes[theme].headerTitleColor }, { fontSize: titleFontSize }]}
placeholder={I18n.t('Search')}
onChangeText={onSearchChangeText}
theme={theme}
testID={testID}
/>
</View>

View File

@ -1,13 +1,13 @@
import React from 'react';
// @ts-ignore // TODO: Remove on react-native update
import { Pressable, Text, View } from 'react-native';
import FastImage from '@rocket.chat/react-native-fast-image';
import FastImage from 'react-native-fast-image';
import { IServerInfo } from '../../definitions';
import Check from '../Check';
import styles, { ROW_HEIGHT } from './styles';
import { themes } from '../../lib/constants';
import { isIOS } from '../../utils/deviceInfo';
import { isIOS } from '../../lib/methods/helpers';
import { useTheme } from '../../theme';
export { ROW_HEIGHT };

View File

@ -0,0 +1,54 @@
import React from 'react';
import { render } from '@testing-library/react-native';
import { FormTextInput } from '.';
const FormTextInputID = 'form-text-input-id';
describe('FormTextInput', () => {
test('should render the component', async () => {
const { findByTestId } = render(<FormTextInput testID={FormTextInputID} />);
const component = await findByTestId('form-text-input-id');
expect(component).toBeTruthy();
});
test('should render the component with left icon', async () => {
const { findByTestId } = render(<FormTextInput testID={FormTextInputID} iconLeft='user' />);
const component = await findByTestId(`${FormTextInputID}-icon-left`);
expect(component).toBeTruthy();
});
test('should render the component with right icon', async () => {
const { findByTestId } = render(<FormTextInput testID={FormTextInputID} iconRight='user' />);
const component = await findByTestId(`${FormTextInputID}-icon-right`);
expect(component).toBeTruthy();
});
test('should render the component with password icon', async () => {
const { findByTestId } = render(<FormTextInput testID={FormTextInputID} secureTextEntry />);
const component = await findByTestId(`${FormTextInputID}-icon-password`);
expect(component).toBeTruthy();
});
test('should render the component with loading', async () => {
const { findByTestId } = render(<FormTextInput testID={FormTextInputID} loading />);
const component = await findByTestId(`${FormTextInputID}-loading`);
expect(component).toBeTruthy();
});
test('should render the component with label', async () => {
const { findByText } = render(<FormTextInput testID={FormTextInputID} label='form text input' />);
const component = await findByText('form text input');
expect(component).toBeTruthy();
});
test('should render the component with error', async () => {
const error = {
reason: 'An error occurred'
};
const { findByText } = render(<FormTextInput testID={FormTextInputID} error={error} />);
const component = await findByText(error.reason);
expect(component).toBeTruthy();
});
});

View File

@ -1,13 +1,13 @@
import React from 'react';
import { StyleProp, StyleSheet, Text, TextInputProps, TextInput as RNTextInput, TextStyle, View, ViewStyle } from 'react-native';
import { BottomSheetTextInput } from '@gorhom/bottom-sheet';
import React, { useState } from 'react';
import { StyleProp, StyleSheet, Text, TextInput as RNTextInput, TextInputProps, TextStyle, View, ViewStyle } from 'react-native';
import Touchable from 'react-native-platform-touchable';
import { useTheme } from '../../theme';
import sharedStyles from '../../views/Styles';
import TextInput from './index';
import { themes } from '../../lib/constants';
import { CustomIcon, TIconsName } from '../CustomIcon';
import ActivityIndicator from '../ActivityIndicator';
import { TSupportedThemes } from '../../theme';
import { CustomIcon, TIconsName } from '../CustomIcon';
import { TextInput } from './TextInput';
const styles = StyleSheet.create({
error: {
@ -58,134 +58,118 @@ export interface IRCTextInputProps extends TextInputProps {
containerStyle?: StyleProp<ViewStyle>;
inputStyle?: StyleProp<TextStyle>;
inputRef?: React.Ref<RNTextInput>;
testID?: string;
iconLeft?: TIconsName;
iconRight?: TIconsName;
left?: JSX.Element;
onIconRightPress?(): void;
theme: TSupportedThemes;
bottomSheet?: boolean;
onClearInput?: () => void;
}
interface IRCTextInputState {
showPassword: boolean;
}
export const FormTextInput = ({
label,
error,
loading,
containerStyle,
inputStyle,
inputRef,
iconLeft,
iconRight,
onClearInput,
value,
left,
testID,
secureTextEntry,
bottomSheet,
placeholder,
...inputProps
}: IRCTextInputProps): React.ReactElement => {
const { colors } = useTheme();
const [showPassword, setShowPassword] = useState(false);
const showClearInput = onClearInput && value && value.length > 0;
const Input = bottomSheet ? BottomSheetTextInput : TextInput;
return (
<View style={[styles.inputContainer, containerStyle]}>
{label ? (
<Text style={[styles.label, { color: colors.titleText }, error?.error && { color: colors.dangerColor }]}>{label}</Text>
) : null}
export default class FormTextInput extends React.PureComponent<IRCTextInputProps, IRCTextInputState> {
static defaultProps = {
error: {},
theme: 'light'
};
state = {
showPassword: false
};
get iconLeft() {
const { testID, iconLeft, theme } = this.props;
return iconLeft ? (
<CustomIcon
name={iconLeft}
testID={testID ? `${testID}-icon-left` : undefined}
size={20}
color={themes[theme].bodyText}
style={[styles.iconContainer, styles.iconLeft]}
/>
) : null;
}
get iconRight() {
const { iconRight, onIconRightPress, theme } = this.props;
return iconRight ? (
<Touchable onPress={onIconRightPress} style={[styles.iconContainer, styles.iconRight]}>
<CustomIcon name={iconRight} size={20} color={themes[theme].bodyText} />
</Touchable>
) : null;
}
get iconPassword() {
const { showPassword } = this.state;
const { testID, theme } = this.props;
return (
<Touchable onPress={this.tooglePassword} style={[styles.iconContainer, styles.iconRight]}>
<CustomIcon
name={showPassword ? 'unread-on-top' : 'unread-on-top-disabled'}
testID={testID ? `${testID}-icon-right` : undefined}
size={20}
color={themes[theme].auxiliaryText}
<View style={styles.wrap}>
<Input
style={[
styles.input,
iconLeft && styles.inputIconLeft,
(secureTextEntry || iconRight) && styles.inputIconRight,
{
backgroundColor: colors.backgroundColor,
borderColor: colors.separatorColor,
color: colors.titleText
},
error?.error && {
color: colors.dangerColor,
borderColor: colors.dangerColor
},
inputStyle
]}
// @ts-ignore ref error
ref={inputRef}
autoCorrect={false}
autoCapitalize='none'
underlineColorAndroid='transparent'
secureTextEntry={secureTextEntry && !showPassword}
testID={testID}
accessibilityLabel={placeholder}
placeholder={placeholder}
value={value}
{...inputProps}
/>
</Touchable>
);
}
get loading() {
const { theme } = this.props;
return <ActivityIndicator style={[styles.iconContainer, styles.iconRight]} color={themes[theme].bodyText} />;
}
tooglePassword = () => {
this.setState(prevState => ({ showPassword: !prevState.showPassword }));
};
render() {
const { showPassword } = this.state;
const {
label,
left,
error,
loading,
secureTextEntry,
containerStyle,
inputRef,
iconLeft,
iconRight,
inputStyle,
testID,
placeholder,
theme,
...inputProps
} = this.props;
const { dangerColor } = themes[theme];
return (
<View style={[styles.inputContainer, containerStyle]}>
{label ? (
<Text style={[styles.label, { color: themes[theme].titleText }, error?.error && { color: dangerColor }]}>{label}</Text>
) : null}
<View style={styles.wrap}>
<TextInput
style={[
styles.input,
iconLeft && styles.inputIconLeft,
(secureTextEntry || iconRight) && styles.inputIconRight,
{
backgroundColor: themes[theme].backgroundColor,
borderColor: themes[theme].separatorColor,
color: themes[theme].titleText
},
error?.error && {
color: dangerColor,
borderColor: dangerColor
},
inputStyle
]}
ref={inputRef}
autoCorrect={false}
autoCapitalize='none'
underlineColorAndroid='transparent'
secureTextEntry={secureTextEntry && !showPassword}
testID={testID}
accessibilityLabel={placeholder}
placeholder={placeholder}
theme={theme}
{...inputProps}
{iconLeft ? (
<CustomIcon
name={iconLeft}
testID={testID ? `${testID}-icon-left` : undefined}
size={20}
color={colors.auxiliaryText}
style={[styles.iconContainer, styles.iconLeft]}
/>
{iconLeft ? this.iconLeft : null}
{iconRight ? this.iconRight : null}
{secureTextEntry ? this.iconPassword : null}
{loading ? this.loading : null}
{left}
</View>
{error && error.reason ? <Text style={[styles.error, { color: dangerColor }]}>{error.reason}</Text> : null}
) : null}
{showClearInput ? (
<Touchable onPress={onClearInput} style={[styles.iconContainer, styles.iconRight]} testID='clear-text-input'>
<CustomIcon name='input-clear' size={20} color={colors.auxiliaryTintColor} />
</Touchable>
) : null}
{iconRight && !showClearInput ? (
<CustomIcon
name={iconRight}
testID={testID ? `${testID}-icon-right` : undefined}
size={20}
color={colors.bodyText}
style={[styles.iconContainer, styles.iconRight]}
/>
) : null}
{secureTextEntry ? (
<Touchable onPress={() => setShowPassword(!showPassword)} style={[styles.iconContainer, styles.iconRight]}>
<CustomIcon
name={showPassword ? 'unread-on-top' : 'unread-on-top-disabled'}
testID={testID ? `${testID}-icon-password` : undefined}
size={20}
color={colors.auxiliaryText}
/>
</Touchable>
) : null}
{loading ? (
<ActivityIndicator
style={[styles.iconContainer, styles.iconRight]}
color={colors.bodyText}
testID={testID ? `${testID}-loading` : undefined}
/>
) : null}
{left}
</View>
);
}
}
{error && error.reason ? <Text style={[styles.error, { color: colors.dangerColor }]}>{error.reason}</Text> : null}
</View>
);
};

View File

@ -3,7 +3,7 @@ import React from 'react';
import { storiesOf } from '@storybook/react-native';
import { View, StyleSheet } from 'react-native';
import FormTextInput from './FormTextInput';
import { FormTextInput } from '.';
const styles = StyleSheet.create({
paddingHorizontal: {
@ -18,14 +18,12 @@ const item = {
longText: 'https://open.rocket.chat/images/logo/android-chrome-512x512.png'
};
const theme = 'light';
stories.add('Short and Long Text', () => (
<>
<View style={styles.paddingHorizontal}>
<FormTextInput label='Short Text' placeholder='placeholder' value={item.name} theme={theme} />
<FormTextInput label='Short Text' placeholder='placeholder' value={item.name} />
<FormTextInput label='Long Text' placeholder='placeholder' value={item.longText} theme={theme} />
<FormTextInput label='Long Text' placeholder='placeholder' value={item.longText} />
</View>
</>
));

View File

@ -0,0 +1,29 @@
import React from 'react';
import { I18nManager, StyleProp, StyleSheet, TextInput as RNTextInput, TextStyle } from 'react-native';
import { IRCTextInputProps } from './FormTextInput';
import { themes } from '../../lib/constants';
import { useTheme } from '../../theme';
const styles = StyleSheet.create({
input: {
...(I18nManager.isRTL ? { textAlign: 'right' } : { textAlign: 'auto' })
}
});
export interface IThemedTextInput extends IRCTextInputProps {
style: StyleProp<TextStyle>;
}
export const TextInput = React.forwardRef<RNTextInput, IThemedTextInput>(({ style, ...props }, ref) => {
const { theme } = useTheme();
return (
<RNTextInput
ref={ref}
style={[{ color: themes[theme].titleText }, style, styles.input]}
placeholderTextColor={themes[theme].auxiliaryText}
keyboardAppearance={theme === 'light' ? 'light' : 'dark'}
{...props}
/>
);
});

View File

@ -0,0 +1,2 @@
export * from './TextInput';
export * from './FormTextInput';

View File

@ -1,29 +0,0 @@
import React from 'react';
import { I18nManager, StyleProp, StyleSheet, TextInput, TextStyle } from 'react-native';
import { IRCTextInputProps } from './FormTextInput';
import { themes } from '../../lib/constants';
import { TSupportedThemes } from '../../theme';
const styles = StyleSheet.create({
input: {
...(I18nManager.isRTL ? { textAlign: 'right' } : { textAlign: 'auto' })
}
});
export interface IThemedTextInput extends IRCTextInputProps {
style: StyleProp<TextStyle>;
theme: TSupportedThemes;
}
const ThemedTextInput = React.forwardRef<TextInput, IThemedTextInput>(({ style, theme, ...props }, ref) => (
<TextInput
ref={ref}
style={[{ color: themes[theme].titleText }, style, styles.input]}
placeholderTextColor={themes[theme].auxiliaryText}
keyboardAppearance={theme === 'light' ? 'light' : 'dark'}
{...props}
/>
));
export default ThemedTextInput;

View File

@ -1,11 +1,10 @@
import React from 'react';
import React, { useEffect } from 'react';
import { StyleSheet } from 'react-native';
import EasyToast from 'react-native-easy-toast';
import { themes } from '../lib/constants';
import EventEmitter from '../lib/methods/helpers/events';
import { useTheme } from '../theme';
import sharedStyles from '../views/Styles';
import EventEmitter from '../utils/events';
import { TSupportedThemes, withTheme } from '../theme';
const styles = StyleSheet.create({
toast: {
@ -21,54 +20,37 @@ const styles = StyleSheet.create({
export const LISTENER = 'Toast';
interface IToastProps {
theme?: TSupportedThemes;
}
let listener: Function;
let toast: EasyToast | null | undefined;
class Toast extends React.Component<IToastProps, any> {
private listener?: Function;
const Toast = (): React.ReactElement => {
const { colors } = useTheme();
private toast: EasyToast | null | undefined;
useEffect(() => {
listener = EventEmitter.addEventListener(LISTENER, showToast);
return () => {
EventEmitter.removeListener(LISTENER, listener);
};
}, []);
componentDidMount() {
this.listener = EventEmitter.addEventListener(LISTENER, this.showToast);
}
const getToastRef = (newToast: EasyToast | null) => (toast = newToast);
shouldComponentUpdate(nextProps: any) {
const { theme } = this.props;
if (nextProps.theme !== theme) {
return true;
}
return false;
}
componentWillUnmount() {
if (this.listener) {
EventEmitter.removeListener(LISTENER, this.listener);
}
}
getToastRef = (toast: EasyToast | null) => (this.toast = toast);
showToast = ({ message }: { message: string }) => {
if (this.toast && this.toast.show) {
this.toast.show(message, 1000);
const showToast = ({ message }: { message: string }) => {
if (toast && toast.show) {
toast.show(message, 1000);
}
};
render() {
const { theme } = this.props;
return (
<EasyToast
ref={this.getToastRef}
// @ts-ignore
position='center'
style={[styles.toast, { backgroundColor: themes[theme!].toastBackground }]}
textStyle={[styles.text, { color: themes[theme!].buttonText }]}
opacity={0.9}
/>
);
}
}
return (
<EasyToast
ref={getToastRef}
// @ts-ignore
position='center'
style={[styles.toast, { backgroundColor: colors.toastBackground }]}
textStyle={[styles.text, { color: colors.buttonText }]}
opacity={0.9}
/>
);
};
export default withTheme(Toast);
export default Toast;

View File

@ -6,9 +6,9 @@ import Modal from 'react-native-modal';
import useDeepCompareEffect from 'use-deep-compare-effect';
import { connect } from 'react-redux';
import FormTextInput from '../TextInput/FormTextInput';
import { FormTextInput } from '../TextInput';
import I18n from '../../i18n';
import EventEmitter from '../../utils/events';
import EventEmitter from '../../lib/methods/helpers/events';
import { useTheme } from '../../theme';
import { themes } from '../../lib/constants';
import Button from '../Button';
@ -116,7 +116,6 @@ const TwoFactor = React.memo(({ isMasterDetail }: { isMasterDetail: boolean }) =
{method?.text ? <Text style={[styles.subtitle, { color }]}>{I18n.t(method.text)}</Text> : null}
<FormTextInput
value={code}
theme={theme}
inputRef={(e: any) => InteractionManager.runAfterInteractions(() => e?.getNativeRef()?.focus())}
returnKeyType='send'
autoCapitalize='none'

View File

@ -10,7 +10,7 @@ import { textParser } from './utils';
import { themes } from '../../lib/constants';
import sharedStyles from '../../views/Styles';
import { CustomIcon } from '../CustomIcon';
import { isAndroid } from '../../utils/deviceInfo';
import { isAndroid } from '../../lib/methods/helpers';
import { useTheme } from '../../theme';
import ActivityIndicator from '../ActivityIndicator';
import { IDatePicker } from './interfaces';

View File

@ -1,6 +1,6 @@
import React from 'react';
import { StyleSheet, View } from 'react-native';
import FastImage from '@rocket.chat/react-native-fast-image';
import FastImage from 'react-native-fast-image';
import { BlockContext } from '@rocket.chat/ui-kit';
import ImageContainer from '../message/Image';

View File

@ -1,52 +1,52 @@
import React from 'react';
import { Text, View } from 'react-native';
import Touchable from 'react-native-platform-touchable';
import FastImage from '@rocket.chat/react-native-fast-image';
import FastImage from 'react-native-fast-image';
import { themes } from '../../../lib/constants';
import { textParser } from '../utils';
import { CustomIcon } from '../../CustomIcon';
import styles from './styles';
import { IItemData } from '.';
import { TSupportedThemes } from '../../../theme';
import { useTheme } from '../../../theme';
interface IChip {
item: IItemData;
onSelect: (item: IItemData) => void;
style?: object;
theme: TSupportedThemes;
}
interface IChips {
items: IItemData[];
onSelect: (item: IItemData) => void;
style?: object;
theme: TSupportedThemes;
}
const keyExtractor = (item: IItemData) => item.value.toString();
const Chip = ({ item, onSelect, style, theme }: IChip) => (
<Touchable
key={item.value}
onPress={() => onSelect(item)}
style={[styles.chip, { backgroundColor: themes[theme].auxiliaryBackground }, style]}
background={Touchable.Ripple(themes[theme].bannerBackground)}>
<>
{item.imageUrl ? <FastImage style={styles.chipImage} source={{ uri: item.imageUrl }} /> : null}
<Text numberOfLines={1} style={[styles.chipText, { color: themes[theme].titleText }]}>
{textParser([item.text])}
</Text>
<CustomIcon name='close' size={16} color={themes[theme].auxiliaryText} />
</>
</Touchable>
);
const Chip = ({ item, onSelect, style }: IChip) => {
const { colors } = useTheme();
return (
<Touchable
key={item.value}
onPress={() => onSelect(item)}
style={[styles.chip, { backgroundColor: colors.auxiliaryBackground }, style]}
background={Touchable.Ripple(colors.bannerBackground)}>
<>
{item.imageUrl ? <FastImage style={styles.chipImage} source={{ uri: item.imageUrl }} /> : null}
<Text numberOfLines={1} style={[styles.chipText, { color: colors.titleText }]}>
{textParser([item.text])}
</Text>
<CustomIcon name='close' size={16} color={colors.auxiliaryText} />
</>
</Touchable>
);
};
Chip.propTypes = {};
const Chips = ({ items, onSelect, style, theme }: IChips) => (
const Chips = ({ items, onSelect, style }: IChips) => (
<View style={styles.chips}>
{items.map(item => (
<Chip key={keyExtractor(item)} item={item} onSelect={onSelect} style={style} theme={theme} />
<Chip key={keyExtractor(item)} item={item} onSelect={onSelect} style={style} />
))}
</View>
);

View File

@ -3,15 +3,13 @@ import { Text, View } from 'react-native';
import Touchable from 'react-native-platform-touchable';
import { CustomIcon } from '../../CustomIcon';
import { themes } from '../../../lib/constants';
import ActivityIndicator from '../../ActivityIndicator';
import styles from './styles';
import { TSupportedThemes } from '../../../theme';
import { useTheme } from '../../../theme';
interface IInput {
children?: JSX.Element;
onPress: () => void;
theme: TSupportedThemes;
inputStyle?: object;
disabled?: boolean | null;
placeholder?: string;
@ -19,21 +17,23 @@ interface IInput {
innerInputStyle?: object;
}
const Input = ({ children, onPress, theme, loading, inputStyle, placeholder, disabled, innerInputStyle }: IInput) => (
<Touchable
onPress={onPress}
style={[{ backgroundColor: themes[theme].backgroundColor }, inputStyle]}
background={Touchable.Ripple(themes[theme].bannerBackground)}
disabled={disabled}>
<View style={[styles.input, { borderColor: themes[theme].separatorColor }, innerInputStyle]}>
{placeholder ? <Text style={[styles.pickerText, { color: themes[theme].auxiliaryText }]}>{placeholder}</Text> : children}
{loading ? (
<ActivityIndicator style={[styles.loading, styles.icon]} />
) : (
<CustomIcon name='chevron-down' size={22} color={themes[theme].auxiliaryText} style={styles.icon} />
)}
</View>
</Touchable>
);
const Input = ({ children, onPress, loading, inputStyle, placeholder, disabled, innerInputStyle }: IInput) => {
const { colors } = useTheme();
return (
<Touchable
onPress={onPress}
style={[{ backgroundColor: colors.backgroundColor }, inputStyle]}
background={Touchable.Ripple(colors.bannerBackground)}
disabled={disabled}>
<View style={[styles.input, { borderColor: colors.separatorColor }, innerInputStyle]}>
{placeholder ? <Text style={[styles.pickerText, { color: colors.auxiliaryText }]}>{placeholder}</Text> : children}
{loading ? (
<ActivityIndicator style={styles.icon} />
) : (
<CustomIcon name='chevron-down' size={22} color={colors.auxiliaryText} style={styles.icon} />
)}
</View>
</Touchable>
);
};
export default Input;

View File

@ -1,61 +1,54 @@
import React from 'react';
import { FlatList, Text } from 'react-native';
import { Text } from 'react-native';
import Touchable from 'react-native-platform-touchable';
import FastImage from '@rocket.chat/react-native-fast-image';
import FastImage from 'react-native-fast-image';
import { FlatList } from 'react-native-gesture-handler';
import Check from '../../Check';
import * as List from '../../List';
import { textParser } from '../utils';
import { themes } from '../../../lib/constants';
import styles from './styles';
import { IItemData } from '.';
import { TSupportedThemes } from '../../../theme';
import { useTheme } from '../../../theme';
interface IItem {
item: IItemData;
selected?: string;
onSelect: Function;
theme: TSupportedThemes;
}
interface IItems {
items: IItemData[];
selected: string[];
onSelect: Function;
theme: TSupportedThemes;
}
const keyExtractor = (item: IItemData) => item.value.toString();
const keyExtractor = (item: IItemData) => item.value?.name || item.text?.text;
// RectButton doesn't work on modal (Android)
const Item = ({ item, selected, onSelect, theme }: IItem) => {
const Item = ({ item, selected, onSelect }: IItem) => {
const itemName = item.value?.name || item.text.text.toLowerCase();
const { colors } = useTheme();
return (
<Touchable
testID={`multi-select-item-${itemName}`}
key={itemName}
onPress={() => onSelect(item)}
style={[styles.item, { backgroundColor: themes[theme].backgroundColor }]}>
<Touchable testID={`multi-select-item-${itemName}`} key={itemName} onPress={() => onSelect(item)} style={[styles.item]}>
<>
{item.imageUrl ? <FastImage style={styles.itemImage} source={{ uri: item.imageUrl }} /> : null}
<Text style={{ color: themes[theme].titleText }}>{textParser([item.text])}</Text>
<Text style={{ color: colors.titleText }}>{textParser([item.text])}</Text>
{selected ? <Check /> : null}
</>
</Touchable>
);
};
const Items = ({ items, selected, onSelect, theme }: IItems) => (
const Items = ({ items, selected, onSelect }: IItems) => (
<FlatList
data={items}
style={[styles.items, { backgroundColor: themes[theme].backgroundColor }]}
contentContainerStyle={[styles.itemContent, { backgroundColor: themes[theme].backgroundColor }]}
style={[styles.items]}
contentContainerStyle={[styles.itemContent]}
keyboardShouldPersistTaps='always'
ItemSeparatorComponent={List.Separator}
keyExtractor={keyExtractor}
renderItem={({ item }) => (
<Item item={item} onSelect={onSelect} theme={theme} selected={selected.find(s => s === item.value)} />
)}
renderItem={({ item }) => <Item item={item} onSelect={onSelect} selected={selected.find(s => s === item.value)} />}
/>
);

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