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 { import mockDeviceInfo from 'react-native-device-info/jest/react-native-device-info-mock';
getModel: () => '',
getReadableVersion: () => '', export default mockDeviceInfo;
getBundleId: () => '',
isTablet: () => false,
hasNotch: () => false
};

View File

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

View File

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

View File

@ -12,7 +12,6 @@ import com.facebook.react.ReactRootView;
import com.facebook.react.ReactActivityDelegate; import com.facebook.react.ReactActivityDelegate;
import com.facebook.react.ReactFragmentActivity; import com.facebook.react.ReactFragmentActivity;
import com.swmansion.gesturehandler.react.RNGestureHandlerEnabledRootView;
import com.zoontek.rnbootsplash.RNBootSplash; import com.zoontek.rnbootsplash.RNBootSplash;
import com.google.gson.Gson; import com.google.gson.Gson;
@ -51,16 +50,6 @@ public class MainActivity extends ReactFragmentActivity {
return "RocketChatRN"; return "RocketChatRN";
} }
@Override
protected ReactActivityDelegate createReactActivityDelegate() {
return new ReactActivityDelegate(this, getMainComponentName()) {
@Override
protected ReactRootView createRootView() {
return new RNGestureHandlerEnabledRootView(MainActivity.this);
}
};
}
// from react-native-orientation // from react-native-orientation
@Override @Override
public void onConfigurationChanged(Configuration newConfig) { 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.ReactActivity;
import com.facebook.react.ReactActivityDelegate; import com.facebook.react.ReactActivityDelegate;
import com.facebook.react.ReactRootView; import com.facebook.react.ReactRootView;
import com.swmansion.gesturehandler.react.RNGestureHandlerEnabledRootView;
public class ShareActivity extends ReactActivity { public class ShareActivity extends ReactActivity {
@Override @Override
protected String getMainComponentName() { protected String getMainComponentName() {
return "ShareRocketChatRN"; 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 targetSdkVersion = 30
ndkVersion = "20.1.5948944" ndkVersion = "20.1.5948944"
glideVersion = "4.11.0" glideVersion = "4.11.0"
kotlin_version = "1.3.50" kotlin_version = "1.6.10"
supportLibVersion = "28.0.0" supportLibVersion = "28.0.0"
libre_build = !(isPlay.toBoolean()) 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" 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 KEY_ALIAS=my-key-alias
KEYSTORE_PASSWORD= KEYSTORE_PASSWORD=
KEY_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 { SetUsernameStackParamList, StackParamList } from './definitions/navigationTypes';
import Navigation from './lib/navigation/appNavigation'; 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'; import { RootEnum } from './definitions';
// Stacks // Stacks
import AuthLoadingView from './views/AuthLoadingView'; import AuthLoadingView from './views/AuthLoadingView';
@ -15,7 +15,7 @@ import OutsideStack from './stacks/OutsideStack';
import InsideStack from './stacks/InsideStack'; import InsideStack from './stacks/InsideStack';
import MasterDetailStack from './stacks/MasterDetailStack'; import MasterDetailStack from './stacks/MasterDetailStack';
import { ThemeContext } from './theme'; import { ThemeContext } from './theme';
import { setCurrentScreen } from './utils/log'; import { setCurrentScreen } from './lib/methods/helpers/log';
// SetUsernameStack // SetUsernameStack
const SetUsername = createStackNavigator<SetUsernameStackParamList>(); const SetUsername = createStackNavigator<SetUsernameStackParamList>();

View File

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

View File

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

View File

@ -19,7 +19,6 @@ interface IBaseReturn extends Action {
type TSubscribeRoom = IBaseReturn; type TSubscribeRoom = IBaseReturn;
type TUnsubscribeRoom = IBaseReturn; type TUnsubscribeRoom = IBaseReturn;
type TCloseRoom = IBaseReturn;
type TRoom = Record<string, any>; type TRoom = Record<string, any>;
@ -45,7 +44,7 @@ interface IUserTyping extends Action {
status: boolean; 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 { export function subscribeRoom(rid: string): TSubscribeRoom {
return { 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 { export function forwardRoom(rid: string, transferData: ITransferData): IForwardRoom {
return { return {
type: ROOM.FORWARD, type: ROOM.FORWARD,

View File

@ -8,7 +8,7 @@ import BottomSheet, { BottomSheetBackdrop } from '@gorhom/bottom-sheet';
import { useDimensions, useOrientation } from '../../dimensions'; import { useDimensions, useOrientation } from '../../dimensions';
import { useTheme } from '../../theme'; import { useTheme } from '../../theme';
import { isIOS, isTablet } from '../../utils/deviceInfo'; import { isIOS, isTablet } from '../../lib/methods/helpers';
import { Handle } from './Handle'; import { Handle } from './Handle';
import { TActionSheetOptions } from './Provider'; import { TActionSheetOptions } from './Provider';
import BottomSheetContent from './BottomSheetContent'; import BottomSheetContent from './BottomSheetContent';
@ -101,6 +101,11 @@ const ActionSheet = React.memo(
</> </>
); );
const onClose = () => {
toggleVisible();
data?.onClose && data?.onClose();
};
const renderBackdrop = useCallback( const renderBackdrop = useCallback(
props => ( props => (
<BottomSheetBackdrop <BottomSheetBackdrop
@ -116,6 +121,10 @@ const ActionSheet = React.memo(
const bottomSheet = isLandscape || isTablet ? styles.bottomSheet : {}; 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 ( return (
<> <>
{children} {children}
@ -130,7 +139,8 @@ const ActionSheet = React.memo(
enablePanDownToClose enablePanDownToClose
style={{ ...styles.container, ...bottomSheet }} style={{ ...styles.container, ...bottomSheet }}
backgroundStyle={{ backgroundColor: colors.focusedBackground }} 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} /> <BottomSheetContent options={data?.options} hide={hide} children={data?.children} hasCancel={data?.hasCancel} />
</BottomSheet> </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; export default BottomSheetContent;

View File

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

View File

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

View File

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

View File

@ -63,5 +63,15 @@ export default StyleSheet.create({
}, },
rightContainer: { rightContainer: {
paddingLeft: 12 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 { themes } from '../lib/constants';
import sharedStyles from '../views/Styles'; import sharedStyles from '../views/Styles';
import { getReadableVersion } from '../utils/deviceInfo'; import { getReadableVersion } from '../lib/methods/helpers';
import I18n from '../i18n'; import I18n from '../i18n';
import { TSupportedThemes } from '../theme'; import { TSupportedThemes } from '../theme';

View File

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

View File

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

View File

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

View File

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

File diff suppressed because one or more lines are too long

View File

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

View File

@ -1,5 +1,5 @@
import React from 'react'; 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'; import { ICustomEmoji } from '../../definitions/IEmoji';

View File

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

View File

@ -5,7 +5,7 @@ import { dequal } from 'dequal';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import orderBy from 'lodash/orderBy'; import orderBy from 'lodash/orderBy';
import { sanitizedRaw } from '@nozbe/watermelondb/RawRecord'; 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 TabBar from './TabBar';
import EmojiCategory from './EmojiCategory'; import EmojiCategory from './EmojiCategory';
@ -14,8 +14,8 @@ import categories from './categories';
import database from '../../lib/database'; import database from '../../lib/database';
import { emojisByCategory } from './emojis'; import { emojisByCategory } from './emojis';
import protectedFunction from '../../lib/methods/helpers/protectedFunction'; import protectedFunction from '../../lib/methods/helpers/protectedFunction';
import shortnameToUnicode from '../../utils/shortnameToUnicode'; import shortnameToUnicode from '../../lib/methods/helpers/shortnameToUnicode';
import log from '../../utils/log'; import log from '../../lib/methods/helpers/log';
import { themes } from '../../lib/constants'; import { themes } from '../../lib/constants';
import { TSupportedThemes, withTheme } from '../../theme'; import { TSupportedThemes, withTheme } from '../../theme';
import { IEmoji, TGetCustomEmoji, IApplicationState, ICustomEmojis, TFrequentlyUsedEmojiModel } from '../../definitions'; 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 { themes } from '../lib/constants';
import sharedStyles from '../views/Styles'; import sharedStyles from '../views/Styles';
import scrollPersistTaps from '../utils/scrollPersistTaps'; import scrollPersistTaps from '../lib/methods/helpers/scrollPersistTaps';
import KeyboardView from './KeyboardView'; import KeyboardView from './KeyboardView';
import { useTheme } from '../theme'; import { useTheme } from '../theme';
import StatusBar from './StatusBar'; import StatusBar from './StatusBar';
import AppVersion from './AppVersion'; import AppVersion from './AppVersion';
import { isTablet } from '../utils/deviceInfo'; import { isTablet } from '../lib/methods/helpers';
import SafeAreaView from './SafeAreaView'; import SafeAreaView from './SafeAreaView';
interface IFormContainer extends ScrollViewProps { 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 React from 'react';
import { isIOS } from '../../utils/deviceInfo'; import { isIOS } from '../../lib/methods/helpers';
import I18n from '../../i18n'; import I18n from '../../i18n';
import Container from './HeaderButtonContainer'; 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 navigation?: any; // TODO: Evaluate proper type
onPress?: () => void;
testID?: string;
} }
// Left // 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> <Container left>
{isIOS ? ( {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> </Container>
)); ));
// Right // Right
export const More = React.memo(({ onPress, testID }: Partial<IHeaderButtonCommon>) => ( export const More = React.memo(({ onPress, testID, ...props }: IHeaderButtonCommon) => (
<Container> <Container>
<Item iconName='kebab' onPress={onPress} testID={testID} /> <Item iconName='kebab' onPress={onPress} testID={testID} {...props} />
</Container> </Container>
)); ));
@ -58,7 +56,7 @@ export const Preferences = React.memo(({ onPress, testID, ...props }: IHeaderBut
)); ));
export const Legal = React.memo( export const Legal = React.memo(
({ navigation, testID, onPress = () => navigation?.navigate('LegalView') }: IHeaderButtonCommon) => ( ({ navigation, testID, onPress = () => navigation?.navigate('LegalView'), ...props }: IHeaderButtonCommon) => (
<More onPress={onPress} testID={testID} /> <More onPress={onPress} testID={testID} {...props} />
) )
); );

View File

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

View File

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

View File

@ -1,6 +1,6 @@
import React from 'react'; import React from 'react';
import { Image } from 'react-native'; 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'; import { types } from './types';
@ -10,7 +10,7 @@ export const ImageComponent = (type?: string): React.ComponentType<Partial<Image
const { Image } = require('react-native'); const { Image } = require('react-native');
Component = Image; Component = Image;
} else { } else {
const FastImage = require('@rocket.chat/react-native-fast-image').default; const FastImage = require('react-native-fast-image');
Component = FastImage; Component = FastImage;
} }
return Component; 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 { themes } from '../../lib/constants';
import { useTheme } from '../../theme'; import { useTheme } from '../../theme';
import { ROW_HEIGHT } from '../RoomItem'; import { ROW_HEIGHT } from '../RoomItem';
import { goRoom } from '../../utils/goRoom'; import { goRoom } from '../../lib/methods/helpers/goRoom';
import Navigation from '../../lib/navigation/appNavigation'; import Navigation from '../../lib/navigation/appNavigation';
import { useOrientation } from '../../dimensions'; import { useOrientation } from '../../dimensions';
import { IApplicationState, ISubscription, SubscriptionType } from '../../definitions'; import { IApplicationState, ISubscription, SubscriptionType } from '../../definitions';

View File

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

View File

@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import { KeyboardAwareScrollView, KeyboardAwareScrollViewProps } from '@codler/react-native-keyboard-aware-scroll-view'; 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 { interface IKeyboardViewProps extends KeyboardAwareScrollViewProps {
keyboardVerticalOffset?: number; keyboardVerticalOffset?: number;

View File

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

View File

@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import { I18nManager, StyleProp, StyleSheet, Text, TextStyle, View } from 'react-native'; 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 { themes } from '../../lib/constants';
import sharedStyles from '../../views/Styles'; import sharedStyles from '../../views/Styles';
import { TSupportedThemes, useTheme } from '../../theme'; 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 { TSupportedThemes, useTheme } from '../../theme';
import { themes } from '../../lib/constants'; import { themes } from '../../lib/constants';
import { CustomIcon } from '../CustomIcon'; import { CustomIcon } from '../CustomIcon';
import shortnameToUnicode from '../../utils/shortnameToUnicode'; import shortnameToUnicode from '../../lib/methods/helpers/shortnameToUnicode';
import CustomEmoji from '../EmojiPicker/CustomEmoji'; import CustomEmoji from '../EmojiPicker/CustomEmoji';
import database from '../../lib/database'; import database from '../../lib/database';
import { Button } from '../ActionSheet'; import { Button } from '../ActionSheet';

View File

@ -6,17 +6,18 @@ import moment from 'moment';
import database from '../../lib/database'; import database from '../../lib/database';
import I18n from '../../i18n'; 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 Navigation from '../../lib/navigation/appNavigation';
import { getMessageTranslation } from '../message/utils'; import { getMessageTranslation } from '../message/utils';
import { LISTENER } from '../Toast'; import { LISTENER } from '../Toast';
import EventEmitter from '../../utils/events'; import EventEmitter from '../../lib/methods/helpers/events';
import { showConfirmationAlert } from '../../utils/info'; import { showConfirmationAlert } from '../../lib/methods/helpers/info';
import { TActionSheetOptionsItem, useActionSheet } from '../ActionSheet'; import { TActionSheetOptionsItem, useActionSheet } from '../ActionSheet';
import Header, { HEADER_HEIGHT, IHeader } from './Header'; 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 { 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'; import { Services } from '../../lib/services';
export interface IMessageActionsProps { 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 React, { useContext, useState } from 'react';
import { TouchableOpacity } from 'react-native'; import { TouchableOpacity } from 'react-native';

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -8,7 +8,7 @@ import Base, { IBase } from './Base';
import Locked from './Base/Locked'; import Locked from './Base/Locked';
import { TYPE } from './constants'; import { TYPE } from './constants';
import { ATTEMPTS_KEY, LOCKED_OUT_TIMER_KEY, MAX_ATTEMPTS, PASSCODE_KEY } from '../../lib/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 { getDiff, getLockedUntil } from './utils';
import { useUserPreferences } from '../../lib/methods/userPreferences'; import { useUserPreferences } from '../../lib/methods/userPreferences';
import I18n from '../../i18n'; 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 React from 'react';
import { Dimensions, View } from 'react-native'; import { Dimensions, SafeAreaView } from 'react-native';
import { storiesOf } from '@storybook/react-native'; import { storiesOf } from '@storybook/react-native';
import { Provider } from 'react-redux'; 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 { longText } from '../../../storybook/utils';
import { ThemeContext } from '../../theme'; import { ThemeContext } from '../../theme';
import { store } from '../../../storybook/stories'; import { store } from '../../../storybook/stories';
import { colors, themes } from '../../lib/constants';
import RoomHeaderComponent from './RoomHeader'; import RoomHeaderComponent from './RoomHeader';
const stories = storiesOf('RoomHeader', module).addDecorator(story => <Provider store={store}>{story()}</Provider>); const stories = storiesOf('RoomHeader', module)
.addDecorator(story => <Provider store={store}>{story()}</Provider>)
// TODO: refactor after react-navigation v6 .addDecorator(story => <SafeAreaProvider>{story()}</SafeAreaProvider>);
const HeaderExample = ({ title }) => (
<Header headerTitle={() => <View style={{ flex: 1, paddingHorizontal: 12 }}>{title()}</View>} />
);
const { width, height } = Dimensions.get('window'); 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 }) => ( const RoomHeader = ({ ...props }) => (
<RoomHeaderComponent <RoomHeaderComponent
width={width} width={width}
@ -27,6 +36,8 @@ const RoomHeader = ({ ...props }) => (
type='p' type='p'
testID={props.title} testID={props.title}
onPress={() => alert('header pressed!')} onPress={() => alert('header pressed!')}
status={props.status}
usersTyping={props.usersTyping}
{...props} {...props}
/> />
); );
@ -82,8 +93,8 @@ stories.add('thread', () => (
)); ));
const ThemeStory = ({ theme }) => ( const ThemeStory = ({ theme }) => (
<ThemeContext.Provider value={{ theme }}> <ThemeContext.Provider value={{ theme, colors: colors[theme] }}>
<HeaderExample title={() => <RoomHeader subtitle='subtitle' />} /> <HeaderExample title={() => <RoomHeader subtitle='subtitle' />} theme={theme} />
</ThemeContext.Provider> </ThemeContext.Provider>
); );

View File

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

File diff suppressed because one or more lines are too long

View File

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

View File

@ -1,25 +1,32 @@
import React from 'react'; 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 { RectButton } from 'react-native-gesture-handler';
import * as Haptics from 'expo-haptics';
import { isRTL } from '../../i18n';
import { CustomIcon } from '../CustomIcon'; 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 styles, { ACTION_WIDTH, LONG_SWIPE, ROW_HEIGHT_CONDENSED } from './styles';
import { ILeftActionsProps, IRightActionsProps } from './interfaces'; 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 CONDENSED_ICON_SIZE = 24;
const EXPANDED_ICON_SIZE = 28; const EXPANDED_ICON_SIZE = 28;
export const LeftActions = React.memo(({ theme, transX, isRead, width, onToggleReadPress, displayMode }: ILeftActionsProps) => { export const LeftActions = React.memo(({ transX, isRead, width, onToggleReadPress, displayMode }: ILeftActionsProps) => {
const translateX = Animated.multiply( const { colors } = useTheme();
transX.interpolate({
inputRange: [0, ACTION_WIDTH], const animatedStyles = useAnimatedStyle(() => ({
outputRange: [-ACTION_WIDTH, 0] transform: [{ translateX: transX.value }]
}), }));
reverse
);
const isCondensed = displayMode === DisplayMode.Condensed; const isCondensed = displayMode === DisplayMode.Condensed;
const viewHeight = isCondensed ? { height: ROW_HEIGHT_CONDENSED } : null; const viewHeight = isCondensed ? { height: ROW_HEIGHT_CONDENSED } : null;
@ -29,20 +36,16 @@ export const LeftActions = React.memo(({ theme, transX, isRead, width, onToggleR
<Animated.View <Animated.View
style={[ style={[
styles.actionLeftButtonContainer, styles.actionLeftButtonContainer,
{ { width: width * 2, backgroundColor: colors.tintColor, right: '100%' },
right: width - ACTION_WIDTH, viewHeight,
width, animatedStyles
transform: [{ translateX }],
backgroundColor: themes[theme].tintColor
},
viewHeight
]}> ]}>
<View style={[styles.actionLeftButtonContainer, viewHeight]}> <View style={[styles.actionLeftButtonContainer, viewHeight]}>
<RectButton style={styles.actionButton} onPress={onToggleReadPress}> <RectButton style={styles.actionButton} onPress={onToggleReadPress}>
<CustomIcon <CustomIcon
size={isCondensed ? CONDENSED_ICON_SIZE : EXPANDED_ICON_SIZE} size={isCondensed ? CONDENSED_ICON_SIZE : EXPANDED_ICON_SIZE}
name={isRead ? 'flag' : 'check'} name={isRead ? 'flag' : 'check'}
color={themes[theme].buttonText} color={colors.buttonText}
/> />
</RectButton> </RectButton>
</View> </View>
@ -51,64 +54,102 @@ export const LeftActions = React.memo(({ theme, transX, isRead, width, onToggleR
); );
}); });
export const RightActions = React.memo( export const RightActions = React.memo(({ transX, favorite, width, toggleFav, onHidePress, displayMode }: IRightActionsProps) => {
({ transX, favorite, width, toggleFav, onHidePress, theme, displayMode }: IRightActionsProps) => { const { colors } = useTheme();
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
);
const isCondensed = displayMode === DisplayMode.Condensed; const animatedFavStyles = useAnimatedStyle(() => ({ transform: [{ translateX: transX.value }] }));
const viewHeight = isCondensed ? { height: ROW_HEIGHT_CONDENSED } : null;
return ( const translateXHide = useSharedValue(0);
<View style={[styles.actionsLeftContainer, viewHeight]} pointerEvents='box-none'>
<Animated.View const triggerHideAnimation = (toValue: number) => {
style={[ Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
styles.actionRightButtonContainer, translateXHide.value = withSpring(toValue, { overshootClamping: true, mass: 0.7 });
{ };
width,
transform: [{ translateX: translateXFav }], useAnimatedReaction(
backgroundColor: themes[theme].hideBackground () => transX.value,
}, (currentTransX, previousTransX) => {
viewHeight // Triggers the animation and hapticFeedback if swipe reaches/unreaches the threshold.
]}> if (I18n.isRTL) {
<RectButton style={[styles.actionButton, { backgroundColor: themes[theme].favoriteBackground }]} onPress={toggleFav}> if (previousTransX && currentTransX > LONG_SWIPE && previousTransX <= LONG_SWIPE) {
<CustomIcon runOnJS(triggerHideAnimation)(ACTION_WIDTH);
size={isCondensed ? CONDENSED_ICON_SIZE : EXPANDED_ICON_SIZE} } else if (previousTransX && currentTransX <= LONG_SWIPE && previousTransX > LONG_SWIPE) {
name={favorite ? 'star-filled' : 'star'} runOnJS(triggerHideAnimation)(0);
color={themes[theme].buttonText} }
/> } else if (previousTransX && currentTransX < -LONG_SWIPE && previousTransX >= -LONG_SWIPE) {
</RectButton> runOnJS(triggerHideAnimation)(-ACTION_WIDTH);
</Animated.View> } else if (previousTransX && currentTransX >= -LONG_SWIPE && previousTransX < -LONG_SWIPE) {
<Animated.View runOnJS(triggerHideAnimation)(0);
style={[ }
styles.actionRightButtonContainer, }
{ );
width,
transform: [{ translateX: translateXHide }] const animatedHideStyles = useAnimatedStyle(() => {
}, if (I18n.isRTL) {
isCondensed && { height: ROW_HEIGHT_CONDENSED } if (transX.value < LONG_SWIPE && transX.value >= 2 * ACTION_WIDTH) {
]}> const parallaxSwipe = interpolate(
<RectButton style={[styles.actionButton, { backgroundColor: themes[theme].hideBackground }]} onPress={onHidePress}> transX.value,
<CustomIcon [2 * ACTION_WIDTH, LONG_SWIPE],
size={isCondensed ? CONDENSED_ICON_SIZE : EXPANDED_ICON_SIZE} [ACTION_WIDTH, ACTION_WIDTH + 0.1 * transX.value]
name='unread-on-top-disabled' );
color={themes[theme].buttonText} return { transform: [{ translateX: parallaxSwipe + translateXHide.value }] };
/> }
</RectButton> return { transform: [{ translateX: transX.value - ACTION_WIDTH + translateXHide.value }] };
</Animated.View> }
</View> 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 React from 'react';
import { View } from 'react-native'; import { View } from 'react-native';
import PropTypes from 'prop-types';
import Avatar from '../Avatar'; import Avatar from '../Avatar';
import { DisplayMode } from '../../lib/constants'; import { DisplayMode } from '../../lib/constants';
import TypeIcon from './TypeIcon'; import TypeIcon from './TypeIcon';
import styles from './styles'; import styles from './styles';
import { IIconOrAvatar } from './interfaces';
const IconOrAvatar = ({ const IconOrAvatar = ({
avatar, avatar,
@ -17,10 +17,9 @@ const IconOrAvatar = ({
isGroupChat, isGroupChat,
teamMain, teamMain,
showLastMessage, showLastMessage,
theme,
displayMode, displayMode,
sourceType sourceType
}) => { }: IIconOrAvatar): React.ReactElement | null => {
if (showAvatar) { if (showAvatar) {
return ( return (
<Avatar text={avatar} size={displayMode === DisplayMode.Condensed ? 36 : 48} type={type} style={styles.avatar} rid={rid} /> <Avatar text={avatar} size={displayMode === DisplayMode.Condensed ? 36 : 48} type={type} style={styles.avatar} rid={rid} />
@ -35,7 +34,6 @@ const IconOrAvatar = ({
prid={prid} prid={prid}
status={status} status={status}
isGroupChat={isGroupChat} isGroupChat={isGroupChat}
theme={theme}
teamMain={teamMain} teamMain={teamMain}
size={24} size={24}
style={{ marginRight: 12 }} style={{ marginRight: 12 }}
@ -48,18 +46,4 @@ const IconOrAvatar = ({
return null; 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; export default IconOrAvatar;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,12 +1,11 @@
import React from 'react'; import React from 'react';
import { Animated } from 'react-native'; import Animated from 'react-native-reanimated';
import { TSupportedThemes } from '../../theme'; import { TSupportedThemes } from '../../theme';
import { TUserStatus, ILastMessage, SubscriptionType, IOmnichannelSource } from '../../definitions'; import { TUserStatus, ILastMessage, SubscriptionType, IOmnichannelSource } from '../../definitions';
export interface ILeftActionsProps { export interface ILeftActionsProps {
theme: TSupportedThemes; transX: Animated.SharedValue<number>;
transX: Animated.AnimatedAddition | Animated.AnimatedMultiplication;
isRead: boolean; isRead: boolean;
width: number; width: number;
onToggleReadPress(): void; onToggleReadPress(): void;
@ -14,8 +13,7 @@ export interface ILeftActionsProps {
} }
export interface IRightActionsProps { export interface IRightActionsProps {
theme: TSupportedThemes; transX: Animated.SharedValue<number>;
transX: Animated.AnimatedAddition | Animated.AnimatedMultiplication;
favorite: boolean; favorite: boolean;
width: number; width: number;
toggleFav(): void; toggleFav(): void;
@ -25,14 +23,12 @@ export interface IRightActionsProps {
export interface ITitleProps { export interface ITitleProps {
name: string; name: string;
theme: TSupportedThemes;
hideUnreadStatus: boolean; hideUnreadStatus: boolean;
alert: boolean; alert: boolean;
} }
export interface IUpdatedAtProps { export interface IUpdatedAtProps {
date: string; date: string;
theme: TSupportedThemes;
hideUnreadStatus: boolean; hideUnreadStatus: boolean;
alert: boolean; alert: boolean;
} }
@ -41,13 +37,12 @@ export interface IWrapperProps {
accessibilityLabel: string; accessibilityLabel: string;
avatar: string; avatar: string;
type: string; type: string;
theme: TSupportedThemes;
rid: string; rid: string;
children: React.ReactElement; children: React.ReactElement;
displayMode: string; displayMode: string;
prid: string; prid: string;
showLastMessage: boolean; showLastMessage: boolean;
status: string; status: TUserStatus;
isGroupChat: boolean; isGroupChat: boolean;
teamMain: boolean; teamMain: boolean;
showAvatar: boolean; showAvatar: boolean;
@ -66,48 +61,43 @@ export interface ITypeIconProps {
sourceType: IOmnichannelSource; sourceType: IOmnichannelSource;
} }
export interface IRoomItemContainerProps { interface IRoomItemTouchables {
[key: string]: string | boolean | Function | number; toggleFav?: (rid: string, favorite: boolean) => Promise<void>;
item: any; toggleRead?: (rid: string, tIsRead: boolean) => Promise<void>;
showLastMessage: boolean; hideChannel?: (rid: string, type: SubscriptionType) => Promise<void>;
id: string; onPress: (item?: any) => void;
onPress: (item: any) => void; onLongPress?: (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;
} }
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; rid: string;
type: SubscriptionType; type: SubscriptionType;
prid: string; prid: string;
name: string; name: string;
avatar: string; avatar: string;
showLastMessage: boolean;
username: string;
testID: string; testID: string;
width: number;
status: TUserStatus; status: TUserStatus;
useRealName: boolean;
theme: TSupportedThemes;
isFocused: boolean;
isGroupChat: boolean; isGroupChat: boolean;
isRead: boolean; isRead: boolean;
teamMain: boolean; teamMain: boolean;
@ -123,21 +113,12 @@ export interface IRoomItemProps {
tunread: []; tunread: [];
tunreadUser: []; tunreadUser: [];
tunreadGroup: []; tunreadGroup: [];
swipeEnabled: boolean;
toggleFav(): void;
toggleRead(): void;
onPress(): void;
onLongPress(): void;
hideChannel(): void;
autoJoin: boolean;
size?: number; size?: number;
showAvatar: boolean;
displayMode: string;
sourceType: IOmnichannelSource; sourceType: IOmnichannelSource;
hideMentionStatus?: boolean;
} }
export interface ILastMessageProps { export interface ILastMessageProps {
theme: TSupportedThemes;
lastMessage: ILastMessage; lastMessage: ILastMessage;
type: SubscriptionType; type: SubscriptionType;
showLastMessage: boolean; showLastMessage: boolean;
@ -146,21 +127,29 @@ export interface ILastMessageProps {
alert: boolean; alert: boolean;
} }
export interface ITouchableProps { export interface ITouchableProps extends IRoomItemTouchables {
children: JSX.Element; children: JSX.Element;
type: string; type: SubscriptionType;
onPress(): void;
onLongPress(): void;
testID: string; testID: string;
width: number; width: number;
favorite: boolean; favorite: boolean;
isRead: boolean; isRead: boolean;
rid: string; rid: string;
toggleFav: Function;
toggleRead: Function;
hideChannel: Function;
theme: TSupportedThemes;
isFocused: boolean; isFocused: boolean;
swipeEnabled: boolean; swipeEnabled: boolean;
displayMode: string; 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 ROW_HEIGHT_CONDENSED = 60 * PixelRatio.getFontScale();
export const ACTION_WIDTH = 80; export const ACTION_WIDTH = 80;
export const SMALL_SWIPE = ACTION_WIDTH / 2; 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({ export default StyleSheet.create({
flex: { 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 { useTheme } from '../theme';
import sharedStyles from '../views/Styles'; import sharedStyles from '../views/Styles';
import { themes } from '../lib/constants'; import { themes } from '../lib/constants';
import TextInput from './TextInput'; import { TextInput } from './TextInput';
import { isIOS, isTablet } from '../utils/deviceInfo'; import { isIOS, isTablet } from '../lib/methods/helpers';
import { useOrientation } from '../dimensions'; import { useOrientation } from '../dimensions';
const styles = StyleSheet.create({ 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 }]} style={[styles.title, isLight && { color: themes[theme].headerTitleColor }, { fontSize: titleFontSize }]}
placeholder={I18n.t('Search')} placeholder={I18n.t('Search')}
onChangeText={onSearchChangeText} onChangeText={onSearchChangeText}
theme={theme}
testID={testID} testID={testID}
/> />
</View> </View>

View File

@ -1,13 +1,13 @@
import React from 'react'; import React from 'react';
// @ts-ignore // TODO: Remove on react-native update // @ts-ignore // TODO: Remove on react-native update
import { Pressable, Text, View } from 'react-native'; 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 { IServerInfo } from '../../definitions';
import Check from '../Check'; import Check from '../Check';
import styles, { ROW_HEIGHT } from './styles'; import styles, { ROW_HEIGHT } from './styles';
import { themes } from '../../lib/constants'; import { themes } from '../../lib/constants';
import { isIOS } from '../../utils/deviceInfo'; import { isIOS } from '../../lib/methods/helpers';
import { useTheme } from '../../theme'; import { useTheme } from '../../theme';
export { ROW_HEIGHT }; 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 { BottomSheetTextInput } from '@gorhom/bottom-sheet';
import { StyleProp, StyleSheet, Text, TextInputProps, TextInput as RNTextInput, TextStyle, View, ViewStyle } from 'react-native'; 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 Touchable from 'react-native-platform-touchable';
import { useTheme } from '../../theme';
import sharedStyles from '../../views/Styles'; import sharedStyles from '../../views/Styles';
import TextInput from './index';
import { themes } from '../../lib/constants';
import { CustomIcon, TIconsName } from '../CustomIcon';
import ActivityIndicator from '../ActivityIndicator'; import ActivityIndicator from '../ActivityIndicator';
import { TSupportedThemes } from '../../theme'; import { CustomIcon, TIconsName } from '../CustomIcon';
import { TextInput } from './TextInput';
const styles = StyleSheet.create({ const styles = StyleSheet.create({
error: { error: {
@ -58,134 +58,118 @@ export interface IRCTextInputProps extends TextInputProps {
containerStyle?: StyleProp<ViewStyle>; containerStyle?: StyleProp<ViewStyle>;
inputStyle?: StyleProp<TextStyle>; inputStyle?: StyleProp<TextStyle>;
inputRef?: React.Ref<RNTextInput>; inputRef?: React.Ref<RNTextInput>;
testID?: string;
iconLeft?: TIconsName; iconLeft?: TIconsName;
iconRight?: TIconsName; iconRight?: TIconsName;
left?: JSX.Element; left?: JSX.Element;
onIconRightPress?(): void; bottomSheet?: boolean;
theme: TSupportedThemes; onClearInput?: () => void;
} }
interface IRCTextInputState { export const FormTextInput = ({
showPassword: boolean; 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> { <View style={styles.wrap}>
static defaultProps = { <Input
error: {}, style={[
theme: 'light' styles.input,
}; iconLeft && styles.inputIconLeft,
(secureTextEntry || iconRight) && styles.inputIconRight,
state = { {
showPassword: false backgroundColor: colors.backgroundColor,
}; borderColor: colors.separatorColor,
color: colors.titleText
get iconLeft() { },
const { testID, iconLeft, theme } = this.props; error?.error && {
return iconLeft ? ( color: colors.dangerColor,
<CustomIcon borderColor: colors.dangerColor
name={iconLeft} },
testID={testID ? `${testID}-icon-left` : undefined} inputStyle
size={20} ]}
color={themes[theme].bodyText} // @ts-ignore ref error
style={[styles.iconContainer, styles.iconLeft]} ref={inputRef}
/> autoCorrect={false}
) : null; autoCapitalize='none'
} underlineColorAndroid='transparent'
secureTextEntry={secureTextEntry && !showPassword}
get iconRight() { testID={testID}
const { iconRight, onIconRightPress, theme } = this.props; accessibilityLabel={placeholder}
return iconRight ? ( placeholder={placeholder}
<Touchable onPress={onIconRightPress} style={[styles.iconContainer, styles.iconRight]}> value={value}
<CustomIcon name={iconRight} size={20} color={themes[theme].bodyText} /> {...inputProps}
</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}
/> />
</Touchable>
);
}
get loading() { {iconLeft ? (
const { theme } = this.props; <CustomIcon
return <ActivityIndicator style={[styles.iconContainer, styles.iconRight]} color={themes[theme].bodyText} />; name={iconLeft}
} testID={testID ? `${testID}-icon-left` : undefined}
size={20}
tooglePassword = () => { color={colors.auxiliaryText}
this.setState(prevState => ({ showPassword: !prevState.showPassword })); style={[styles.iconContainer, styles.iconLeft]}
};
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 ? this.iconLeft : null} ) : null}
{iconRight ? this.iconRight : null}
{secureTextEntry ? this.iconPassword : null} {showClearInput ? (
{loading ? this.loading : null} <Touchable onPress={onClearInput} style={[styles.iconContainer, styles.iconRight]} testID='clear-text-input'>
{left} <CustomIcon name='input-clear' size={20} color={colors.auxiliaryTintColor} />
</View> </Touchable>
{error && error.reason ? <Text style={[styles.error, { color: dangerColor }]}>{error.reason}</Text> : null} ) : 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> </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 { storiesOf } from '@storybook/react-native';
import { View, StyleSheet } from 'react-native'; import { View, StyleSheet } from 'react-native';
import FormTextInput from './FormTextInput'; import { FormTextInput } from '.';
const styles = StyleSheet.create({ const styles = StyleSheet.create({
paddingHorizontal: { paddingHorizontal: {
@ -18,14 +18,12 @@ const item = {
longText: 'https://open.rocket.chat/images/logo/android-chrome-512x512.png' longText: 'https://open.rocket.chat/images/logo/android-chrome-512x512.png'
}; };
const theme = 'light';
stories.add('Short and Long Text', () => ( stories.add('Short and Long Text', () => (
<> <>
<View style={styles.paddingHorizontal}> <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>
</> </>
)); ));

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 { StyleSheet } from 'react-native';
import EasyToast from 'react-native-easy-toast'; 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 sharedStyles from '../views/Styles';
import EventEmitter from '../utils/events';
import { TSupportedThemes, withTheme } from '../theme';
const styles = StyleSheet.create({ const styles = StyleSheet.create({
toast: { toast: {
@ -21,54 +20,37 @@ const styles = StyleSheet.create({
export const LISTENER = 'Toast'; export const LISTENER = 'Toast';
interface IToastProps { let listener: Function;
theme?: TSupportedThemes; let toast: EasyToast | null | undefined;
}
class Toast extends React.Component<IToastProps, any> { const Toast = (): React.ReactElement => {
private listener?: Function; const { colors } = useTheme();
private toast: EasyToast | null | undefined; useEffect(() => {
listener = EventEmitter.addEventListener(LISTENER, showToast);
return () => {
EventEmitter.removeListener(LISTENER, listener);
};
}, []);
componentDidMount() { const getToastRef = (newToast: EasyToast | null) => (toast = newToast);
this.listener = EventEmitter.addEventListener(LISTENER, this.showToast);
}
shouldComponentUpdate(nextProps: any) { const showToast = ({ message }: { message: string }) => {
const { theme } = this.props; if (toast && toast.show) {
if (nextProps.theme !== theme) { toast.show(message, 1000);
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);
} }
}; };
render() { return (
const { theme } = this.props; <EasyToast
return ( ref={getToastRef}
<EasyToast // @ts-ignore
ref={this.getToastRef} position='center'
// @ts-ignore style={[styles.toast, { backgroundColor: colors.toastBackground }]}
position='center' textStyle={[styles.text, { color: colors.buttonText }]}
style={[styles.toast, { backgroundColor: themes[theme!].toastBackground }]} opacity={0.9}
textStyle={[styles.text, { color: themes[theme!].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 useDeepCompareEffect from 'use-deep-compare-effect';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import FormTextInput from '../TextInput/FormTextInput'; import { FormTextInput } from '../TextInput';
import I18n from '../../i18n'; import I18n from '../../i18n';
import EventEmitter from '../../utils/events'; import EventEmitter from '../../lib/methods/helpers/events';
import { useTheme } from '../../theme'; import { useTheme } from '../../theme';
import { themes } from '../../lib/constants'; import { themes } from '../../lib/constants';
import Button from '../Button'; 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} {method?.text ? <Text style={[styles.subtitle, { color }]}>{I18n.t(method.text)}</Text> : null}
<FormTextInput <FormTextInput
value={code} value={code}
theme={theme}
inputRef={(e: any) => InteractionManager.runAfterInteractions(() => e?.getNativeRef()?.focus())} inputRef={(e: any) => InteractionManager.runAfterInteractions(() => e?.getNativeRef()?.focus())}
returnKeyType='send' returnKeyType='send'
autoCapitalize='none' autoCapitalize='none'

View File

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

View File

@ -1,6 +1,6 @@
import React from 'react'; import React from 'react';
import { StyleSheet, View } from 'react-native'; 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 { BlockContext } from '@rocket.chat/ui-kit';
import ImageContainer from '../message/Image'; import ImageContainer from '../message/Image';

View File

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

View File

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

View File

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

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